Skip to content
Merged
1 change: 1 addition & 0 deletions public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
72 changes: 41 additions & 31 deletions src/components/InvalidLink/index.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -17,44 +24,47 @@ export default function InvalidLink(props: InvalidLinkProps): ReactElement {
const idAffix = "invalid-link";

return (
<Grid container justifyContent="center">
<Card style={{ width: 450 }}>
<Grid2 container justifyContent="center">
<Card sx={{ width: 450 }}>
<CardHeader
title={
<Typography align="center" variant="h5">
{t(props.textId)}
</Typography>
}
/>

<CardContent>
<Typography variant="h5" align="center" gutterBottom>
{t(props.textId)}
</Typography>
{/* User Guide, Sign Up, and Log In buttons */}
<Grid container justifyContent="flex-end" spacing={2}>
<Grid item xs={4} sm={6}>
<Grid2 container spacing={2}>
<Grid2 size="grow">
<Button id={`${idAffix}-guide`} onClick={() => openUserGuide()}>
<Help />
</Button>
</Grid>
</Grid2>

<Grid item xs={4} sm={3}>
<Button
id={`${idAffix}-signUp`}
onClick={() => {
navigate(Path.Signup);
}}
>
{t("login.signUp")}
</Button>
</Grid>
<Button
id={`${idAffix}-signUp`}
onClick={() => {
navigate(Path.Signup);
}}
variant="contained"
>
{t("login.signUp")}
</Button>

<Grid item xs={4} sm={3}>
<Button
id={`${idAffix}-login`}
onClick={() => {
navigate(Path.Login);
}}
>
{t("login.login")}
</Button>
</Grid>
</Grid>
<Button
id={`${idAffix}-login`}
onClick={() => {
navigate(Path.Login);
}}
variant="contained"
>
{t("login.login")}
</Button>
</Grid2>
</CardContent>
</Card>
</Grid>
</Grid2>
);
}
2 changes: 0 additions & 2 deletions src/components/Login/Captcha.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down Expand Up @@ -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) }}
/>
) : (
<Fragment />
Expand Down
236 changes: 115 additions & 121 deletions src/components/Login/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import {
Button,
Card,
CardContent,
Grid,
CardHeader,
Grid2,
Link,
Stack,
TextFieldProps,
Typography,
} from "@mui/material";
Expand All @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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));
}
};
Expand All @@ -108,129 +112,119 @@ export default function Login(): ReactElement {
});

return (
<Grid container justifyContent="center">
<Card style={{ width: 450 }}>
<form id={LoginId.Form} onSubmit={logIn}>
<CardContent>
{/* Title */}
<Typography variant="h5" align="center" gutterBottom>
<Grid2 container justifyContent="center">
<Card sx={{ width: 450 }}>
{/* Title */}
<CardHeader
title={
<Typography align="center" variant="h5">
{t(LoginTextId.Title)}
</Typography>
}
/>

<CardContent>
<form id={LoginId.Form} onSubmit={logIn}>
<Stack spacing={2}>
{/* Username field */}
<NormalizedTextField
{...defaultTextFieldProps(LoginId.FieldUsername)}
autoComplete="username"
autoFocus
error={usernameError}
helperText={
usernameError ? t(LoginTextId.FieldError) : undefined
}
label={t(LoginTextId.LabelUsername)}
onChange={handleUpdateUsername}
value={username}
/>

{/* Password field */}
<NormalizedTextField
{...defaultTextFieldProps(LoginId.FieldPassword)}
autoComplete="current-password"
error={passwordError}
helperText={
passwordError
? password
? t(LoginTextId.ErrorPassword)
: t(LoginTextId.FieldError)
: undefined
}
label={t(LoginTextId.LabelPassword)}
onChange={handleUpdatePassword}
type="password"
value={password}
/>

{/* "Forgot password?" link to reset password */}
{RuntimeConfig.getInstance().emailServicesEnabled() && (
<Typography>
<Link
href={"#"}
onClick={() => router.navigate(Path.PwRequest)}
underline="hover"
variant="subtitle2"
>
{t(LoginTextId.LinkForgotPassword)}
</Link>
</Typography>
)}

{/* "Failed to log in" */}
{status === LoginStatus.Failure && (
<Typography sx={{ color: "error.main" }} variant="body2">
{t(
loginError.includes("401")
? LoginTextId.Error401
: LoginTextId.ErrorUnknown
)}
</Typography>
)}

<Captcha setSuccess={setIsVerified} />

{/* User Guide, Sign Up, and Log In buttons */}
<Grid2 container spacing={2}>
<Grid2 size="grow">
<Button
data-testid={LoginId.ButtonUserGuide}
id={LoginId.ButtonUserGuide}
onClick={() => openUserGuide()}
>
<Help />
</Button>
</Grid2>

{/* Username field */}
<NormalizedTextField
{...defaultTextFieldProps(LoginId.FieldUsername)}
autoComplete="username"
autoFocus
error={usernameError}
helperText={usernameError ? t(LoginTextId.FieldError) : undefined}
label={t(LoginTextId.LabelUsername)}
onChange={handleUpdateUsername}
value={username}
/>

{/* Password field */}
<NormalizedTextField
{...defaultTextFieldProps(LoginId.FieldPassword)}
autoComplete="current-password"
error={passwordError}
helperText={passwordError ? t(LoginTextId.FieldError) : undefined}
label={t(LoginTextId.LabelPassword)}
onChange={handleUpdatePassword}
type="password"
value={password}
/>

{/* "Forgot password?" link to reset password */}
{RuntimeConfig.getInstance().emailServicesEnabled() && (
<Typography>
<Link
href={"#"}
onClick={() => router.navigate(Path.PwRequest)}
underline="hover"
variant="subtitle2"
>
{t(LoginTextId.LinkForgotPassword)}
</Link>
</Typography>
)}

{/* "Failed to log in" */}
{status === LoginStatus.Failure && (
<Typography
style={{ color: "red", marginBottom: 24, marginTop: 24 }}
variant="body2"
>
{t(
loginError.includes("401")
? LoginTextId.Error401
: LoginTextId.ErrorUnknown
)}
</Typography>
)}

<Captcha setSuccess={setIsVerified} />

{/* User Guide, Sign Up, and Log In buttons */}
<Grid container justifyContent="space-between">
<Grid item xs={1}>
<Button
data-testid={LoginId.ButtonUserGuide}
id={LoginId.ButtonUserGuide}
onClick={() => openUserGuide()}
data-testid={LoginId.ButtonSignUp}
id={LoginId.ButtonSignUp}
onClick={() => router.navigate(Path.Signup)}
variant="outlined"
>
<Help />
{t(LoginTextId.ButtonSignUp)}
</Button>
</Grid>

<Grid
container
item
justifyContent="flex-end"
spacing={2}
xs="auto"
>
<Grid item>
<Button
data-testid={LoginId.ButtonSignUp}
id={LoginId.ButtonSignUp}
onClick={() => router.navigate(Path.Signup)}
variant="outlined"
>
{t(LoginTextId.ButtonSignUp)}
</Button>
</Grid>

<Grid item>
<LoadingButton
buttonProps={{
"data-testid": LoginId.ButtonLogIn,
id: LoginId.ButtonLogIn,
type: "submit",
}}
disabled={!isVerified}
loading={status === LoginStatus.InProgress}
>
{t(LoginTextId.ButtonLogin)}
</LoadingButton>
</Grid>
</Grid>
</Grid>

{/* Login announcement banner */}
{!!banner && (
<Typography
style={{
marginTop: theme.spacing(2),
padding: theme.spacing(1),
}}
>
{banner}
</Typography>
)}
</CardContent>
</form>

<LoadingButton
buttonProps={{
"data-testid": LoginId.ButtonLogIn,
id: LoginId.ButtonLogIn,
type: "submit",
}}
disabled={!isVerified}
loading={status === LoginStatus.InProgress}
>
{t(LoginTextId.ButtonLogin)}
</LoadingButton>
</Grid2>

{/* Login announcement banner */}
{!!banner && <Typography>{banner}</Typography>}
</Stack>
</form>
</CardContent>
</Card>
</Grid>
</Grid2>
);
}
Loading
Loading