diff --git a/src/contexts/Auth/AuthState.jsx b/src/contexts/Auth/AuthState.jsx index 3fa34b50f8597e53ea3d7dc44e82bbff7a54a409..1cdd6014fabca1a831f62d8543120d9c7d348daf 100755 --- a/src/contexts/Auth/AuthState.jsx +++ b/src/contexts/Auth/AuthState.jsx @@ -26,22 +26,11 @@ function AuthState({ children }) { // ### LOGIN async function login(credentials) { - let result = {}; - try { - result = await api.post( - '/users/login', - credentials, - { withCredentials: true } - ); - } catch (error) { - result = await api.post( - '/users/adminlogin', - credentials, - { withCredentials: true } - ); - // try to match output with normal user - result.data.record = { ...result.data.admin, isAdmin: true }; - } + const result = await api.post( + '/auth/login', + credentials, + { withCredentials: true } + ); // set current user to login and merge accessToken into currentUser dispatchCurrentUser({ type: USER_ACTIONS.SET, payload: { ...result.data.record } }); setAccessToken(result.data.token); @@ -59,6 +48,11 @@ function AuthState({ children }) { return result; } + // ### REQUEST RESEND VERIFICATION TOKEN + function requestVerificationToken(email) { + return api.post('/auth/verification', { email }); + } + // ### REQUEST PASSWORD RESET function requestPasswordReset(email) { return api.post('/users/requestpasswordreset', { email }); @@ -76,6 +70,7 @@ function AuthState({ children }) { login, currentUser, logout, + requestVerificationToken, requestPasswordReset, requestEmailReset, USER_ACTIONS, diff --git a/src/pages/User/ResendVerificationToken.jsx b/src/pages/User/ResendVerificationToken.jsx new file mode 100644 index 0000000000000000000000000000000000000000..b899c8eaf8c8d67b22a37bbcde0d43f591ffbfbb --- /dev/null +++ b/src/pages/User/ResendVerificationToken.jsx @@ -0,0 +1,71 @@ +import { zodResolver } from '@hookform/resolvers/zod'; +import React, { useRef, useState } from 'react'; +import { Helmet } from 'react-helmet-async'; +import { FormProvider, useForm, useFormContext } 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 + // ################################# + const schema = z.object({ + email: z.string().email(), + }); + + // ################################# + // HOOKS + // ################################# + const methods = useForm({ + resolver: zodResolver(schema), + mode: 'onSubmit' + }); + + // ################################# + // FUNCTIONS + // ################################# + // ### IMPORT FUNCTION FROM AuthContext + const { requestVerificationToken } = useAuth(); + + // ### HANDLE SUBMITTING FORM + async function handleSendForm(inputs) { + try { + // send data to function + const result = await requestVerificationToken(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">resend verification token</Heading> + <FormProvider {...methods} > + <form onSubmit={methods.handleSubmit(handleSendForm)}> + <Input name='email' type='mail' title='E-Mail' className='h-16' required={true} /> + <Submit value='request' /> + </form> + </FormProvider> + + <div className="my-4 flex justify-between"> + <Link to="/signup/insertToken">Back to Verification</Link> + </div> + </> + ); +} + +export default React.memo(ForgotPassword); \ No newline at end of file diff --git a/src/pages/User/Signup.jsx b/src/pages/User/Signup.jsx index d3eb5da3c879e7b26f774a3b33f07a61526999ce..988e11ee2c3a52988ce0d55965be3ee5288f97c5 100644 --- a/src/pages/User/Signup.jsx +++ b/src/pages/User/Signup.jsx @@ -25,10 +25,10 @@ function Signup() { password: z.string().refine((val) => val && validator.isStrongPassword(val), { message: 'This value must be min 6 characters long and contain uppercase, lowercase, number, specialchar.', }), - passwordConfirm: z.string(), - }).refine((data) => data.password === data.passwordConfirm, { + confirmPassword: z.string(), + }).refine((data) => data.password === data.confirmPassword, { message: "Passwords don't match", - path: ["passwordConfirm"], + path: ["confirmPassword"], }); // ################################# // HOOKS @@ -49,7 +49,7 @@ function Signup() { username: '', email: '', password: '', - passwordConfirm: '' + confirmPassword: '' } }); @@ -61,9 +61,9 @@ function Signup() { async function handleSendForm(record) { try { // send data to API - const result = await api.post(`/users/signup`, record); + const result = await api.post(`/users`, record); // if successfull redirect to login page - redirect(`/login`); + redirect(`/signup/insertToken`); // FIX: flash message not dislayed setFlashMsg(result.data?.message); @@ -88,13 +88,14 @@ function Signup() { <Input name='username' type='text' title='username' className='h-16' /> <Input name='email' type='mail' title='E-Mail' className='h-16' /> <Input name='password' type='password' title='password' className='h-16' /> - <Input name='passwordConfirm' type='password' title='confirm password' className='h-16' /> + <Input name='confirmPassword' type='password' title='confirm password' className='h-16' /> <Submit value='Signup' /> </form> </FormProvider> - <div className="mt-4"> + <div className="mt-4 flex justify-between"> <Link to="/login">Back to Login</Link> + <Link to="/signup/insertToken">verify Account</Link> </div> </> ); diff --git a/src/pages/User/Verify.jsx b/src/pages/User/Verify.jsx index 16de1398d7ca15508b7274e418f968f024af7b99..cd6f66071f4ef0a985b12b78dba8020d23647fe2 100644 --- a/src/pages/User/Verify.jsx +++ b/src/pages/User/Verify.jsx @@ -49,7 +49,7 @@ function Verify() { // TRY UPDATE try { // send data - const result = await api.post(`/users/confirmverification`, inputs); + const result = await api.patch(`/auth/verification`, inputs); redirect('/login'); // set flash message setFlashMsg(result.data?.message); @@ -77,6 +77,7 @@ function Verify() { <div className="my-4 flex justify-between"> <Link to="/login">Back to Login</Link> + <Link to="/signup/resend_token">Resend Token</Link> </div> </> ); diff --git a/src/routes/Sitemap.jsx b/src/routes/Sitemap.jsx index ddb8a2262b6213cf53a0aeec0430c33f9b6ebbcd..3b99f040621ec9f74750e30b43c2ead7a2dabb33 100644 --- a/src/routes/Sitemap.jsx +++ b/src/routes/Sitemap.jsx @@ -58,6 +58,7 @@ export const sitemap = [{ { path: '/signup', children: [ { index: true, element: loadComponent('User/Signup') }, + { path: 'resend_token', element: loadComponent('User/ResendVerificationToken') }, { path: ':token', element: loadComponent('User/Verify') } ] }, diff --git a/src/utils/AxiosConfig.js b/src/utils/AxiosConfig.js index a32332c37ae7dc569943fa64148851687d4b3f15..8478be0cb700cb507b6d87bd6f7ff13d28392dda 100755 --- a/src/utils/AxiosConfig.js +++ b/src/utils/AxiosConfig.js @@ -29,21 +29,40 @@ api.interceptors.request.use( // ### RESPONSE INTERCEPTOR // refreshes accessToken if needed api.interceptors.response.use( - async (res) => { - switch (res.config.url) { - case '/users/refreshjwt': - case '/users/logout': - case '/users/login': - return res; - default: - const refresh = await api.get( - '/users/refreshjwt', + (res) => { + return res; + }, + async (err) => { + // console.log(err); + // save original request config + const originalConfig = err.config; + // if access denied and not a retry already + // BUG: Infinit loop because _retry isn't set at runtime + // console.log(originalConfig); + // console.log(JSON.stringify(originalConfig)); + if (originalConfig && err?.response?.status === 403 && originalConfig._retry !== true) { + // patch config to remember it's a retry + originalConfig._retry = true; + console.log('trying to refresh the accessToken and rerun the request'); + // console.log('retry', err.code, originalConfig._retry); + // refresh access token + try { + const result = await api.get( + '/auth', {}, { withCredentials: true } ); - localStorage.setItem("accessToken", JSON.stringify(refresh.data.token)); - return res; + // TODO: don't store accessToken in localStorage, keep in memory only + localStorage.setItem("accessToken", JSON.stringify(result.data.accessToken)); + // run retry + return api(originalConfig); + + } catch (error) { + return Promise.reject(error); + } } + return Promise.reject(err); + } );