Add update password form

pull/3813/head
Kamran Ahmed 2 years ago
parent e849eeeca5
commit 33d7257ed2
  1. 2
      src/components/AuthenticationFlow/ForgotPasswordForm.tsx
  2. 4
      src/components/Authenticator/authenticator.ts
  3. 2
      src/components/Breadcrumbs.astro
  4. 50
      src/components/DownloadPopup.astro
  5. 2
      src/components/Navigation/AccountDropdown.astro
  6. 2
      src/components/Navigation/Navigation.astro
  7. 45
      src/components/Profile/profile-details.tsx
  8. 4
      src/components/RoadmapHeader.astro
  9. 219
      src/components/Setting/ChangePasswordForm.tsx
  10. 8
      src/components/Setting/SettingSidebar.astro
  11. 181
      src/components/Setting/UpdatePasswordForm.tsx
  12. 41
      src/components/SubscribePopup.astro
  13. 9
      src/pages/forgot-password.astro
  14. 4
      src/pages/settings/update-password.astro
  15. 0
      src/pages/settings/update-profile.astro

@ -2,7 +2,7 @@ import { useState } from 'preact/hooks';
import Spinner from '../Spinner';
import { httpPost } from '../../lib/http';
export default function ForgotPasswordForm() {
export function ForgotPasswordForm() {
const [email, setEmail] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');

@ -32,8 +32,8 @@ function showHideGuestElements(hideOrShow: 'hide' | 'show' = 'hide') {
// Prepares the UI for the user who is logged in
function handleGuest() {
const authenticatedRoutes = [
'/settings/profile',
'/settings/change-password',
'/settings/update-profile',
'/settings/update-password',
];
showHideAuthElements('hide');

@ -10,7 +10,7 @@ const { breadcrumbs, roadmapId } = Astro.props;
---
<div class='py-7 pb-6'>
<!-- Desktop breadcrums -->
<!-- Desktop breadcrumbs -->
<p class='text-gray-500 container hidden sm:block'>
{
breadcrumbs.map((breadcrumb, counter) => {

@ -1,50 +0,0 @@
---
import Popup from './Popup/Popup.astro';
import CaptchaFields from './Captcha/CaptchaFields.astro';
---
<Popup id='download-popup' title='Download' subtitle='Enter your email below to receive the download link.'>
<form
action='https://news.roadmap.sh/subscribe'
method='POST'
accept-charset='utf-8'
target='_blank'
captcha-form
>
<input type='hidden' name='gdpr' value='true' />
<input
type='email'
name='email'
id='email'
required
autofocus
class='w-full rounded-md border text-md py-2.5 px-3 mb-2'
placeholder='Enter your Email'
/>
<CaptchaFields />
<input type='hidden' name='list' value='tTqz1w7nexY3cWDpLnI88Q' />
<input type='hidden' name='subform' value='yes' />
<button
type='submit'
name='submit'
class='text-white bg-gradient-to-r from-amber-700 to-blue-800 hover:from-amber-800 hover:to-blue-900 font-regular rounded-md text-md px-5 py-2.5 w-full text-center mr-2'
submit-download-form
>
Send Link
</button>
</form>
</Popup>
<script>
document.querySelector('[submit-download-form]')?.addEventListener('click', () => {
window.fireEvent({
category: 'Subscription',
action: 'Submitted Popup Form',
label: 'Download Roadmap Popup',
});
});
</script>

@ -24,7 +24,7 @@ import Icon from '../AstroIcon.astro';
<ul>
<li class='px-1'>
<a
href='/settings/profile'
href='/settings/update-profile'
class='block rounded px-4 py-2 text-sm font-medium text-slate-100 hover:bg-slate-700'
>
Settings

@ -93,7 +93,7 @@ import AccountDropdown from './AccountDropdown.astro';
<!-- Links for logged in users -->
<li data-auth-required class='hidden'>
<a
href='/settings/profile'
href='/settings/update-profile'
class='text-xl hover:text-blue-300 md:text-lg'
>
Settings

@ -1,45 +0,0 @@
import { useAuth } from '../../hooks/use-auth';
export default function ProfileDetails() {
const { user, isLoading } = useAuth();
return (
<div className="py-10 pb-20">
<h1 className="text-3xl font-bold sm:text-4xl">Profile</h1>
<p className="mt-2">Here you can view your profile details.</p>
<div className="mt-5 space-y-4">
<div>
<label className="text-slate-500">Name</label>
<div className="mt-1">
{isLoading || !user ? (
<Skeleton />
) : (
<h2 className="text-xl font-medium text-slate-800">
{user?.name}
</h2>
)}
</div>
</div>
<div>
<label className="text-slate-500">Email</label>
<div className="mt-1">
{isLoading || !user ? (
<Skeleton className="w-64" />
) : (
<h2 className="text-xl font-medium text-slate-800">
{user?.email}
</h2>
)}
</div>
</div>
</div>
</div>
);
}
function Skeleton({ className }: { className?: string }) {
return (
<div
className={`h-7 w-36 animate-pulse rounded-md bg-slate-100 ${className}`}
/>
);
}

@ -1,10 +1,8 @@
---
import DownloadPopup from './DownloadPopup.astro';
import Icon from './AstroIcon.astro';
import LoginPopup from './AuthenticationFlow/LoginPopup.astro';
import RoadmapHint from './RoadmapHint.astro';
import RoadmapNote from './RoadmapNote.astro';
import SubscribePopup from './SubscribePopup.astro';
import TopicSearch from './TopicSearch/TopicSearch.astro';
import YouTubeAlert from './YouTubeAlert.astro';
@ -34,8 +32,6 @@ const isRoadmapReady = !isUpcoming;
---
<LoginPopup />
<DownloadPopup />
<SubscribePopup />
<div class='border-b'>
<div class='container relative py-5 sm:py-12'>

@ -1,219 +0,0 @@
import { useCallback, useEffect, useState } from 'preact/hooks';
import Cookies from 'js-cookie';
import Spinner from '../Spinner';
import {TOKEN_COOKIE_NAME} from "../../lib/jwt";
export default function ChangePasswordForm() {
const [authProvider, setAuthProvider] = useState<
'email' | 'google' | 'github' | null
>(null);
const [currentPassword, setCurrentPassword] = useState('');
const [newPassword, setNewPassword] = useState('');
const [newPasswordConfirmation, setNewPasswordConfirmation] = useState('');
const [message, setMessage] = useState<{
type: 'error' | 'success' | 'info';
message: string;
}>();
const [isLoading, setIsLoading] = useState(false);
const handleSubmit = (e: Event) => {
e.preventDefault();
setIsLoading(true);
if (newPassword !== newPasswordConfirmation) {
setMessage({
type: 'error',
message: 'Passwords do not match',
});
setIsLoading(false);
return;
}
const headers = new Headers();
headers.append('Content-Type', 'application/json');
headers.append(
'Cookie',
`${TOKEN_COOKIE_NAME}=${Cookies.get(TOKEN_COOKIE_NAME)}`
);
fetch('http://localhost:8080/v1-update-password', {
method: 'POST',
credentials: 'include',
headers,
body: JSON.stringify({
oldPassword: authProvider === 'email' ? currentPassword : 'social-auth',
password: newPassword,
confirmPassword: newPasswordConfirmation,
}),
})
.then(async (res) => {
const json = await res.json();
if (res.ok) {
return json;
} else {
throw new Error(json.message);
}
})
.then((data) => {
setIsLoading(false);
setCurrentPassword('');
setNewPassword('');
setNewPasswordConfirmation('');
fetchProfile();
setMessage({
type: 'success',
message: 'Password updated successfully',
});
})
.catch((err) => {
setIsLoading(false);
setMessage({
type: 'error',
message: err.message,
});
});
};
const fetchProfile = useCallback(async () => {
// Set the loading state
setIsLoading(true);
// Create headers with the cookie
const headers = new Headers();
headers.append('Content-Type', 'application/json');
headers.append(
'Cookie',
`${TOKEN_COOKIE_NAME}=${Cookies.get(TOKEN_COOKIE_NAME)}`
);
try {
const res = await fetch('http://localhost:8080/v1-me', {
method: 'POST',
credentials: 'include',
headers,
});
const json = await res.json();
if (json.status === 401) {
// If the user is not authenticated, redirect to the login page
// Clear the cookie
Cookies.remove(TOKEN_COOKIE_NAME);
window.location.href = '/login';
}
if (res.ok) {
setAuthProvider(json.authProvider);
} else {
throw new Error(json.message);
}
} catch (error: any) {
setMessage({
type: 'error',
message: error?.message || 'Something went wrong',
});
}
setIsLoading(false);
}, []);
// Make a request to the backend to fill in the form with the current values
useEffect(() => {
fetchProfile();
}, []);
return (
<form onSubmit={handleSubmit}>
<h2 className="text-3xl font-bold sm:text-4xl">Password</h2>
<p className="mt-2">Manage settings for your account passwords</p>
<div className="mt-8 space-y-4">
{authProvider === 'email' && (
<div className="flex w-full flex-col">
<label
for="current-password"
className="text-sm leading-none text-slate-500"
>
Current Password
</label>
<input
type="password"
name="current-password"
id="current-password"
className="mt-2 block w-full appearance-none rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none transition duration-150 ease-in-out placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1"
required
minLength={6}
placeholder="Current password"
value={currentPassword}
onInput={(e) =>
setCurrentPassword((e.target as HTMLInputElement).value)
}
/>
</div>
)}
<div className="flex w-full flex-col">
<label
for="new-password"
className="text-sm leading-none text-slate-500"
>
New Password
</label>
<input
type="password"
name="new-password"
id="new-password"
className="mt-2 block w-full appearance-none rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none transition duration-150 ease-in-out placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1"
required
minLength={6}
placeholder="New password"
value={newPassword}
onInput={(e) =>
setNewPassword((e.target as HTMLInputElement).value)
}
/>
</div>
<div className="flex w-full flex-col">
<label
for="new-password-confirmation"
className="text-sm leading-none text-slate-500"
>
New Password Confirm
</label>
<input
type="password"
name="new-password-confirmation"
id="new-password-confirmation"
className="mt-2 block w-full appearance-none rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none transition duration-150 ease-in-out placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1"
required
minLength={6}
placeholder="New password confirm"
value={newPasswordConfirmation}
onInput={(e) =>
setNewPasswordConfirmation((e.target as HTMLInputElement).value)
}
/>
</div>
{message && (
<div
className={`mt-2 rounded-lg p-2 ${
message.type === 'error'
? 'bg-red-100 text-red-700'
: message.type === 'success'
? 'bg-green-100 text-green-700'
: 'bg-blue-100 text-blue-700'
}`}
>
{message.message}
</div>
)}
<button
className="!mt-5 inline-flex h-10 min-w-[120px] items-center justify-center rounded-lg border border-slate-300 bg-black p-2 px-4 text-sm font-medium text-white outline-none transition duration-150 ease-in-out focus:ring-2 focus:ring-black focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-60"
type="submit"
disabled={isLoading}
>
{isLoading ? <Spinner className="text-white" /> : 'Change'}
</button>
</div>
</form>
);
}

@ -16,14 +16,14 @@ export interface Props {
<ul class='space-y-1'>
<li>
<a
href='/settings/profile'
href='/settings/update-profile'
class=`block w-full rounded px-2 py-1.5 font-medium text-slate-900 hover:bg-slate-200 ${pageUrl === 'profile' ? 'bg-slate-100' : ''}`
>Profile</a
>
</li>
<li>
<a
href='/settings/change-password'
href='/settings/update-password'
class=`block w-full rounded px-2 py-1.5 font-medium text-slate-900 hover:bg-slate-200 ${pageUrl === 'change-password' ? 'bg-slate-100' : ''}`
>Change password</a
>
@ -46,14 +46,14 @@ export interface Props {
>
<li>
<a
href='/settings/profile'
href='/settings/update-profile'
class=`block w-full rounded px-2 py-1.5 font-medium text-slate-900 hover:bg-slate-200 ${pageUrl === 'profile' ? 'bg-slate-100' : ''}`
>Profile</a
>
</li>
<li>
<a
href='/settings/change-password'
href='/settings/update-password'
class=`block w-full rounded px-2 py-1.5 font-medium text-slate-900 hover:bg-slate-200 ${pageUrl === 'change-password' ? 'bg-slate-100' : ''}`
>Change password</a
>

@ -0,0 +1,181 @@
import { useCallback, useEffect, useState } from 'preact/hooks';
import Cookies from 'js-cookie';
import Spinner from '../Spinner';
import { TOKEN_COOKIE_NAME } from '../../lib/jwt';
import { httpGet, httpPost } from '../../lib/http';
export default function UpdatePasswordForm() {
const [authProvider, setAuthProvider] = useState('');
const [currentPassword, setCurrentPassword] = useState('');
const [newPassword, setNewPassword] = useState('');
const [newPasswordConfirmation, setNewPasswordConfirmation] = useState('');
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
const [isLoading, setIsLoading] = useState(false);
const handleSubmit = async (e: Event) => {
e.preventDefault();
setIsLoading(true);
setError('');
setSuccess('');
if (newPassword !== newPasswordConfirmation) {
setError('Passwords do not match');
setIsLoading(false);
return;
}
const { response, error } = await httpPost(
`${import.meta.env.PUBLIC_API_URL}/v1-update-password`,
{
oldPassword: authProvider === 'email' ? currentPassword : 'social-auth',
password: newPassword,
confirmPassword: newPasswordConfirmation,
}
);
if (error) {
setError(error.message || 'Something went wrong');
setIsLoading(false);
return;
}
setError('');
setCurrentPassword('');
setNewPassword('');
setNewPasswordConfirmation('');
setSuccess('Password updated successfully');
setIsLoading(false);
};
const loadProfile = async () => {
setIsLoading(true);
const { error, response } = await httpGet(
`${import.meta.env.PUBLIC_API_URL}/v1-me`
);
if (error || !response) {
if (error?.status === 401) {
Cookies.remove(TOKEN_COOKIE_NAME);
window.location.reload();
return;
}
setIsLoading(false);
setError(error?.message || 'Something went wrong');
return;
}
const { authProvider } = response;
setAuthProvider(authProvider);
setIsLoading(false);
};
useEffect(() => {
loadProfile().finally(() => {
// Hide page loader
});
}, []);
return (
<form onSubmit={handleSubmit}>
<h2 className="text-3xl font-bold sm:text-4xl">Password</h2>
<p className="mt-2">Manage settings for your account passwords</p>
<div className="mt-8 space-y-4">
{authProvider === 'email' && (
<div className="flex w-full flex-col">
<label
for="current-password"
className="text-sm leading-none text-slate-500"
>
Current Password
</label>
<input
disabled={authProvider !== 'email'}
type="password"
name="current-password"
id="current-password"
className="mt-2 block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1 disabled:bg-gray-100"
required
minLength={6}
placeholder="Current password"
value={currentPassword}
onInput={(e) =>
setCurrentPassword((e.target as HTMLInputElement).value)
}
/>
</div>
)}
<div className="flex w-full flex-col">
<label
for="new-password"
className="text-sm leading-none text-slate-500"
>
New Password
</label>
<input
type="password"
name="new-password"
id="new-password"
className="mt-2 block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1"
required
minLength={6}
placeholder="New password"
value={newPassword}
onInput={(e) =>
setNewPassword((e.target as HTMLInputElement).value)
}
/>
</div>
<div className="flex w-full flex-col">
<label
for="new-password-confirmation"
className="text-sm leading-none text-slate-500"
>
New Password Confirm
</label>
<input
type="password"
name="new-password-confirmation"
id="new-password-confirmation"
className="mt-2 block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1"
required
minLength={6}
placeholder="New password confirm"
value={newPasswordConfirmation}
onInput={(e) =>
setNewPasswordConfirmation((e.target as HTMLInputElement).value)
}
/>
</div>
{error && (
<p class="mt-2 rounded-lg bg-red-100 p-2 text-red-700">{error}</p>
)}
{success && (
<p class="mt-2 rounded-lg bg-green-100 p-2 text-green-700">
{success}
</p>
)}
<button
type="submit"
disabled={isLoading}
className="inline-flex w-full items-center justify-center rounded-lg bg-black p-2 py-3 text-sm font-medium text-white outline-none focus:ring-2 focus:ring-black focus:ring-offset-1 disabled:bg-gray-400"
>
{isLoading ? 'Please wait...' : 'Update Password'}
</button>
</div>
</form>
);
}

@ -1,41 +0,0 @@
---
import Popup from './Popup/Popup.astro';
import CaptchaFields from './Captcha/CaptchaFields.astro';
---
<Popup id='subscribe-popup' title='Subscribe' subtitle='Enter your email below to receive updates.'>
<form
action='https://news.roadmap.sh/subscribe'
method='POST'
accept-charset='utf-8'
target='_blank'
captcha-form
>
<input type='hidden' name='gdpr' value='true' />
<input
type='email'
name='email'
required
autofocus
class='w-full rounded-md border text-md py-2.5 px-3 mb-2'
placeholder='Enter your Email'
/>
<CaptchaFields />
<input type='hidden' name='list' value='tTqz1w7nexY3cWDpLnI88Q' />
<input type='hidden' name='subform' value='yes' />
<button
type='submit'
name='submit'
class='text-white bg-gradient-to-r from-amber-700 to-blue-800 hover:from-amber-800 hover:to-blue-900 font-regular rounded-md text-md px-5 py-2.5 w-full text-center mr-2'
ga-category='Subscription'
ga-action='Submitted Popup Form'
ga-label='Subscribe Roadmap Popup'
>
Subscribe
</button>
</form>
</Popup>

@ -1,5 +1,5 @@
---
import ForgotPasswordForm from '../components/Profile/ForgotPasswordForm';
import { ForgotPasswordForm } from '../components/AuthenticationFlow/ForgotPasswordForm';
import SettingLayout from '../layouts/SettingLayout.astro';
---
@ -9,9 +9,12 @@ import SettingLayout from '../layouts/SettingLayout.astro';
class='mx-auto flex flex-col items-start justify-start pb-28 pt-10 sm:max-w-[400px] sm:items-center sm:justify-center sm:pt-20'
>
<div class='mb-2 text-left sm:mb-5 sm:text-center'>
<h1 class='mb-2 text-3xl font-semibold sm:mb-5 sm:text-5xl'>Forgot Password?</h1>
<h1 class='mb-2 text-3xl font-semibold sm:mb-5 sm:text-5xl'>
Forgot Password?
</h1>
<p class='mb-3 text-base leading-6 text-gray-600'>
Enter your email address below and we will send you a link to reset your password.
Enter your email address below and we will send you a link to reset
your password.
</p>
</div>

@ -1,11 +1,11 @@
---
import ChangePasswordForm from '../../components/Setting/ChangePasswordForm';
import UpdatePasswordForm from '../../components/Setting/UpdatePasswordForm';
import SettingSidebar from '../../components/Setting/SettingSidebar.astro';
import SettingLayout from '../../layouts/SettingLayout.astro';
---
<SettingLayout title='Change Password' description=''>
<SettingSidebar pageUrl='change-password' name='Change Password'>
<ChangePasswordForm client:load />
<UpdatePasswordForm client:load />
</SettingSidebar>
</SettingLayout>
Loading…
Cancel
Save