diff --git a/api/cache/read/useReadHasCurrentUserCache.ts b/api/cache/read/useReadHasCurrentUserCache.ts index 830c716e..2fe12999 100644 --- a/api/cache/read/useReadHasCurrentUserCache.ts +++ b/api/cache/read/useReadHasCurrentUserCache.ts @@ -1,6 +1,7 @@ import type { ApolloClient, InMemoryCache } from '@apollo/client'; -import HasCurrentUserFragment from 'graphql/fragments/currentUser/hasCurrentUser.graphql'; +// TODO: Maybe rename query? +import HasCurrentUserFragment from 'graphql/fragments/hasCurrentUser.graphql'; import type { Me } from 'api/types/user/user'; import { readCacheFragmentApi } from 'api/hooks/useReadCacheFragmentHook'; diff --git a/api/mutations/update/useUpdatePasswordMutation.ts b/api/mutations/update/useUpdatePasswordMutation.ts new file mode 100644 index 00000000..dc6bc472 --- /dev/null +++ b/api/mutations/update/useUpdatePasswordMutation.ts @@ -0,0 +1,26 @@ +import type { MutationHookOptions, MutationTuple, MutationResult } from '@apollo/client'; + +import UpdatePasswordMutation from 'graphql/mutations/updatePassword.graphql'; + +import type { UpdatePasswordVariables, UpdatePasswordData } from '../../types/user/updatePasswordApiType'; +import useMutation from '../../hooks/useMutationHook'; + +type UpdatePasswordResponseData = { + updatePassword: UpdatePasswordData; +}; + +type UpdatePasswordRequestVariables = { + input: UpdatePasswordVariables; +}; + +type UpdatePasswordMutationOptions = MutationHookOptions; + +type UpdatePasswordMutationTuple = MutationTuple; + +export type UpdatePasswordMutationResult = MutationResult; + +const useUpdatePasswordMutation = (options: UpdatePasswordMutationOptions): UpdatePasswordMutationTuple => { + return useMutation(UpdatePasswordMutation, options); +}; + +export default useUpdatePasswordMutation; diff --git a/api/types/user/updatePasswordApiType.ts b/api/types/user/updatePasswordApiType.ts new file mode 100644 index 00000000..61c91e74 --- /dev/null +++ b/api/types/user/updatePasswordApiType.ts @@ -0,0 +1,8 @@ +export type UpdatePasswordData = { + accessToken: string; +}; + +export type UpdatePasswordVariables = { + password: string; + resetToken: string | string[] | undefined; +}; diff --git a/components/pages/newPassword/NewPasswordPage.jsx b/components/pages/newPassword/NewPasswordPage.jsx new file mode 100644 index 00000000..a5e2926e --- /dev/null +++ b/components/pages/newPassword/NewPasswordPage.jsx @@ -0,0 +1,41 @@ +import React from 'react'; +import Router from 'next/router'; + +import { withApolloClient } from 'lib/withApolloClient'; +import WithAuth from 'lib/auth/withAuth'; + +import { HOME } from 'config/routes'; +import { NotifierProvider } from 'contexts/NotifierContext'; + +import DefaultTemplate from 'components/shared/templates/DefaultTemplate'; +import Notifier from 'components/shared/atoms/Notifier'; + +import NewPasswordForm from './components/NewPasswordForm'; + +import { PageContentWrapper } from './styled'; + +const NewPasswordPage = ({ query }) => { + return ( + + + + + + + + + ); +}; + +NewPasswordPage.getInitialProps = ({ res, accessTokenManager }) => { + if (accessTokenManager.accessToken) { + if (res) { + res.redirect(302, HOME); + } else { + Router.push(HOME); + } + } + return {}; +}; + +export default withApolloClient(WithAuth(NewPasswordPage)); diff --git a/components/pages/newPassword/components/NewPasswordForm/NewPasswordForm.tsx b/components/pages/newPassword/components/NewPasswordForm/NewPasswordForm.tsx new file mode 100644 index 00000000..ca8a1746 --- /dev/null +++ b/components/pages/newPassword/components/NewPasswordForm/NewPasswordForm.tsx @@ -0,0 +1,77 @@ +import React from 'react'; +import { Form, Formik, FormikProps } from 'formik'; +import * as Yup from 'yup'; +import { useRouter } from 'next/router'; + +import useUpdatePassword from 'lib/apollo/hooks/actions/useUpdatePassword'; + +import Button from 'components/shared/atoms/Button'; +import FormFieldInput from 'components/shared/atoms/FormField'; +import Loader from 'components/shared/atoms/Loader'; +import { FormFieldType } from 'types/formsType'; +import passwordRegExp from 'config/passwordRegExp'; + +import { FieldWrapper, FormContentWrapper, SubmitButtonWrapper } from './styled'; + +const SignInValidationSchema = Yup.object().shape({ + password: Yup.string() + .required('This field is required') + .trim() + .min(6, 'The minimum password length is 6 characters') + .matches(passwordRegExp, 'Password must contain upper and lower case characters and numbers'), + passwordConfirmation: Yup.string() + .oneOf([Yup.ref('password')], 'Password does not match') + .required('This field is required'), +}); + +type FormValues = { + password: string; + passwordConfirmation: string; +}; + +const initialValues: FormValues = { + password: '', + passwordConfirmation: '', +}; + +const SignInFormContent = ({ isSubmitting }: FormikProps) => ( + +
+ + + + + + + + + +
+
+); + +const NewPasswordForm = () => { + const { query } = useRouter(); + const { reset_token: resetToken } = query; + const [updatePassword, updatePasswordState] = useUpdatePassword(); + + const onSubmit = async ({ password }: { password: string }) => { + await updatePassword({ password, resetToken }); + }; + + return ( + <> + + {updatePasswordState.loading && Loading...} + + ); +}; + +export default NewPasswordForm; diff --git a/components/pages/newPassword/components/NewPasswordForm/index.ts b/components/pages/newPassword/components/NewPasswordForm/index.ts new file mode 100644 index 00000000..60ab94d1 --- /dev/null +++ b/components/pages/newPassword/components/NewPasswordForm/index.ts @@ -0,0 +1 @@ +export { default } from './NewPasswordForm'; diff --git a/components/pages/newPassword/components/NewPasswordForm/styled.ts b/components/pages/newPassword/components/NewPasswordForm/styled.ts new file mode 100644 index 00000000..4658fddd --- /dev/null +++ b/components/pages/newPassword/components/NewPasswordForm/styled.ts @@ -0,0 +1,13 @@ +import styled from 'styled-components'; + +export const FormContentWrapper = styled.div` + width: 40rem; +`; + +export const FieldWrapper = styled.div` + margin-top: 1rem; +`; + +export const SubmitButtonWrapper = styled.div` + margin-top: 2rem; +`; diff --git a/components/pages/newPassword/index.ts b/components/pages/newPassword/index.ts new file mode 100644 index 00000000..7529d38b --- /dev/null +++ b/components/pages/newPassword/index.ts @@ -0,0 +1 @@ +export { default } from './NewPasswordPage'; diff --git a/components/pages/newPassword/styled.ts b/components/pages/newPassword/styled.ts new file mode 100644 index 00000000..3a7d76a1 --- /dev/null +++ b/components/pages/newPassword/styled.ts @@ -0,0 +1,6 @@ +import styled from 'styled-components'; + +export const PageContentWrapper = styled.div` + display: flex; + justify-content: center; +`; diff --git a/components/pages/signUp/components/SignUpForm/SignUpForm.tsx b/components/pages/signUp/components/SignUpForm/SignUpForm.tsx index da3ad380..e660ad7b 100644 --- a/components/pages/signUp/components/SignUpForm/SignUpForm.tsx +++ b/components/pages/signUp/components/SignUpForm/SignUpForm.tsx @@ -8,11 +8,10 @@ import Button from 'components/shared/atoms/Button'; import FormFieldInput from 'components/shared/atoms/FormField'; import Loader from 'components/shared/atoms/Loader'; import { FormFieldType } from 'types/formsType'; +import passwordRegExp from 'config/passwordRegExp'; import { FieldWrapper, FormContentWrapper, SubmitButtonWrapper } from './styled'; -const passwordRegularExp = /^(?=.*[0-9])(?=.*[A-Z])(?=.*[a-z])([0-9A-Za-z#$@&!?.*^{}<>;,)(~'"=_%+-]+)$/; - const initialValues = { firstName: '', lastName: '', @@ -28,7 +27,7 @@ const SignUpValidationSchema = Yup.object().shape({ .required('This field is required') .trim() .min(6, 'The minimum password length is 6 characters') - .matches(passwordRegularExp, 'Password must contain upper and lower case characters and numbers'), + .matches(passwordRegExp, 'Password must contain upper and lower case characters and numbers'), }); type ValuesFromFormik = Parameters[0]>[0]; diff --git a/config/passwordRegExp.ts b/config/passwordRegExp.ts new file mode 100644 index 00000000..e67be637 --- /dev/null +++ b/config/passwordRegExp.ts @@ -0,0 +1 @@ +export default /^(?=.*[0-9])(?=.*[A-Z])(?=.*[a-z])([0-9A-Za-z#$@&!?.*^{}<>;,)(~'"=_%+-]+)$/; diff --git a/config/routes.ts b/config/routes.ts index c807fb2c..5aaf5bf3 100644 --- a/config/routes.ts +++ b/config/routes.ts @@ -6,3 +6,4 @@ export const STATIC_PAGE = '/static-page'; export const SIGNIN = '/signin'; export const SIGNUP = '/signup'; export const RECOVERY_PASSWORD = '/recovery-password'; +export const NEW_PASSWORD = '/new-password'; diff --git a/graphql/fragments/currentUser/hasCurrentUser.graphql b/graphql/fragments/hasCurrentUser.graphql similarity index 100% rename from graphql/fragments/currentUser/hasCurrentUser.graphql rename to graphql/fragments/hasCurrentUser.graphql diff --git a/graphql/mutations/updatePassword.graphql b/graphql/mutations/updatePassword.graphql new file mode 100644 index 00000000..4a5cd28b --- /dev/null +++ b/graphql/mutations/updatePassword.graphql @@ -0,0 +1,6 @@ +mutation UpdatePassword($input: UpdatePasswordInput!) { + updatePassword(input: $input) { + accessToken + refreshToken + } +} diff --git a/lib/apollo/hooks/actions/useUpdatePassword.ts b/lib/apollo/hooks/actions/useUpdatePassword.ts new file mode 100644 index 00000000..d0aecc5f --- /dev/null +++ b/lib/apollo/hooks/actions/useUpdatePassword.ts @@ -0,0 +1,36 @@ +import { useRouter } from 'next/router'; + +import { SIGNIN } from 'config/routes'; +import { useNotifier } from 'contexts/NotifierContext'; + +import type { UpdatePasswordVariables } from 'api/types/user/updatePasswordApiType'; +import type { UpdatePasswordMutationResult } from 'api/mutations/update/useUpdatePasswordMutation'; +import useUpdatePasswordMutation from 'api/mutations/update/useUpdatePasswordMutation'; + +const useUpdatePassword = (): [(variables: UpdatePasswordVariables) => Promise, UpdatePasswordMutationResult] => { + const { setError, setSuccess } = useNotifier(); + const router = useRouter(); + + const onCompleted = () => { + setSuccess('Пароль успешно изменен'); + setTimeout(() => router.push(SIGNIN), 1000); + }; + + const [mutation, mutationResult] = useUpdatePasswordMutation({ + onCompleted, + }); + + const mutate = async ({ password, resetToken }: UpdatePasswordVariables) => { + const updatePasswordInput = { password, resetToken }; + + try { + await mutation({ variables: { input: updatePasswordInput } }); + } catch (error) { + if (setError) setError(error); + } + }; + + return [mutate, mutationResult]; +}; + +export default useUpdatePassword; diff --git a/pages/new-password.ts b/pages/new-password.ts new file mode 100644 index 00000000..0693350b --- /dev/null +++ b/pages/new-password.ts @@ -0,0 +1 @@ +export { default } from 'components/pages/newPassword';