From 1eb8a09a1561a4ff5144afbf521ab6ec8adde85c Mon Sep 17 00:00:00 2001
From: "Embruch, Gerd" <gerd.embruch@uni-hamburg.de>
Date: Tue, 2 Jul 2024 15:39:03 +0200
Subject: [PATCH] added change email

---
 README.md                            |  8 ++-
 README_tmp.html                      | 14 ++++-
 src/contexts/Auth/AuthState.jsx      |  6 ++
 src/pages/User/ChangeMail.jsx        | 88 ++++++++++++++++++++++++++++
 src/pages/User/ForgotPassword.jsx    |  7 ++-
 src/pages/User/Login.jsx             |  2 +-
 src/pages/User/RequestChangeMail.jsx | 73 +++++++++++++++++++++++
 src/pages/User/ResetPassword.jsx     |  8 +--
 src/routes/Sitemap.jsx               | 25 +++++++-
 src/utils/AxiosConfig.js             |  4 +-
 10 files changed, 218 insertions(+), 17 deletions(-)
 create mode 100644 src/pages/User/ChangeMail.jsx
 create mode 100644 src/pages/User/RequestChangeMail.jsx

diff --git a/README.md b/README.md
index 33d5cc0..badefbb 100644
--- a/README.md
+++ b/README.md
@@ -23,5 +23,9 @@ cp ./.env.template.local ./.env.production.local
 - [PM2](https://pm2.keymetrics.io/)
 
 # Roadmap
-- [ ] complete pages incl. password request and RAGChat
-- [ ] fix errors
\ No newline at end of file
+- [ ] complete pages
+  - [ ] sign up
+  - [ ] onboarding / RAGChat
+- [ ] fix errors
+  - [ ] fix JWT 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 b3a3df1..65000c3 100644
--- a/README_tmp.html
+++ b/README_tmp.html
@@ -389,8 +389,18 @@ cp ./.env.template.local ./.env.production.local
 </ul>
 <h1 id="roadmap">Roadmap</h1>
 <ul>
-<li><input type="checkbox" id="checkbox0"><label for="checkbox0">complete pages incl. password request and RAGChat</label></li>
-<li><input type="checkbox" id="checkbox1"><label for="checkbox1">fix errors</label></li>
+<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="checkbox2"><label for="checkbox2">onboarding / RAGChat</label></li>
+</ul>
+</li>
+<li><input type="checkbox" id="checkbox3"><label for="checkbox3">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 &amp; submit on cleanLayout</label></li>
+</ul>
+</li>
 </ul>
 
 </body>
diff --git a/src/contexts/Auth/AuthState.jsx b/src/contexts/Auth/AuthState.jsx
index 74e01ae..ec1df97 100755
--- a/src/contexts/Auth/AuthState.jsx
+++ b/src/contexts/Auth/AuthState.jsx
@@ -54,6 +54,11 @@ function AuthState({ children }) {
     return api.post('/users/requestpasswordreset', { email });
   }
 
+  // ### REQUEST PASSWORD RESET
+  function requestEmailReset(email) {
+    return api.post('/users/requestemailchange', { email });
+  }
+
   // ### RETURN
   return (
     <AuthContext.Provider
@@ -62,6 +67,7 @@ function AuthState({ children }) {
         currentUser,
         logout,
         requestPasswordReset,
+        requestEmailReset,
         USER_ACTIONS,
         dispatchCurrentUser
       }}>
diff --git a/src/pages/User/ChangeMail.jsx b/src/pages/User/ChangeMail.jsx
new file mode 100644
index 0000000..a546c8d
--- /dev/null
+++ b/src/pages/User/ChangeMail.jsx
@@ -0,0 +1,88 @@
+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 ForgotPassword() {
+  // #################################
+  // VALIDATION SCHEMA
+  // #################################
+  // TODO limit file size via .env
+  // TODO check for file types
+  const schema = z.object({
+    token: z.string().min(1),
+    password: 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/confirmemailchange`, inputs);
+      redirect('/change_email');
+      // 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}] change E-Mail</title></Helmet>
+
+      <Heading level="1">e-mail reset</Heading>
+      <FormProvider {...methods} >
+        <form onSubmit={methods.handleSubmit(handleSendForm)}>
+          <Input name='token' type='text' title='confirm token' required={true} />
+          <Input name='password' type='password' title='current password' autoFocus={true} required={true} />
+          <Submit value='submit' />
+        </form>
+      </FormProvider>
+
+      <div className="my-4 flex justify-between">
+        <Link to="/change_email">Back to request</Link>
+      </div>
+    </>
+  );
+}
+
+export default React.memo(ForgotPassword);
\ No newline at end of file
diff --git a/src/pages/User/ForgotPassword.jsx b/src/pages/User/ForgotPassword.jsx
index 09c7ec4..3d84cb4 100644
--- a/src/pages/User/ForgotPassword.jsx
+++ b/src/pages/User/ForgotPassword.jsx
@@ -1,7 +1,7 @@
 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 { FormProvider, useForm, useFormContext } from 'react-hook-form';
 import { Link } from 'react-router-dom';
 import { z } from 'zod';
 import Input from '../../components/form/Input';
@@ -57,14 +57,15 @@ function ForgotPassword() {
 
       <Heading level="1">request password reset</Heading>
       <FormProvider {...methods} >
-        <form onSubmit={methods.handleSubmit(handleSendForm)} noValidate>
+        <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">
+      <div className="my-4 flex justify-between">
         <Link to="/login">Back to Login</Link>
+        {methods.formState.isSubmitted && <Link to="/reset_password/-">Proceed</Link>}
       </div>
     </>
   );
diff --git a/src/pages/User/Login.jsx b/src/pages/User/Login.jsx
index bfefe51..cd81662 100644
--- a/src/pages/User/Login.jsx
+++ b/src/pages/User/Login.jsx
@@ -106,7 +106,7 @@ function Login() {
       </FormProvider>
 
       <div className="mt-4">
-        <Link to="/forgot_password">Forgot your Password?</Link>
+        <Link to="/reset_password">Forgot your Password?</Link>
       </div>
     </>
   );
diff --git a/src/pages/User/RequestChangeMail.jsx b/src/pages/User/RequestChangeMail.jsx
new file mode 100644
index 0000000..dfef9ef
--- /dev/null
+++ b/src/pages/User/RequestChangeMail.jsx
@@ -0,0 +1,73 @@
+import { zodResolver } from '@hookform/resolvers/zod';
+import React 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 RequestChangeMail() {
+  // #################################
+  // 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 { requestEmailReset } = useAuth();
+
+  // ### HANDLE SUBMITTING FORM
+  async function handleSendForm(inputs) {
+    try {
+      // send data to login function
+      const result = await requestEmailReset(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}] change E-Mail</title></Helmet>
+
+      <Heading level="1">request e-mail reset</Heading>
+      <FormProvider {...methods} >
+        <form onSubmit={methods.handleSubmit(handleSendForm)}>
+          <Input name='email' type='mail' title='new E-Mail address' required={true} />
+          <Submit value='request' />
+        </form>
+      </FormProvider>
+
+      <div className="my-4 flex justify-between">
+        <Link to="/change_email/-">I already recieved a token</Link>
+      </div>
+    </>
+  );
+}
+
+export default React.memo(RequestChangeMail);
\ No newline at end of file
diff --git a/src/pages/User/ResetPassword.jsx b/src/pages/User/ResetPassword.jsx
index e52c9f8..12fa32d 100644
--- a/src/pages/User/ResetPassword.jsx
+++ b/src/pages/User/ResetPassword.jsx
@@ -32,7 +32,7 @@ function ResetPasswordForm() {
   // #################################
   // ### CONNECT AUTH CONTEXT
   const { logout } = useAuth();
-
+  // FETCH TOKEN FROM URL
   const { token } = useParams();
 
   // ### PREPARE FORM
@@ -44,7 +44,7 @@ function ResetPasswordForm() {
     }
   });
 
-  // ### ENABLE Redirections
+  // ### ENABLE REDIRECTIONS
   const redirect = useNavigate();
 
   // #################################
@@ -57,8 +57,6 @@ function ResetPasswordForm() {
 
     // TRY UPDATE
     try {
-      // prepare form data
-      inputs.token = token;
       // send data
       const result = await api.post(`/users/confirmpasswordreset`, inputs);
       await logout();
@@ -93,7 +91,7 @@ function ResetPasswordForm() {
       </FormProvider>
 
       <div className="mt-4">
-        <Link to="/login">Login</Link>
+        <Link to="/login">Back to Login</Link>
       </div>
     </>
   );
diff --git a/src/routes/Sitemap.jsx b/src/routes/Sitemap.jsx
index 359bd04..657f1dc 100644
--- a/src/routes/Sitemap.jsx
+++ b/src/routes/Sitemap.jsx
@@ -23,8 +23,29 @@ export const sitemap = [{
         element: loadComponent('Onboarding/Onboarding', true, true),
         handle: { crumb: () => <Link to="/onboarding">Onboarding</Link> }
       },
+      // REQUEST CHANGE EMAIL ADDRESS
+      {
+        title: 'Change eMail',
+        path: '/change_email',
+        handle: { crumb: () => <Link to="/change_email">Change eMail</Link> },
+        children: [
+          { index: true, element: loadComponent('User/RequestChangeMail') },
+          // CHANGE EMAIL ADDRESS
+          {
+            title: 'Change eMail',
+            path: ':token',
+            hidden: true,
+            element: loadComponent('User/ChangeMail')
+          }
+        ]
+      },
       // LOGOUT
-      { title: 'Logout', path: '/logout', element: loadComponent('User/Logout', true, true), handle: { crumb: () => <Link to="/profile/logout">Logout</Link> } }
+      {
+        title: 'Logout',
+        path: '/logout',
+        element: loadComponent('User/Logout', true, true),
+        handle: { crumb: () => <Link to="/profile/logout">Logout</Link> }
+      }
     ]
   }]
 }, {
@@ -32,7 +53,7 @@ export const sitemap = [{
     // LOGIN
     { path: '/login', element: loadComponent('User/Login') },
     // FORGOT PASSWORD
-    { path: '/forgot_password', element: loadComponent('User/ForgotPassword') },
+    { path: '/reset_password', element: loadComponent('User/ForgotPassword') },
     // RESET PASSWORD
     { path: '/reset_password/:token', element: loadComponent('User/ResetPassword') },
     // ERROR
diff --git a/src/utils/AxiosConfig.js b/src/utils/AxiosConfig.js
index 98abfd7..2310743 100755
--- a/src/utils/AxiosConfig.js
+++ b/src/utils/AxiosConfig.js
@@ -47,8 +47,8 @@ api.interceptors.response.use(
       // console.log('retry', err.code, originalConfig._retry);
       // refresh access token
       try {
-        const result = await api.post(
-          '/auth/token',
+        const result = await api.get(
+          '/users/refreshjwt',
           {},
           { withCredentials: true }
         );
-- 
GitLab