Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
96 changes: 75 additions & 21 deletions src/app/components/elements/inputs/RegistrationPasswordInputs.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import React, { useState } from "react";
import { Col, FormFeedback, FormGroup, Label, Row, UncontrolledTooltip } from "reactstrap";
import { PasswordFeedback, ValidationUser } from "../../../../IsaacAppTypes";
import { Col, FormGroup, Label, Row, UncontrolledTooltip } from "reactstrap";
import { ValidationUser } from "../../../../IsaacAppTypes";
import { Immutable } from "immer";
import { PASSWORD_REQUIREMENTS, passwordDebounce, validatePassword } from "../../../services";
import {
PASSWORD_REQUIREMENTS,
validatePassword,
validatePasswordMatch,
getPasswordValidationErrors,
} from "../../../services";
import Password from "./Password";

interface PasswordInputProps {
userToUpdate: Immutable<ValidationUser>;
setUserToUpdate: (user: Immutable<ValidationUser>) => void;
Expand All @@ -22,9 +28,19 @@ export const RegistrationPasswordInputs = ({
defaultPassword,
}: PasswordInputProps) => {
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
const [passwordFeedback, setPasswordFeedback] = useState<PasswordFeedback | null>(null);
const [passwordTouched, setPasswordTouched] = useState(false);
const [confirmTouched, setConfirmTouched] = useState(false);

const passwordIsValid = validatePassword(unverifiedPassword || "");
const passwordsMatch = validatePasswordMatch(unverifiedPassword || "", userToUpdate.password || "");
const bothPasswordsValid = passwordIsValid && passwordsMatch;

// Get specific validation errors for display
const passwordErrors = unverifiedPassword ? getPasswordValidationErrors(unverifiedPassword) : [];

const passwordIsValid = userToUpdate.password == unverifiedPassword && validatePassword(userToUpdate.password || "");
// Show errors if submission attempted OR if user has touched the field and there are errors
const showPasswordErrors = (submissionAttempted || passwordTouched) && !!unverifiedPassword && !passwordIsValid;
const showMatchError = (submissionAttempted || confirmTouched) && !!userToUpdate.password && !passwordsMatch;

return (
<Row>
Expand All @@ -42,21 +58,49 @@ export const RegistrationPasswordInputs = ({
isPasswordVisible={isPasswordVisible}
setIsPasswordVisible={setIsPasswordVisible}
defaultValue={defaultPassword}
invalid={showPasswordErrors ? true : undefined}
onChange={(e) => {
passwordDebounce(e.target.value, setPasswordFeedback);
const newValue = e.target.value;
setUnverifiedPassword(newValue);
// Mark as touched when user starts typing
if (newValue.length > 0) {
setPasswordTouched(true);
}
}}
onBlur={(e) => {
setUnverifiedPassword(e.target.value);
passwordDebounce(e.target.value, setPasswordFeedback);
const newValue = e.target.value;
setUnverifiedPassword(newValue);
setPasswordTouched(true);
}}
showToggleIcon={true}
required={true}
/>
{passwordFeedback && (
<span className="float-right small mt-1">
<strong>Password strength: </strong>
<span id="password-strength-feedback">{passwordFeedback.feedbackText}</span>
</span>

{/* Show validation errors */}
{showPasswordErrors && passwordErrors.length > 0 && (
<div className="invalid-feedback d-block mt-2">
<strong>Password requirements:</strong>
<ul className="mb-0 pl-3 mt-1" style={{ fontSize: "0.875rem" }}>
{passwordErrors.map((error) => {
const parts = error.split(/\n/);
return (
<li key={error}>
{parts.map((line, i) => (
<React.Fragment key={`${error}-${i}`}>
{i > 0 && <br />}
{line}
</React.Fragment>
))}
</li>
);
})}
</ul>
</div>
)}

{/* If no password entered on submit */}
{submissionAttempted && (!unverifiedPassword || unverifiedPassword.length === 0) && (
<div className="invalid-feedback d-block mt-2">Password is required</div>
)}
</FormGroup>
</Col>
Expand All @@ -70,20 +114,30 @@ export const RegistrationPasswordInputs = ({
isPasswordVisible={isPasswordVisible}
setIsPasswordVisible={setIsPasswordVisible}
disabled={!unverifiedPassword}
invalid={submissionAttempted && !passwordIsValid}
invalid={showMatchError || (submissionAttempted && !bothPasswordsValid) ? true : undefined}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setUserToUpdate({ ...userToUpdate, password: e.target.value });
if (e.target.value.length > 0) {
setConfirmTouched(true);
}
}}
onBlur={() => {
setConfirmTouched(true);
}}
ariaDescribedBy="invalidPassword"
required={true}
/>
{/* Feedback that appears for password match before submission */}
<FormFeedback id="invalidPassword" className="always-show">
{userToUpdate.password &&
(!(userToUpdate.password == unverifiedPassword)
? "Passwords don't match."
: !validatePassword(userToUpdate.password || "") && PASSWORD_REQUIREMENTS)}
</FormFeedback>

{/* Feedback for password match */}
{showMatchError && <div className="invalid-feedback d-block mt-2">Passwords do not match</div>}

{(submissionAttempted || confirmTouched) && userToUpdate.password && passwordsMatch && !passwordIsValid && (
<div className="invalid-feedback d-block mt-2">Please ensure your password meets all requirements</div>
)}

{submissionAttempted && (!userToUpdate.password || userToUpdate.password.length === 0) && (
<div className="invalid-feedback d-block mt-2">Please confirm your password</div>
)}
</FormGroup>
</Col>
</Row>
Expand Down
4 changes: 1 addition & 3 deletions src/app/services/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,7 @@ export const getPasswordValidationErrors = (password: string): string[] => {
errors.push("Password must contain at least one uppercase letter");
}
if (!/[!-/:-@[-`{-~]/.test(password)) {
errors.push(
"Password must contain at least one punctuation character (e.g., !@#$%^&*()-_=+[]{};:'\",.<>/?\\|`~)",
);
errors.push("Password must contain at least one special character \n (e.g., !@#$%^&*()-_=+[]{};:'\",.<>/?\\|`~)");
}
return errors;
} else {
Expand Down
5 changes: 3 additions & 2 deletions src/test/pages/StudentRegistration.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import userEvent from "@testing-library/user-event";
import { StudentRegistration } from "../../app/components/pages/StudentRegistration";
import * as actions from "../../app/state/actions";
import { rest } from "msw";
import { API_PATH, PASSWORD_REQUIREMENTS } from "../../app/services";
import { API_PATH } from "../../app/services";
import { registrationMockUser, registrationUserData } from "../../mocks/data";

const registerUserSpy = jest.spyOn(actions, "registerUser");
Expand Down Expand Up @@ -82,7 +82,8 @@ describe("Student Registration", () => {
const consentCheckbox = screen.getByRole("checkbox", { name: "Consent checkbox" });
await userEvent.click(consentCheckbox);
await clickButton("Register my account");
const pwErrorMessage = screen.getByText(PASSWORD_REQUIREMENTS);
// Check for password validation error messages (wrongPassword is "test1234" which lacks uppercase and special char)
const pwErrorMessage = screen.getByText(/Password requirements:/i);
expect(pwErrorMessage).toBeVisible();
const generalError = screen.getByRole("heading", {
name: /please fill out all fields/i,
Expand Down
7 changes: 4 additions & 3 deletions src/test/pages/TeacherRegistration.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import userEvent from "@testing-library/user-event";
import { TeacherRegistration } from "../../app/components/pages/TeacherRegistration";
import * as actions from "../../app/state/actions";
import { rest } from "msw";
import { API_PATH, PASSWORD_REQUIREMENTS } from "../../app/services";
import { API_PATH } from "../../app/services";
import { registrationMockUser, registrationUserData } from "../../mocks/data";

const registerUserSpy = jest.spyOn(actions, "registerUser");
Expand Down Expand Up @@ -124,11 +124,12 @@ describe("Teacher Registration", () => {
await fillFormCorrectly(false, "teacher");
const formFields = getFormFields();
const { password, confirmPassword, stage, noSchool, email } = formFields;
const pwErrorMessage = screen.getByText(PASSWORD_REQUIREMENTS);
// Check for password validation error messages (wrongPassword is "test1234" which lacks uppercase and special char)
const pwErrorMessage = screen.getByText(/Password requirements:/i);
expect(pwErrorMessage).toBeVisible();
// update PW to meet requirements but not match the confirmation, and observe error changes
await fillTextField(confirmPassword(), registrationUserData.password);
const newPwErrorMessage = screen.getByText(/Passwords don't match/i);
const newPwErrorMessage = screen.getByText(/Passwords do not match/i);
expect(newPwErrorMessage).toBeVisible();
// update PW to meet requirements and try to submit, observe error messages for school, stage and email address
await fillTextField(password(), registrationUserData.password);
Expand Down
Loading