diff --git a/packages/web/app/src/pages/auth-reset-password.tsx b/packages/web/app/src/pages/auth-reset-password.tsx index cac83c5ca..91a69cc44 100644 --- a/packages/web/app/src/pages/auth-reset-password.tsx +++ b/packages/web/app/src/pages/auth-reset-password.tsx @@ -1,7 +1,10 @@ import { useCallback } from 'react'; import { useForm } from 'react-hook-form'; import { useSessionContext } from 'supertokens-auth-react/recipe/session'; -import { sendPasswordResetEmail } from 'supertokens-auth-react/recipe/thirdpartyemailpassword'; +import { + sendPasswordResetEmail, + submitNewPassword, +} from 'supertokens-auth-react/recipe/thirdpartyemailpassword'; import z from 'zod'; import { AuthCard, AuthCardContent, AuthCardHeader, AuthCardStack } from '@/components/auth'; import { Button } from '@/components/ui/button'; @@ -31,7 +34,7 @@ const ResetPasswordFormSchema = z.object({ type ResetPasswordFormValues = z.infer; -function AuthResetPassword(props: { email: string | null; redirectToPath: string }) { +function AuthResetPasswordEmail(props: { email: string | null; redirectToPath: string }) { const initialEmail = props.email ?? ''; const resetEmail = useMutation({ @@ -190,11 +193,163 @@ function AuthResetPassword(props: { email: string | null; redirectToPath: string ); } -export function AuthResetPasswordPage(props: { email: string | null; redirectToPath: string }) { +const NewPasswordFormSchema = z.object({ + newPassword: z.string().min(8, 'Password must be at least 8 characters'), +}); + +type NewPasswordFormValues = z.infer; + +function AuthPasswordNew(props: { token: string; redirectToPath: string }) { + const changePassword = useMutation({ + mutationFn: submitNewPassword, + onSuccess(data) { + const status = data.status; + + switch (status) { + case 'OK': { + toast({ + title: 'Password changed', + description: 'You can now sign in with your new password.', + }); + break; + } + case 'FIELD_ERROR': { + for (const field of data.formFields) { + if (field.id === 'password') { + form.setError('newPassword', { + type: 'manual', + message: field.error, + }); + } else { + toast({ + title: 'Field error', + description: field.error, + variant: 'destructive', + }); + } + } + break; + } + case 'RESET_PASSWORD_INVALID_TOKEN_ERROR': { + toast({ + title: 'Link expired', + description: 'Please request a new password reset link.', + variant: 'destructive', + }); + break; + } + default: { + exhaustiveGuard(status); + } + } + }, + onError(error) { + console.error(error); + toast({ + title: 'An error occurred', + description: error.message, + variant: 'destructive', + }); + }, + }); + const form = useForm({ + mode: 'onSubmit', + resolver: zodResolver(NewPasswordFormSchema), + defaultValues: { + newPassword: '', + }, + disabled: changePassword.isPending, + }); + const { toast } = useToast(); + + const onSubmit = useCallback( + (data: NewPasswordFormValues) => { + console.log('onSubmit'); + changePassword.reset(); + changePassword.mutate({ + formFields: [ + { + id: 'password', + value: data.newPassword, + }, + ], + }); + }, + [changePassword.mutate], + ); + + const session = useSessionContext(); + + if (session.loading) { + // AuthPage component already shows a loading state + return null; + } + + const isSent = changePassword.isSuccess && changePassword.data.status === 'OK'; + + if (isSent) { + return ; + } + + return ( + + + +
+ + ( + + New password + + + + + + )} + /> + + + + +
+ + Back to login + +
+
+
+ ); +} + +export function AuthResetPasswordPage(props: { + email: string | null; + token: string | null; + redirectToPath: string; +}) { return ( <> - + {props.token ? ( + + ) : ( + + )} ); } diff --git a/packages/web/app/src/router.tsx b/packages/web/app/src/router.tsx index d92f17f43..6ac3b31e5 100644 --- a/packages/web/app/src/router.tsx +++ b/packages/web/app/src/router.tsx @@ -190,17 +190,22 @@ const authIndexRoute = createRoute({ const AuthResetPasswordRouteSearch = AuthSharedSearch.extend({ email: z.string().optional(), + token: z.string().optional(), }); const authResetPasswordRoute = createRoute({ getParentRoute: () => authRoute, path: 'reset-password', - validateSearch(search) { - return AuthResetPasswordRouteSearch.parse(search); - }, + validateSearch: AuthResetPasswordRouteSearch.parse, component: function AuthResetPasswordRoute() { - const { email, redirectToPath } = authResetPasswordRoute.useSearch(); - return ; + const { email, token, redirectToPath } = authResetPasswordRoute.useSearch(); + return ( + + ); }, });