From 6f38e94fbafec314db9b3f8fc6d258ecc2fdd75b Mon Sep 17 00:00:00 2001 From: Galav Date: Sun, 23 Mar 2025 20:21:06 -0400 Subject: [PATCH 01/42] Added page for Forgot Password --- package-lock.json | 45 +++++++++++++++-- src/App.tsx | 2 + src/pages/Authentication/ForgotPassword.tsx | 55 +++++++++++++++++++++ 3 files changed, 97 insertions(+), 5 deletions(-) create mode 100644 src/pages/Authentication/ForgotPassword.tsx diff --git a/package-lock.json b/package-lock.json index 3ef4561e..ec5213eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "@types/react-router-dom": "^5.3.3", "axios": "^1.4.0", "bootstrap": "^5.3.3", - "chart.js": "^3.7.0", + "chart.js": "^4.1.1", "formik": "^2.2.9", "jquery": "^3.7.1", "jwt-decode": "^3.1.2", @@ -38,7 +38,7 @@ "react-redux": "^8.0.5", "react-router-dom": "^6.11.1", "react-scripts": "^5.0.1", - "recharts": "^2.12.3", + "recharts": "^2.0.0", "redux-persist": "^6.0.0", "sass": "^1.62.1", "save": "^2.9.0", @@ -3045,6 +3045,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", @@ -5760,9 +5766,16 @@ } }, "node_modules/chart.js": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.7.0.tgz", - "integrity": "sha512-31gVuqqKp3lDIFmzpKIrBeum4OpZsQjSIAqlOpgjosHDJZlULtvwLEZKtEhIAZc7JMPaHlYMys40Qy9Mf+1AAg==" + "version": "4.4.8", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.8.tgz", + "integrity": "sha512-IkGZlVpXP+83QpMm4uxEiGqSI7jFizwVtF3+n5Pc3k7sMO+tkd0qxh2OzLhenM0K80xtmAONWGBn082EiBQSDA==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } }, "node_modules/check-types": { "version": "11.2.3", @@ -16603,6 +16616,28 @@ "node": ">= 0.8" } }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/void-elements": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", diff --git a/src/App.tsx b/src/App.tsx index 27736ba3..5a248435 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,6 +3,7 @@ import { createBrowserRouter, Navigate, RouterProvider } from "react-router-dom" import AdministratorLayout from "./layout/Administrator"; import ManageUserTypes, { loader as loadUsers } from "./pages/Administrator/ManageUserTypes"; import Login from "./pages/Authentication/Login"; +import ForgotPassword from "./pages/Authentication/ForgotPassword"; import Logout from "./pages/Authentication/Logout"; import InstitutionEditor, { loadInstitution } from "./pages/Institutions/InstitutionEditor"; import Institutions, { loadInstitutions } from "./pages/Institutions/Institutions"; @@ -49,6 +50,7 @@ function App() { children: [ { index: true, element: } /> }, { path: "login", element: }, + { path: "forgot-password", element: }, { path: "logout", element: } /> }, // Add the ViewTeamGrades route { diff --git a/src/pages/Authentication/ForgotPassword.tsx b/src/pages/Authentication/ForgotPassword.tsx new file mode 100644 index 00000000..401e01fe --- /dev/null +++ b/src/pages/Authentication/ForgotPassword.tsx @@ -0,0 +1,55 @@ +import React, {useState} from "react"; +import {useNavigate} from "react-router-dom"; +import axios from "axios"; +import { alertActions } from "../../store/slices/alertSlice"; +import { useDispatch } from "react-redux"; + +const ForgotPassword = () =>{ + + const [email, setEmail] = useState(''); + const dispatch = useDispatch(); + + const handleSubmit = async (e: React.MouseEvent) => { + + try { + console.log(email); + await axios.post("http://localhost:3002/password_resets", { email }); + dispatch( + alertActions.showAlert({ + variant: "success", + message: `A link to reset your password has been sent to your e-mail address.`, + }) + ); + } catch (error) { + dispatch( + alertActions.showAlert({ + variant: "danger", + message: `No account is associated with the e-mail address: "${email}". Please try again.`, + }) + ); + } + }; + + + return ( +
+
+

Forgotten Your Password?

+
+
+ Enter the e-mail address associated with your account +
+ setEmail(e.target.value)} + required + style={{marginTop: '5px', marginBottom: '5px', height: '20px', border: '1px solid black'}} + /> +
+ +
+ ); + +}; + +export default ForgotPassword; \ No newline at end of file From c160dd82cfd0e62e482a5ef87ca4947b32712dd3 Mon Sep 17 00:00:00 2001 From: galav12 Date: Mon, 24 Mar 2025 04:59:09 -0400 Subject: [PATCH 02/42] Added Reset Password Page --- src/App.tsx | 2 + src/pages/Authentication/ForgotPassword.tsx | 24 +++-- src/pages/Authentication/ResetPassword.tsx | 100 ++++++++++++++++++++ 3 files changed, 116 insertions(+), 10 deletions(-) create mode 100644 src/pages/Authentication/ResetPassword.tsx diff --git a/src/App.tsx b/src/App.tsx index 5a248435..25f3c0e5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,6 +4,7 @@ import AdministratorLayout from "./layout/Administrator"; import ManageUserTypes, { loader as loadUsers } from "./pages/Administrator/ManageUserTypes"; import Login from "./pages/Authentication/Login"; import ForgotPassword from "./pages/Authentication/ForgotPassword"; +import ResetPassword from "./pages/Authentication/ResetPassword"; import Logout from "./pages/Authentication/Logout"; import InstitutionEditor, { loadInstitution } from "./pages/Institutions/InstitutionEditor"; import Institutions, { loadInstitutions } from "./pages/Institutions/Institutions"; @@ -51,6 +52,7 @@ function App() { { index: true, element: } /> }, { path: "login", element: }, { path: "forgot-password", element: }, + { path: "password_edit/check_reset_url", element: }, { path: "logout", element: } /> }, // Add the ViewTeamGrades route { diff --git a/src/pages/Authentication/ForgotPassword.tsx b/src/pages/Authentication/ForgotPassword.tsx index 401e01fe..3ffff8d7 100644 --- a/src/pages/Authentication/ForgotPassword.tsx +++ b/src/pages/Authentication/ForgotPassword.tsx @@ -1,6 +1,5 @@ import React, {useState} from "react"; -import {useNavigate} from "react-router-dom"; -import axios from "axios"; +import axios, { AxiosError } from 'axios'; import { alertActions } from "../../store/slices/alertSlice"; import { useDispatch } from "react-redux"; @@ -12,8 +11,8 @@ const ForgotPassword = () =>{ const handleSubmit = async (e: React.MouseEvent) => { try { - console.log(email); - await axios.post("http://localhost:3002/password_resets", { email }); + await axios.post("http://localhost:3002/api/v1/password_resets", { email }); + dispatch( alertActions.showAlert({ variant: "success", @@ -21,12 +20,17 @@ const ForgotPassword = () =>{ }) ); } catch (error) { - dispatch( - alertActions.showAlert({ - variant: "danger", - message: `No account is associated with the e-mail address: "${email}". Please try again.`, - }) - ); + if (error instanceof AxiosError && error.response && error.response.data) { + const { error: errorMessage} = error.response.data; + if (errorMessage) { + dispatch( + alertActions.showAlert({ + variant: "danger", + message: errorMessage, + }) + ); + } + } } }; diff --git a/src/pages/Authentication/ResetPassword.tsx b/src/pages/Authentication/ResetPassword.tsx new file mode 100644 index 00000000..04123d4f --- /dev/null +++ b/src/pages/Authentication/ResetPassword.tsx @@ -0,0 +1,100 @@ +import React, { useState } from "react"; +import { useLocation, useNavigate } from "react-router-dom"; +import axios, { AxiosError } from "axios"; +import { alertActions } from "../../store/slices/alertSlice"; +import { useDispatch } from "react-redux"; + +const ResetPassword = () => { + const location = useLocation(); + const navigate = useNavigate(); + const dispatch = useDispatch(); + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const queryParams = new URLSearchParams(location.search); + const token = queryParams.get("token"); + + // Handle form submission + const handleSubmit = async (e: React.MouseEvent) => { + + + if (password.length < 6) { + dispatch( + alertActions.showAlert({ + variant: "danger", + message: "Password should be at least 6 letters long." + }) + ); + return; + } + else if (password !== confirmPassword) { + dispatch( + alertActions.showAlert({ + variant: "danger", + message: "Passwords do not match." + }) + ); + return; + } + else { + try { + // Send password reset request to the backend + await axios.put(`http://localhost:3002/api/v1/password_resets/${token}`, { + user: { password }, + }); + + navigate("/login") + + dispatch( + alertActions.showAlert({ + variant: "success", + message: `Password Successfully Updated`, + }) + ); + + } catch (error) { + if (error instanceof AxiosError && error.response && error.response.data) { + const { error: errorMessage} = error.response.data; + if (errorMessage) { + dispatch( + alertActions.showAlert({ + variant: "danger", + message: errorMessage, + }) + ); + } + } + } + } + }; + + return ( +
+
+

Reset Your Password

+
+ Password: +
+ setPassword(e.target.value)} + required + style={{marginTop: '5px', marginBottom: '5px', height: '20px', border: '1px solid black'}} + /> +
+ Confirm Password: +
+ setConfirmPassword(e.target.value)} + required + style={{marginTop: '5px', marginBottom: '5px', height: '20px', border: '1px solid black'}} + /> +
+ +
+ ); +}; + +export default ResetPassword; From f686384f2e5ed407552a13d96ae5a327dd12d3cb Mon Sep 17 00:00:00 2001 From: John Weisz Date: Thu, 5 Mar 2026 18:15:31 -0500 Subject: [PATCH 03/42] add reset pages --- src/App.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/App.tsx b/src/App.tsx index 59c76165..eb937e4e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -13,6 +13,8 @@ import ViewScores from "./pages/Assignments/ViewScores"; import ViewSubmissions from "./pages/Assignments/ViewSubmissions"; import SubmittedContent from "./pages/Assignments/SubmittedContent"; import Login from "./pages/Authentication/Login"; +import ForgotPassword from "./pages/Authentication/ForgotPassword"; +import ResetPassword from "./pages/Authentication/ResetPassword"; import Logout from "./pages/Authentication/Logout"; import Courses from "./pages/Courses/Course"; import CourseEditor from "./pages/Courses/CourseEditor"; @@ -63,6 +65,8 @@ function App() { children: [ { index: true, element: } /> }, { path: "login", element: }, + { path: "forgot-password", element: }, + { path: "password_edit/check_reset_url", element: }, { path: "logout", element: } /> }, { From c8e43ddb6d16f13545f513a3f2502ece71a14509 Mon Sep 17 00:00:00 2001 From: John Weisz Date: Thu, 5 Mar 2026 18:33:30 -0500 Subject: [PATCH 04/42] use env base url --- src/pages/Authentication/ResetPassword.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/Authentication/ResetPassword.tsx b/src/pages/Authentication/ResetPassword.tsx index 04123d4f..86e0d62e 100644 --- a/src/pages/Authentication/ResetPassword.tsx +++ b/src/pages/Authentication/ResetPassword.tsx @@ -4,6 +4,8 @@ import axios, { AxiosError } from "axios"; import { alertActions } from "../../store/slices/alertSlice"; import { useDispatch } from "react-redux"; +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3002'; + const ResetPassword = () => { const location = useLocation(); const navigate = useNavigate(); @@ -38,7 +40,7 @@ const ResetPassword = () => { else { try { // Send password reset request to the backend - await axios.put(`http://localhost:3002/api/v1/password_resets/${token}`, { + await axios.put(`${API_BASE_URL}/api/v1/password_resets/${token}`, { user: { password }, }); From 6623f3f7101135ebd2a6189f2705283a8b4009fc Mon Sep 17 00:00:00 2001 From: John Weisz Date: Thu, 5 Mar 2026 18:35:31 -0500 Subject: [PATCH 05/42] add env base url and fix service path --- src/pages/Authentication/ForgotPassword.tsx | 4 +++- src/pages/Authentication/ResetPassword.tsx | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pages/Authentication/ForgotPassword.tsx b/src/pages/Authentication/ForgotPassword.tsx index 3ffff8d7..10174e20 100644 --- a/src/pages/Authentication/ForgotPassword.tsx +++ b/src/pages/Authentication/ForgotPassword.tsx @@ -3,6 +3,8 @@ import axios, { AxiosError } from 'axios'; import { alertActions } from "../../store/slices/alertSlice"; import { useDispatch } from "react-redux"; +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3002'; + const ForgotPassword = () =>{ const [email, setEmail] = useState(''); @@ -11,7 +13,7 @@ const ForgotPassword = () =>{ const handleSubmit = async (e: React.MouseEvent) => { try { - await axios.post("http://localhost:3002/api/v1/password_resets", { email }); + await axios.post(`${API_BASE_URL}/password_resets`, { email }); dispatch( alertActions.showAlert({ diff --git a/src/pages/Authentication/ResetPassword.tsx b/src/pages/Authentication/ResetPassword.tsx index 86e0d62e..7c223f3f 100644 --- a/src/pages/Authentication/ResetPassword.tsx +++ b/src/pages/Authentication/ResetPassword.tsx @@ -40,7 +40,7 @@ const ResetPassword = () => { else { try { // Send password reset request to the backend - await axios.put(`${API_BASE_URL}/api/v1/password_resets/${token}`, { + await axios.put(`${API_BASE_URL}/password_resets/${token}`, { user: { password }, }); From d02f2ebe8bab2e0cbcd701a324c07e2a23ba6f49 Mon Sep 17 00:00:00 2001 From: josev814 Date: Fri, 6 Mar 2026 12:13:27 -0500 Subject: [PATCH 06/42] Centralizing the Api contant API_BASE_URL is pulled in multiple locations, by setting this as a contant in a centralized location other pages can pull in this constant This gives us one sourcce of truth for the API_BASE_URL rather than having it scattered in multiple areas Added additional information in the readme file on where to set env variables for docker compose Additionally, included a sample.env file which can be used to show what to include in the env file and how to get docker to load it Added .env to the gitignore list --- .gitignore | 1 + README.md | 3 +++ docker-compose.yml | 3 +-- sample.env | 9 +++++++++ src/contants/Api.ts | 1 + src/pages/Authentication/ForgotPassword.tsx | 3 +-- vite.config.ts | 2 +- 7 files changed, 17 insertions(+), 5 deletions(-) create mode 100644 sample.env create mode 100644 src/contants/Api.ts diff --git a/.gitignore b/.gitignore index 1578f516..1ef7d2ba 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ # misc .DS_Store +.env .env.local .env.development.local .env.test.local diff --git a/README.md b/README.md index b87cb004..0f7fb2ad 100644 --- a/README.md +++ b/README.md @@ -44,3 +44,6 @@ You don’t have to ever use `eject`. The curated feature set is suitable for sm You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). To learn React, check out the [React documentation](https://reactjs.org/). + +## Docker ENV +To define environment variables for the docker compose stack, reference the sample.env file diff --git a/docker-compose.yml b/docker-compose.yml index 4ad3bc7a..2c27df97 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,5 +6,4 @@ services: container_name: expertiza-frontend ports: - "8080:80" - restart: unless-stopped - + restart: unless-stopped \ No newline at end of file diff --git a/sample.env b/sample.env new file mode 100644 index 00000000..cff18485 --- /dev/null +++ b/sample.env @@ -0,0 +1,9 @@ +# To define an env variables duplicate this file and save as .env.development.local +# Change VITE_API_BASE_URL to the appropriate url +# Open the command line and when running docker compose +# pass in --env-file and set VITE_API_BASE_URL to the appropriate url in that file +# For production, create a .env file in the root directory. +# Set VITE_API_BASE_URL in the .env to the appropriate url. +# There is no need to pass in --env-file for production since compose will pick up the .env file by default. + +VITE_API_BASE_URL="localhost:3002" diff --git a/src/contants/Api.ts b/src/contants/Api.ts new file mode 100644 index 00000000..ed52bb5a --- /dev/null +++ b/src/contants/Api.ts @@ -0,0 +1 @@ +export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || "http://localhost:3002"; diff --git a/src/pages/Authentication/ForgotPassword.tsx b/src/pages/Authentication/ForgotPassword.tsx index 10174e20..11d6db06 100644 --- a/src/pages/Authentication/ForgotPassword.tsx +++ b/src/pages/Authentication/ForgotPassword.tsx @@ -2,8 +2,7 @@ import React, {useState} from "react"; import axios, { AxiosError } from 'axios'; import { alertActions } from "../../store/slices/alertSlice"; import { useDispatch } from "react-redux"; - -const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3002'; +import { API_BASE_URL } from '@/constants/Api'; const ForgotPassword = () =>{ diff --git a/vite.config.ts b/vite.config.ts index 8913fb38..22d6e099 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -22,7 +22,7 @@ export default defineConfig({ host: "0.0.0.0", proxy: { "/api": { - target: "http://localhost:3002", // backend Rails server + target: import.meta.env.VITE_API_BASE_URL || "http://localhost:3002", // backend Rails server changeOrigin: true, secure: false, }, From 8a0f77690f62c4c56a48de04d7c12d7c82290ce6 Mon Sep 17 00:00:00 2001 From: josev814 Date: Fri, 6 Mar 2026 12:14:07 -0500 Subject: [PATCH 07/42] fixing directory name --- src/{contants => constants}/Api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/{contants => constants}/Api.ts (71%) diff --git a/src/contants/Api.ts b/src/constants/Api.ts similarity index 71% rename from src/contants/Api.ts rename to src/constants/Api.ts index ed52bb5a..82dca5cf 100644 --- a/src/contants/Api.ts +++ b/src/constants/Api.ts @@ -1 +1 @@ -export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || "http://localhost:3002"; +export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || "http://localhost:3002"; \ No newline at end of file From 7f95d34ed04d7226296a0f6babe13259fdc9d75d Mon Sep 17 00:00:00 2001 From: JaredM2028 Date: Fri, 6 Mar 2026 16:30:58 -0500 Subject: [PATCH 08/42] Dispatch alert before naviation to ensure the alert renders before unmounting. --- .gitignore | 1 + src/pages/Authentication/ResetPassword.tsx | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 1ef7d2ba..0a5f94e2 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ npm-debug.log* yarn-debug.log* yarn-error.log* +.vscode/settings.json diff --git a/src/pages/Authentication/ResetPassword.tsx b/src/pages/Authentication/ResetPassword.tsx index 7c223f3f..c9fb5147 100644 --- a/src/pages/Authentication/ResetPassword.tsx +++ b/src/pages/Authentication/ResetPassword.tsx @@ -44,8 +44,6 @@ const ResetPassword = () => { user: { password }, }); - navigate("/login") - dispatch( alertActions.showAlert({ variant: "success", @@ -53,6 +51,8 @@ const ResetPassword = () => { }) ); + navigate("/login"); + } catch (error) { if (error instanceof AxiosError && error.response && error.response.data) { const { error: errorMessage} = error.response.data; From 202dc06cfe2d5344762de3569637ae4c613cafcb Mon Sep 17 00:00:00 2001 From: JaredM2028 Date: Fri, 6 Mar 2026 16:42:03 -0500 Subject: [PATCH 09/42] Protection against null password tokens. Broken link, manual navigation, bookmarking, or expired tokens. --- src/pages/Authentication/ResetPassword.tsx | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/pages/Authentication/ResetPassword.tsx b/src/pages/Authentication/ResetPassword.tsx index c9fb5147..b285a0df 100644 --- a/src/pages/Authentication/ResetPassword.tsx +++ b/src/pages/Authentication/ResetPassword.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import { useLocation, useNavigate } from "react-router-dom"; import axios, { AxiosError } from "axios"; import { alertActions } from "../../store/slices/alertSlice"; @@ -15,6 +15,19 @@ const ResetPassword = () => { const queryParams = new URLSearchParams(location.search); const token = queryParams.get("token"); + // Ensure the token is present when the component mounts + useEffect(() => { + if (!token) { + dispatch( + alertActions.showAlert({ + variant: "danger", + message: "Invalid or missing token.", + }) + ); + navigate("/login"); + } + }, []); + // Handle form submission const handleSubmit = async (e: React.MouseEvent) => { From f711f054e8b00a9a4fd744ccb2a3734cf6c1de5d Mon Sep 17 00:00:00 2001 From: JaredM2028 Date: Fri, 6 Mar 2026 16:49:02 -0500 Subject: [PATCH 10/42] API_BASE_URL constant pulled in from @/constants/Api for ResetPassword_tsx. --- src/pages/Authentication/ResetPassword.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pages/Authentication/ResetPassword.tsx b/src/pages/Authentication/ResetPassword.tsx index b285a0df..2f8d06df 100644 --- a/src/pages/Authentication/ResetPassword.tsx +++ b/src/pages/Authentication/ResetPassword.tsx @@ -3,8 +3,7 @@ import { useLocation, useNavigate } from "react-router-dom"; import axios, { AxiosError } from "axios"; import { alertActions } from "../../store/slices/alertSlice"; import { useDispatch } from "react-redux"; - -const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3002'; +import { API_BASE_URL } from '@/constants/Api'; const ResetPassword = () => { const location = useLocation(); From 6bed931c2998db14472e8ba2d8d51d64b9624cd8 Mon Sep 17 00:00:00 2001 From: JaredM2028 Date: Fri, 6 Mar 2026 17:08:16 -0500 Subject: [PATCH 11/42] Standardize styling to match Login_tsx. --- src/pages/Authentication/ForgotPassword.tsx | 40 ++++++++------ src/pages/Authentication/ResetPassword.tsx | 58 ++++++++++++--------- tsconfig.json | 3 ++ 3 files changed, 59 insertions(+), 42 deletions(-) diff --git a/src/pages/Authentication/ForgotPassword.tsx b/src/pages/Authentication/ForgotPassword.tsx index 11d6db06..5a88fb54 100644 --- a/src/pages/Authentication/ForgotPassword.tsx +++ b/src/pages/Authentication/ForgotPassword.tsx @@ -1,4 +1,5 @@ import React, {useState} from "react"; +import { Button, Col, Container, Form } from "react-bootstrap"; import axios, { AxiosError } from 'axios'; import { alertActions } from "../../store/slices/alertSlice"; import { useDispatch } from "react-redux"; @@ -37,22 +38,29 @@ const ForgotPassword = () =>{ return ( -
-
-

Forgotten Your Password?

-
-
- Enter the e-mail address associated with your account -
- setEmail(e.target.value)} - required - style={{marginTop: '5px', marginBottom: '5px', height: '20px', border: '1px solid black'}} - /> -
- -
+ + +

Forgotten Your Password?

+

Enter the e-mail address associated with your account

+ + Email Address + setEmail(e.target.value)} + required + /> + + + +
); }; diff --git a/src/pages/Authentication/ResetPassword.tsx b/src/pages/Authentication/ResetPassword.tsx index 2f8d06df..f39220e4 100644 --- a/src/pages/Authentication/ResetPassword.tsx +++ b/src/pages/Authentication/ResetPassword.tsx @@ -1,4 +1,5 @@ import React, { useState, useEffect } from "react"; +import { Button, Col, Container, Form } from "react-bootstrap"; import { useLocation, useNavigate } from "react-router-dom"; import axios, { AxiosError } from "axios"; import { alertActions } from "../../store/slices/alertSlice"; @@ -82,32 +83,37 @@ const ResetPassword = () => { }; return ( -
-
-

Reset Your Password

-
- Password: -
- setPassword(e.target.value)} - required - style={{marginTop: '5px', marginBottom: '5px', height: '20px', border: '1px solid black'}} - /> -
- Confirm Password: -
- setConfirmPassword(e.target.value)} - required - style={{marginTop: '5px', marginBottom: '5px', height: '20px', border: '1px solid black'}} - /> -
- -
+ + +

Reset Your Password

+ + Password + setPassword(e.target.value)} + required + /> + + + Confirm Password + setConfirmPassword(e.target.value)} + required + /> + + + +
); }; diff --git a/tsconfig.json b/tsconfig.json index 885fce5e..fd6872b1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,6 +7,9 @@ "esnext", ], "baseUrl": "src", + "paths": { + "@/*": ["*"] + }, "allowJs": true, "skipLibCheck": true, "esModuleInterop": true, From b85b63e8804c950f32c9a3127df07b63f296ea7d Mon Sep 17 00:00:00 2001 From: JaredM2028 Date: Fri, 6 Mar 2026 17:30:22 -0500 Subject: [PATCH 12/42] Using Yup and Formik --- src/pages/Authentication/ForgotPassword.tsx | 77 ++++++---- src/pages/Authentication/ResetPassword.tsx | 148 ++++++++++---------- 2 files changed, 120 insertions(+), 105 deletions(-) diff --git a/src/pages/Authentication/ForgotPassword.tsx b/src/pages/Authentication/ForgotPassword.tsx index 5a88fb54..0e98f04e 100644 --- a/src/pages/Authentication/ForgotPassword.tsx +++ b/src/pages/Authentication/ForgotPassword.tsx @@ -1,29 +1,39 @@ -import React, {useState} from "react"; -import { Button, Col, Container, Form } from "react-bootstrap"; -import axios, { AxiosError } from 'axios'; +import React from "react"; +import { Button, Col, Container } from "react-bootstrap"; +import { Form, Formik, FormikHelpers } from "formik"; +import FormInput from "../../components/Form/FormInput"; +import axios, { AxiosError } from "axios"; import { alertActions } from "../../store/slices/alertSlice"; import { useDispatch } from "react-redux"; -import { API_BASE_URL } from '@/constants/Api'; +import { API_BASE_URL } from "../../constants/Api"; +import * as Yup from "yup"; -const ForgotPassword = () =>{ +interface IForgotPasswordFormValues { + email: string; +} - const [email, setEmail] = useState(''); - const dispatch = useDispatch(); +const validationSchema = Yup.object({ + email: Yup.string().email("Invalid email address").required("Required"), +}); - const handleSubmit = async (e: React.MouseEvent) => { +const ForgotPassword = () => { + const dispatch = useDispatch(); + const onSubmit = async ( + values: IForgotPasswordFormValues, + submitProps: FormikHelpers + ) => { try { - await axios.post(`${API_BASE_URL}/password_resets`, { email }); - + await axios.post(`${API_BASE_URL}/password_resets`, { email: values.email }); dispatch( alertActions.showAlert({ variant: "success", - message: `A link to reset your password has been sent to your e-mail address.`, + message: "A link to reset your password has been sent to your e-mail address.", }) ); } catch (error) { if (error instanceof AxiosError && error.response && error.response.data) { - const { error: errorMessage} = error.response.data; + const { error: errorMessage } = error.response.data; if (errorMessage) { dispatch( alertActions.showAlert({ @@ -34,35 +44,42 @@ const ForgotPassword = () =>{ } } } + submitProps.setSubmitting(false); }; - return (

Forgotten Your Password?

Enter the e-mail address associated with your account

- - Email Address - setEmail(e.target.value)} - required - /> - - + {(formik) => ( +
+ + + + )} +
); - }; export default ForgotPassword; \ No newline at end of file diff --git a/src/pages/Authentication/ResetPassword.tsx b/src/pages/Authentication/ResetPassword.tsx index f39220e4..5362a680 100644 --- a/src/pages/Authentication/ResetPassword.tsx +++ b/src/pages/Authentication/ResetPassword.tsx @@ -1,17 +1,32 @@ -import React, { useState, useEffect } from "react"; -import { Button, Col, Container, Form } from "react-bootstrap"; +import React, { useEffect } from "react"; +import { Button, Col, Container } from "react-bootstrap"; +import { Form, Formik, FormikHelpers } from "formik"; +import FormInput from "../../components/Form/FormInput"; import { useLocation, useNavigate } from "react-router-dom"; import axios, { AxiosError } from "axios"; import { alertActions } from "../../store/slices/alertSlice"; import { useDispatch } from "react-redux"; -import { API_BASE_URL } from '@/constants/Api'; +import { API_BASE_URL } from "../../constants/Api"; +import * as Yup from "yup"; + +interface IResetPasswordFormValues { + password: string; + confirmPassword: string; +} + +const validationSchema = Yup.object({ + password: Yup.string() + .min(6, "Password must be at least 6 characters") + .required("Required"), + confirmPassword: Yup.string() + .oneOf([Yup.ref("password")], "Passwords do not match") + .required("Required"), +}); const ResetPassword = () => { const location = useLocation(); const navigate = useNavigate(); const dispatch = useDispatch(); - const [password, setPassword] = useState(""); - const [confirmPassword, setConfirmPassword] = useState(""); const queryParams = new URLSearchParams(location.search); const token = queryParams.get("token"); @@ -28,90 +43,73 @@ const ResetPassword = () => { } }, []); - // Handle form submission - const handleSubmit = async (e: React.MouseEvent) => { - - - if (password.length < 6) { - dispatch( - alertActions.showAlert({ - variant: "danger", - message: "Password should be at least 6 letters long." - }) - ); - return; - } - else if (password !== confirmPassword) { + const onSubmit = async ( + values: IResetPasswordFormValues, + submitProps: FormikHelpers + ) => { + try { + // Send password reset request to the backend + await axios.put(`${API_BASE_URL}/password_resets/${token}`, { + user: { password: values.password }, + }); dispatch( alertActions.showAlert({ - variant: "danger", - message: "Passwords do not match." + variant: "success", + message: "Password Successfully Updated", }) ); - return; - } - else { - try { - // Send password reset request to the backend - await axios.put(`${API_BASE_URL}/password_resets/${token}`, { - user: { password }, - }); - - dispatch( - alertActions.showAlert({ - variant: "success", - message: `Password Successfully Updated`, - }) - ); - - navigate("/login"); - - } catch (error) { - if (error instanceof AxiosError && error.response && error.response.data) { - const { error: errorMessage} = error.response.data; - if (errorMessage) { - dispatch( - alertActions.showAlert({ - variant: "danger", - message: errorMessage, - }) - ); - } + navigate("/login"); + } catch (error) { + if (error instanceof AxiosError && error.response && error.response.data) { + const { error: errorMessage } = error.response.data; + if (errorMessage) { + dispatch( + alertActions.showAlert({ + variant: "danger", + message: errorMessage, + }) + ); } } } + submitProps.setSubmitting(false); }; return (

Reset Your Password

- - Password - setPassword(e.target.value)} - required - /> - - - Confirm Password - setConfirmPassword(e.target.value)} - required - /> - - + {(formik) => ( +
+ + + + + )} +
); From fae498da89032bc993f03a7e86c4e2032a1f51dc Mon Sep 17 00:00:00 2001 From: JaredM2028 Date: Fri, 6 Mar 2026 17:47:36 -0500 Subject: [PATCH 13/42] Had to tweak vite config file to make it run on my machine. --- vite.config.ts | 47 +++++++++++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/vite.config.ts b/vite.config.ts index 22d6e099..8706929a 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,31 +1,34 @@ // vite.config.ts -import { defineConfig } from "vite"; +import { defineConfig, loadEnv } from "vite"; import react from "@vitejs/plugin-react"; import path from "path"; // https://vitejs.dev/config/ -export default defineConfig({ - plugins: [react()], - resolve: { - alias: { - '@': path.resolve(__dirname, './src'), - 'components': path.resolve(__dirname, './src/components'), - 'utils': path.resolve(__dirname, './src/utils'), - 'store': path.resolve(__dirname, "./src/store"), - 'hooks': path.resolve(__dirname, "./src/hooks"), - 'pages': path.resolve(__dirname, "./src/pages"), - 'assets': path.resolve(__dirname, "./src/assets"), +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, process.cwd(), ""); + return { + plugins: [react()], + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + 'components': path.resolve(__dirname, './src/components'), + 'utils': path.resolve(__dirname, './src/utils'), + 'store': path.resolve(__dirname, "./src/store"), + 'hooks': path.resolve(__dirname, "./src/hooks"), + 'pages': path.resolve(__dirname, "./src/pages"), + 'assets': path.resolve(__dirname, "./src/assets"), + }, }, - }, - server: { - port: 3000, // frontend runs here - host: "0.0.0.0", - proxy: { - "/api": { - target: import.meta.env.VITE_API_BASE_URL || "http://localhost:3002", // backend Rails server - changeOrigin: true, - secure: false, + server: { + port: 3000, // frontend runs here + host: "0.0.0.0", + proxy: { + "/api": { + target: env.VITE_API_BASE_URL || "http://localhost:3002", // backend Rails server + changeOrigin: true, + secure: false, + }, }, }, - }, + }; }); From 4efd32a01b0fb9b69355927b3be04cfad540985a Mon Sep 17 00:00:00 2001 From: JaredM2028 Date: Fri, 6 Mar 2026 18:27:16 -0500 Subject: [PATCH 14/42] Reverting config files to original --- tsconfig.json | 3 --- vite.config.ts | 49 +++++++++++++++++++++++-------------------------- 2 files changed, 23 insertions(+), 29 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index fd6872b1..885fce5e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,9 +7,6 @@ "esnext", ], "baseUrl": "src", - "paths": { - "@/*": ["*"] - }, "allowJs": true, "skipLibCheck": true, "esModuleInterop": true, diff --git a/vite.config.ts b/vite.config.ts index 8706929a..5b45c5c6 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,34 +1,31 @@ // vite.config.ts -import { defineConfig, loadEnv } from "vite"; +import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; import path from "path"; // https://vitejs.dev/config/ -export default defineConfig(({ mode }) => { - const env = loadEnv(mode, process.cwd(), ""); - return { - plugins: [react()], - resolve: { - alias: { - '@': path.resolve(__dirname, './src'), - 'components': path.resolve(__dirname, './src/components'), - 'utils': path.resolve(__dirname, './src/utils'), - 'store': path.resolve(__dirname, "./src/store"), - 'hooks': path.resolve(__dirname, "./src/hooks"), - 'pages': path.resolve(__dirname, "./src/pages"), - 'assets': path.resolve(__dirname, "./src/assets"), - }, +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + 'components': path.resolve(__dirname, './src/components'), + 'utils': path.resolve(__dirname, './src/utils'), + 'store': path.resolve(__dirname, "./src/store"), + 'hooks': path.resolve(__dirname, "./src/hooks"), + 'pages': path.resolve(__dirname, "./src/pages"), + 'assets': path.resolve(__dirname, "./src/assets"), }, - server: { - port: 3000, // frontend runs here - host: "0.0.0.0", - proxy: { - "/api": { - target: env.VITE_API_BASE_URL || "http://localhost:3002", // backend Rails server - changeOrigin: true, - secure: false, - }, + }, + server: { + port: 3000, // frontend runs here + host: "0.0.0.0", + proxy: { + "/api": { + target: import.meta.env.VITE_API_BASE_URL || "http://localhost:3002", // backend Rails server + changeOrigin: true, + secure: false, }, }, - }; -}); + }, +}); \ No newline at end of file From 95f57f94256416fedeeb9ffbc6c355ffe821a25b Mon Sep 17 00:00:00 2001 From: JaredM2028 Date: Fri, 6 Mar 2026 18:29:02 -0500 Subject: [PATCH 15/42] Revert gitignore --- .gitignore | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 0a5f94e2..6518cc5b 100644 --- a/.gitignore +++ b/.gitignore @@ -22,5 +22,4 @@ npm-debug.log* yarn-debug.log* -yarn-error.log* -.vscode/settings.json +yarn-error.log* \ No newline at end of file From 35c019279bfe44e63ad1d3d1a0f469daae2d0d64 Mon Sep 17 00:00:00 2001 From: Jose Date: Fri, 6 Mar 2026 18:40:09 -0500 Subject: [PATCH 16/42] Update vite.config.ts for backend url We can't use import.meta here we need to load the env first and then call env varsity. --- vite.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vite.config.ts b/vite.config.ts index 22d6e099..8913fb38 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -22,7 +22,7 @@ export default defineConfig({ host: "0.0.0.0", proxy: { "/api": { - target: import.meta.env.VITE_API_BASE_URL || "http://localhost:3002", // backend Rails server + target: "http://localhost:3002", // backend Rails server changeOrigin: true, secure: false, }, From 58bf0c2f01f7a376ff84b2182ea4d4676c2d8f3e Mon Sep 17 00:00:00 2001 From: JaredM2028 Date: Sat, 7 Mar 2026 14:04:34 -0500 Subject: [PATCH 17/42] Restored vite config to repo original. --- vite.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vite.config.ts b/vite.config.ts index 5b45c5c6..38806560 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -22,7 +22,7 @@ export default defineConfig({ host: "0.0.0.0", proxy: { "/api": { - target: import.meta.env.VITE_API_BASE_URL || "http://localhost:3002", // backend Rails server + target: "http://localhost:3002", // backend Rails server changeOrigin: true, secure: false, }, From e9dae03a451812875de7fda303871574f735aa44 Mon Sep 17 00:00:00 2001 From: JaredM2028 Date: Sat, 7 Mar 2026 14:21:32 -0500 Subject: [PATCH 18/42] Addressed feedback for ResetPassword --- src/pages/Authentication/ResetPassword.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/Authentication/ResetPassword.tsx b/src/pages/Authentication/ResetPassword.tsx index 5362a680..0c9edaf3 100644 --- a/src/pages/Authentication/ResetPassword.tsx +++ b/src/pages/Authentication/ResetPassword.tsx @@ -41,7 +41,7 @@ const ResetPassword = () => { ); navigate("/login"); } - }, []); + }, [token, dispatch, navigate]); const onSubmit = async ( values: IResetPasswordFormValues, From 58d7de8be3520909da0f65bdcd119808a5c7defe Mon Sep 17 00:00:00 2001 From: JaredM2028 Date: Sat, 7 Mar 2026 14:33:53 -0500 Subject: [PATCH 19/42] Attempting to remove vite config from commit history --- vite.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vite.config.ts b/vite.config.ts index 38806560..8913fb38 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -28,4 +28,4 @@ export default defineConfig({ }, }, }, -}); \ No newline at end of file +}); From 54c92b2ee9272201abe0f189a4aa22e868252971 Mon Sep 17 00:00:00 2001 From: JaredM2028 Date: Sat, 7 Mar 2026 15:06:59 -0500 Subject: [PATCH 20/42] Strengthen fallback alert for all submission errors in ForgotPassword and ResetPassword. --- src/pages/Authentication/ForgotPassword.tsx | 21 +++++++++++++-------- src/pages/Authentication/ResetPassword.tsx | 21 +++++++++++++-------- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/src/pages/Authentication/ForgotPassword.tsx b/src/pages/Authentication/ForgotPassword.tsx index 0e98f04e..0d054532 100644 --- a/src/pages/Authentication/ForgotPassword.tsx +++ b/src/pages/Authentication/ForgotPassword.tsx @@ -34,14 +34,19 @@ const ForgotPassword = () => { } catch (error) { if (error instanceof AxiosError && error.response && error.response.data) { const { error: errorMessage } = error.response.data; - if (errorMessage) { - dispatch( - alertActions.showAlert({ - variant: "danger", - message: errorMessage, - }) - ); - } + dispatch( + alertActions.showAlert({ + variant: "danger", + message: errorMessage || "An error occurred. Please try again.", + }) + ); + } else { + dispatch( + alertActions.showAlert({ + variant: "danger", + message: "An error occurred. Please try again.", + }) + ); } } submitProps.setSubmitting(false); diff --git a/src/pages/Authentication/ResetPassword.tsx b/src/pages/Authentication/ResetPassword.tsx index 0c9edaf3..13a73b93 100644 --- a/src/pages/Authentication/ResetPassword.tsx +++ b/src/pages/Authentication/ResetPassword.tsx @@ -62,14 +62,19 @@ const ResetPassword = () => { } catch (error) { if (error instanceof AxiosError && error.response && error.response.data) { const { error: errorMessage } = error.response.data; - if (errorMessage) { - dispatch( - alertActions.showAlert({ - variant: "danger", - message: errorMessage, - }) - ); - } + dispatch( + alertActions.showAlert({ + variant: "danger", + message: errorMessage || "An error occurred. Please try again.", + }) + ); + } else { + dispatch( + alertActions.showAlert({ + variant: "danger", + message: "An error occurred. Please try again.", + }) + ); } } submitProps.setSubmitting(false); From dca5ef4979845874c4cedb2da303b0744b7d9827 Mon Sep 17 00:00:00 2001 From: JaredM2028 Date: Mon, 9 Mar 2026 20:23:00 -0400 Subject: [PATCH 21/42] Improve fallback and condense error catching --- src/pages/Authentication/ForgotPassword.tsx | 21 ++++++++------------- src/pages/Authentication/ResetPassword.tsx | 21 ++++++++------------- 2 files changed, 16 insertions(+), 26 deletions(-) diff --git a/src/pages/Authentication/ForgotPassword.tsx b/src/pages/Authentication/ForgotPassword.tsx index 0d054532..a0863d68 100644 --- a/src/pages/Authentication/ForgotPassword.tsx +++ b/src/pages/Authentication/ForgotPassword.tsx @@ -32,22 +32,17 @@ const ForgotPassword = () => { }) ); } catch (error) { + let errorFallback = "An error occurred. Please try again."; if (error instanceof AxiosError && error.response && error.response.data) { const { error: errorMessage } = error.response.data; - dispatch( - alertActions.showAlert({ - variant: "danger", - message: errorMessage || "An error occurred. Please try again.", - }) - ); - } else { - dispatch( - alertActions.showAlert({ - variant: "danger", - message: "An error occurred. Please try again.", - }) - ); + errorFallback = errorMessage || errorFallback; } + dispatch( + alertActions.showAlert({ + variant: "danger", + message: errorFallback, + }) + ); } submitProps.setSubmitting(false); }; diff --git a/src/pages/Authentication/ResetPassword.tsx b/src/pages/Authentication/ResetPassword.tsx index 13a73b93..04da9fc6 100644 --- a/src/pages/Authentication/ResetPassword.tsx +++ b/src/pages/Authentication/ResetPassword.tsx @@ -60,22 +60,17 @@ const ResetPassword = () => { ); navigate("/login"); } catch (error) { + let errorFallback = "An error occurred. Please try again."; if (error instanceof AxiosError && error.response && error.response.data) { const { error: errorMessage } = error.response.data; - dispatch( - alertActions.showAlert({ - variant: "danger", - message: errorMessage || "An error occurred. Please try again.", - }) - ); - } else { - dispatch( - alertActions.showAlert({ - variant: "danger", - message: "An error occurred. Please try again.", - }) - ); + errorFallback = errorMessage || errorFallback; } + dispatch( + alertActions.showAlert({ + variant: "danger", + message: errorFallback, + }) + ); } submitProps.setSubmitting(false); }; From e645db318c65e8798a35b172cad3f8c1f395d0f8 Mon Sep 17 00:00:00 2001 From: John Weisz Date: Wed, 11 Mar 2026 19:48:45 -0400 Subject: [PATCH 22/42] use header h2 --- src/pages/Authentication/ForgotPassword.tsx | 2 +- src/pages/Authentication/ResetPassword.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/Authentication/ForgotPassword.tsx b/src/pages/Authentication/ForgotPassword.tsx index a0863d68..1b906825 100644 --- a/src/pages/Authentication/ForgotPassword.tsx +++ b/src/pages/Authentication/ForgotPassword.tsx @@ -50,7 +50,7 @@ const ForgotPassword = () => { return ( -

Forgotten Your Password?

+

Forgotten Your Password?

Enter the e-mail address associated with your account

{ return ( -

Reset Your Password

+

Reset Your Password

Date: Wed, 11 Mar 2026 20:33:44 -0400 Subject: [PATCH 23/42] adjust prompt style/text --- src/pages/Authentication/ForgotPassword.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/Authentication/ForgotPassword.tsx b/src/pages/Authentication/ForgotPassword.tsx index 1b906825..5b4ffc64 100644 --- a/src/pages/Authentication/ForgotPassword.tsx +++ b/src/pages/Authentication/ForgotPassword.tsx @@ -51,7 +51,6 @@ const ForgotPassword = () => {

Forgotten Your Password?

-

Enter the e-mail address associated with your account

{ > {(formik) => (
+

Enter the email associated with your account

Date: Thu, 12 Mar 2026 01:01:01 -0400 Subject: [PATCH 24/42] Adding tests to cover ForgotPassword with vitest - Majority of the code is covered except for lines 37-39 ## ForgotPassword update Forgot password didn't trim the email input which would cause false positives --- .../__tests__/ForgotPassword.test.tsx | 171 ++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 src/pages/Authentication/__tests__/ForgotPassword.test.tsx diff --git a/src/pages/Authentication/__tests__/ForgotPassword.test.tsx b/src/pages/Authentication/__tests__/ForgotPassword.test.tsx new file mode 100644 index 00000000..01e09ef8 --- /dev/null +++ b/src/pages/Authentication/__tests__/ForgotPassword.test.tsx @@ -0,0 +1,171 @@ +import React, { act } from "react"; +import { render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import ForgotPassword from "../ForgotPassword"; +import { Provider } from "react-redux"; +import { configureStore } from "@reduxjs/toolkit"; +import alertReducer from "store/slices/alertSlice"; +import { vi } from "vitest"; +import axios from "axios"; +import { AxiosError } from "axios"; + +vi.mock('axios'); + +const mockStore = configureStore({ + reducer: { + alert: alertReducer, + }, +}); + +const validEmail = 'test@example.com'; +const submitText = /request password reset/i; + +describe('Test Forgot Password Displays Correctly', () => { + it('Renders the component correctly', () => { + render( + + + + ); + expect(screen.getByRole('heading', { level: 2 })).toHaveTextContent(/forgotten your password\?/i); + expect(screen.getByText(/enter the email associated with your account/i)).toBeInTheDocument(); + expect(screen.getByRole('button', {name: submitText})).toBeInTheDocument(); + }); + + it('Renders email input field', () => { + render( + + + + ); + const emailInput = screen.getByRole('textbox', {name: /email address/i}); + expect(emailInput).toBeInTheDocument(); + }); +}); + +describe('Test Forgot Password Form Validations', () => { + it('Does not submit form with empty email', async () => { + const user = userEvent.setup(); + render( + + + + ); + + let emailInput = screen.getByRole('textbox'); + let submitButton = screen.getByRole('button', {name: submitText}); + + await user.click(emailInput); + await user.tab(); + await user.click(submitButton); + + expect(axios.post).not.toHaveBeenCalled(); + + expect(screen.getByText(/required/i)).toBeInTheDocument(); + }); + + it('Does not submit form with invalid email', async () => { + const user = userEvent.setup(); + render( + + + + ); + + let emailInput = screen.getByRole('textbox'); + let submitButton = screen.getByRole('button', {name: submitText}); + + await user.type(emailInput, 'bademail'); + await user.tab(); + + await waitFor(() => { + expect(screen.getByText(/invalid email address/i)).toBeInTheDocument(); + }); + expect(submitButton).toBeDisabled(); + expect(axios.post).not.toHaveBeenCalled(); + }); +}); + +describe('Test Forgot Password Api Error', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('Handles API unavailable', async () => { + const user = userEvent.setup(); + (axios.post as any).mockRejectedValue( + new AxiosError("Network Error", 'ERR_NETWORK', undefined, undefined, { + status: 0, + statusText: 'Network Error', + data: {}, + headers: {}, + config: {}, + }) + ); + + render( + + + + ); + + let emailInput = screen.getByRole('textbox'); + let submitButton = screen.getByRole('button', {name: submitText}); + + await user.type(emailInput, validEmail); + await user.click(submitButton); + + await waitFor(() => { + const state = mockStore.getState(); + expect(state.alert.message).toBe('An error occurred. Please try again.'); + expect(state.alert.variant).toBe('danger'); + }); + + expect(axios.post).toHaveBeenCalledWith( + expect.stringContaining('/password_resets'), {email: validEmail} + ); + }); +}); + +describe('Test Successful Password Reset Request', () => { + it('submit form successfully', async () => { + const user = userEvent.setup(); + (axios.post as any).mockResolvedValue({ + status: 200, + data: { message: 'If the email exists, a reset link has been sent.'}, + }); + + render( + + + + ); + + let emailInput = screen.getByRole('textbox'); + let submitButton = screen.getByRole('button', {name: submitText}); + + await user.type(emailInput, validEmail); + await user.click(submitButton); + + expect(axios.post).toHaveBeenCalledWith( + expect.stringContaining('/password_resets'), {email: validEmail} + ); + + await waitFor(() => { + const state = mockStore.getState(); + expect(state.alert.variant).toBe('success'); + expect(state.alert.message).toBe('A link to reset your password has been sent to your e-mail address.'); + }); + }); +}); +// // Mock the useAPI hook to return mock assignments +// vi.mock("hooks/useAPI", () => ({ +// default: () => ({ +// error: null, +// isLoading: false, +// data: { +// initialUser: userData +// }, +// sendRequest: vi.fn(), +// }) +// })); From 8237ed68d4972455937c1474f8830a550c4e02d4 Mon Sep 17 00:00:00 2001 From: josev814 Date: Thu, 12 Mar 2026 01:02:37 -0400 Subject: [PATCH 25/42] forgot to commit forgotpassword update --- src/pages/Authentication/ForgotPassword.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/Authentication/ForgotPassword.tsx b/src/pages/Authentication/ForgotPassword.tsx index 5b4ffc64..2982fa90 100644 --- a/src/pages/Authentication/ForgotPassword.tsx +++ b/src/pages/Authentication/ForgotPassword.tsx @@ -13,7 +13,7 @@ interface IForgotPasswordFormValues { } const validationSchema = Yup.object({ - email: Yup.string().email("Invalid email address").required("Required"), + email: Yup.string().trim().email("Invalid email address").required("Required"), }); const ForgotPassword = () => { From 4bebeff5e125d5e0ceabec6c7e407ea54dee6480 Mon Sep 17 00:00:00 2001 From: John Weisz Date: Mon, 16 Mar 2026 17:51:03 -0400 Subject: [PATCH 26/42] revert changes --- sample.env | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 sample.env diff --git a/sample.env b/sample.env deleted file mode 100644 index cff18485..00000000 --- a/sample.env +++ /dev/null @@ -1,9 +0,0 @@ -# To define an env variables duplicate this file and save as .env.development.local -# Change VITE_API_BASE_URL to the appropriate url -# Open the command line and when running docker compose -# pass in --env-file and set VITE_API_BASE_URL to the appropriate url in that file -# For production, create a .env file in the root directory. -# Set VITE_API_BASE_URL in the .env to the appropriate url. -# There is no need to pass in --env-file for production since compose will pick up the .env file by default. - -VITE_API_BASE_URL="localhost:3002" From 296dd27a2ba8b935ebe195ef47d931e3458611da Mon Sep 17 00:00:00 2001 From: John Weisz Date: Mon, 16 Mar 2026 17:51:52 -0400 Subject: [PATCH 27/42] revert changes --- .gitignore | 4 +--- README.md | 3 --- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 6518cc5b..9d20cc77 100644 --- a/.gitignore +++ b/.gitignore @@ -14,12 +14,10 @@ # misc .DS_Store -.env .env.local .env.development.local .env.test.local .env.production.local npm-debug.log* -yarn-debug.log* -yarn-error.log* \ No newline at end of file +yarn-debug.log* \ No newline at end of file diff --git a/README.md b/README.md index 0f7fb2ad..b87cb004 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,3 @@ You don’t have to ever use `eject`. The curated feature set is suitable for sm You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). To learn React, check out the [React documentation](https://reactjs.org/). - -## Docker ENV -To define environment variables for the docker compose stack, reference the sample.env file From caf6dfc0fc923d29f57dbef9b2507118873ea4ab Mon Sep 17 00:00:00 2001 From: John Weisz Date: Mon, 16 Mar 2026 17:52:49 -0400 Subject: [PATCH 28/42] revert changes --- .gitignore | 2 +- docker-compose.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 9d20cc77..03367d8d 100644 --- a/.gitignore +++ b/.gitignore @@ -20,4 +20,4 @@ .env.production.local npm-debug.log* -yarn-debug.log* \ No newline at end of file +yarn-debug.log* diff --git a/docker-compose.yml b/docker-compose.yml index 2c27df97..3253e94f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,4 +6,4 @@ services: container_name: expertiza-frontend ports: - "8080:80" - restart: unless-stopped \ No newline at end of file + restart: unless-stopped From 847c4976b8876486ce7668d53106f438cb2046ed Mon Sep 17 00:00:00 2001 From: John Weisz Date: Mon, 16 Mar 2026 17:53:44 -0400 Subject: [PATCH 29/42] revert changes --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 03367d8d..1578f516 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ npm-debug.log* yarn-debug.log* +yarn-error.log* From e4db480a763035f31d0197c2c20c3fd2f4702bfe Mon Sep 17 00:00:00 2001 From: josev814 Date: Tue, 17 Mar 2026 07:16:13 -0400 Subject: [PATCH 30/42] apply ai suggestions from PR review --- .../__tests__/ForgotPassword.test.tsx | 110 ++++++++---------- 1 file changed, 49 insertions(+), 61 deletions(-) diff --git a/src/pages/Authentication/__tests__/ForgotPassword.test.tsx b/src/pages/Authentication/__tests__/ForgotPassword.test.tsx index 01e09ef8..a049c235 100644 --- a/src/pages/Authentication/__tests__/ForgotPassword.test.tsx +++ b/src/pages/Authentication/__tests__/ForgotPassword.test.tsx @@ -1,4 +1,4 @@ -import React, { act } from "react"; +import React from "react"; import { render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import ForgotPassword from "../ForgotPassword"; @@ -11,45 +11,52 @@ import { AxiosError } from "axios"; vi.mock('axios'); -const mockStore = configureStore({ - reducer: { - alert: alertReducer, - }, +beforeEach(() => { + vi.clearAllMocks(); +}); + +const makeMockStore = () => configureStore({ + reducer: { + alert: alertReducer, + }, }); const validEmail = 'test@example.com'; const submitText = /request password reset/i; describe('Test Forgot Password Displays Correctly', () => { - it('Renders the component correctly', () => { - render( - - - - ); - expect(screen.getByRole('heading', { level: 2 })).toHaveTextContent(/forgotten your password\?/i); - expect(screen.getByText(/enter the email associated with your account/i)).toBeInTheDocument(); - expect(screen.getByRole('button', {name: submitText})).toBeInTheDocument(); - }); + it('Renders the component correctly', () => { + const store = makeMockStore(); + render( + + + + ); + expect(screen.getByRole('heading', { level: 2 })).toHaveTextContent(/forgotten your password\?/i); + expect(screen.getByText(/enter the email associated with your account/i)).toBeInTheDocument(); + expect(screen.getByRole('button', {name: submitText})).toBeInTheDocument(); + }); - it('Renders email input field', () => { - render( - - - - ); - const emailInput = screen.getByRole('textbox', {name: /email address/i}); - expect(emailInput).toBeInTheDocument(); - }); + it('Renders email input field', () => { + const store = makeMockStore(); + render( + + + + ); + const emailInput = screen.getByRole('textbox', {name: /email address/i}); + expect(emailInput).toBeInTheDocument(); + }); }); describe('Test Forgot Password Form Validations', () => { it('Does not submit form with empty email', async () => { const user = userEvent.setup(); + const store = makeMockStore(); render( - - - + + + ); let emailInput = screen.getByRole('textbox'); @@ -66,10 +73,11 @@ describe('Test Forgot Password Form Validations', () => { it('Does not submit form with invalid email', async () => { const user = userEvent.setup(); + const store = makeMockStore(); render( - - - + + + ); let emailInput = screen.getByRole('textbox'); @@ -87,26 +95,17 @@ describe('Test Forgot Password Form Validations', () => { }); describe('Test Forgot Password Api Error', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - it('Handles API unavailable', async () => { const user = userEvent.setup(); (axios.post as any).mockRejectedValue( - new AxiosError("Network Error", 'ERR_NETWORK', undefined, undefined, { - status: 0, - statusText: 'Network Error', - data: {}, - headers: {}, - config: {}, - }) + new AxiosError("Network Error", 'ERR_NETWORK') ); + const store = makeMockStore(); render( - - - + + + ); let emailInput = screen.getByRole('textbox'); @@ -116,7 +115,7 @@ describe('Test Forgot Password Api Error', () => { await user.click(submitButton); await waitFor(() => { - const state = mockStore.getState(); + const state = store.getState(); expect(state.alert.message).toBe('An error occurred. Please try again.'); expect(state.alert.variant).toBe('danger'); }); @@ -134,11 +133,11 @@ describe('Test Successful Password Reset Request', () => { status: 200, data: { message: 'If the email exists, a reset link has been sent.'}, }); - + const store = makeMockStore(); render( - - - + + + ); let emailInput = screen.getByRole('textbox'); @@ -152,20 +151,9 @@ describe('Test Successful Password Reset Request', () => { ); await waitFor(() => { - const state = mockStore.getState(); + const state = store.getState(); expect(state.alert.variant).toBe('success'); expect(state.alert.message).toBe('A link to reset your password has been sent to your e-mail address.'); }); }); }); -// // Mock the useAPI hook to return mock assignments -// vi.mock("hooks/useAPI", () => ({ -// default: () => ({ -// error: null, -// isLoading: false, -// data: { -// initialUser: userData -// }, -// sendRequest: vi.fn(), -// }) -// })); From f27129a3e8872141fcc45acd52464154fe564b6f Mon Sep 17 00:00:00 2001 From: JaredM2028 Date: Tue, 17 Mar 2026 19:42:17 -0400 Subject: [PATCH 31/42] Initial tests --- .../__tests__/ResetPassword.test.tsx | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 src/pages/Authentication/__tests__/ResetPassword.test.tsx diff --git a/src/pages/Authentication/__tests__/ResetPassword.test.tsx b/src/pages/Authentication/__tests__/ResetPassword.test.tsx new file mode 100644 index 00000000..e79c2bfd --- /dev/null +++ b/src/pages/Authentication/__tests__/ResetPassword.test.tsx @@ -0,0 +1,50 @@ +import React from "react"; +import { render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import ForgotPassword from "../ForgotPassword"; +import { Provider } from "react-redux"; +import { configureStore } from "@reduxjs/toolkit"; +import alertReducer from "store/slices/alertSlice"; +import { vi } from "vitest"; +import axios from "axios"; +import { AxiosError } from "axios"; + +vi.mock('axios'); + +beforeEach(() => { + vi.clearAllMocks(); +}); + +const makeMockStore = () => configureStore({ + reducer: { + alert: alertReducer, + }, +}); + +const validEmail = 'test@example.com'; +const submitText = /request password reset/i; + +describe('Test Forgot Password Displays Correctly', () => { + it('Renders the component correctly', () => { + const store = makeMockStore(); + render( + + + + ); + expect(screen.getByRole('heading', { level: 2 })).toHaveTextContent(/forgotten your password\?/i); + expect(screen.getByText(/enter the email associated with your account/i)).toBeInTheDocument(); + expect(screen.getByRole('button', {name: submitText})).toBeInTheDocument(); + }); + + it('Renders email input field', () => { + const store = makeMockStore(); + render( + + + + ); + const emailInput = screen.getByRole('textbox', {name: /email address/i}); + expect(emailInput).toBeInTheDocument(); + }); +}); \ No newline at end of file From bfd3f21f03528a1a571393c377aa6bfd483f5a83 Mon Sep 17 00:00:00 2001 From: JaredM2028 Date: Tue, 17 Mar 2026 19:47:13 -0400 Subject: [PATCH 32/42] The file was broken because I screwed up git so I fixed it and put the initial PW reset tests back in. --- .../__tests__/ResetPassword.test.tsx | 72 ++++++++++++------- 1 file changed, 47 insertions(+), 25 deletions(-) diff --git a/src/pages/Authentication/__tests__/ResetPassword.test.tsx b/src/pages/Authentication/__tests__/ResetPassword.test.tsx index e79c2bfd..9ef0e215 100644 --- a/src/pages/Authentication/__tests__/ResetPassword.test.tsx +++ b/src/pages/Authentication/__tests__/ResetPassword.test.tsx @@ -1,50 +1,72 @@ import React from "react"; import { render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import ForgotPassword from "../ForgotPassword"; +import ResetPassword from "../ResetPassword"; import { Provider } from "react-redux"; import { configureStore } from "@reduxjs/toolkit"; import alertReducer from "store/slices/alertSlice"; +import { MemoryRouter, Route, Routes } from "react-router-dom"; import { vi } from "vitest"; import axios from "axios"; import { AxiosError } from "axios"; -vi.mock('axios'); +vi.mock("axios"); beforeEach(() => { vi.clearAllMocks(); }); -const makeMockStore = () => configureStore({ +// Simulate arriving from a password-reset email link. +const mockStore = configureStore({ reducer: { alert: alertReducer, }, }); -const validEmail = 'test@example.com'; -const submitText = /request password reset/i; +// Renders ResetPassword inside the required Provider + Router context. +const renderComponent = (token: string | null = "valid-token") => { + const search = token ? `?token=${token}` : ""; + return render( + + + + } /> + {/* Provide a /login route so navigate('/login') doesn't throw */} + Login Page} /> + + + + ); +}; -describe('Test Forgot Password Displays Correctly', () => { - it('Renders the component correctly', () => { - const store = makeMockStore(); - render( - - - +// Test for password reset form rendering and validation +describe("Test Reset Password Displays Correctly", () => { + it("renders the component correctly", () => { + renderComponent(); + + // The page heading should be present + expect(screen.getByRole("heading", { level: 2 })).toHaveTextContent( + /reset your password/i ); - expect(screen.getByRole('heading', { level: 2 })).toHaveTextContent(/forgotten your password\?/i); - expect(screen.getByText(/enter the email associated with your account/i)).toBeInTheDocument(); - expect(screen.getByRole('button', {name: submitText})).toBeInTheDocument(); + + // Both password fields must be in the document + expect(screen.getByLabelText(/^password$/i)).toBeInTheDocument(); + expect(screen.getByLabelText(/confirm password/i)).toBeInTheDocument(); + + // The submit button must be present + expect( + screen.getByRole("button", { name: /reset password/i }) + ).toBeInTheDocument(); }); - it('Renders email input field', () => { - const store = makeMockStore(); - render( - - - - ); - const emailInput = screen.getByRole('textbox', {name: /email address/i}); - expect(emailInput).toBeInTheDocument(); + it("submit button is disabled when fields are empty", () => { + renderComponent(); + + // Formik's `disabled={!(formik.isValid && formik.dirty)}` means the button + // should be disabled on initial render before the user touches anything. + expect( + screen.getByRole("button", { name: /reset password/i }) + ).toBeDisabled(); }); -}); \ No newline at end of file +}); + From 38dc0ee1a78978528c65b899194ba1d9bebce733 Mon Sep 17 00:00:00 2001 From: JaredM2028 Date: Tue, 17 Mar 2026 19:48:11 -0400 Subject: [PATCH 33/42] Testing correct and incorrect password reset submissions. --- .../__tests__/ResetPassword.test.tsx | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/src/pages/Authentication/__tests__/ResetPassword.test.tsx b/src/pages/Authentication/__tests__/ResetPassword.test.tsx index 9ef0e215..6043a3b0 100644 --- a/src/pages/Authentication/__tests__/ResetPassword.test.tsx +++ b/src/pages/Authentication/__tests__/ResetPassword.test.tsx @@ -70,3 +70,73 @@ describe("Test Reset Password Displays Correctly", () => { }); }); +const validPassword = "validpassword"; + +describe("Test Reset Password Form Validations", () => { + it("does not submit form when password is too short", async () => { + const user = userEvent.setup(); + renderComponent(); + + const passwordInput = screen.getByLabelText(/^password$/i); + const submitButton = screen.getByRole("button", { name: /reset password/i }); + + await user.type(passwordInput, "abc"); + await user.tab(); + + await waitFor(() => { + expect(screen.getByText(/password must be at least 6 characters/i)).toBeInTheDocument(); + }); + expect(submitButton).toBeDisabled(); + expect(axios.put).not.toHaveBeenCalled(); + }); + + it("does not submit form when passwords do not match", async () => { + const user = userEvent.setup(); + renderComponent(); + + const passwordInput = screen.getByLabelText(/^password$/i); + const confirmInput = screen.getByLabelText(/confirm password/i); + const submitButton = screen.getByRole("button", { name: /reset password/i }); + + await user.type(passwordInput, validPassword); + await user.type(confirmInput, "differentpassword"); + await user.tab(); // Simulates pressing tab to trigger validation with both fields filled + + await waitFor(() => { + expect(screen.getByText(/passwords do not match/i)).toBeInTheDocument(); + }); + expect(submitButton).toBeDisabled(); + expect(axios.put).not.toHaveBeenCalled(); + }); +}); + +describe("Test Successful Password Reset", () => { + it("submits form successfully", async () => { + const user = userEvent.setup(); + (axios.put as any).mockResolvedValue({ + status: 200, + data: { message: "Password Successfully Updated" }, + }); + + renderComponent(); + + const passwordInput = screen.getByLabelText(/^password$/i); + const confirmInput = screen.getByLabelText(/confirm password/i); + const submitButton = screen.getByRole("button", { name: /reset password/i }); + + await user.type(passwordInput, validPassword); + await user.type(confirmInput, validPassword); + await user.tab(); // simulates pressing tab to trigger validation with both fields filled + await user.click(submitButton); + + await waitFor(() => { + expect(axios.put).toHaveBeenCalledWith( + expect.stringContaining("/password_resets/valid-token"), + { user: { password: validPassword } } + ); + const state = mockStore.getState(); + expect(state.alert.variant).toBe("success"); + expect(state.alert.message).toBe("Password Successfully Updated"); + }); + }); +}); From 6da78a4bb4a07db5791ba2452ded9a92c6fbfeed Mon Sep 17 00:00:00 2001 From: JaredM2028 Date: Tue, 17 Mar 2026 19:49:32 -0400 Subject: [PATCH 34/42] Testing API error. --- .../__tests__/ResetPassword.test.tsx | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/pages/Authentication/__tests__/ResetPassword.test.tsx b/src/pages/Authentication/__tests__/ResetPassword.test.tsx index 6043a3b0..e2cdf3c8 100644 --- a/src/pages/Authentication/__tests__/ResetPassword.test.tsx +++ b/src/pages/Authentication/__tests__/ResetPassword.test.tsx @@ -140,3 +140,34 @@ describe("Test Successful Password Reset", () => { }); }); }); + +describe("Test Reset Password Api Error", () => { + it("handles API unavailable", async () => { + const user = userEvent.setup(); + (axios.put as any).mockRejectedValue( + new AxiosError("Network Error", "ERR_NETWORK") + ); + + renderComponent(); + + const passwordInput = screen.getByLabelText(/^password$/i); + const confirmInput = screen.getByLabelText(/confirm password/i); + const submitButton = screen.getByRole("button", { name: /reset password/i }); + + await user.type(passwordInput, validPassword); + await user.type(confirmInput, validPassword); + await user.tab(); // Simulates pressing tab to trigger validation with both fields filled + await user.click(submitButton); + + await waitFor(() => { + const state = mockStore.getState(); + expect(state.alert.message).toBe("An error occurred. Please try again."); + expect(state.alert.variant).toBe("danger"); + }); + + expect(axios.put).toHaveBeenCalledWith( + expect.stringContaining("/password_resets/valid-token"), + { user: { password: validPassword } } + ); + }); +}); From 52a3ec567b89b499165736085d82bb10507352f2 Mon Sep 17 00:00:00 2001 From: JaredM2028 Date: Tue, 17 Mar 2026 20:27:55 -0400 Subject: [PATCH 35/42] Refactored and added some extra coverage. --- .../__tests__/ResetPassword.test.tsx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/pages/Authentication/__tests__/ResetPassword.test.tsx b/src/pages/Authentication/__tests__/ResetPassword.test.tsx index e2cdf3c8..25afd824 100644 --- a/src/pages/Authentication/__tests__/ResetPassword.test.tsx +++ b/src/pages/Authentication/__tests__/ResetPassword.test.tsx @@ -39,7 +39,21 @@ const renderComponent = (token: string | null = "valid-token") => { ); }; -// Test for password reset form rendering and validation +describe("Test Reset Password Missing Token", () => { + it("redirects to login and shows error when token is missing", async () => { + renderComponent(null); + + await waitFor(() => { + const state = mockStore.getState(); + expect(state.alert.variant).toBe("danger"); + expect(state.alert.message).toBe("Invalid or missing token."); + }); + + // The component should have navigated away — login page is rendered instead + expect(screen.getByText(/login page/i)).toBeInTheDocument(); + }); +}); + describe("Test Reset Password Displays Correctly", () => { it("renders the component correctly", () => { renderComponent(); From 0136a1c3432e51483f70a8ffbc727c79743940a5 Mon Sep 17 00:00:00 2001 From: JaredM2028 Date: Tue, 17 Mar 2026 20:44:36 -0400 Subject: [PATCH 36/42] Adding coverage for API server error message. --- .../__tests__/ResetPassword.test.tsx | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/pages/Authentication/__tests__/ResetPassword.test.tsx b/src/pages/Authentication/__tests__/ResetPassword.test.tsx index 25afd824..f967daec 100644 --- a/src/pages/Authentication/__tests__/ResetPassword.test.tsx +++ b/src/pages/Authentication/__tests__/ResetPassword.test.tsx @@ -184,4 +184,34 @@ describe("Test Reset Password Api Error", () => { { user: { password: validPassword } } ); }); + + it("shows server error message when API returns one", async () => { + const user = userEvent.setup(); + const serverError = new AxiosError("Token has expired.", "ERR_BAD_REQUEST"); + (serverError as any).response = { + status: 422, + statusText: "Unprocessable Entity", + data: { error: "Token has expired." }, + headers: {}, + config: {} as any, + }; + (axios.put as any).mockRejectedValue(serverError); + + renderComponent(); + + const passwordInput = screen.getByLabelText(/^password$/i); + const confirmInput = screen.getByLabelText(/confirm password/i); + const submitButton = screen.getByRole("button", { name: /reset password/i }); + + await user.type(passwordInput, validPassword); + await user.type(confirmInput, validPassword); + await user.tab(); + await user.click(submitButton); + + await waitFor(() => { + const state = mockStore.getState(); + expect(state.alert.message).toBe("Token has expired."); + expect(state.alert.variant).toBe("danger"); + }); + }); }); From 7a50237cd203f22793970105c3f99ee8677dbaba Mon Sep 17 00:00:00 2001 From: JaredM2028 Date: Thu, 19 Mar 2026 17:36:28 -0400 Subject: [PATCH 37/42] Replaced shared mockStore with makeMockStore. --- .../__tests__/ResetPassword.test.tsx | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/src/pages/Authentication/__tests__/ResetPassword.test.tsx b/src/pages/Authentication/__tests__/ResetPassword.test.tsx index f967daec..f37aef0e 100644 --- a/src/pages/Authentication/__tests__/ResetPassword.test.tsx +++ b/src/pages/Authentication/__tests__/ResetPassword.test.tsx @@ -16,18 +16,20 @@ beforeEach(() => { vi.clearAllMocks(); }); -// Simulate arriving from a password-reset email link. -const mockStore = configureStore({ - reducer: { - alert: alertReducer, - }, -}); +const makeMockStore = () => + configureStore({ + reducer: { + alert: alertReducer, + }, + }); // Renders ResetPassword inside the required Provider + Router context. +// Returns the store so each test can inspect its own isolated alert state. const renderComponent = (token: string | null = "valid-token") => { + const store = makeMockStore(); const search = token ? `?token=${token}` : ""; - return render( - + render( + } /> @@ -37,14 +39,15 @@ const renderComponent = (token: string | null = "valid-token") => { ); + return store; }; describe("Test Reset Password Missing Token", () => { it("redirects to login and shows error when token is missing", async () => { - renderComponent(null); + const store = renderComponent(null); await waitFor(() => { - const state = mockStore.getState(); + const state = store.getState(); expect(state.alert.variant).toBe("danger"); expect(state.alert.message).toBe("Invalid or missing token."); }); @@ -132,7 +135,7 @@ describe("Test Successful Password Reset", () => { data: { message: "Password Successfully Updated" }, }); - renderComponent(); + const store = renderComponent(); const passwordInput = screen.getByLabelText(/^password$/i); const confirmInput = screen.getByLabelText(/confirm password/i); @@ -148,7 +151,7 @@ describe("Test Successful Password Reset", () => { expect.stringContaining("/password_resets/valid-token"), { user: { password: validPassword } } ); - const state = mockStore.getState(); + const state = store.getState(); expect(state.alert.variant).toBe("success"); expect(state.alert.message).toBe("Password Successfully Updated"); }); @@ -162,7 +165,7 @@ describe("Test Reset Password Api Error", () => { new AxiosError("Network Error", "ERR_NETWORK") ); - renderComponent(); + const store = renderComponent(); const passwordInput = screen.getByLabelText(/^password$/i); const confirmInput = screen.getByLabelText(/confirm password/i); @@ -174,7 +177,7 @@ describe("Test Reset Password Api Error", () => { await user.click(submitButton); await waitFor(() => { - const state = mockStore.getState(); + const state = store.getState(); expect(state.alert.message).toBe("An error occurred. Please try again."); expect(state.alert.variant).toBe("danger"); }); @@ -197,7 +200,7 @@ describe("Test Reset Password Api Error", () => { }; (axios.put as any).mockRejectedValue(serverError); - renderComponent(); + const store = renderComponent(); const passwordInput = screen.getByLabelText(/^password$/i); const confirmInput = screen.getByLabelText(/confirm password/i); @@ -209,7 +212,7 @@ describe("Test Reset Password Api Error", () => { await user.click(submitButton); await waitFor(() => { - const state = mockStore.getState(); + const state = store.getState(); expect(state.alert.message).toBe("Token has expired."); expect(state.alert.variant).toBe("danger"); }); From ecaf60b10755efce4edbdd78e678a2b6464058f2 Mon Sep 17 00:00:00 2001 From: JaredM2028 Date: Thu, 19 Mar 2026 17:52:42 -0400 Subject: [PATCH 38/42] Avoiding race conditions. --- src/pages/Authentication/__tests__/ResetPassword.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/Authentication/__tests__/ResetPassword.test.tsx b/src/pages/Authentication/__tests__/ResetPassword.test.tsx index f37aef0e..8d916761 100644 --- a/src/pages/Authentication/__tests__/ResetPassword.test.tsx +++ b/src/pages/Authentication/__tests__/ResetPassword.test.tsx @@ -53,7 +53,7 @@ describe("Test Reset Password Missing Token", () => { }); // The component should have navigated away — login page is rendered instead - expect(screen.getByText(/login page/i)).toBeInTheDocument(); + expect(await screen.findByText(/login page/i)).toBeInTheDocument(); }); }); From a2fd150d8f0b53b80572285af59eaf664ae57db1 Mon Sep 17 00:00:00 2001 From: John Weisz Date: Mon, 23 Mar 2026 15:25:07 -0400 Subject: [PATCH 39/42] remove validateOnChange --- src/pages/Authentication/ResetPassword.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/Authentication/ResetPassword.tsx b/src/pages/Authentication/ResetPassword.tsx index 37d08167..4ed478c0 100644 --- a/src/pages/Authentication/ResetPassword.tsx +++ b/src/pages/Authentication/ResetPassword.tsx @@ -83,7 +83,6 @@ const ResetPassword = () => { initialValues={{ password: "", confirmPassword: "" }} onSubmit={onSubmit} validationSchema={validationSchema} - validateOnChange={false} > {(formik) => ( From 24f8cceeaab4eeb21b83f8f4496c0c51c3bfd91b Mon Sep 17 00:00:00 2001 From: John Weisz Date: Mon, 23 Mar 2026 16:04:27 -0400 Subject: [PATCH 40/42] remove validateOnChange for consistency --- src/pages/Authentication/ForgotPassword.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/Authentication/ForgotPassword.tsx b/src/pages/Authentication/ForgotPassword.tsx index 2982fa90..4681d3d7 100644 --- a/src/pages/Authentication/ForgotPassword.tsx +++ b/src/pages/Authentication/ForgotPassword.tsx @@ -55,7 +55,6 @@ const ForgotPassword = () => { initialValues={{ email: "" }} onSubmit={onSubmit} validationSchema={validationSchema} - validateOnChange={false} > {(formik) => ( From 881f8a609526dba3074a72133a121c4cd6abb508 Mon Sep 17 00:00:00 2001 From: John Weisz Date: Sat, 28 Mar 2026 15:14:45 -0400 Subject: [PATCH 41/42] rm docker-compose.yml diffs --- docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.yml b/docker-compose.yml index 3253e94f..4ad3bc7a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,3 +7,4 @@ services: ports: - "8080:80" restart: unless-stopped + From 1cdc6ae37b595d9e814bd705b90e55fa47e81f50 Mon Sep 17 00:00:00 2001 From: josev814 Date: Fri, 3 Apr 2026 18:29:47 -0400 Subject: [PATCH 42/42] When viewing the login/forgot/reset pages the container doesn't have padding above the container Changing from mt-xxl-5 to mt-5 resolves the issue and keeps a top margin of 5 This change also needed to be done on the Login page --- src/pages/Authentication/ForgotPassword.tsx | 2 +- src/pages/Authentication/Login.tsx | 2 +- src/pages/Authentication/ResetPassword.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/Authentication/ForgotPassword.tsx b/src/pages/Authentication/ForgotPassword.tsx index 4681d3d7..c8bcb12f 100644 --- a/src/pages/Authentication/ForgotPassword.tsx +++ b/src/pages/Authentication/ForgotPassword.tsx @@ -48,7 +48,7 @@ const ForgotPassword = () => { }; return ( - +

Forgotten Your Password?

{ }; return ( - +

Login

{ }; return ( - +

Reset Your Password