Files
Checkmate/client/src/Pages/Auth/SetNewPassword.jsx
T
Michal Šmahel 74915f6840 fix: Strict password validation in login form
Strict password validation is for registration, change password,
reset password and similar forms. In login form, we don't want
to interrupt user with unnecessary validation as it's completely
irrelevant what they actually entered. The one thing it's about
is the correctness of the email and the password.

Disabling strict validation for password is a kind of security
through obscurity, which could filter some untargeted attempts
for trying passwords by bots. If the bot immediatelly receives
notes about the requirements, it has more imformation for better
attacks.
2025-06-09 21:54:47 +02:00

270 lines
7.2 KiB
React

import { useId } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useNavigate } from "react-router-dom";
import { useParams } from "react-router-dom";
import { useTheme } from "@emotion/react";
import { Box, Stack, Typography, Button } from "@mui/material";
import { setNewPassword } from "../../Features/Auth/authSlice";
import { createToast } from "../../Utils/toastUtils";
import { newOrChangedCredentials } from "../../Validation/validation";
import Check from "../../Components/Check/Check";
import TextInput from "../../Components/Inputs/TextInput";
import { PasswordEndAdornment } from "../../Components/Inputs/TextInput/Adornments";
import IconBox from "../../Components/IconBox";
import LockIcon from "../../assets/icons/lock.svg?react";
import Logo from "../../assets/icons/checkmate-icon.svg?react";
import Background from "../../assets/Images/background-grid.svg?react";
import "./index.css";
import { useValidatePassword } from "./hooks/useValidatePassword";
import { useTranslation } from "react-i18next";
const SetNewPassword = () => {
const navigate = useNavigate();
const dispatch = useDispatch();
const theme = useTheme();
const { t } = useTranslation();
const passwordId = useId();
const confirmPasswordId = useId();
const { form, errors, handleChange, feedbacks } = useValidatePassword();
const { isLoading } = useSelector((state) => state.auth);
const { token } = useParams();
const handleSubmit = async (e) => {
e.preventDefault();
const { error } = newOrChangedCredentials.validate(form, {
abortEarly: false,
context: { password: form.password },
});
if (error) {
createToast({
body:
error.details && error.details.length > 0
? error.details[0].message
: "Error validating data.",
});
} else {
const action = await dispatch(setNewPassword({ token, form }));
if (action.payload.success) {
navigate("/new-password-confirmed");
createToast({
body: "Your password was reset successfully.",
});
} else {
const errorMessage = action.payload
? action.payload.msg
: "Unable to reset password. Please try again later or contact support.";
createToast({
body: errorMessage,
});
}
}
};
return (
<Stack
className="set-new-password-page auth"
overflow="hidden"
sx={{
"& h1": {
color: theme.palette.primary.main,
fontWeight: 600,
fontSize: 24,
},
"& p": {
/* TODO font size from theme */
fontSize: 14,
color: theme.palette.primary.contrastTextSecondary,
},
}}
>
<Box
className="background-pattern-svg"
sx={{
"& svg g g:last-of-type path": {
stroke: theme.palette.primary.lowContrast,
},
}}
>
<Background style={{ width: "100%" }} />
</Box>
<Stack
direction="row"
alignItems="center"
px={theme.spacing(12)}
gap={theme.spacing(4)}
>
<Logo style={{ borderRadius: theme.shape.borderRadius }} />
<Typography sx={{ userSelect: "none" }}>{t("common.appName")}</Typography>
</Stack>
<Stack
width="100%"
maxWidth={600}
flex={1}
justifyContent="center"
px={{ xs: theme.spacing(12), lg: theme.spacing(20) }}
pb={theme.spacing(12)}
mx="auto"
sx={{
"& > .MuiStack-root": {
border: 1,
borderRadius: theme.spacing(5),
borderColor: theme.palette.primary.lowContrast,
backgroundColor: theme.palette.primary.main,
padding: {
xs: theme.spacing(12),
sm: theme.spacing(20),
},
},
}}
>
<Stack
gap={{ xs: theme.spacing(8), sm: theme.spacing(12) }}
alignItems="center"
textAlign="center"
>
<Box>
<Stack
direction="row"
justifyContent="center"
>
<IconBox
height={48}
width={48}
minWidth={48}
borderRadius={12}
svgWidth={24}
svgHeight={24}
mb={theme.spacing(4)}
>
<LockIcon alt="lock icon" />
</IconBox>
</Stack>
<Typography component="h1">{t("authSetNewPasswordTitle")}</Typography>
<Typography>{t("authSetNewPasswordDescription")}</Typography>
</Box>
<Box
width="100%"
textAlign="left"
sx={{
"& .input-error": {
display: "none",
},
}}
>
<Box
component="form"
noValidate
spellCheck={false}
onSubmit={handleSubmit}
>
<TextInput
id={passwordId}
type="password"
name="password"
label={t("commonPassword")}
isRequired={true}
placeholder="••••••••"
value={form.password}
onChange={handleChange}
error={errors.password ? true : false}
helperText={errors.password}
endAdornment={<PasswordEndAdornment />}
/>
</Box>
<Box
component="form"
noValidate
spellCheck={false}
onSubmit={handleSubmit}
>
<TextInput
id={confirmPasswordId}
type="password"
name="confirm"
label={t("authSetNewPasswordConfirmPassword")}
isRequired={true}
placeholder="••••••••"
value={form.confirm}
onChange={handleChange}
error={errors.confirm ? true : false}
helperText={errors.confirm}
endAdornment={<PasswordEndAdornment />}
/>
</Box>
<Stack
gap={theme.spacing(4)}
mb={theme.spacing(12)}
>
<Check
noHighlightText={t("authPasswordMustBeAtLeast")}
text={t("authPasswordCharactersLong")}
variant={feedbacks.length}
/>
<Check
noHighlightText={t("authPasswordMustContainAtLeast")}
text={t("authPasswordSpecialCharacter")}
variant={feedbacks.special}
/>
<Check
noHighlightText={t("authPasswordMustContainAtLeast")}
text={t("authPasswordOneNumber")}
variant={feedbacks.number}
/>
<Check
noHighlightText={t("authPasswordMustContainAtLeast")}
text={t("authPasswordUpperCharacter")}
variant={feedbacks.uppercase}
/>
<Check
noHighlightText={t("authPasswordMustContainAtLeast")}
text={t("authPasswordLowerCharacter")}
variant={feedbacks.lowercase}
/>
<Check
noHighlightText={t("authPasswordConfirmAndPassword")}
text={t("authPasswordMustMatch")}
variant={feedbacks.confirm}
/>
</Stack>
</Box>
<Button
variant="contained"
color="accent"
loading={isLoading}
onClick={handleSubmit}
disabled={
form.password.length === 0 ||
form.confirm.length === 0 ||
Object.keys(errors).length !== 0
}
sx={{ width: "100%", maxWidth: 400 }}
>
{t("authSetNewPasswordResetPassword")}
</Button>
</Stack>
</Stack>
<Box
textAlign="center"
p={theme.spacing(12)}
>
<Typography display="inline-block">{t("goBackTo")} —</Typography>
<Typography
component="span"
color={theme.palette.primary.main}
ml={theme.spacing(2)}
onClick={() => navigate("/login")}
sx={{ userSelect: "none" }}
>
{t("authLoginTitle")}
</Typography>
</Box>
</Stack>
);
};
export default SetNewPassword;