Skip to content
Open
Show file tree
Hide file tree
Changes from 54 commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
6f38e94
Added page for Forgot Password
galav12 Mar 24, 2025
c160dd8
Added Reset Password Page
galav12 Mar 24, 2025
c81dc61
Merge pull request #4 from itsJash/forgot-password
itsJash Mar 24, 2025
4ad0cf0
Merge remote-tracking branch 'jash/main' into E2604-password-reset
johnmweisz Mar 4, 2026
f686384
add reset pages
johnmweisz Mar 5, 2026
c8e43dd
use env base url
johnmweisz Mar 5, 2026
6623f3f
add env base url and fix service path
johnmweisz Mar 5, 2026
7f34383
Merge pull request #2 from johnmweisz/E2604-password-reset
JaredM2028 Mar 6, 2026
d02f2eb
Centralizing the Api contant
josev814 Mar 6, 2026
8a0f776
fixing directory name
josev814 Mar 6, 2026
a73c3a2
Merge pull request #3 from johnmweisz/jvargas6/api_url_configurable
josev814 Mar 6, 2026
7f95d34
Dispatch alert before naviation to ensure the alert renders before un…
JaredM2028 Mar 6, 2026
202dc06
Protection against null password tokens. Broken link, manual navigati…
JaredM2028 Mar 6, 2026
f711f05
API_BASE_URL constant pulled in from @/constants/Api for ResetPasswor…
JaredM2028 Mar 6, 2026
6bed931
Standardize styling to match Login_tsx.
JaredM2028 Mar 6, 2026
b85b63e
Using Yup and Formik
JaredM2028 Mar 6, 2026
fae498d
Had to tweak vite config file to make it run on my machine.
JaredM2028 Mar 6, 2026
4efd32a
Reverting config files to original
JaredM2028 Mar 6, 2026
95f57f9
Revert gitignore
JaredM2028 Mar 6, 2026
35c0192
Update vite.config.ts for backend url
josev814 Mar 6, 2026
ced6c9e
Merge pull request #7 from johnmweisz/josev814-patch-1
josev814 Mar 7, 2026
58bf0c2
Restored vite config to repo original.
JaredM2028 Mar 7, 2026
e9dae03
Addressed feedback for ResetPassword
JaredM2028 Mar 7, 2026
58d7de8
Attempting to remove vite config from commit history
JaredM2028 Mar 7, 2026
5634ab5
Merge branch 'development' into front_end_refinements_pt_1
JaredM2028 Mar 7, 2026
54c92b2
Strengthen fallback alert for all submission errors in ForgotPassword…
JaredM2028 Mar 7, 2026
17302b8
Merge remote-tracking branch 'refs/remotes/origin/front_end_refinemen…
JaredM2028 Mar 7, 2026
dca5ef4
Improve fallback and condense error catching
JaredM2028 Mar 10, 2026
734f24b
Merge pull request #5 from johnmweisz/front_end_refinements_pt_1
johnmweisz Mar 10, 2026
e645db3
use header h2
johnmweisz Mar 11, 2026
8aafdbc
Merge pull request #9 from johnmweisz/jweisz/fix-header
johnmweisz Mar 11, 2026
6c33f89
adjust prompt style/text
johnmweisz Mar 12, 2026
07ba090
Merge pull request #10 from johnmweisz/jweisz/adjust-prompt
JaredM2028 Mar 12, 2026
4b5d549
Adding tests to cover ForgotPassword with vitest
josev814 Mar 12, 2026
8237ed6
forgot to commit forgotpassword update
josev814 Mar 12, 2026
8f0ffe3
Merge branch 'main' into development
johnmweisz Mar 12, 2026
16e641e
Merge branch 'development' into jvargas6/vitest_forgot_password
josev814 Mar 14, 2026
03d8857
Merge branch 'main' into development
johnmweisz Mar 16, 2026
4bebeff
revert changes
johnmweisz Mar 16, 2026
296dd27
revert changes
johnmweisz Mar 16, 2026
caf6dfc
revert changes
johnmweisz Mar 16, 2026
847c497
revert changes
johnmweisz Mar 16, 2026
86b34db
Merge branch 'development' into jvargas6/vitest_forgot_password
josev814 Mar 17, 2026
e4db480
apply ai suggestions from PR review
josev814 Mar 17, 2026
ac459d3
Merge pull request #11 from johnmweisz/jvargas6/vitest_forgot_password
josev814 Mar 17, 2026
f27129a
Initial tests
JaredM2028 Mar 17, 2026
bfd3f21
The file was broken because I screwed up git so I fixed it and put th…
JaredM2028 Mar 17, 2026
38dc0ee
Testing correct and incorrect password reset submissions.
JaredM2028 Mar 17, 2026
6da78a4
Testing API error.
JaredM2028 Mar 17, 2026
52a3ec5
Refactored and added some extra coverage.
JaredM2028 Mar 18, 2026
0136a1c
Adding coverage for API server error message.
JaredM2028 Mar 18, 2026
7a50237
Replaced shared mockStore with makeMockStore.
JaredM2028 Mar 19, 2026
ecaf60b
Avoiding race conditions.
JaredM2028 Mar 19, 2026
243e42b
Merge pull request #12 from johnmweisz/vitest_reset_password
JaredM2028 Mar 20, 2026
db9306d
Merge branch 'expertiza:main' into development
johnmweisz Mar 22, 2026
a2fd150
remove validateOnChange
johnmweisz Mar 23, 2026
24f8cce
remove validateOnChange for consistency
johnmweisz Mar 23, 2026
dea3a39
Merge pull request #18 from johnmweisz/jweisz/fix-submit-disabled
johnmweisz Mar 23, 2026
881f8a6
rm docker-compose.yml diffs
johnmweisz Mar 28, 2026
1cdc6ae
When viewing the login/forgot/reset pages the container doesn't have …
josev814 Apr 3, 2026
8b76b9a
Merge pull request #19 from johnmweisz/jvargas6/ui_form_padding
johnmweisz Apr 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,3 @@ services:
ports:
- "8080:80"
restart: unless-stopped

4 changes: 4 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -63,6 +65,8 @@ function App() {
children: [
{ index: true, element: <ProtectedRoute element={<Home />} /> },
{ path: "login", element: <Login /> },
{ path: "forgot-password", element: <ForgotPassword /> },
{ path: "password_edit/check_reset_url", element: <ResetPassword /> },
{ path: "logout", element: <ProtectedRoute element={<Logout />} /> },

{
Expand Down
1 change: 1 addition & 0 deletions src/constants/Api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || "http://localhost:3002";
85 changes: 85 additions & 0 deletions src/pages/Authentication/ForgotPassword.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
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 * as Yup from "yup";

interface IForgotPasswordFormValues {
email: string;
}

const validationSchema = Yup.object({
email: Yup.string().trim().email("Invalid email address").required("Required"),
});

const ForgotPassword = () => {
const dispatch = useDispatch();

const onSubmit = async (
values: IForgotPasswordFormValues,
submitProps: FormikHelpers<IForgotPasswordFormValues>
) => {
try {
await axios.post(`${API_BASE_URL}/password_resets`, { email: values.email });
Comment thread
johnmweisz marked this conversation as resolved.
dispatch(
alertActions.showAlert({
variant: "success",
message: "A link to reset your password has been sent to your e-mail address.",
})
);
} 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;
errorFallback = errorMessage || errorFallback;
}
dispatch(
alertActions.showAlert({
variant: "danger",
message: errorFallback,
})
);
}
submitProps.setSubmitting(false);
};

return (
<Container className="d-flex justify-content-center mt-xxl-5">
<Col xs={12} md={6} lg={4}>
<h2 className="text-center">Forgotten Your Password?</h2>
<Formik
initialValues={{ email: "" }}
onSubmit={onSubmit}
validationSchema={validationSchema}
validateOnChange={false}
>
{(formik) => (
<Form>
<p>Enter the email associated with your account</p>
<FormInput
controlId="forgot-password-email"
label="Email Address"
name="email"
type="email"
/>
<Button
style={{ width: "100%" }}
variant="primary"
type="submit"
disabled={!(formik.isValid && formik.dirty) || formik.isSubmitting}
>
Request Password Reset
</Button>
</Form>
)}
</Formik>
</Col>
</Container>
);
};

export default ForgotPassword;
118 changes: 118 additions & 0 deletions src/pages/Authentication/ResetPassword.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
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 * 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 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");
}
}, [token, dispatch, navigate]);

const onSubmit = async (
values: IResetPasswordFormValues,
submitProps: FormikHelpers<IResetPasswordFormValues>
) => {
try {
// Send password reset request to the backend
await axios.put(`${API_BASE_URL}/password_resets/${token}`, {
Comment thread
johnmweisz marked this conversation as resolved.
user: { password: values.password },
});
dispatch(
alertActions.showAlert({
variant: "success",
message: "Password Successfully Updated",
})
);
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;
errorFallback = errorMessage || errorFallback;
}
dispatch(
alertActions.showAlert({
variant: "danger",
message: errorFallback,
})
);
}
submitProps.setSubmitting(false);
};

return (
<Container className="d-flex justify-content-center mt-xxl-5">
<Col xs={12} md={6} lg={4}>
<h2 className="text-center">Reset Your Password</h2>
<Formik
initialValues={{ password: "", confirmPassword: "" }}
onSubmit={onSubmit}
validationSchema={validationSchema}
validateOnChange={false}
>
{(formik) => (
<Form>
<FormInput
controlId="reset-password"
label="Password"
name="password"
type="password"
/>
<FormInput
controlId="reset-confirm-password"
label="Confirm Password"
name="confirmPassword"
type="password"
/>
<Button
style={{ width: "100%", marginTop: "8px" }}
variant="primary"
type="submit"
disabled={!(formik.isValid && formik.dirty) || formik.isSubmitting}
>
Reset Password
</Button>
</Form>
)}
</Formik>
</Col>
</Container>
);
};

export default ResetPassword;
159 changes: 159 additions & 0 deletions src/pages/Authentication/__tests__/ForgotPassword.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
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 = '[email protected]';
const submitText = /request password reset/i;

describe('Test Forgot Password Displays Correctly', () => {
it('Renders the component correctly', () => {
const store = makeMockStore();
render(
<Provider store={store}>
<ForgotPassword />
</Provider>
);
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(
<Provider store={store}>
<ForgotPassword />
</Provider>
);
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(
<Provider store={store}>
<ForgotPassword />
</Provider>
);

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();
const store = makeMockStore();
render(
<Provider store={store}>
<ForgotPassword />
</Provider>
);

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', () => {
it('Handles API unavailable', async () => {
const user = userEvent.setup();
(axios.post as any).mockRejectedValue(
new AxiosError("Network Error", 'ERR_NETWORK')
);

const store = makeMockStore();
render(
<Provider store={store}>
<ForgotPassword />
</Provider>
);

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 = store.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.'},
});
const store = makeMockStore();
render(
<Provider store={store}>
<ForgotPassword />
</Provider>
);

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 = 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.');
});
});
});
Loading