diff --git a/client/src/Components/v2/design-elements/BasePage.tsx b/client/src/Components/v2/design-elements/BasePage.tsx
index ec429d5f6..48c836e54 100644
--- a/client/src/Components/v2/design-elements/BasePage.tsx
+++ b/client/src/Components/v2/design-elements/BasePage.tsx
@@ -177,7 +177,7 @@ export const MonitorBasePageWithStates = ({
error={error}
{...props}
>
- {priorityFallback}
+ {priorityFallback}
);
}
diff --git a/client/src/Components/v2/design-elements/BulletPointCheck.tsx b/client/src/Components/v2/design-elements/BulletPointCheck.tsx
index 2893ee4c2..4bd1ddf3c 100644
--- a/client/src/Components/v2/design-elements/BulletPointCheck.tsx
+++ b/client/src/Components/v2/design-elements/BulletPointCheck.tsx
@@ -27,6 +27,7 @@ export const BulletPointCheck = ({
{
+ const defaults: SetNewPasswordFormData = {
+ password: "",
+ confirm: "",
+ };
+
+ return {
+ schema: setNewPasswordSchema,
+ defaults,
+ };
+};
diff --git a/client/src/Pages/Auth/CheckEmail.jsx b/client/src/Pages/Auth/CheckEmail.jsx
deleted file mode 100644
index 10cc2ecc6..000000000
--- a/client/src/Pages/Auth/CheckEmail.jsx
+++ /dev/null
@@ -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("", "3"),
- },
- {
- body: t("auth.forgotPassword.toasts.redirect").replace("", "2"),
- },
- {
- body: t("auth.forgotPassword.toasts.redirect").replace("", "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("", 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 (
-
-
-
-
-
-
- {t("common.appName")}
-
- .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),
- },
- },
- }}
- >
-
-
-
-
-
-
-
- {t("auth.forgotPassword.heading")}
-
-
- {email || "username@email.com"}
-
- ),
- }}
- />
-
-
-
-
-
- ),
- }}
- />
-
-
-
-
-
- Account Password"
- : "auth.forgotPassword.links.login"
- }
- components={{
- a: (
-
- ),
- }}
- />
-
-
-
- );
-};
-
-export default CheckEmail;
diff --git a/client/src/Pages/Auth/NewPasswordConfirmed.jsx b/client/src/Pages/Auth/NewPasswordConfirmed.jsx
deleted file mode 100644
index c92e52552..000000000
--- a/client/src/Pages/Auth/NewPasswordConfirmed.jsx
+++ /dev/null
@@ -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 (
-
-
-
-
-
-
- {t("common.appName")}
-
- .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),
- },
- },
- }}
- >
-
-
-
-
-
-
-
- {t("auth.forgotPassword.heading")}
-
- {t("auth.forgotPassword.subheadings.stepFour")}
-
-
-
-
-
-
-
-
- ),
- }}
- />
-
-
-
- );
-};
-
-export default NewPasswordConfirmed;
diff --git a/client/src/Pages/Auth/SetNewPassword.jsx b/client/src/Pages/Auth/SetNewPassword.jsx
deleted file mode 100644
index ada5d6246..000000000
--- a/client/src/Pages/Auth/SetNewPassword.jsx
+++ /dev/null
@@ -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 (
-
-
-
-
-
-
- {t("common.appName")}
-
- .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),
- },
- },
- }}
- >
-
-
-
-
-
-
-
- {t("auth.forgotPassword.heading")}
- {t("auth.forgotPassword.subheadings.stepThree")}
-
-
-
- }
- />
-
-
- }
- />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- navigate("/login")}
- sx={{ userSelect: "none" }}
- />
- ),
- }}
- />
-
-
-
- );
-};
-
-export default SetNewPassword;
diff --git a/client/src/Pages/Auth/SetNewPassword/index.tsx b/client/src/Pages/Auth/SetNewPassword/index.tsx
new file mode 100644
index 000000000..09bd565f1
--- /dev/null
+++ b/client/src/Pages/Auth/SetNewPassword/index.tsx
@@ -0,0 +1,142 @@
+import {
+ BaseAuthPage,
+ BulletPointCheck,
+ TextLink,
+} from "@/Components/v2/design-elements";
+import { Button, TextField } from "@/Components/v2/inputs";
+import Stack from "@mui/material/Stack";
+import { useTheme } from "@mui/material/styles";
+import { useTranslation } from "react-i18next";
+import { useForm, Controller } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod/dist/zod.js";
+import { useSetNewPasswordForm } from "@/Hooks/useSetNewPasswordForm";
+import type { SetNewPasswordFormData } from "@/Validation/setNewPassword";
+import { specialCharPattern } from "@/Validation/patterns";
+import { useParams, useNavigate } from "react-router-dom";
+import { usePost } from "@/Hooks/UseApi";
+import { useDispatch } from "react-redux";
+import { setAuthState } from "@/Features/Auth/authSlice";
+
+const SetNewPasswordPage = () => {
+ const theme = useTheme();
+ const { t } = useTranslation();
+ const { token } = useParams<{ token: string }>();
+ const navigate = useNavigate();
+ const dispatch = useDispatch();
+ const { post, loading } = usePost();
+
+ const { schema, defaults } = useSetNewPasswordForm();
+
+ const { control, handleSubmit, watch } = useForm({
+ resolver: zodResolver(schema),
+ defaultValues: defaults,
+ });
+
+ const password = watch("password");
+ const confirm = watch("confirm");
+
+ const getVariant = (condition: boolean): "success" | "info" => {
+ return condition ? "success" : "info";
+ };
+
+ const hasLength = password.length >= 8;
+ const hasSpecial = specialCharPattern.test(password);
+ const hasNumber = /\d/.test(password);
+ const hasUppercase = /[A-Z]/.test(password);
+ const hasLowercase = /[a-z]/.test(password);
+ const passwordsMatch = password.length > 0 && password === confirm;
+
+ const onSubmit = async (data: SetNewPasswordFormData) => {
+ if (loading) return;
+
+ const result = await post("/auth/recovery/reset", {
+ password: data.password,
+ recoveryToken: token,
+ });
+
+ if (result?.success) {
+ dispatch(setAuthState(result));
+ navigate("/uptime");
+ }
+ };
+
+ return (
+
+ (
+
+ )}
+ />
+ (
+
+ )}
+ />
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default SetNewPasswordPage;
diff --git a/client/src/Pages/Auth/components/PasswordTooltip.jsx b/client/src/Pages/Auth/components/PasswordTooltip.jsx
deleted file mode 100644
index 361850aca..000000000
--- a/client/src/Pages/Auth/components/PasswordTooltip.jsx
+++ /dev/null
@@ -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 (
-
-
-
-
-
-
-
-
- }
- 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,
- },
- },
- }}
- >
- {children}
-
- );
-};
-
-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;
diff --git a/client/src/Pages/Auth/hooks/usePasswordFeedback.jsx b/client/src/Pages/Auth/hooks/usePasswordFeedback.jsx
deleted file mode 100644
index 73e8fb738..000000000
--- a/client/src/Pages/Auth/hooks/usePasswordFeedback.jsx
+++ /dev/null
@@ -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;
diff --git a/client/src/Pages/Auth/hooks/useValidatePassword.jsx b/client/src/Pages/Auth/hooks/useValidatePassword.jsx
deleted file mode 100644
index 014804d35..000000000
--- a/client/src/Pages/Auth/hooks/useValidatePassword.jsx
+++ /dev/null
@@ -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 };
diff --git a/client/src/Pages/Auth/index.css b/client/src/Pages/Auth/index.css
deleted file mode 100644
index 86ab05c13..000000000
--- a/client/src/Pages/Auth/index.css
+++ /dev/null
@@ -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;
- }
-}
diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx
index 4c68c0367..9f7238aa5 100644
--- a/client/src/Routes/index.jsx
+++ b/client/src/Routes/index.jsx
@@ -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.jsx";
-import AuthNewPasswordConfirmed from "../Pages/Auth/NewPasswordConfirmed.jsx";
+import AuthSetNewPassword from "../Pages/Auth/SetNewPassword";
// Uptime
import Uptime from "../Pages/Uptime/Monitors";
@@ -417,17 +415,15 @@ const Routes = () => {
>
}
/>
- }
- />
}
- />
- }
+ element={
+ <>
+
+
+
+ >
+ }
/>
/[A-Z]/.test(val), "Password must contain an uppercase letter")
+ .refine((val) => /[a-z]/.test(val), "Password must contain a lowercase letter")
+ .refine((val) => /\d/.test(val), "Password must contain a number")
+ .refine(
+ (val) => specialCharPattern.test(val),
+ "Password must contain a special character"
+ ),
+ confirm: z.string().min(1, "Please confirm your password"),
+ })
+ .refine((data) => data.password === data.confirm, {
+ message: "Passwords do not match",
+ path: ["confirm"],
+ });
+
+export type SetNewPasswordFormData = z.infer;
diff --git a/client/src/locales/en.json b/client/src/locales/en.json
index 4b3885cf2..35132ce6c 100644
--- a/client/src/locales/en.json
+++ b/client/src/locales/en.json
@@ -93,6 +93,14 @@
},
"navigation": {
"continue": "Continue"
+ },
+ "passwordRules": {
+ "length": "Must be at least 8 characters long",
+ "special": "Must contain at least one special character (!?@#$%^&*()-_=+[]{}|;:'\",./\\~`)",
+ "number": "Must contain at least one number",
+ "uppercase": "Must contain at least one upper character",
+ "lowercase": "Must contain at least one lower character",
+ "match": "Passwords must match"
}
},
"forgotPassword": {
@@ -619,6 +627,7 @@
},
"auth": {
"common": {
+ "passwordRules": {},
"form": {
"option": {
"email": {
@@ -665,6 +674,10 @@
"linkText": "login"
}
}
+ },
+ "setNewPassword": {
+ "title": "Reset your password",
+ "subtitle": "Your new password must be different from previously used passwords."
}
},
"checks": {
@@ -1064,7 +1077,6 @@
"duration": {
"label": "Duration"
},
-
"startTime": {
"label": "Start time"
}