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>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; </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>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td>
    <details>
<summary><strong>reset.tsx</strong><dd><code>Add new reset password page
with form</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </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>&nbsp;
</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>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </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>&nbsp;
&nbsp; &nbsp; </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:
Hassan Ben Jobrane
2024-12-30 12:51:38 +01:00
committed by GitHub
parent 414896491f
commit c9dca09478
5 changed files with 174 additions and 22 deletions

View File

@@ -0,0 +1,5 @@
---
'@nhost/dashboard': minor
---
feat: add reset password form

View File

@@ -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"

View File

@@ -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>
);
};

View 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>
);
};

View File

@@ -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&apos;t have an account?{' '}
<NavLink href="/signup" color="white">
Sign Up