diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json
index 759ed34594..34cf6e7386 100644
--- a/public/locales/en/translation.json
+++ b/public/locales/en/translation.json
@@ -82,6 +82,7 @@
"resetRequestInstructions": "We will send a one-time reset link for your account to your email.",
"submit": "Submit",
"resetFail": "Password reset error",
+ "resetSuccess": "Password reset successful",
"resetDone": "If you have correctly entered your email or username, a password reset link has been sent to your email address.",
"backToLogin": "Back To Login"
},
diff --git a/src/components/InvalidLink/index.tsx b/src/components/InvalidLink/index.tsx
index 2ba1c9402e..5ce22d8e5b 100644
--- a/src/components/InvalidLink/index.tsx
+++ b/src/components/InvalidLink/index.tsx
@@ -1,5 +1,12 @@
import { Help } from "@mui/icons-material";
-import { Button, Card, CardContent, Grid, Typography } from "@mui/material";
+import {
+ Button,
+ Card,
+ CardContent,
+ CardHeader,
+ Grid2,
+ Typography,
+} from "@mui/material";
import { ReactElement } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router";
@@ -17,44 +24,47 @@ export default function InvalidLink(props: InvalidLinkProps): ReactElement {
const idAffix = "invalid-link";
return (
-
-
+
+
+
+ {t(props.textId)}
+
+ }
+ />
+
-
- {t(props.textId)}
-
{/* User Guide, Sign Up, and Log In buttons */}
-
-
+
+
-
+
-
-
-
+
-
-
-
-
+
+
-
+
);
}
diff --git a/src/components/Login/Captcha.tsx b/src/components/Login/Captcha.tsx
index b1b03a5ed4..30ed3979b9 100644
--- a/src/components/Login/Captcha.tsx
+++ b/src/components/Login/Captcha.tsx
@@ -5,7 +5,6 @@ import { toast } from "react-toastify";
import { verifyCaptchaToken } from "backend";
import { RuntimeConfig } from "types/runtimeConfig";
-import theme from "types/theme";
export interface CaptchaProps {
/** Parent function to call when CAPTCHA succeeds or fails. */
@@ -42,7 +41,6 @@ export default function Captcha(props: CaptchaProps): ReactElement {
onSuccess={verify}
options={{ language: i18n.resolvedLanguage, theme: "light" }}
siteKey={RuntimeConfig.getInstance().captchaSiteKey()}
- style={{ marginBottom: theme.spacing(1) }}
/>
) : (
diff --git a/src/components/Login/Login.tsx b/src/components/Login/Login.tsx
index bb5d43ce84..d4b1af267e 100644
--- a/src/components/Login/Login.tsx
+++ b/src/components/Login/Login.tsx
@@ -3,8 +3,10 @@ import {
Button,
Card,
CardContent,
- Grid,
+ CardHeader,
+ Grid2,
Link,
+ Stack,
TextFieldProps,
Typography,
} from "@mui/material";
@@ -29,9 +31,9 @@ import { type StoreState } from "rootRedux/types";
import router from "router/browserRouter";
import { Path } from "types/path";
import { RuntimeConfig } from "types/runtimeConfig";
-import theme from "types/theme";
import { NormalizedTextField } from "utilities/fontComponents";
import { openUserGuide } from "utilities/pathUtilities";
+import { meetsPasswordRequirements } from "utilities/utilities";
export enum LoginId {
ButtonLogIn = "login-log-in-button",
@@ -46,6 +48,7 @@ export enum LoginTextId {
ButtonLogin = "login.login",
ButtonSignUp = "login.signUp",
Error401 = "login.failed",
+ ErrorPassword = "login.passwordRequirements",
ErrorUnknown = "login.failedUnknownReason",
FieldError = "login.required",
LabelPassword = "login.password",
@@ -90,10 +93,11 @@ export default function Login(): ReactElement {
const logIn = (e: FormEvent): void => {
e.preventDefault();
const p = password.trim();
+ const pOk = meetsPasswordRequirements(p);
const u = username.trim();
- setPasswordError(!p);
+ setPasswordError(!pOk);
setUsernameError(!u);
- if (p && u) {
+ if (pOk && u) {
dispatch(asyncLogIn(u, p));
}
};
@@ -108,129 +112,119 @@ export default function Login(): ReactElement {
});
return (
-
-
-
+
+
+ {t(LoginTextId.ButtonLogin)}
+
+
+
+ {/* Login announcement banner */}
+ {!!banner && {banner}}
+
+
+
-
+
);
}
diff --git a/src/components/Login/Signup.tsx b/src/components/Login/Signup.tsx
index 35ff3f1b3e..f491a57124 100644
--- a/src/components/Login/Signup.tsx
+++ b/src/components/Login/Signup.tsx
@@ -2,7 +2,9 @@ import {
Button,
Card,
CardContent,
- Grid,
+ CardHeader,
+ Grid2,
+ Stack,
TextField,
TextFieldProps,
Typography,
@@ -187,95 +189,94 @@ export default function Signup(props: SignupProps): ReactElement {
});
return (
-
-
-
+
+
+
+
-
+
);
}
diff --git a/src/components/PasswordReset/Request.tsx b/src/components/PasswordReset/Request.tsx
index f8acf7d088..d17062d288 100644
--- a/src/components/PasswordReset/Request.tsx
+++ b/src/components/PasswordReset/Request.tsx
@@ -1,4 +1,12 @@
-import { Button, Card, Grid, Typography } from "@mui/material";
+import {
+ Button,
+ Card,
+ CardContent,
+ CardHeader,
+ Grid2,
+ Stack,
+ Typography,
+} from "@mui/material";
import { FormEvent, ReactElement, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router";
@@ -40,71 +48,81 @@ export default function ResetRequest(): ReactElement {
};
return (
-
-
-
- {t("passwordReset.resetRequestTitle")}
-
- {isDone ? (
- <>
- {t("passwordReset.resetDone")}
-
+
+
+
+ {t("passwordReset.resetRequestTitle")}
+
+ }
+ />
+
+
+ {isDone ? (
+
+ {t("passwordReset.resetDone")}
+
-
- >
- ) : (
- <>
-
- {t("passwordReset.resetRequestInstructions")}
-
+
+ ) : (
-
+
+
+ {t("passwordReset.resetRequestInstructions")}
+
+
setEmailOrUsername(e.target.value)}
required
- type="text"
- style={{ width: "100%" }}
value={emailOrUsername}
- variant="outlined"
/>
-
-
+
-
-
- onSubmit,
- variant: "contained",
- }}
- disabled={!emailOrUsername || !isVerified}
- loading={isLoading}
- >
- {t("passwordReset.submit")}
-
-
+
+ {/* Back-to-login and Submit buttons */}
+
+
+
+
+ {t("passwordReset.submit")}
+
+
+
- >
- )}
+ )}
+
-
+
);
}
diff --git a/src/components/PasswordReset/ResetPage.tsx b/src/components/PasswordReset/ResetPage.tsx
index ae4f6955de..fca0f30ad0 100644
--- a/src/components/PasswordReset/ResetPage.tsx
+++ b/src/components/PasswordReset/ResetPage.tsx
@@ -1,14 +1,16 @@
-import ExitToAppIcon from "@mui/icons-material/ExitToApp";
-import { Button, Card, Grid, Typography } from "@mui/material";
import {
- type FormEvent,
- type ReactElement,
- useCallback,
- useEffect,
- useState,
-} from "react";
+ Button,
+ Card,
+ CardContent,
+ CardHeader,
+ Grid2,
+ Stack,
+ Typography,
+} from "@mui/material";
+import { type FormEvent, type ReactElement, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate, useParams } from "react-router";
+import { toast } from "react-toastify";
import { resetPassword, validateResetToken } from "backend";
import InvalidLink from "components/InvalidLink";
@@ -16,23 +18,12 @@ import { Path } from "types/path";
import { NormalizedTextField } from "utilities/fontComponents";
import { meetsPasswordRequirements } from "utilities/utilities";
-export enum PasswordResetTestIds {
+export enum PasswordResetIds {
Password = "PasswordReset.password",
- PasswordReqError = "PasswordReset.requirements-error",
ConfirmPassword = "PasswordReset.confirm-password",
- PasswordMatchError = "PasswordReset.match-error",
- PasswordResetFail = "PasswordReset.reset-fail",
- BackToLoginButton = "PasswordReset.button.back-to-login",
SubmitButton = "PasswordReset.button.submit",
}
-enum RequestState {
- None,
- Attempt,
- Fail,
- Success,
-}
-
export default function PasswordReset(): ReactElement {
const navigate = useNavigate();
const { token } = useParams();
@@ -44,26 +35,15 @@ export default function PasswordReset(): ReactElement {
const [passwordConfirm, setPasswordConfirm] = useState("");
const [passwordFitsRequirements, setPasswordFitsRequirements] =
useState(false);
- const [requestState, setRequestState] = useState(RequestState.None);
- const validateLink = useCallback(async (): Promise => {
+ useEffect(() => {
if (token) {
- setIsValidLink(await validateResetToken(token));
+ validateResetToken(token).then(setIsValidLink);
}
}, [token]);
- useEffect(() => {
- validateLink();
- });
-
- const backToLogin = (e: FormEvent): void => {
- e.preventDefault();
- navigate(Path.Login);
- };
-
const onSubmit = async (e: FormEvent): Promise => {
if (token) {
- setRequestState(RequestState.Attempt);
await asyncReset(token, password);
e.preventDefault();
}
@@ -81,114 +61,71 @@ export default function PasswordReset(): ReactElement {
const asyncReset = async (token: string, password: string): Promise => {
if (await resetPassword(token, password)) {
- setRequestState(RequestState.Success);
- navigate(Path.Login);
+ toast.success(t("passwordReset.resetSuccess"));
} else {
- setRequestState(RequestState.Fail);
+ toast.error(t("passwordReset.resetFail"));
}
+ navigate(Path.Login);
};
return isValidLink ? (
-
-
-
-
- {t("passwordReset.resetTitle")}
-
-
+
+
+
+ {t("passwordReset.resetTitle")}
+
+ }
+ />
+
+
+
onChangePassword(e.target.value, passwordConfirm)
}
+ type="password"
+ value={password}
/>
- {!passwordFitsRequirements && (
-
- {t("login.passwordRequirements")}
-
- )}
-
-
+
0}
+ fullWidth
+ helperText={
+ !isPasswordConfirmed &&
+ passwordConfirm.length > 0 &&
+ t("login.confirmPasswordError")
+ }
+ id={PasswordResetIds.ConfirmPassword}
+ inputProps={{ "data-testid": PasswordResetIds.ConfirmPassword }}
label={t("login.confirmPassword")}
+ onChange={(e) => onChangePassword(password, e.target.value)}
type="password"
value={passwordConfirm}
- style={{ width: "100%" }}
- margin="normal"
- error={!isPasswordConfirmed && passwordConfirm.length > 0}
- onChange={(e) => onChangePassword(password, e.target.value)}
/>
- {!isPasswordConfirmed && passwordConfirm.length > 0 && (
-
- {t("login.confirmPasswordError")}
-
- )}
-
-
-
- {requestState === RequestState.Fail ? (
- <>
-
- {t("passwordReset.resetFail")}
-
-
- >
- ) : (
-
- )}
-
-
-
+
+
+
-
+
) : (
);
diff --git a/src/components/PasswordReset/tests/Request.test.tsx b/src/components/PasswordReset/tests/Request.test.tsx
index 44f4d16359..dd8a43479e 100644
--- a/src/components/PasswordReset/tests/Request.test.tsx
+++ b/src/components/PasswordReset/tests/Request.test.tsx
@@ -46,18 +46,21 @@ describe("ResetRequest", () => {
await renderUserSettings();
// Before
- const button = screen.getByRole("button");
- expect(button).toBeDisabled();
+ const login = screen.getByTestId(PasswordRequestIds.ButtonLogin);
+ const submit = screen.getByTestId(PasswordRequestIds.ButtonSubmit);
+ expect(login).toBeEnabled();
+ expect(submit).toBeDisabled();
// Agent
const field = screen.getByTestId(PasswordRequestIds.FieldEmailOrUsername);
await agent.type(field, "a");
// After
- expect(button).toBeEnabled();
+ expect(login).toBeEnabled();
+ expect(submit).toBeEnabled();
});
- it("after submit, removes text field and submit button and reveals login button", async () => {
+ it("after submit, removes text field and submit button", async () => {
// Setup
const agent = userEvent.setup();
await renderUserSettings();
@@ -66,19 +69,19 @@ describe("ResetRequest", () => {
expect(
screen.queryByTestId(PasswordRequestIds.FieldEmailOrUsername)
).toBeTruthy();
+ expect(screen.queryByTestId(PasswordRequestIds.ButtonLogin)).toBeTruthy();
expect(screen.queryByTestId(PasswordRequestIds.ButtonSubmit)).toBeTruthy();
- expect(screen.queryByTestId(PasswordRequestIds.ButtonLogin)).toBeNull();
// Agent
const field = screen.getByTestId(PasswordRequestIds.FieldEmailOrUsername);
await agent.type(field, "a");
- await agent.click(screen.getByRole("button"));
+ await agent.click(screen.getByTestId(PasswordRequestIds.ButtonSubmit));
// After
expect(
screen.queryByTestId(PasswordRequestIds.FieldEmailOrUsername)
).toBeNull();
- expect(screen.queryByTestId(PasswordRequestIds.ButtonSubmit)).toBeNull();
expect(screen.queryByTestId(PasswordRequestIds.ButtonLogin)).toBeTruthy();
+ expect(screen.queryByTestId(PasswordRequestIds.ButtonSubmit)).toBeNull();
});
});
diff --git a/src/components/PasswordReset/tests/ResetPage.test.tsx b/src/components/PasswordReset/tests/ResetPage.test.tsx
index c75acf6967..23a31a5d9e 100644
--- a/src/components/PasswordReset/tests/ResetPage.test.tsx
+++ b/src/components/PasswordReset/tests/ResetPage.test.tsx
@@ -13,7 +13,7 @@ import { MemoryRouter, Route, Routes } from "react-router";
import configureMockStore from "redux-mock-store";
import PasswordReset, {
- PasswordResetTestIds,
+ PasswordResetIds,
} from "components/PasswordReset/ResetPage";
import { Path } from "types/path";
@@ -65,115 +65,56 @@ const customRender = async (
};
describe("PasswordReset", () => {
- it("renders with password length error", async () => {
+ it("disables button when password too short", async () => {
const user = userEvent.setup();
await customRender();
const shortPassword = "foo";
- const passwdField = screen.getByTestId(PasswordResetTestIds.Password);
- const passwdConfirm = screen.getByTestId(
- PasswordResetTestIds.ConfirmPassword
- );
+ const passwdField = screen.getByTestId(PasswordResetIds.Password);
+ const passwdConfirm = screen.getByTestId(PasswordResetIds.ConfirmPassword);
await user.type(passwdField, shortPassword);
await user.type(passwdConfirm, shortPassword);
- const reqErrors = screen.queryAllByTestId(
- PasswordResetTestIds.PasswordReqError
- );
- const confirmErrors = screen.queryAllByTestId(
- PasswordResetTestIds.PasswordMatchError
- );
- const submitButton = screen.getByTestId(PasswordResetTestIds.SubmitButton);
-
- expect(reqErrors.length).toBeGreaterThan(0);
- expect(confirmErrors.length).toBe(0);
+ const submitButton = screen.getByTestId(PasswordResetIds.SubmitButton);
expect(submitButton.closest("button")).toBeDisabled();
});
- it("renders with password match error", async () => {
+ it("disables button when passwords don't match", async () => {
const user = userEvent.setup();
await customRender();
const passwordEntry = "password";
const confirmEntry = "passward";
- const passwdField = screen.getByTestId(PasswordResetTestIds.Password);
- const passwdConfirm = screen.getByTestId(
- PasswordResetTestIds.ConfirmPassword
- );
+ const passwdField = screen.getByTestId(PasswordResetIds.Password);
+ const passwdConfirm = screen.getByTestId(PasswordResetIds.ConfirmPassword);
await user.type(passwdField, passwordEntry);
await user.type(passwdConfirm, confirmEntry);
- const reqErrors = screen.queryAllByTestId(
- PasswordResetTestIds.PasswordReqError
- );
- const confirmErrors = screen.queryAllByTestId(
- PasswordResetTestIds.PasswordMatchError
- );
- const submitButton = screen.getByTestId(PasswordResetTestIds.SubmitButton);
-
- expect(reqErrors.length).toBe(0);
- expect(confirmErrors.length).toBeGreaterThan(0);
+ const submitButton = screen.getByTestId(PasswordResetIds.SubmitButton);
expect(submitButton.closest("button")).toBeDisabled();
});
- it("renders with no password errors", async () => {
+ it("enables button when passwords are long enough and match", async () => {
const user = userEvent.setup();
await customRender();
const passwordEntry = "password";
- const confirmEntry = "password";
- const passwdField = screen.getByTestId(PasswordResetTestIds.Password);
- const passwdConfirm = screen.getByTestId(
- PasswordResetTestIds.ConfirmPassword
- );
+ const passwdField = screen.getByTestId(PasswordResetIds.Password);
+ const passwdConfirm = screen.getByTestId(PasswordResetIds.ConfirmPassword);
await user.type(passwdField, passwordEntry);
- await user.type(passwdConfirm, confirmEntry);
-
- const reqErrors = screen.queryAllByTestId(
- PasswordResetTestIds.PasswordReqError
- );
- const confirmErrors = screen.queryAllByTestId(
- PasswordResetTestIds.PasswordMatchError
- );
- const submitButton = screen.getByTestId(PasswordResetTestIds.SubmitButton);
+ await user.type(passwdConfirm, passwordEntry);
- expect(reqErrors.length).toBe(0);
- expect(confirmErrors.length).toBe(0);
+ const submitButton = screen.getByTestId(PasswordResetIds.SubmitButton);
expect(submitButton.closest("button")).toBeEnabled();
});
- it("renders with expire error", async () => {
- // rerender the component with the resetFailure prop set.
- const user = userEvent.setup();
- await customRender();
-
- const passwordEntry = "password";
- const confirmEntry = "password";
- const passwdField = screen.getByTestId(PasswordResetTestIds.Password);
- const passwdConfirm = screen.getByTestId(
- PasswordResetTestIds.ConfirmPassword
- );
-
- await user.type(passwdField, passwordEntry);
- await user.type(passwdConfirm, confirmEntry);
-
- const submitButton = screen.getByTestId(PasswordResetTestIds.SubmitButton);
- mockPasswordReset.mockResolvedValueOnce(false);
- await user.click(submitButton);
-
- const resetErrors = screen.queryAllByTestId(
- PasswordResetTestIds.PasswordResetFail
- );
- expect(resetErrors.length).toBeGreaterThan(0);
- });
-
it("renders the InvalidLink component if token not valid", async () => {
mockValidateResetToken.mockResolvedValueOnce(false);
await customRender();
- for (const id of Object.values(PasswordResetTestIds)) {
+ for (const id of Object.values(PasswordResetIds)) {
expect(screen.queryAllByTestId(id)).toHaveLength(0);
}
// The textId will show up as text because t() is mocked to return its input.