feat(dashboard): add change password form (#3089)
### **User description** resolves https://github.com/nhost/nhost/issues/3058 ___ ### **PR Type** Enhancement, Bug fix ___ ### **Description** - Renamed the existing reset password page to `NewPasswordPage` and updated its form handling. - Added a new `ResetPasswordPage` with a form to change the password, including validation and success/error handling. - Updated the "Forgot password?" link in the sign-in page to direct users to the new password page. ___ ### **Changes walkthrough** 📝 <table><thead><tr><th></th><th align="left">Relevant files</th></tr></thead><tbody><tr><td><strong>Enhancement</strong></td><td><table> <tr> <td> <details> <summary><strong>new.tsx</strong><dd><code>Rename and update reset password page</code> </dd></summary> <hr> dashboard/src/pages/password/new.tsx <li>Renamed <code>ResetPasswordPage</code> to <code>NewPasswordPage</code>.<br> <li> Updated form type from <code>ResetPasswordFormValues</code> to <br><code>NewPasswordFormValues</code>.<br> <li> Changed layout title to "Request Password Reset".<br> </details> </td> <td><a href="https://github.com/nhost/nhost/pull/3089/files#diff-153045bbcb44ce952fbd9ee585c63109891973ab4d1ecc1e1b5edf8f981b1259">+8/-6</a> </td> </tr> <tr> <td> <details> <summary><strong>reset.tsx</strong><dd><code>Add new reset password page with form</code> </dd></summary> <hr> dashboard/src/pages/password/reset.tsx <li>Introduced a new <code>ResetPasswordPage</code> component.<br> <li> Implemented form for changing password with validation.<br> <li> Added navigation to sign-in page upon successful password change.<br> </details> </td> <td><a href="https://github.com/nhost/nhost/pull/3089/files#diff-0b60d94b63e36ce54a4dafb098322e11b9a130defb0f48984f8b3e71461e8011">+144/-0</a> </td> </tr> </table></td></tr><tr><td><strong>Bug fix</strong></td><td><table> <tr> <td> <details> <summary><strong>email.tsx</strong><dd><code>Update forgot password link and formatting</code> </dd></summary> <hr> dashboard/src/pages/signin/email.tsx <li>Updated "Forgot password?" link to point to the new password page.<br> <li> Minor formatting adjustments in the component.<br> </details> </td> <td><a href="https://github.com/nhost/nhost/pull/3089/files#diff-b5d7db4460066bc114cb766771612d6f908bd6e440f40de98e4ac311a26b50cd">+4/-4</a> </td> </tr> </table></td></tr></tr></tbody></table> ___ > 💡 **PR-Agent usage**: Comment `/help "your question"` on any pull request to receive relevant information
This commit is contained in:
committed by
GitHub
parent
414896491f
commit
c9dca09478
5
.changeset/tidy-camels-complain.md
Normal file
5
.changeset/tidy-camels-complain.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@nhost/dashboard': minor
|
||||
---
|
||||
|
||||
feat: add reset password form
|
||||
@@ -21,23 +21,22 @@ export default function UnauthenticatedLayout({
|
||||
const router = useRouter();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { isAuthenticated, isLoading } = useAuthenticationStatus();
|
||||
const isOnResetPassword = router.route === '/password/reset';
|
||||
|
||||
useEffect(() => {
|
||||
if (!isPlatform || (!isLoading && isAuthenticated)) {
|
||||
router.push('/');
|
||||
// we do not want to redirect if the user tries to reset their password
|
||||
if (!isOnResetPassword) {
|
||||
router.push('/');
|
||||
}
|
||||
}
|
||||
}, [isLoading, isAuthenticated, router, isPlatform]);
|
||||
}, [isLoading, isAuthenticated, router, isPlatform, isOnResetPassword]);
|
||||
|
||||
if (!isPlatform || isLoading || isAuthenticated) {
|
||||
if ((!isPlatform || isLoading || isAuthenticated) && !isOnResetPassword) {
|
||||
return (
|
||||
<BaseLayout {...props}>
|
||||
<LoadingScreen
|
||||
sx={{ backgroundColor: (theme) => theme.palette.background.default }}
|
||||
slotProps={{
|
||||
activityIndicator: {
|
||||
className: 'text-white',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</BaseLayout>
|
||||
);
|
||||
@@ -59,19 +58,19 @@ export default function UnauthenticatedLayout({
|
||||
|
||||
<RetryableErrorBoundary>
|
||||
<Box
|
||||
className="flex items-center min-h-screen"
|
||||
className="flex min-h-screen items-center"
|
||||
sx={{ backgroundColor: (theme) => theme.palette.common.black }}
|
||||
>
|
||||
<Container
|
||||
rootClassName="bg-transparent h-full"
|
||||
className="grid items-center w-full h-full gap-12 pt-8 pb-12 bg-transparent justify-items-center lg:grid-cols-2 lg:gap-4 lg:pb-0 lg:pt-0"
|
||||
className="grid h-full w-full items-center justify-items-center gap-12 bg-transparent pb-12 pt-8 lg:grid-cols-2 lg:gap-4 lg:pb-0 lg:pt-0"
|
||||
>
|
||||
<div className="relative z-10 order-2 grid w-full max-w-[544px] grid-flow-row gap-12 lg:order-1">
|
||||
{children}
|
||||
</div>
|
||||
|
||||
<div className="relative z-0 order-1 flex h-full w-full items-center justify-center md:min-h-[150px] lg:order-2 lg:min-h-[none]">
|
||||
<div className="absolute top-0 bottom-0 left-0 right-0 flex items-center justify-center w-full h-full max-w-xl mx-auto overflow-hidden opacity-70">
|
||||
<div className="absolute bottom-0 left-0 right-0 top-0 mx-auto flex h-full w-full max-w-xl items-center justify-center overflow-hidden opacity-70">
|
||||
<Image
|
||||
priority
|
||||
src="/assets/line-grid.svg"
|
||||
|
||||
@@ -19,7 +19,7 @@ const validationSchema = Yup.object({
|
||||
email: Yup.string().label('Email').email().required(),
|
||||
});
|
||||
|
||||
export type ResetPasswordFormValues = Yup.InferType<typeof validationSchema>;
|
||||
export type NewPasswordFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
const StyledInput = styled(Input)({
|
||||
backgroundColor: 'transparent',
|
||||
@@ -28,10 +28,10 @@ const StyledInput = styled(Input)({
|
||||
},
|
||||
});
|
||||
|
||||
export default function ResetPasswordPage() {
|
||||
export default function NewPasswordPage() {
|
||||
const { resetPassword, error, isSent } = useResetPassword();
|
||||
|
||||
const form = useForm<ResetPasswordFormValues>({
|
||||
const form = useForm<NewPasswordFormValues>({
|
||||
reValidateMode: 'onSubmit',
|
||||
defaultValues: {
|
||||
email: '',
|
||||
@@ -52,9 +52,11 @@ export default function ResetPasswordPage() {
|
||||
);
|
||||
}, [error]);
|
||||
|
||||
async function handleSubmit({ email }: ResetPasswordFormValues) {
|
||||
async function handleSubmit({ email }: NewPasswordFormValues) {
|
||||
try {
|
||||
await resetPassword(email);
|
||||
await resetPassword(email, {
|
||||
redirectTo: '/password/reset',
|
||||
});
|
||||
} catch {
|
||||
toast.error(
|
||||
'An error occurred while signing up. Please try again.',
|
||||
@@ -124,8 +126,10 @@ export default function ResetPasswordPage() {
|
||||
);
|
||||
}
|
||||
|
||||
ResetPasswordPage.getLayout = function getLayout(page: ReactElement) {
|
||||
NewPasswordPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
<UnauthenticatedLayout title="Reset Password">{page}</UnauthenticatedLayout>
|
||||
<UnauthenticatedLayout title="Request Password Reset">
|
||||
{page}
|
||||
</UnauthenticatedLayout>
|
||||
);
|
||||
};
|
||||
144
dashboard/src/pages/password/reset.tsx
Normal file
144
dashboard/src/pages/password/reset.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
import { NavLink } from '@/components/common/NavLink';
|
||||
import { Form } from '@/components/form/Form';
|
||||
import { UnauthenticatedLayout } from '@/components/layout/UnauthenticatedLayout';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { Input, inputClasses } from '@/components/ui/v2/Input';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { getToastStyleProps } from '@/utils/constants/settings';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { styled } from '@mui/material';
|
||||
import { useChangePassword } from '@nhost/nextjs';
|
||||
import { useRouter } from 'next/router';
|
||||
import type { ReactElement } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const validationSchema = Yup.object({
|
||||
newPassword: Yup.string()
|
||||
.label('New Password')
|
||||
.required('New Password is required'),
|
||||
confirmNewPassword: Yup.string()
|
||||
.label('Confirm New Password')
|
||||
.required('Confirm New Password is required')
|
||||
.oneOf([Yup.ref('newPassword')], 'Passwords must match'),
|
||||
});
|
||||
|
||||
export type ResetPasswordFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
const StyledInput = styled(Input)({
|
||||
backgroundColor: 'transparent',
|
||||
[`& .${inputClasses.input}`]: {
|
||||
backgroundColor: 'transparent !important',
|
||||
},
|
||||
});
|
||||
|
||||
export default function ResetPasswordPage() {
|
||||
const router = useRouter();
|
||||
const { changePassword } = useChangePassword();
|
||||
|
||||
const form = useForm<ResetPasswordFormValues>({
|
||||
reValidateMode: 'onSubmit',
|
||||
defaultValues: {
|
||||
newPassword: '',
|
||||
confirmNewPassword: '',
|
||||
},
|
||||
resolver: yupResolver(validationSchema),
|
||||
});
|
||||
|
||||
const { register, formState } = form;
|
||||
|
||||
async function handleSubmit({ newPassword }: ResetPasswordFormValues) {
|
||||
try {
|
||||
const password = newPassword;
|
||||
|
||||
const { isError, error } = await changePassword(password);
|
||||
|
||||
if (isError) {
|
||||
toast.error(
|
||||
`An error occurred while changing your password: ${error.message}`,
|
||||
getToastStyleProps(),
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success('Password was updated successfully.');
|
||||
router.push('/');
|
||||
} catch {
|
||||
toast.error(
|
||||
'An error occurred while updating your password. Please try again.',
|
||||
getToastStyleProps(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Text
|
||||
variant="h2"
|
||||
component="h1"
|
||||
className="text-center text-3.5xl font-semibold lg:text-4.5xl"
|
||||
>
|
||||
Change password
|
||||
</Text>
|
||||
|
||||
<Box className="grid grid-flow-row gap-4 rounded-md border bg-transparent p-6 lg:p-12">
|
||||
<FormProvider {...form}>
|
||||
<Form
|
||||
onSubmit={handleSubmit}
|
||||
className="grid grid-flow-row gap-4 bg-transparent"
|
||||
>
|
||||
<StyledInput
|
||||
{...register('newPassword')}
|
||||
type="password"
|
||||
id="newPassword"
|
||||
label="New Password"
|
||||
fullWidth
|
||||
inputProps={{ min: 2, max: 128 }}
|
||||
error={!!formState.errors.newPassword}
|
||||
helperText={formState.errors.newPassword?.message}
|
||||
/>
|
||||
|
||||
<StyledInput
|
||||
{...register('confirmNewPassword')}
|
||||
type="password"
|
||||
id="confirmNewPassword"
|
||||
label="Confirm New Password"
|
||||
fullWidth
|
||||
inputProps={{ min: 2, max: 128 }}
|
||||
error={!!formState.errors.confirmNewPassword}
|
||||
helperText={formState.errors.confirmNewPassword?.message}
|
||||
/>
|
||||
|
||||
<Button
|
||||
className="!bg-white !text-black disabled:!text-black disabled:!text-opacity-60"
|
||||
size="large"
|
||||
type="submit"
|
||||
disabled={formState.isSubmitting}
|
||||
loading={formState.isSubmitting}
|
||||
>
|
||||
Change password
|
||||
</Button>
|
||||
</Form>
|
||||
</FormProvider>
|
||||
</Box>
|
||||
|
||||
<Text color="secondary" className="text-center text-base lg:text-lg">
|
||||
Go back to{' '}
|
||||
<NavLink href="/signin/email" color="white" className="font-medium">
|
||||
Sign In
|
||||
</NavLink>
|
||||
</Text>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
ResetPasswordPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
<UnauthenticatedLayout title="Request Password Reset">
|
||||
{page}
|
||||
</UnauthenticatedLayout>
|
||||
);
|
||||
};
|
||||
@@ -85,7 +85,7 @@ export default function EmailSignUpPage() {
|
||||
Sign In
|
||||
</Text>
|
||||
|
||||
<Box className="grid grid-flow-row gap-4 p-6 bg-transparent border rounded-md lg:p-12">
|
||||
<Box className="grid grid-flow-row gap-4 rounded-md border bg-transparent p-6 lg:p-12">
|
||||
<FormProvider {...form}>
|
||||
<Form
|
||||
onSubmit={handleSubmit}
|
||||
@@ -123,9 +123,9 @@ export default function EmailSignUpPage() {
|
||||
/>
|
||||
|
||||
<NavLink
|
||||
href="/reset-password"
|
||||
href="/password/new"
|
||||
color="white"
|
||||
className="font-semibold justify-self-start"
|
||||
className="justify-self-start font-semibold"
|
||||
>
|
||||
Forgot password?
|
||||
</NavLink>
|
||||
@@ -150,7 +150,7 @@ export default function EmailSignUpPage() {
|
||||
</FormProvider>
|
||||
</Box>
|
||||
|
||||
<Text color="secondary" className="text-base text-center lg:text-lg">
|
||||
<Text color="secondary" className="text-center text-base lg:text-lg">
|
||||
Don't have an account?{' '}
|
||||
<NavLink href="/signup" color="white">
|
||||
Sign Up
|
||||
|
||||
Reference in New Issue
Block a user