cleanup
This commit is contained in:
1 parent
28affa1cd1
commit
b47e17fe91
8 files changed
-1052
No files matched your search
@@ -1,248 +0,0 @@
|
||||
import { Box, Button, Stack, Typography } from "@mui/material";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
import { createToast } from "../../Utils/toastUtils.jsx";
|
||||
import { forgotPassword } from "../../Features/Auth/authSlice.js";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import Background from "@/assets/Images/background-grid.svg?react";
|
||||
import Logo from "@/assets/icons/checkmate-icon.svg?react";
|
||||
import IconBox from "@/Components/v1/IconBox/index.jsx";
|
||||
import Icon from "@/Components/v1/Icon";
|
||||
import "./index.css";
|
||||
|
||||
const CheckEmail = () => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
|
||||
const [email, setEmail] = useState();
|
||||
const [disabled, setDisabled] = useState(false);
|
||||
useEffect(() => {
|
||||
setEmail(sessionStorage.getItem("email"));
|
||||
}, []);
|
||||
|
||||
// TODO - fix
|
||||
const openMail = () => {
|
||||
window.location.href = "mailto:";
|
||||
};
|
||||
|
||||
const toastFail = [
|
||||
{
|
||||
body: t("auth.forgotPassword.toasts.emailNotFound"),
|
||||
},
|
||||
{
|
||||
body: t("auth.forgotPassword.toasts.redirect").replace("<seconds/>", "3"),
|
||||
},
|
||||
{
|
||||
body: t("auth.forgotPassword.toasts.redirect").replace("<seconds/>", "2"),
|
||||
},
|
||||
{
|
||||
body: t("auth.forgotPassword.toasts.redirect").replace("<seconds/>", "1"),
|
||||
},
|
||||
];
|
||||
|
||||
const resendToken = async () => {
|
||||
setDisabled(true); // prevent resent button from being spammed
|
||||
if (!email) {
|
||||
let index = 0;
|
||||
const interval = setInterval(() => {
|
||||
if (index < toastFail.length) {
|
||||
createToast(toastFail[index]);
|
||||
index++;
|
||||
} else {
|
||||
clearInterval(interval);
|
||||
navigate("/forgot-password");
|
||||
}
|
||||
}, 1000);
|
||||
} else {
|
||||
const form = { email: email };
|
||||
const action = await dispatch(forgotPassword(form));
|
||||
if (action.payload.success) {
|
||||
createToast({
|
||||
body: t("auth.forgotPassword.toasts.sent").replace("<email/>", form.email),
|
||||
});
|
||||
setDisabled(false);
|
||||
} else {
|
||||
if (action.payload) {
|
||||
// dispatch errors
|
||||
createToast({
|
||||
body: action.payload.msg, // FIXME: Potential untranslated string
|
||||
});
|
||||
} else {
|
||||
// unknown errors
|
||||
createToast({
|
||||
body: t("common.toasts.unknownError"),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleNavigate = () => {
|
||||
sessionStorage.removeItem("email");
|
||||
location.state?.from
|
||||
? navigate(location.state.from, { replace: true })
|
||||
: navigate("/login");
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack
|
||||
className="check-email-page auth"
|
||||
overflow="hidden"
|
||||
sx={{
|
||||
"& h1": {
|
||||
color: theme.palette.primary.main,
|
||||
fontWeight: 600,
|
||||
fontSize: 22,
|
||||
},
|
||||
/* TODO font size from theme */
|
||||
"& p": { color: theme.palette.primary.contrastTextSecondary, fontSize: 13.5 },
|
||||
"& span": { fontSize: "inherit" },
|
||||
}}
|
||||
>
|
||||
<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(20)}
|
||||
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(10) }}
|
||||
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)}
|
||||
>
|
||||
<Icon name="Mail" />
|
||||
</IconBox>
|
||||
</Stack>
|
||||
<Typography component="h1">{t("auth.forgotPassword.heading")}</Typography>
|
||||
<Typography>
|
||||
<Trans
|
||||
i18nKey="auth.forgotPassword.subheadings.stepTwo"
|
||||
components={{
|
||||
email: (
|
||||
<Typography
|
||||
className="email-sent-to"
|
||||
component="span"
|
||||
>
|
||||
{email || "username@email.com"}
|
||||
</Typography>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="accent"
|
||||
onClick={openMail}
|
||||
sx={{
|
||||
width: "100%",
|
||||
maxWidth: 400,
|
||||
}}
|
||||
>
|
||||
{t("auth.forgotPassword.buttons.openEmail")}
|
||||
</Button>
|
||||
<Typography sx={{ alignSelf: "center", mt: theme.spacing(6) }}>
|
||||
<Trans
|
||||
i18nKey="auth.forgotPassword.links.resend"
|
||||
components={{
|
||||
a: (
|
||||
<Typography
|
||||
component="span"
|
||||
onClick={resendToken}
|
||||
sx={{
|
||||
color: theme.palette.accent.main,
|
||||
userSelect: "none",
|
||||
pointerEvents: disabled ? "none" : "auto",
|
||||
cursor: disabled ? "default" : "pointer",
|
||||
opacity: disabled ? 0.5 : 1,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Box
|
||||
textAlign="center"
|
||||
p={theme.spacing(12)}
|
||||
>
|
||||
<Typography display="inline-block">
|
||||
<Trans
|
||||
i18nKey={
|
||||
location.state?.from === "/account/password"
|
||||
? "Go back to <a>Account Password</a>"
|
||||
: "auth.forgotPassword.links.login"
|
||||
}
|
||||
components={{
|
||||
a: (
|
||||
<Typography
|
||||
component="span"
|
||||
color={theme.palette.accent.main}
|
||||
ml={theme.spacing(2)}
|
||||
onClick={handleNavigate}
|
||||
sx={{ userSelect: "none" }}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default CheckEmail;
|
||||
@@ -1,142 +0,0 @@
|
||||
import { Box, Button, Stack, Typography } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { clearAuthState } from "../../Features/Auth/authSlice.js";
|
||||
import Background from "@/assets/Images/background-grid.svg?react";
|
||||
import Logo from "@/assets/icons/checkmate-icon.svg?react";
|
||||
import IconBox from "@/Components/v1/IconBox/index.jsx";
|
||||
import Icon from "@/Components/v1/Icon";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import "./index.css";
|
||||
|
||||
const NewPasswordConfirmed = () => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleNavigate = () => {
|
||||
dispatch(clearAuthState());
|
||||
navigate("/login");
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack
|
||||
className="password-confirmed-page auth"
|
||||
overflow="hidden"
|
||||
sx={{
|
||||
"& h1": {
|
||||
color: theme.palette.primary.main,
|
||||
fontWeight: 600,
|
||||
fontSize: 21,
|
||||
},
|
||||
/* TODO font size from theme*/
|
||||
"& p": { fontSize: 13.5, 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(20)}
|
||||
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)}
|
||||
>
|
||||
<Icon name="CheckCircle" />
|
||||
</IconBox>
|
||||
</Stack>
|
||||
<Typography component="h1">{t("auth.forgotPassword.heading")}</Typography>
|
||||
<Typography mb={theme.spacing(2)}>
|
||||
{t("auth.forgotPassword.subheadings.stepFour")}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => navigate("/uptime")}
|
||||
sx={{
|
||||
width: "100%",
|
||||
maxWidth: 400,
|
||||
}}
|
||||
>
|
||||
{t("auth.common.navigation.continue")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Box
|
||||
textAlign="center"
|
||||
p={theme.spacing(12)}
|
||||
>
|
||||
<Typography display="inline-block">
|
||||
<Trans
|
||||
i18nKey="auth.forgotPassword.links.login"
|
||||
components={{
|
||||
a: (
|
||||
<Typography
|
||||
component="span"
|
||||
color={theme.palette.accent.main}
|
||||
ml={theme.spacing(2)}
|
||||
onClick={handleNavigate}
|
||||
sx={{ userSelect: "none" }}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewPasswordConfirmed;
|
||||
@@ -1,116 +0,0 @@
|
||||
import Check from "@/Components/v1/Check/Check.jsx";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import { Tooltip, useTheme } from "@mui/material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const PasswordTooltip = ({ feedback, form, children }) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const hasPassword = form.password.length > 0;
|
||||
const hasInvalidFeedback = Object.values(feedback).some(
|
||||
(status) => status !== "success"
|
||||
);
|
||||
const showPasswordTooltip = hasPassword && hasInvalidFeedback;
|
||||
return (
|
||||
<Tooltip
|
||||
placement="right"
|
||||
arrow
|
||||
open={showPasswordTooltip}
|
||||
title={
|
||||
<Stack
|
||||
gap={theme.spacing(4)}
|
||||
mb={{ xs: theme.spacing(6), sm: theme.spacing(8) }}
|
||||
>
|
||||
<Check
|
||||
noHighlightText={
|
||||
t("auth.common.inputs.password.rules.length.beginning") +
|
||||
" " +
|
||||
t("auth.common.inputs.password.rules.length.highlighted")
|
||||
}
|
||||
variant={feedback.length}
|
||||
/>
|
||||
<Check
|
||||
noHighlightText={
|
||||
t("auth.common.inputs.password.rules.special.beginning") +
|
||||
" " +
|
||||
t("auth.common.inputs.password.rules.special.highlighted")
|
||||
}
|
||||
variant={feedback.special}
|
||||
/>
|
||||
<Check
|
||||
noHighlightText={
|
||||
t("auth.common.inputs.password.rules.number.beginning") +
|
||||
" " +
|
||||
t("auth.common.inputs.password.rules.number.highlighted")
|
||||
}
|
||||
variant={feedback.number}
|
||||
/>
|
||||
<Check
|
||||
noHighlightText={
|
||||
t("auth.common.inputs.password.rules.uppercase.beginning") +
|
||||
" " +
|
||||
t("auth.common.inputs.password.rules.uppercase.highlighted")
|
||||
}
|
||||
variant={feedback.uppercase}
|
||||
/>
|
||||
<Check
|
||||
noHighlightText={
|
||||
t("auth.common.inputs.password.rules.lowercase.beginning") +
|
||||
" " +
|
||||
t("auth.common.inputs.password.rules.lowercase.highlighted")
|
||||
}
|
||||
variant={feedback.lowercase}
|
||||
/>
|
||||
<Check
|
||||
noHighlightText={
|
||||
t("auth.common.inputs.password.rules.match.beginning") +
|
||||
" " +
|
||||
t("auth.common.inputs.password.rules.match.highlighted")
|
||||
}
|
||||
variant={feedback.confirm}
|
||||
/>
|
||||
</Stack>
|
||||
}
|
||||
slotProps={{
|
||||
tooltip: {
|
||||
sx: {
|
||||
backgroundColor: theme.palette.tertiary.main,
|
||||
border: `0.5px solid ${theme.palette.primary.lowContrast}90`,
|
||||
borderRadius: theme.spacing(4),
|
||||
color: theme.palette.primary.contrastText,
|
||||
width: "auto",
|
||||
maxWidth: { xs: "25vw", md: "none" },
|
||||
whiteSpace: { xs: "normal", md: "nowrap" },
|
||||
paddingTop: theme.spacing(8),
|
||||
px: theme.spacing(8),
|
||||
},
|
||||
},
|
||||
arrow: {
|
||||
sx: {
|
||||
color: theme.palette.tertiary.main,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<span style={{ display: "inline-block", width: "100%" }}>{children}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
PasswordTooltip.propTypes = {
|
||||
feedback: PropTypes.shape({
|
||||
length: PropTypes.string,
|
||||
special: PropTypes.string,
|
||||
number: PropTypes.string,
|
||||
uppercase: PropTypes.string,
|
||||
lowercase: PropTypes.string,
|
||||
confirm: PropTypes.string,
|
||||
}),
|
||||
form: PropTypes.shape({
|
||||
password: PropTypes.string.isRequired,
|
||||
}),
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
export default PasswordTooltip;
|
||||
@@ -1,71 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { newOrChangedCredentials } from "../../../Validation/validation.js";
|
||||
|
||||
const usePasswordFeedback = () => {
|
||||
const [feedback, setFeedback] = useState({});
|
||||
const getFeedbackStatus = (form, errors, field, criteria) => {
|
||||
const fieldErrors = errors?.[field];
|
||||
const isFieldEmpty = form?.[field]?.length === 0;
|
||||
const hasError = fieldErrors?.includes(criteria) || fieldErrors?.includes("empty");
|
||||
const isCorrect = !isFieldEmpty && !hasError;
|
||||
|
||||
if (isCorrect) {
|
||||
return "success";
|
||||
} else if (hasError) {
|
||||
return "error";
|
||||
} else {
|
||||
return "info";
|
||||
}
|
||||
};
|
||||
|
||||
const handlePasswordFeedback = (updatedForm, name, value, form, errors, setErrors) => {
|
||||
const validateValue = { [name]: value };
|
||||
const validateOptions = { abortEarly: false, context: { password: form.password } };
|
||||
if (name === "password" && form.confirm.length > 0) {
|
||||
validateValue.confirm = form.confirm;
|
||||
validateOptions.context = { password: value };
|
||||
} else if (name === "confirm") {
|
||||
validateValue.password = form.password;
|
||||
}
|
||||
const { error } = newOrChangedCredentials.validate(validateValue, validateOptions);
|
||||
|
||||
const pwdErrors = error?.details.map((error) => ({
|
||||
path: error.path[0],
|
||||
type: error.type,
|
||||
}));
|
||||
|
||||
const errorsByPath =
|
||||
pwdErrors &&
|
||||
pwdErrors.reduce((acc, { path, type }) => {
|
||||
if (!acc[path]) {
|
||||
acc[path] = [];
|
||||
}
|
||||
acc[path].push(type);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const oldErrors = { ...errors };
|
||||
if (name === "password") {
|
||||
oldErrors.password = undefined;
|
||||
} else if (name === "confirm") {
|
||||
oldErrors.confirm = undefined;
|
||||
}
|
||||
const newErrors = { ...oldErrors, ...errorsByPath };
|
||||
|
||||
setErrors(newErrors);
|
||||
|
||||
const newFeedback = {
|
||||
length: getFeedbackStatus(updatedForm, errorsByPath, "password", "string.min"),
|
||||
special: getFeedbackStatus(updatedForm, errorsByPath, "password", "special"),
|
||||
number: getFeedbackStatus(updatedForm, errorsByPath, "password", "number"),
|
||||
uppercase: getFeedbackStatus(updatedForm, errorsByPath, "password", "uppercase"),
|
||||
lowercase: getFeedbackStatus(updatedForm, errorsByPath, "password", "lowercase"),
|
||||
confirm: getFeedbackStatus(updatedForm, errorsByPath, "confirm", "different"),
|
||||
};
|
||||
|
||||
setFeedback(newFeedback);
|
||||
};
|
||||
return { feedback, handlePasswordFeedback, getFeedbackStatus };
|
||||
};
|
||||
|
||||
export default usePasswordFeedback;
|
||||
@@ -1,70 +0,0 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { newOrChangedCredentials } from "../../../Validation/validation.js";
|
||||
|
||||
const getFeedbackStatus = (form, errors, field, criteria) => {
|
||||
const fieldErrors = errors[field];
|
||||
const isFieldEmpty = form[field].length === 0;
|
||||
const hasError = fieldErrors?.includes(criteria) || fieldErrors?.includes("empty");
|
||||
const isCorrect = !isFieldEmpty && !hasError;
|
||||
|
||||
if (isCorrect) {
|
||||
return "success";
|
||||
} else if (hasError) {
|
||||
return "error";
|
||||
} else {
|
||||
return "info";
|
||||
}
|
||||
};
|
||||
|
||||
function useValidatePassword() {
|
||||
const [errors, setErrors] = useState({});
|
||||
const [form, setForm] = useState({
|
||||
password: "",
|
||||
confirm: "",
|
||||
});
|
||||
|
||||
const handleChange = (event) => {
|
||||
const { value, name } = event.target;
|
||||
setForm((prev) => ({ ...prev, [name]: value }));
|
||||
|
||||
const validateValue = { [name]: value };
|
||||
const validateOptions = { abortEarly: false, context: { password: form.password } };
|
||||
if (name === "password" && form.confirm.length > 0) {
|
||||
validateValue.confirm = form.confirm;
|
||||
validateOptions.context = { password: value };
|
||||
} else if (name === "confirm") {
|
||||
validateValue.password = form.password;
|
||||
}
|
||||
|
||||
const { error } = newOrChangedCredentials.validate(validateValue, validateOptions);
|
||||
const errors = error?.details.map((error) => ({
|
||||
path: error.path[0],
|
||||
type: error.type,
|
||||
}));
|
||||
const errorsByPath =
|
||||
errors &&
|
||||
errors.reduce((acc, { path, type }) => {
|
||||
if (!acc[path]) {
|
||||
acc[path] = [];
|
||||
}
|
||||
acc[path].push(type);
|
||||
return acc;
|
||||
}, {});
|
||||
setErrors(() => (errorsByPath ? { ...errorsByPath } : {}));
|
||||
};
|
||||
|
||||
const feedbacks = useMemo(
|
||||
() => ({
|
||||
length: getFeedbackStatus(form, errors, "password", "string.min"),
|
||||
special: getFeedbackStatus(form, errors, "password", "special"),
|
||||
number: getFeedbackStatus(form, errors, "password", "number"),
|
||||
uppercase: getFeedbackStatus(form, errors, "password", "uppercase"),
|
||||
lowercase: getFeedbackStatus(form, errors, "password", "lowercase"),
|
||||
confirm: getFeedbackStatus(form, errors, "confirm", "different"),
|
||||
}),
|
||||
[form, errors]
|
||||
);
|
||||
|
||||
return { handleChange, feedbacks, form, errors };
|
||||
}
|
||||
export { useValidatePassword };
|
||||
@@ -1,112 +0,0 @@
|
||||
/* AUTH */
|
||||
.auth {
|
||||
height: 100vh;
|
||||
}
|
||||
.auth h1 {
|
||||
font-size: var(--env-var-font-size-xlarge);
|
||||
font-weight: 600;
|
||||
}
|
||||
.auth button:not(.MuiIconButton-root) {
|
||||
font-size: var(--env-var-font-size-medium-plus);
|
||||
}
|
||||
|
||||
.auth p + span {
|
||||
opacity: 0.8;
|
||||
cursor: pointer;
|
||||
transition: opacity 300ms ease-in;
|
||||
}
|
||||
.auth p > span:not(.email-sent-to) {
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
cursor: pointer;
|
||||
transition: all 200ms;
|
||||
}
|
||||
.auth p > span:not(.email-sent-to):hover {
|
||||
text-underline-offset: 4px;
|
||||
}
|
||||
.auth p + span:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
.auth button:not(.MuiIconButton-root) {
|
||||
user-select: none;
|
||||
border-radius: var(--env-var-radius-2);
|
||||
line-height: 1;
|
||||
}
|
||||
.auth .field svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
.auth .field h3.MuiTypography-root,
|
||||
.auth .field .input-error,
|
||||
.auth .check span.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
.auth form + form {
|
||||
margin: var(--env-var-spacing-1-plus) 0;
|
||||
}
|
||||
.auth .email-sent-to {
|
||||
font-weight: 600;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.auth > .MuiStack-root:nth-of-type(2) {
|
||||
height: var(--env-var-nav-bar-height);
|
||||
}
|
||||
.auth .background-pattern-svg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -30%);
|
||||
z-index: -1;
|
||||
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
height: 100%;
|
||||
max-height: 800px;
|
||||
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.auth .field {
|
||||
position: relative;
|
||||
}
|
||||
.auth .input-error {
|
||||
position: absolute;
|
||||
top: calc(100% - 2px);
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.auth h1 {
|
||||
font-size: var(--env-var-font-size-large-plus);
|
||||
}
|
||||
.auth button:not(.MuiIconButton-root),
|
||||
.auth .field input,
|
||||
.auth p,
|
||||
.auth span {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
.auth .check span.MuiTypography-root,
|
||||
.auth .field .input-error,
|
||||
.auth .field h3.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-small-plus);
|
||||
}
|
||||
.auth .check span > span {
|
||||
display: none;
|
||||
}
|
||||
.auth form + form {
|
||||
margin: var(--env-var-spacing-1-plus) 0;
|
||||
}
|
||||
.auth .background-pattern-svg {
|
||||
max-width: 750px;
|
||||
max-height: 750px;
|
||||
}
|
||||
|
||||
.forgot-password-page h1,
|
||||
.check-email-page h1,
|
||||
.password-confirmed-page h1,
|
||||
.set-new-password-page h1 {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
@@ -1,283 +0,0 @@
|
||||
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.js";
|
||||
import { createToast } from "../../Utils/toastUtils.jsx";
|
||||
import { newOrChangedCredentials } from "../../Validation/validation.js";
|
||||
import Check from "@/Components/v1/Check/Check.jsx";
|
||||
import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx";
|
||||
import { PasswordEndAdornment } from "@/Components/v1/Inputs/TextInput/Adornments/index.jsx";
|
||||
import IconBox from "@/Components/v1/IconBox/index.jsx";
|
||||
import Logo from "@/assets/icons/checkmate-icon.svg?react";
|
||||
import Background from "@/assets/Images/background-grid.svg?react";
|
||||
import Icon from "@/Components/v1/Icon";
|
||||
import "./index.css";
|
||||
import { useValidatePassword } from "./hooks/useValidatePassword.jsx";
|
||||
import { Trans, 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 // FIXME: Potential untranslated string
|
||||
: t("auth.common.errors.validation"),
|
||||
});
|
||||
} else {
|
||||
const action = await dispatch(setNewPassword({ token, form }));
|
||||
if (action.payload.success) {
|
||||
navigate("/new-password-confirmed");
|
||||
createToast({
|
||||
body: t("auth.forgotPassword.toasts.success"),
|
||||
});
|
||||
} else {
|
||||
const errorMessage = action.payload
|
||||
? action.payload.msg // FIXME: Potential untranslated string
|
||||
: t("auth.forgotPassword.toasts.error");
|
||||
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)}
|
||||
>
|
||||
<Icon name="Lock" />
|
||||
</IconBox>
|
||||
</Stack>
|
||||
<Typography component="h1">{t("auth.forgotPassword.heading")}</Typography>
|
||||
<Typography>{t("auth.forgotPassword.subheadings.stepThree")}</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("auth.common.inputs.password.label")}
|
||||
isRequired={true}
|
||||
placeholder="••••••••"
|
||||
value={form.password}
|
||||
onChange={handleChange}
|
||||
error={errors.password ? true : false}
|
||||
helperText={
|
||||
errors.password === "auth.common.inputs.password.errors.empty"
|
||||
? t(errors.password)
|
||||
: ""
|
||||
} // Other errors are related to required password conditions and are visualized below the input
|
||||
endAdornment={<PasswordEndAdornment />}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
component="form"
|
||||
noValidate
|
||||
spellCheck={false}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<TextInput
|
||||
id={confirmPasswordId}
|
||||
type="password"
|
||||
name="confirm"
|
||||
label={t("auth.common.inputs.passwordConfirm.label")}
|
||||
isRequired={true}
|
||||
placeholder={t("auth.common.inputs.passwordConfirm.placeholder")}
|
||||
value={form.confirm}
|
||||
onChange={handleChange}
|
||||
error={errors.confirm ? true : false}
|
||||
helperText={t(errors.confirm)} // Localization keys are in validation.js
|
||||
endAdornment={<PasswordEndAdornment />}
|
||||
/>
|
||||
</Box>
|
||||
<Stack
|
||||
gap={theme.spacing(4)}
|
||||
mb={theme.spacing(12)}
|
||||
>
|
||||
<Check
|
||||
noHighlightText={t("auth.common.inputs.password.rules.length.beginning")}
|
||||
text={t("auth.common.inputs.password.rules.length.highlighted")}
|
||||
variant={feedbacks.length}
|
||||
/>
|
||||
<Check
|
||||
noHighlightText={t("auth.common.inputs.password.rules.special.beginning")}
|
||||
text={t("auth.common.inputs.password.rules.special.highlighted")}
|
||||
variant={feedbacks.special}
|
||||
/>
|
||||
<Check
|
||||
noHighlightText={t("auth.common.inputs.password.rules.number.beginning")}
|
||||
text={t("auth.common.inputs.password.rules.number.highlighted")}
|
||||
variant={feedbacks.number}
|
||||
/>
|
||||
<Check
|
||||
noHighlightText={t(
|
||||
"auth.common.inputs.password.rules.uppercase.beginning"
|
||||
)}
|
||||
text={t("auth.common.inputs.password.rules.uppercase.highlighted")}
|
||||
variant={feedbacks.uppercase}
|
||||
/>
|
||||
<Check
|
||||
noHighlightText={t(
|
||||
"auth.common.inputs.password.rules.lowercase.beginning"
|
||||
)}
|
||||
text={t("auth.common.inputs.password.rules.lowercase.highlighted")}
|
||||
variant={feedbacks.lowercase}
|
||||
/>
|
||||
<Check
|
||||
noHighlightText={t("auth.common.inputs.password.rules.match.beginning")}
|
||||
text={t("auth.common.inputs.password.rules.match.highlighted")}
|
||||
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("auth.forgotPassword.buttons.resetPassword")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Box
|
||||
textAlign="center"
|
||||
p={theme.spacing(12)}
|
||||
>
|
||||
<Typography display="inline-block">
|
||||
<Trans
|
||||
i18nKey="auth.forgotPassword.links.login"
|
||||
components={{
|
||||
a: (
|
||||
<Typography
|
||||
component="span"
|
||||
color={theme.palette.primary.main}
|
||||
ml={theme.spacing(2)}
|
||||
onClick={() => navigate("/login")}
|
||||
sx={{ userSelect: "none" }}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default SetNewPassword;
|
||||
@@ -11,9 +11,7 @@ import NotFound from "../Pages/NotFound/index.jsx";
|
||||
import AuthLogin from "../Pages/Auth/Login";
|
||||
import AuthRegister from "@/Pages/Auth/Register";
|
||||
import AuthForgotPassword from "@/Pages/Auth/Recovery";
|
||||
import AuthCheckEmail from "../Pages/Auth/CheckEmail.jsx";
|
||||
import AuthSetNewPassword from "../Pages/Auth/SetNewPassword";
|
||||
import AuthNewPasswordConfirmed from "../Pages/Auth/NewPasswordConfirmed.jsx";
|
||||
|
||||
// Uptime
|
||||
import Uptime from "../Pages/Uptime/Monitors";
|
||||
@@ -417,10 +415,6 @@ const Routes = () => {
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/check-email"
|
||||
element={<AuthCheckEmail />}
|
||||
/>
|
||||
<Route
|
||||
path="/set-new-password/:token"
|
||||
element={
|
||||
@@ -431,10 +425,6 @@ const Routes = () => {
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/new-password-confirmed"
|
||||
element={<AuthNewPasswordConfirmed />}
|
||||
/>
|
||||
<Route
|
||||
path="/status/public/:url"
|
||||
element={
|
||||
|
||||
Reference in new issue
Block a user