From 75930d105eced9eb1e2d64773e428432a4f30249 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Sun, 3 Aug 2025 08:46:07 +0300 Subject: [PATCH] [frontend]: add reset password --- .../src/app/account/recovery/[token]/page.tsx | 20 +++ frontend/src/app/account/recovery/page.tsx | 20 +++ .../features/auth/forms/NewPasswordForm.tsx | 119 ++++++++++++++++++ .../features/auth/forms/ResetPasswordForm.tsx | 113 +++++++++++++++++ frontend/src/graphql/generated/output.ts | 76 +++++++++++ .../metations/auth/NewPassword.graphql | 3 + .../metations/auth/ResetPassword.graphql | 3 + .../src/schemas/auth/new-password.schema.ts | 12 ++ .../src/schemas/auth/reset-password.schema.ts | 7 ++ 9 files changed, 373 insertions(+) create mode 100644 frontend/src/app/account/recovery/[token]/page.tsx create mode 100644 frontend/src/app/account/recovery/page.tsx create mode 100644 frontend/src/components/features/auth/forms/NewPasswordForm.tsx create mode 100644 frontend/src/components/features/auth/forms/ResetPasswordForm.tsx create mode 100644 frontend/src/graphql/metations/auth/NewPassword.graphql create mode 100644 frontend/src/graphql/metations/auth/ResetPassword.graphql create mode 100644 frontend/src/schemas/auth/new-password.schema.ts create mode 100644 frontend/src/schemas/auth/reset-password.schema.ts diff --git a/frontend/src/app/account/recovery/[token]/page.tsx b/frontend/src/app/account/recovery/[token]/page.tsx new file mode 100644 index 0000000..810f5f3 --- /dev/null +++ b/frontend/src/app/account/recovery/[token]/page.tsx @@ -0,0 +1,20 @@ +import { NewPasswordForm } from '@/components/features/auth/forms/NewPasswordForm'; +import { Metadata } from 'next'; +import { getTranslations } from 'next-intl/server'; +import React from 'react'; + +export async function generateMetadata(): Promise { + const t = await getTranslations('auth.newPassword'); + + return { + title: t('heading') + } +} + +const NewPasswordPage = () => { + return ( + + ); +}; + +export default NewPasswordPage; diff --git a/frontend/src/app/account/recovery/page.tsx b/frontend/src/app/account/recovery/page.tsx new file mode 100644 index 0000000..58fa8b0 --- /dev/null +++ b/frontend/src/app/account/recovery/page.tsx @@ -0,0 +1,20 @@ +import { ResetPasswordForm } from '@/components/features/auth/forms/ResetPasswordForm'; +import { Metadata } from 'next'; +import { getTranslations } from 'next-intl/server'; +import React from 'react'; + +export async function generateMetadata(): Promise { + const t = await getTranslations('auth.resetPassword'); + + return { + title: t('heading') + } +} + +const ResetPasswordPage = () => { + return ( + + ); +}; + +export default ResetPasswordPage; diff --git a/frontend/src/components/features/auth/forms/NewPasswordForm.tsx b/frontend/src/components/features/auth/forms/NewPasswordForm.tsx new file mode 100644 index 0000000..b1e40b4 --- /dev/null +++ b/frontend/src/components/features/auth/forms/NewPasswordForm.tsx @@ -0,0 +1,119 @@ +'use client' + +import { zodResolver } from '@hookform/resolvers/zod' +import { useTranslations } from 'next-intl' +import { useParams, useRouter } from 'next/navigation' +import { useForm } from 'react-hook-form' +import { toast } from 'sonner' + +import { Button } from '@/components/ui/common/Button' +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel +} from '@/components/ui/common/Form' +import { Input } from '@/components/ui/common/Input' + +import { useNewPasswordMutation } from '@/graphql/generated/output' + + +import AuthWrapper from '../AuthWrapper' +import { newPasswordSchema, TypeNewPasswordSchema } from '@/schemas/auth/new-password.schema' + +export function NewPasswordForm() { + const t = useTranslations('auth.newPassword') + + const router = useRouter() + const params = useParams<{ token: string }>() + + const form = useForm({ + resolver: zodResolver(newPasswordSchema), + defaultValues: { + password: '', + passwordRepeat: '' + } + }) + + const [newPassword, { loading: isLoadingNew }] = useNewPasswordMutation({ + onCompleted(data) { + toast.success(t('successMessage')) + router.push('/account/login') + }, + onError() { + toast.error(t('errorMessage')) + } + }) + + const { isValid } = form.formState + + function onSubmit(data: TypeNewPasswordSchema) { + newPassword({ variables: { data: { ...data, token: params.token } } }) + } + + return ( + +
+ + ( + + {t('passwordLabel')} + + + + + {t('passwordDescription')} + + + )} + /> + ( + + + {t('passwordRepeatLabel')} + + + + + + {t('passwordRepeatDescription')} + + + )} + /> + + + +
+ ) +} diff --git a/frontend/src/components/features/auth/forms/ResetPasswordForm.tsx b/frontend/src/components/features/auth/forms/ResetPasswordForm.tsx new file mode 100644 index 0000000..f5b6acb --- /dev/null +++ b/frontend/src/components/features/auth/forms/ResetPasswordForm.tsx @@ -0,0 +1,113 @@ +'use client' + +import { zodResolver } from '@hookform/resolvers/zod' +import { CircleCheck } from 'lucide-react' +import { useTranslations } from 'next-intl' +import { useState } from 'react' +import { useForm } from 'react-hook-form' +import { toast } from 'sonner' + +import { + Alert, + AlertDescription, + AlertTitle +} from '@/components/ui/common/Alert' +import { Button } from '@/components/ui/common/Button' +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel +} from '@/components/ui/common/Form' +import { Input } from '@/components/ui/common/Input' + +import { useResetPasswordMutation } from '@/graphql/generated/output' + +import { + type TypeResetPasswordSchema, + resetPasswordSchema +} from '@/schemas/auth/reset-password.schema' + +import AuthWrapper from '../AuthWrapper' + +export function ResetPasswordForm() { + const t = useTranslations('auth.resetPassword') + + const [isSuccess, setIsSuccess] = useState(false) + + const form = useForm({ + resolver: zodResolver(resetPasswordSchema), + defaultValues: { + email: '' + } + }) + + const [resetPassword, { loading: isLoadingReset }] = + useResetPasswordMutation({ + onCompleted() { + setIsSuccess(true) + }, + onError() { + toast.error(t('errorMessage')) + } + }) + + const { isValid } = form.formState + + function onSubmit(data: TypeResetPasswordSchema) { + void resetPassword({ variables: { data } }) + } + + return ( + + {isSuccess ? ( + + + {t('successAlertTitle')} + + {t('successAlertDescription')} + + + ) : ( +
+ + ( + + {t('emailLabel')} + + + + + {t('emailDescription')} + + + )} + /> + + + + )} +
+ ) +} diff --git a/frontend/src/graphql/generated/output.ts b/frontend/src/graphql/generated/output.ts index 23bee96..08e6a52 100644 --- a/frontend/src/graphql/generated/output.ts +++ b/frontend/src/graphql/generated/output.ts @@ -605,6 +605,20 @@ export type LoginUserMutationVariables = Exact<{ export type LoginUserMutation = { __typename?: 'Mutation', loginUser: { __typename?: 'AuthModel', message?: string | null, user?: { __typename?: 'UserModel', id: string, name: string, email: string } | null } }; +export type NewPasswordMutationVariables = Exact<{ + data: NewPasswordInput; +}>; + + +export type NewPasswordMutation = { __typename?: 'Mutation', setNewPassword: boolean }; + +export type ResetPasswordMutationVariables = Exact<{ + data: ResetPasswordInput; +}>; + + +export type ResetPasswordMutation = { __typename?: 'Mutation', resetPassword: boolean }; + export type VerifyAccountMutationVariables = Exact<{ data: VerificationInput; }>; @@ -693,6 +707,68 @@ export function useLoginUserMutation(baseOptions?: Apollo.MutationHookOptions; export type LoginUserMutationResult = Apollo.MutationResult; export type LoginUserMutationOptions = Apollo.BaseMutationOptions; +export const NewPasswordDocument = gql` + mutation NewPassword($data: NewPasswordInput!) { + setNewPassword(data: $data) +} + `; +export type NewPasswordMutationFn = Apollo.MutationFunction; + +/** + * __useNewPasswordMutation__ + * + * To run a mutation, you first call `useNewPasswordMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useNewPasswordMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [newPasswordMutation, { data, loading, error }] = useNewPasswordMutation({ + * variables: { + * data: // value for 'data' + * }, + * }); + */ +export function useNewPasswordMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(NewPasswordDocument, options); + } +export type NewPasswordMutationHookResult = ReturnType; +export type NewPasswordMutationResult = Apollo.MutationResult; +export type NewPasswordMutationOptions = Apollo.BaseMutationOptions; +export const ResetPasswordDocument = gql` + mutation ResetPassword($data: ResetPasswordInput!) { + resetPassword(data: $data) +} + `; +export type ResetPasswordMutationFn = Apollo.MutationFunction; + +/** + * __useResetPasswordMutation__ + * + * To run a mutation, you first call `useResetPasswordMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useResetPasswordMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [resetPasswordMutation, { data, loading, error }] = useResetPasswordMutation({ + * variables: { + * data: // value for 'data' + * }, + * }); + */ +export function useResetPasswordMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(ResetPasswordDocument, options); + } +export type ResetPasswordMutationHookResult = ReturnType; +export type ResetPasswordMutationResult = Apollo.MutationResult; +export type ResetPasswordMutationOptions = Apollo.BaseMutationOptions; export const VerifyAccountDocument = gql` mutation VerifyAccount($data: VerificationInput!) { verifyAccount(data: $data) { diff --git a/frontend/src/graphql/metations/auth/NewPassword.graphql b/frontend/src/graphql/metations/auth/NewPassword.graphql new file mode 100644 index 0000000..7728dec --- /dev/null +++ b/frontend/src/graphql/metations/auth/NewPassword.graphql @@ -0,0 +1,3 @@ +mutation NewPassword($data: NewPasswordInput!) { + setNewPassword(data: $data) +} diff --git a/frontend/src/graphql/metations/auth/ResetPassword.graphql b/frontend/src/graphql/metations/auth/ResetPassword.graphql new file mode 100644 index 0000000..5311026 --- /dev/null +++ b/frontend/src/graphql/metations/auth/ResetPassword.graphql @@ -0,0 +1,3 @@ +mutation ResetPassword($data: ResetPasswordInput!) { + resetPassword(data: $data) +} diff --git a/frontend/src/schemas/auth/new-password.schema.ts b/frontend/src/schemas/auth/new-password.schema.ts new file mode 100644 index 0000000..e114a6a --- /dev/null +++ b/frontend/src/schemas/auth/new-password.schema.ts @@ -0,0 +1,12 @@ +import { z } from 'zod' + +export const newPasswordSchema = z + .object({ + password: z.string().min(8), + passwordRepeat: z.string().min(8) + }) + .refine(data => data.password === data.passwordRepeat, { + path: ['passwordRepeat'] + }) + +export type TypeNewPasswordSchema = z.infer diff --git a/frontend/src/schemas/auth/reset-password.schema.ts b/frontend/src/schemas/auth/reset-password.schema.ts new file mode 100644 index 0000000..b09d915 --- /dev/null +++ b/frontend/src/schemas/auth/reset-password.schema.ts @@ -0,0 +1,7 @@ +import { z } from 'zod' + +export const resetPasswordSchema = z.object({ + email: z.string().email() +}) + +export type TypeResetPasswordSchema = z.infer