diff --git a/src/contexts/Auth/AuthState.jsx b/src/contexts/Auth/AuthState.jsx index c4bd1d7c0e04f0cbcfa78dd7dc44a7fa15014c26..74e01aeba11ec51449ca40fc8d49ab3084feac11 100755 --- a/src/contexts/Auth/AuthState.jsx +++ b/src/contexts/Auth/AuthState.jsx @@ -49,13 +49,19 @@ function AuthState({ children }) { return result; } + // ### REQUEST PASSWORD RESET + function requestPasswordReset(email) { + return api.post('/users/requestpasswordreset', { email }); + } + // ### RETURN return ( <AuthContext.Provider value={{ login, - logout, currentUser, + logout, + requestPasswordReset, USER_ACTIONS, dispatchCurrentUser }}> diff --git a/src/pages/User/ForgotPassword.jsx b/src/pages/User/ForgotPassword.jsx new file mode 100644 index 0000000000000000000000000000000000000000..09c7ec4243f1314e014982f6e896970ce4407c77 --- /dev/null +++ b/src/pages/User/ForgotPassword.jsx @@ -0,0 +1,73 @@ +import { zodResolver } from '@hookform/resolvers/zod'; +import React, { useRef, useState } from 'react'; +import { Helmet } from 'react-helmet-async'; +import { FormProvider, useForm } from 'react-hook-form'; +import { Link } from 'react-router-dom'; +import { z } from 'zod'; +import Input from '../../components/form/Input'; +import Submit from '../../components/form/Submit'; +import { useAuth } from '../../contexts/Auth/AuthState'; +import { mergeBackendValidation, setFlashMsg } from '../../utils/ErrorHandling'; +import Heading from '../../components/font/Heading'; + +function ForgotPassword() { + // ################################# + // VALIDATION SCHEMA + // ################################# + // TODO limit file size via .env + // TODO check for file types + const schema = z.object({ + email: z.string().email(), + }); + + // ################################# + // HOOKS + // ################################# + const methods = useForm({ + resolver: zodResolver(schema), + mode: 'onSubmit' + }); + + // ################################# + // FUNCTIONS + // ################################# + // ### IMPORT FUNCTION FROM AuthContext + const { requestPasswordReset } = useAuth(); + + // ### HANDLE SUBMITTING FORM + async function handleSendForm(inputs) { + try { + // send data to login function + const result = await requestPasswordReset(inputs.email); + // set flash message + setFlashMsg(result.data?.message); + } catch (error) { + // catch the error + mergeBackendValidation(error.response.status, error.response.data, methods.setError); + } + } + + // ################################# + // OUTPUT + // ################################# + return ( + <> + {/* render page title */} + <Helmet><title>[{import.meta.env.VITE_APP_NAME}] Password forgot</title></Helmet> + + <Heading level="1">request password reset</Heading> + <FormProvider {...methods} > + <form onSubmit={methods.handleSubmit(handleSendForm)} noValidate> + <Input name='email' type='mail' title='E-Mail' className='h-16' required={true} /> + <Submit value='request' /> + </form> + </FormProvider> + + <div className="my-4"> + <Link to="/login">Back to Login</Link> + </div> + </> + ); +} + +export default React.memo(ForgotPassword); \ No newline at end of file diff --git a/src/pages/User/ResetPassword.jsx b/src/pages/User/ResetPassword.jsx new file mode 100644 index 0000000000000000000000000000000000000000..e52c9f887c8c54083267e220ba69acbd4c71be24 --- /dev/null +++ b/src/pages/User/ResetPassword.jsx @@ -0,0 +1,102 @@ +import { zodResolver } from '@hookform/resolvers/zod'; +import React from 'react'; +import { FormProvider, useForm } from 'react-hook-form'; +import { Link, useNavigate, useParams } from 'react-router-dom'; +import { z } from 'zod'; +import Input from '../../components/form/Input'; +import Submit from '../../components/form/Submit'; +import api from '../../utils/AxiosConfig'; +import { mergeBackendValidation, setFlashMsg } from '../../utils/ErrorHandling'; +import { isStrongPassword } from 'validator'; +import { useAuth } from '../../contexts/Auth/AuthState'; +import { Helmet } from 'react-helmet-async'; +import Heading from '../../components/font/Heading'; + + +function ResetPasswordForm() { + // ################################# + // VALIDATION SCHEMA + // ################################# + const schema = z.object({ + password: z.string().refine((val) => val && isStrongPassword(val), { + message: 'This field must be min 6 characters long and contain uppercase, lowercase, number, specialchar.', + }), + passwordConfirm: z.string(), + }).refine((data) => data.password === data.passwordConfirm, { + message: "Passwords don't match", + path: ["passwordConfirm"], + }); + + // ################################# + // HOOKS + // ################################# + // ### CONNECT AUTH CONTEXT + const { logout } = useAuth(); + + const { token } = useParams(); + + // ### PREPARE FORM + const methods = useForm({ + resolver: zodResolver(schema), + mode: 'onBlur', + defaultValues: { + token: token, + } + }); + + // ### ENABLE Redirections + const redirect = useNavigate(); + + // ################################# + // FUNCTIONS + // ################################# + + + // ### HANDLE SUBMITTING FORM + async function handleSendForm(inputs) { + + // TRY UPDATE + try { + // prepare form data + inputs.token = token; + // send data + const result = await api.post(`/users/confirmpasswordreset`, inputs); + await logout(); + redirect('/login'); + // set flash message + setFlashMsg(result.data?.message); + } catch (error) { + // catch the error + console.error(error); + mergeBackendValidation(error.response.status, error.response.data, methods.setError); + } + } + + + // ################################# + // OUTPUT + // ################################# + return ( + <> + {/* render page title */} + <Helmet><title>[{import.meta.env.VITE_APP_NAME}] Reset Password</title></Helmet> + + <Heading level="1">password reset</Heading> + <FormProvider {...methods} > + <form onSubmit={methods.handleSubmit(handleSendForm)}> + <Input name='token' type='text' title='confirm token' className='h-16' /> + <Input name='password' type='password' title='new password' className='h-16' autoFocus={true} /> + <Input name='passwordConfirm' type='password' title='confirm password' className='h-16' /> + + <Submit value='Reset' /> + </form> + </FormProvider> + + <div className="mt-4"> + <Link to="/login">Login</Link> + </div> + </> + ); +} + +export default React.memo(ResetPasswordForm); \ No newline at end of file diff --git a/src/routes/Sitemap.jsx b/src/routes/Sitemap.jsx index c0163e9b3056928c4de2905e27c69893baaddd2d..359bd0483891c54722a64257142a7c8b4e7c59dc 100644 --- a/src/routes/Sitemap.jsx +++ b/src/routes/Sitemap.jsx @@ -29,8 +29,12 @@ export const sitemap = [{ }] }, { title: 'Others', element: <CleanLayout />, children: [ - // USER + // LOGIN { path: '/login', element: loadComponent('User/Login') }, + // FORGOT PASSWORD + { path: '/forgot_password', element: loadComponent('User/ForgotPassword') }, + // RESET PASSWORD + { path: '/reset_password/:token', element: loadComponent('User/ResetPassword') }, // ERROR { path: '*', element: loadComponent('Err404') } ]