diff --git a/README.md b/README.md index badefbbe04d6853433596e9576e7c0c95488f74a..6f186f24bd9e9d301942082c0dcc1bba04d7fa98 100644 --- a/README.md +++ b/README.md @@ -24,8 +24,10 @@ cp ./.env.template.local ./.env.production.local # Roadmap - [ ] complete pages - - [ ] sign up + - [ ] resend verification code - [ ] onboarding / RAGChat + - [ ] admin-login + - [ ] admin-page with LLM options - [ ] fix errors - - [ ] fix JWT renewal + - [ ] fix axios' JWT auto renewal - [ ] check width of label & submit on cleanLayout \ No newline at end of file diff --git a/README_tmp.html b/README_tmp.html index 65000c308a3357bf3a9e339b03685235493940f7..dca7ce2f8632d1df4cb41ace219df8acd580d714 100644 --- a/README_tmp.html +++ b/README_tmp.html @@ -391,14 +391,16 @@ cp ./.env.template.local ./.env.production.local <ul> <li><input type="checkbox" id="checkbox0"><label for="checkbox0">complete pages</label> <ul> -<li><input type="checkbox" id="checkbox1"><label for="checkbox1">sign up</label></li> +<li><input type="checkbox" id="checkbox1"><label for="checkbox1">resend verification code</label></li> <li><input type="checkbox" id="checkbox2"><label for="checkbox2">onboarding / RAGChat</label></li> +<li><input type="checkbox" id="checkbox3"><label for="checkbox3">admin-login</label></li> +<li><input type="checkbox" id="checkbox4"><label for="checkbox4">admin-page with LLM options</label></li> </ul> </li> -<li><input type="checkbox" id="checkbox3"><label for="checkbox3">fix errors</label> +<li><input type="checkbox" id="checkbox5"><label for="checkbox5">fix errors</label> <ul> -<li><input type="checkbox" id="checkbox4"><label for="checkbox4">fix JWT renewal</label></li> -<li><input type="checkbox" id="checkbox5"><label for="checkbox5">check width of label & submit on cleanLayout</label></li> +<li><input type="checkbox" id="checkbox6"><label for="checkbox6">fix axios' JWT auto renewal</label></li> +<li><input type="checkbox" id="checkbox7"><label for="checkbox7">check width of label & submit on cleanLayout</label></li> </ul> </li> </ul> diff --git a/src/components/form/FlatListEdit copy.jsx b/src/components/form/FlatListEdit copy.jsx index f2e35c02f6f9a75dd9927d14a1337c5163367124..e3bbe1522bcf2c2efab7768b2b406a1f79004095 100644 --- a/src/components/form/FlatListEdit copy.jsx +++ b/src/components/form/FlatListEdit copy.jsx @@ -60,7 +60,6 @@ function FlatListEdit({ methods, initialItems, fieldName, keyName, validator, ta const isMail = validator(input); return isMail.success ?? keyName; }); - console.log('validInputs', validInputs); // handle invalid inputs // strip off valid entries from input to get remaining invalid inputs diff --git a/src/components/form/FlatListEdit.jsx b/src/components/form/FlatListEdit.jsx index 54a954c269c1c1d6d0a552230d5197bb52a4abcc..bce29cd7b312f9c1a1cbd7bacaeaa6b8067ee90a 100644 --- a/src/components/form/FlatListEdit.jsx +++ b/src/components/form/FlatListEdit.jsx @@ -49,7 +49,6 @@ function FlatListEdit({ initialItems = [], fieldName = '', tooltip = '', columns }); // exit if no valid items found if (validInputs.length === 0) return; - console.log('validInputs', validInputs); // handle invalid inputs // strip off valid entries from input to get remaining invalid inputs @@ -60,7 +59,6 @@ function FlatListEdit({ initialItems = [], fieldName = '', tooltip = '', columns // set error message on input methods.setError('addItem', { message: 'invalid entries remaining' }); } - console.log('invalidInputs', invalidInputs); // handle valid inputs diff --git a/src/contexts/Auth/AuthState.jsx b/src/contexts/Auth/AuthState.jsx index ec1df97bc8e8cf7914356fdb940765ee6762806c..fd3694299bdb0b4a657fc9ec7f4637b1e1b14ea1 100755 --- a/src/contexts/Auth/AuthState.jsx +++ b/src/contexts/Auth/AuthState.jsx @@ -31,7 +31,6 @@ function AuthState({ children }) { credentials, { withCredentials: true } ); - console.log; // set current user to login and merge accessToken into currentUser dispatchCurrentUser({ type: USER_ACTIONS.SET, payload: { ...result.data.record } }); setAccessToken(result.data.token); diff --git a/src/pages/User/ChangeMail.jsx b/src/pages/User/ChangeMail.jsx index a546c8db81936c84bb6df7f4fe6c9badde840440..20cf1de688ceb90fc529c4ec2a08d6841cf937fc 100644 --- a/src/pages/User/ChangeMail.jsx +++ b/src/pages/User/ChangeMail.jsx @@ -56,7 +56,6 @@ function ForgotPassword() { setFlashMsg(result.data?.message); } catch (error) { // catch the error - console.error(error); mergeBackendValidation(error.response.status, error.response.data, methods.setError); } } diff --git a/src/pages/User/Login.jsx b/src/pages/User/Login.jsx index cd81662b9f1bade2067f3c94cb43a38ccbf2f1c9..d8d73a6ea4285808c4c2197677727ea5dba22016 100644 --- a/src/pages/User/Login.jsx +++ b/src/pages/User/Login.jsx @@ -82,7 +82,6 @@ function Login() { setFlashMsg(result.data?.message); } catch (err) { - console.log(err); // merge front & backend validation errors mergeBackendValidation(err.response.status, err.response.data, methods.setError); } @@ -105,8 +104,9 @@ function Login() { </form> </FormProvider> - <div className="mt-4"> - <Link to="/reset_password">Forgot your Password?</Link> + <div className="mt-4 flex justify-between"> + <Link to="/reset_password">Reset Password</Link> + <Link to="/signup">Create an account</Link> </div> </> ); diff --git a/src/pages/User/Logout.jsx b/src/pages/User/Logout.jsx index ba40eae6960cca52be5b3c31832393718363c0fc..92358a3e6c981fe8057ddbf85341f92d77a02cce 100644 --- a/src/pages/User/Logout.jsx +++ b/src/pages/User/Logout.jsx @@ -28,7 +28,6 @@ function Logout() { // set flash mmessagesg setFlashMsg(result.data?.message); } catch (err) { - console.error(err); mergeBackendValidation(500, err); } } diff --git a/src/pages/User/ResetPassword.jsx b/src/pages/User/ResetPassword.jsx index 12fa32d6f6762e3924259fdaf5ca7cd82f70d0bd..1950c9c7189f7ac947421156e54e837ef25150f4 100644 --- a/src/pages/User/ResetPassword.jsx +++ b/src/pages/User/ResetPassword.jsx @@ -65,7 +65,6 @@ function ResetPasswordForm() { setFlashMsg(result.data?.message); } catch (error) { // catch the error - console.error(error); mergeBackendValidation(error.response.status, error.response.data, methods.setError); } } diff --git a/src/pages/User/Signup.jsx b/src/pages/User/Signup.jsx new file mode 100644 index 0000000000000000000000000000000000000000..9823a68a1f1178bafaed90b710025b5a6a28c02d --- /dev/null +++ b/src/pages/User/Signup.jsx @@ -0,0 +1,103 @@ +import React, { useEffect } from 'react'; +import { Link, useLocation, useNavigate } from 'react-router-dom'; +import { useAuth } from '../../contexts/Auth/AuthState'; +import { FormProvider, useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from "zod"; +import { toast } from 'react-toastify'; +import { mergeBackendValidation, setFlashMsg } from '../../utils/ErrorHandling'; +import Input from '../../components/form/Input'; +import Submit from '../../components/form/Submit'; +import { Helmet } from 'react-helmet-async'; +import Heading from '../../components/font/Heading'; +import validator from 'validator'; +import api from '../../utils/AxiosConfig'; + +function Signup() { + + // ################################# + // VALIDATION SCHEMA + // ################################# + const schema = z.object({ + name: z.string().min(1), + username: z.string().min(1), + email: z.string().email(), + 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, { + message: "Passwords don't match", + path: ["passwordConfirm"], + }); + // ################################# + // HOOKS + // ################################# + // ### CONNECT AUTH CONTEXT + const { signup } = useAuth(); + // ### MAKE USE OF NAVIGATION + const redirect = useNavigate(); + + // ### MAKE USE OF location state to fetch former requested page + const { state } = useLocation(); + // ### PREPARE FORM + const methods = useForm({ + resolver: zodResolver(schema), + mode: 'onBlur', + defaultValues: { + name: '', + username: '', + email: '', + password: '', + passwordConfirm: '' + } + }); + + + // ################################# + // FUNCTIONS + // ################################# + // ### HANDLE SUBMITTING LOGIN FORM + async function handleSendForm(record) { + try { + // send data to API + const result = await api.post(`/users/signup`, record); + // if successfull redirect to login page + redirect(`/login`); + // FIX: flash message not dislayed + setFlashMsg(result.data?.message); + + } catch (err) { + // merge front & backend validation errors + mergeBackendValidation(err.response.status, err.response.data, methods.setError); + } + } + + // ################################# + // OUTPUT + // ################################# + return ( + <> + {/* render page title */} + <Helmet><title>[{import.meta.env.VITE_APP_NAME}]</title></Helmet> + + <Heading level="1">ZBH-Portal LogIn</Heading> + <FormProvider {...methods} > + <form onSubmit={methods.handleSubmit(handleSendForm)}> + <Input name='name' type='text' title='Name' className='h-16' autoFocus={true} /> + <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' /> + <Submit value='Signup' /> + </form> + </FormProvider> + + <div className="mt-4"> + <Link to="/login">Back to Login</Link> + </div> + </> + ); +} + +export default Signup; \ No newline at end of file diff --git a/src/pages/User/Verify.jsx b/src/pages/User/Verify.jsx new file mode 100644 index 0000000000000000000000000000000000000000..16de1398d7ca15508b7274e418f968f024af7b99 --- /dev/null +++ b/src/pages/User/Verify.jsx @@ -0,0 +1,85 @@ +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, useNavigate, useParams } 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'; +import api from '../../utils/AxiosConfig'; + +function Verify() { + // ################################# + // VALIDATION SCHEMA + // ################################# + // TODO limit file size via .env + // TODO check for file types + const schema = z.object({ + token: z.string().min(1), + }); + + // ################################# + // HOOKS + // ################################# + // FETCH TOKEN FROM URL + const { token } = useParams(); + + // SET FORM + const methods = useForm({ + resolver: zodResolver(schema), + mode: 'onSubmit', + defaultValues: { + token: token, + } + }); + + // ### ENABLE REDIRECTIONS + const redirect = useNavigate(); + + // ################################# + // FUNCTIONS + // ################################# + + // ### HANDLE SUBMITTING FORM + async function handleSendForm(inputs) { + + // TRY UPDATE + try { + // send data + const result = await api.post(`/users/confirmverification`, inputs); + redirect('/login'); + // 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}] change E-Mail</title></Helmet> + + <Heading level="1">Verify Account</Heading> + <FormProvider {...methods} > + <form onSubmit={methods.handleSubmit(handleSendForm)}> + <Input name='token' type='text' title='confirm token' required={true} /> + <Submit value='Verify' /> + </form> + </FormProvider> + + <div className="my-4 flex justify-between"> + <Link to="/login">Back to Login</Link> + </div> + </> + ); +} + +export default React.memo(Verify); \ No newline at end of file diff --git a/src/routes/Sitemap.jsx b/src/routes/Sitemap.jsx index 657f1dc59013f4dc9352703a514771996e95bf8f..0b7664b104e5268d7e4bbaf0f5433e408912d7b3 100644 --- a/src/routes/Sitemap.jsx +++ b/src/routes/Sitemap.jsx @@ -50,6 +50,13 @@ export const sitemap = [{ }] }, { title: 'Others', element: <CleanLayout />, children: [ + // SIGNUP + { + path: '/signup', children: [ + { index: true, element: loadComponent('User/Signup') }, + { path: ':token', element: loadComponent('User/Verify') } + ] + }, // LOGIN { path: '/login', element: loadComponent('User/Login') }, // FORGOT PASSWORD diff --git a/src/utils/AxiosConfig.js b/src/utils/AxiosConfig.js index 23107430504efa4f3a4104366d89b347e76970a7..7e9e6a7e011d239f87a4314a14b8367a82d7dcec 100755 --- a/src/utils/AxiosConfig.js +++ b/src/utils/AxiosConfig.js @@ -33,13 +33,13 @@ api.interceptors.response.use( return res; }, async (err) => { - // console.log(err); + // console.log('JWT error: ', 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)); + // console.log('originalConfig: ',originalConfig); + // console.log('originalConfig: ',JSON.stringify(originalConfig)); if (originalConfig && err?.response?.status === 403 && originalConfig._retry !== true) { // patch config to remember it's a retry originalConfig._retry = true;