diff --git a/Client/src/App.jsx b/Client/src/App.jsx
index 001758713..6cb2f433a 100644
--- a/Client/src/App.jsx
+++ b/Client/src/App.jsx
@@ -1,7 +1,10 @@
+import { useEffect } from "react";
import { Routes, Route } from "react-router-dom";
+import { useSelector } from "react-redux";
+import { useDispatch } from "react-redux";
+// import "./App.css";
import "react-toastify/dist/ReactToastify.css";
import { ToastContainer } from "react-toastify";
-// import "./App.css";
import NotFound from "./Pages/NotFound";
import Login from "./Pages/Auth/Login";
import Register from "./Pages/Auth/Register/Register";
@@ -21,22 +24,19 @@ import ProtectedRoute from "./Components/ProtectedRoute";
import Details from "./Pages/Monitors/Details";
import AdvancedSettings from "./Pages/AdvancedSettings";
import Maintenance from "./Pages/Maintenance";
-import withAdminCheck from "./HOC/withAdminCheck";
-import withAdminProp from "./HOC/withAdminProp";
import Configure from "./Pages/Monitors/Configure";
import PageSpeed from "./Pages/PageSpeed";
import CreatePageSpeed from "./Pages/PageSpeed/CreatePageSpeed";
import CreateNewMaintenanceWindow from "./Pages/Maintenance/CreateMaintenance";
import PageSpeedDetails from "./Pages/PageSpeed/Details";
import PageSpeedConfigure from "./Pages/PageSpeed/Configure";
+import withAdminCheck from "./HOC/withAdminCheck";
+import withAdminProp from "./HOC/withAdminProp";
import { ThemeProvider } from "@emotion/react";
import lightTheme from "./Utils/Theme/lightTheme";
import darkTheme from "./Utils/Theme/darkTheme";
-import { useSelector } from "react-redux";
import { CssBaseline } from "@mui/material";
-import { useEffect } from "react";
-import { useDispatch } from "react-redux";
-import { getAppSettings, updateAppSettings } from "./Features/Settings/settingsSlice";
+import { getAppSettings } from "./Features/Settings/settingsSlice";
import { logger } from "./Utils/Logger"; // Import the logger
import { networkService } from "./main";
function App() {
@@ -181,16 +181,18 @@ function App() {
path="/register"
element={}
/>
+
}
/>
- {/* } /> */}
+
}
/>
+
}
diff --git a/Client/src/Components/Check/Check.jsx b/Client/src/Components/Check/Check.jsx
index 7ef51137e..01a023345 100644
--- a/Client/src/Components/Check/Check.jsx
+++ b/Client/src/Components/Check/Check.jsx
@@ -20,7 +20,7 @@ import { useTheme } from "@emotion/react";
*
* @returns {React.Element} The `Check` component with a check icon and a label, defined by the `text` prop.
*/
-const Check = ({ text, variant = "info", outlined = false }) => {
+const Check = ({ text, noHighlightText, variant = "info", outlined = false }) => {
const theme = useTheme();
const colors = {
success: theme.palette.success.main,
@@ -54,6 +54,7 @@ const Check = ({ text, variant = "info", outlined = false }) => {
opacity: 0.8,
}}
>
+ {noHighlightText && {noHighlightText}}{" "}
{text}
@@ -62,6 +63,7 @@ const Check = ({ text, variant = "info", outlined = false }) => {
Check.propTypes = {
text: PropTypes.oneOfType([PropTypes.string, PropTypes.element]).isRequired,
+ noHighlightText: PropTypes.string,
variant: PropTypes.oneOf(["info", "error", "success"]),
outlined: PropTypes.bool,
};
diff --git a/Client/src/Components/Dialog/genericDialog.jsx b/Client/src/Components/Dialog/genericDialog.jsx
index 0b37d75e8..e3ebd51b1 100644
--- a/Client/src/Components/Dialog/genericDialog.jsx
+++ b/Client/src/Components/Dialog/genericDialog.jsx
@@ -12,7 +12,7 @@ const GenericDialog = ({ title, description, open, onClose, theme, children }) =
aria-describedby={ariaDescribedBy}
open={open}
onClose={onClose}
- onClick={(e)=>e.stopPropagation()}
+ onClick={(e) => e.stopPropagation()}
>
setVisible((show) => !show)}
- tabIndex={-1}
sx={{
color: theme.palette.border.dark,
padding: theme.spacing(1),
- "&:focus": {
- outline: "none",
+ "&:focus-visible": {
+ outline: `2px solid ${theme.palette.primary.main}`,
+ outlineOffset: `2px`,
},
"& .MuiTouchRipple-root": {
pointerEvents: "none",
diff --git a/Client/src/Components/TabPanels/Account/PasswordPanel.jsx b/Client/src/Components/TabPanels/Account/PasswordPanel.jsx
index 700e27fb9..0ea313d6c 100644
--- a/Client/src/Components/TabPanels/Account/PasswordPanel.jsx
+++ b/Client/src/Components/TabPanels/Account/PasswordPanel.jsx
@@ -35,7 +35,6 @@ const PasswordPanel = () => {
confirm: "",
});
const [errors, setErrors] = useState({});
-
const handleChange = (event) => {
const { value, id } = event.target;
const name = idToName[id];
@@ -110,20 +109,32 @@ const PasswordPanel = () => {
onSubmit={handleSubmit}
noValidate
spellCheck="false"
- gap={theme.spacing(20)}
+ gap={theme.spacing(26)}
+ maxWidth={"80ch"}
+ marginInline={"auto"}
>
-
-
- Current password
-
-
+
+
+
+
+ Current password
+
{
error={errors[idToName["edit-current-password"]]}
/>
-
-
- New password
-
+
+
+ New password
+
+
{
error={errors[idToName["edit-new-password"]]}
/>
-
-
- Confirm new password
-
+
+
+ Confirm new password
+
+
{
error={errors[idToName["edit-confirm-password"]]}
/>
-
-
-
+ {Object.keys(errors).length > 0 && (
+
-
+ )}
{
value !== "").length === 0
+ }
sx={{
px: theme.spacing(12),
mt: theme.spacing(20),
diff --git a/Client/src/Components/TabPanels/Account/ProfilePanel.jsx b/Client/src/Components/TabPanels/Account/ProfilePanel.jsx
index 710d6a59f..0875891f5 100644
--- a/Client/src/Components/TabPanels/Account/ProfilePanel.jsx
+++ b/Client/src/Components/TabPanels/Account/ProfilePanel.jsx
@@ -370,8 +370,6 @@ const ProfilePanel = () => {
)}
- {/* TODO - Update ModalPopup Component with @mui for reusability */}
-
>
);
@@ -235,7 +248,7 @@ const StepTwo = ({ form, errors, onSubmit, onChange, onBack }) => {
return (
<>
@@ -244,64 +257,76 @@ const StepTwo = ({ form, errors, onSubmit, onChange, onBack }) => {
Enter your password
-
+
-
-
+
+
+
-
-
-
-
{
stroke: theme.palette.other.icon,
},
},
+ "&:focus-visible": {
+ outline: `2px solid ${theme.palette.primary.main}`,
+ outlineOffset: `2px`,
+ },
}}
>
@@ -110,389 +113,6 @@ LandingPage.propTypes = {
onSignup: PropTypes.func,
};
-/**
- * Renders the first step of the sign up process.
- *
- * @param {Object} props
- * @param {Object} props.form - Form state object.
- * @param {Object} props.errors - Object containing form validation errors.
- * @param {Function} props.onSubmit - Callback function to handle form submission.
- * @param {Function} props.onChange - Callback function to handle form input changes.
- * @param {Function} props.onBack - Callback function to handle "Back" button click.
- * @returns {JSX.Element}
- */
-const StepOne = ({ form, errors, onSubmit, onChange, onBack }) => {
- const theme = useTheme();
- const inputRef = useRef(null);
-
- useEffect(() => {
- if (inputRef.current) {
- inputRef.current.focus();
- }
- }, []);
-
- return (
- <>
-
-
- Sign Up
- Enter your personal details
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- >
- );
-};
-
-StepOne.propTypes = {
- form: PropTypes.object,
- errors: PropTypes.object,
- onSubmit: PropTypes.func,
- onChange: PropTypes.func,
- onBack: PropTypes.func,
-};
-
-/**
- * Renders the second step of the sign up process.
- *
- * @param {Object} props
- * @param {Object} props.form - Form state object.
- * @param {Object} props.errors - Object containing form validation errors.
- * @param {Function} props.onSubmit - Callback function to handle form submission.
- * @param {Function} props.onChange - Callback function to handle form input changes.
- * @param {Function} props.onBack - Callback function to handle "Back" button click.
- * @returns {JSX.Element}
- */
-const StepTwo = ({ form, errors, onSubmit, onChange, onBack }) => {
- const theme = useTheme();
- const inputRef = useRef(null);
-
- useEffect(() => {
- if (inputRef.current) {
- inputRef.current.focus();
- }
- }, []);
-
- return (
- <>
-
-
- Sign Up
- Enter your email address
-
-
-
- (e.target.value = e.target.value.toLowerCase())}
- onChange={onChange}
- error={errors.email}
- ref={inputRef}
- />
-
-
-
-
-
-
-
- >
- );
-};
-
-StepTwo.propTypes = {
- form: PropTypes.object,
- errors: PropTypes.object,
- onSubmit: PropTypes.func,
- onChange: PropTypes.func,
- onBack: PropTypes.func,
-};
-
-/**
- * Renders the third step of the sign up process.
- *
- * @param {Object} props
- * @param {Object} props.form - Form state object.
- * @param {Object} props.errors - Object containing form validation errors.
- * @param {Function} props.onSubmit - Callback function to handle form submission.
- * @param {Function} props.onChange - Callback function to handle form input changes.
- * @param {Function} props.onBack - Callback function to handle "Back" button click.
- * @returns {JSX.Element}
- */
-const StepThree = ({ form, errors, onSubmit, onChange, onBack }) => {
- const theme = useTheme();
- const inputRef = useRef(null);
-
- useEffect(() => {
- if (inputRef.current) {
- inputRef.current.focus();
- }
- }, []);
-
- return (
- <>
-
-
- Sign Up
- Create your password
-
-
-
-
-
-
-
-
-
-
- Must be at least 8 characters
- long
- >
- }
- variant={
- errors?.password === "Password is required"
- ? "error"
- : form.password === ""
- ? "info"
- : form.password.length < 8
- ? "error"
- : "success"
- }
- />
-
- Must contain one special
- character and a number
- >
- }
- variant={
- errors?.password === "Password is required"
- ? "error"
- : form.password === ""
- ? "info"
- : !/^(?=.*[!@#$%^&*(),.?":{}|])(?=.*\d).+$/.test(form.password)
- ? "error"
- : "success"
- }
- />
-
- Must contain at least one
- upper and lower character
- >
- }
- variant={
- errors?.password === "Password is required"
- ? "error"
- : form.password === ""
- ? "info"
- : !/^(?=.*[A-Z])(?=.*[a-z]).+$/.test(form.password)
- ? "error"
- : "success"
- }
- />
-
-
-
-
-
-
-
- >
- );
-};
-
-StepThree.propTypes = {
- form: PropTypes.object,
- errors: PropTypes.object,
- onSubmit: PropTypes.func,
- onChange: PropTypes.func,
- onBack: PropTypes.func,
-};
-
const Register = ({ isSuperAdmin }) => {
const dispatch = useDispatch();
const navigate = useNavigate();
@@ -596,14 +216,16 @@ const Register = ({ isSuperAdmin }) => {
// Attempts account registration
const handleStepThree = async (e) => {
e.preventDefault();
-
+ const { password, confirm } = e.target.elements;
let registerForm = {
...form,
+ password: password.value,
+ confirm: confirm.value,
role: isSuperAdmin ? ["superadmin"] : form.role,
inviteToken: token ? token : "", // Add the token to the request for verification
};
let error = validateForm(registerForm, {
- context: { password: form.password },
+ context: { password: registerForm.password },
});
if (error) {
handleError(error);
@@ -615,18 +237,16 @@ const Register = ({ isSuperAdmin }) => {
if (action.payload.success) {
const authToken = action.payload.data;
localStorage.setItem("token", authToken);
- navigate("/");
+ navigate("/monitors");
createToast({
body: "Welcome! Your account was created successfully.",
});
} else {
if (action.payload) {
- // dispatch errors
createToast({
body: action.payload.msg,
});
} else {
- // unknown errors
createToast({
body: "Unknown error.",
});
@@ -732,10 +352,10 @@ const Register = ({ isSuperAdmin }) => {
/>
) : step === 3 ? (
setStep(2)}
/>
) : (
diff --git a/Client/src/Pages/Auth/Register/StepOne/index.jsx b/Client/src/Pages/Auth/Register/StepOne/index.jsx
new file mode 100644
index 000000000..4a6997ef4
--- /dev/null
+++ b/Client/src/Pages/Auth/Register/StepOne/index.jsx
@@ -0,0 +1,132 @@
+import { useEffect, useRef } from "react";
+import PropTypes from "prop-types";
+import { useTheme } from "@emotion/react";
+import { Box, Button, Stack, Typography } from "@mui/material";
+import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
+import Field from "../../../../Components/Inputs/Field";
+
+StepOne.propTypes = {
+ form: PropTypes.object,
+ errors: PropTypes.object,
+ onSubmit: PropTypes.func,
+ onChange: PropTypes.func,
+ onBack: PropTypes.func,
+};
+
+/**
+ * Renders the first step of the sign up process.
+ *
+ * @param {Object} props
+ * @param {Object} props.form - Form state object.
+ * @param {Object} props.errors - Object containing form validation errors.
+ * @param {Function} props.onSubmit - Callback function to handle form submission.
+ * @param {Function} props.onChange - Callback function to handle form input changes.
+ * @param {Function} props.onBack - Callback function to handle "Back" button click.
+ * @returns {JSX.Element}
+ */
+
+function StepOne({ form, errors, onSubmit, onChange, onBack }) {
+ const theme = useTheme();
+ const inputRef = useRef(null);
+
+ useEffect(() => {
+ if (inputRef.current) {
+ inputRef.current.focus();
+ }
+ }, []);
+
+ return (
+ <>
+ {/* TODO this stack should be a component */}
+
+
+ Sign Up
+ Enter your personal details
+
+
+
+
+
+
+
+
+ {/* TODO buttons should be a component should be a component */}
+
+
+
+
+
+ >
+ );
+}
+
+export { StepOne };
diff --git a/Client/src/Pages/Auth/Register/StepThree/index.jsx b/Client/src/Pages/Auth/Register/StepThree/index.jsx
new file mode 100644
index 000000000..13d6a130f
--- /dev/null
+++ b/Client/src/Pages/Auth/Register/StepThree/index.jsx
@@ -0,0 +1,172 @@
+import { useEffect, useRef } from "react";
+import PropTypes from "prop-types";
+import { useTheme } from "@emotion/react";
+import { Box, Button, Stack, Typography } from "@mui/material";
+import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
+import Field from "../../../../Components/Inputs/Field";
+import Check from "../../../../Components/Check/Check";
+import { useValidatePassword } from "../../hooks/useValidatePassword";
+
+StepThree.propTypes = {
+ onSubmit: PropTypes.func,
+ onBack: PropTypes.func,
+};
+
+/**
+ * Renders the third step of the sign up process.
+ *
+ * @param {Object} props
+ * @param {Function} props.onSubmit - Callback function to handle form submission.
+ * @param {Function} props.onBack - Callback function to handle "Back" button click.
+ * @returns {JSX.Element}
+ */
+function StepThree({ onSubmit, onBack }) {
+ const theme = useTheme();
+ const inputRef = useRef(null);
+
+ useEffect(() => {
+ if (inputRef.current) {
+ inputRef.current.focus();
+ }
+ }, []);
+
+ const { handleChange, feedbacks, form, errors } = useValidatePassword();
+ return (
+ <>
+
+
+ Sign Up
+ Create your password
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
+
+export { StepThree };
diff --git a/Client/src/Pages/Auth/Register/StepTwo/index.jsx b/Client/src/Pages/Auth/Register/StepTwo/index.jsx
new file mode 100644
index 000000000..e4236c0bc
--- /dev/null
+++ b/Client/src/Pages/Auth/Register/StepTwo/index.jsx
@@ -0,0 +1,116 @@
+import { useEffect, useRef } from "react";
+import PropTypes from "prop-types";
+import { useTheme } from "@emotion/react";
+import { Box, Button, Stack, Typography } from "@mui/material";
+import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
+import Field from "../../../../Components/Inputs/Field";
+
+StepTwo.propTypes = {
+ form: PropTypes.object,
+ errors: PropTypes.object,
+ onSubmit: PropTypes.func,
+ onChange: PropTypes.func,
+ onBack: PropTypes.func,
+};
+
+/**
+ * Renders the second step of the sign up process.
+ *
+ * @param {Object} props
+ * @param {Object} props.form - Form state object.
+ * @param {Object} props.errors - Object containing form validation errors.
+ * @param {Function} props.onSubmit - Callback function to handle form submission.
+ * @param {Function} props.onChange - Callback function to handle form input changes.
+ * @param {Function} props.onBack - Callback function to handle "Back" button click.
+ * @returns {JSX.Element}
+ */
+function StepTwo({ form, errors, onSubmit, onChange, onBack }) {
+ const theme = useTheme();
+ const inputRef = useRef(null);
+
+ useEffect(() => {
+ if (inputRef.current) {
+ inputRef.current.focus();
+ }
+ }, []);
+
+ return (
+ <>
+
+
+ Sign Up
+ Enter your email address
+
+
+
+ (e.target.value = e.target.value.toLowerCase())}
+ onChange={onChange}
+ error={errors.email}
+ ref={inputRef}
+ />
+
+
+
+
+
+
+ >
+ );
+}
+
+export { StepTwo };
diff --git a/Client/src/Pages/Auth/SetNewPassword.jsx b/Client/src/Pages/Auth/SetNewPassword.jsx
index 6c8f7418e..77e621acd 100644
--- a/Client/src/Pages/Auth/SetNewPassword.jsx
+++ b/Client/src/Pages/Auth/SetNewPassword.jsx
@@ -1,56 +1,43 @@
+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 } from "@mui/material";
+import LoadingButton from "@mui/lab/LoadingButton";
import { setNewPassword } from "../../Features/Auth/authSlice";
import { createToast } from "../../Utils/toastUtils";
-import { Box, Stack, Typography } from "@mui/material";
-import { useTheme } from "@emotion/react";
-import { useParams } from "react-router-dom";
-import { useState } from "react";
import { credentials } from "../../Validation/validation";
-import { useNavigate } from "react-router-dom";
import Check from "../../Components/Check/Check";
import Field from "../../Components/Inputs/Field";
-import LockIcon from "../../assets/icons/lock.svg?react";
-import Background from "../../assets/Images/background-grid.svg?react";
-import Logo from "../../assets/icons/bwu-icon.svg?react";
-import LoadingButton from "@mui/lab/LoadingButton";
-import "./index.css";
import { IconBox } from "./styled";
+import LockIcon from "../../assets/icons/lock.svg?react";
+import Logo from "../../assets/icons/bwu-icon.svg?react";
+import Background from "../../assets/Images/background-grid.svg?react";
+import "./index.css";
+import { useValidatePassword } from "./hooks/useValidatePassword";
const SetNewPassword = () => {
const navigate = useNavigate();
const dispatch = useDispatch();
const theme = useTheme();
- const [errors, setErrors] = useState({});
- const [form, setForm] = useState({
- password: "",
- confirm: "",
- });
+ const passwordId = useId();
+ const confirmPasswordId = useId();
- const idMap = {
- "register-password-input": "password",
- "confirm-password-input": "confirm",
- };
+ const { form, errors, handleChange, feedbacks } = useValidatePassword();
const { isLoading } = useSelector((state) => state.auth);
const { token } = useParams();
const handleSubmit = async (e) => {
e.preventDefault();
-
- const passwordForm = { ...form };
- const { error } = credentials.validate(passwordForm, {
+ const { error } = credentials.validate(form, {
abortEarly: false,
context: { password: form.password },
});
if (error) {
- // validation errors
- const newErrors = {};
- error.details.forEach((err) => {
- newErrors[err.path[0]] = err.message;
- });
- setErrors(newErrors);
createToast({
body:
error.details && error.details.length > 0
@@ -58,50 +45,23 @@ const SetNewPassword = () => {
: "Error validating data.",
});
} else {
- delete passwordForm.confirm;
- const action = await dispatch(setNewPassword({ token: token, form: passwordForm }));
+ const action = await dispatch(setNewPassword({ token, form }));
if (action.payload.success) {
navigate("/new-password-confirmed");
createToast({
body: "Your password was reset successfully.",
});
} else {
- if (action.payload) {
- // dispatch errors
- createToast({
- body: action.payload.msg,
- });
- } else {
- // unknown errors
- createToast({
- body: "Unknown error.",
- });
- }
+ const errorMessage = action.payload
+ ? action.payload.msg
+ : "Unable to reset password. Please try again later or contact support.";
+ createToast({
+ body: errorMessage,
+ });
}
}
};
- const handleChange = (event) => {
- const { value, id } = event.target;
- const name = idMap[id];
- setForm((prev) => ({
- ...prev,
- [name]: value,
- }));
-
- const { error } = credentials.validate(
- { [name]: value },
- { abortEarly: false, context: { password: form.password } }
- );
-
- setErrors((prev) => {
- const prevErrors = { ...prev };
- if (error) prevErrors[name] = error.details[0].message;
- else delete prevErrors[name];
- return prevErrors;
- });
- };
-
return (
{
onSubmit={handleSubmit}
>
{
onSubmit={handleSubmit}
>
{
mb={theme.spacing(12)}
>
- Must be at least 8
- characters long
- >
- }
- variant={
- errors?.password === "Password is required"
- ? "error"
- : form.password === ""
- ? "info"
- : form.password.length < 8
- ? "error"
- : "success"
- }
+ noHighlightText={"Must be at least"}
+ text={"8 characters long"}
+ variant={feedbacks.length}
/>
- Must contain one special
- character and a number
- >
- }
- variant={
- errors?.password === "Password is required"
- ? "error"
- : form.password === ""
- ? "info"
- : !/^(?=.*[!@#$%^&*(),.?":{}|])(?=.*\d).+$/.test(form.password)
- ? "error"
- : "success"
- }
+ noHighlightText={"Must contain at least"}
+ text={"one special character"}
+ variant={feedbacks.special}
/>
- Must contain at least one
- upper and lower character
- >
- }
- variant={
- errors?.password === "Password is required"
- ? "error"
- : form.password === ""
- ? "info"
- : !/^(?=.*[A-Z])(?=.*[a-z]).+$/.test(form.password)
- ? "error"
- : "success"
- }
+ noHighlightText={"Must contain at least"}
+ text={"one number"}
+ variant={feedbacks.number}
+ />
+
+
+
@@ -277,7 +218,11 @@ const SetNewPassword = () => {
color="primary"
loading={isLoading}
onClick={handleSubmit}
- disabled={Object.keys(errors).length !== 0}
+ disabled={
+ form.password.length === 0 ||
+ form.confirm.length === 0 ||
+ Object.keys(errors).length !== 0
+ }
sx={{ width: "100%", maxWidth: 400 }}
>
Reset password
diff --git a/Client/src/Pages/Auth/hooks/useValidatePassword.jsx b/Client/src/Pages/Auth/hooks/useValidatePassword.jsx
new file mode 100644
index 000000000..b49d0944b
--- /dev/null
+++ b/Client/src/Pages/Auth/hooks/useValidatePassword.jsx
@@ -0,0 +1,70 @@
+import { useMemo, useState } from "react";
+import { credentials } from "../../../Validation/validation";
+
+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 } = credentials.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/Validation/validation.js b/Client/src/Validation/validation.js
index 08b1632c3..7ac5ae40c 100644
--- a/Client/src/Validation/validation.js
+++ b/Client/src/Validation/validation.js
@@ -16,25 +16,37 @@ const passwordSchema = joi
.string()
.trim()
.min(8)
+ .custom((value, helpers) => {
+ if (!/[A-Z]/.test(value)) {
+ return helpers.error("uppercase");
+ }
+ return value;
+ })
+ .custom((value, helpers) => {
+ if (!/[a-z]/.test(value)) {
+ return helpers.error("lowercase");
+ }
+ return value;
+ })
+ .custom((value, helpers) => {
+ if (!/\d/.test(value)) {
+ return helpers.error("number");
+ }
+ return value;
+ })
+ .custom((value, helpers) => {
+ if (!/[!?@#$%^&*()\-_=+[\]{};:'",.<>~`|\\/]/.test(value)) {
+ return helpers.error("special");
+ }
+ return value;
+ })
.messages({
"string.empty": "Password is required",
"string.min": "Password must be at least 8 characters long",
- })
- .custom((value, helpers) => {
- if (!/[A-Z]/.test(value)) {
- return helpers.message("Password must contain at least one uppercase letter");
- }
- if (!/[a-z]/.test(value)) {
- return helpers.message("Password must contain at least one lowercase letter");
- }
- if (!/\d/.test(value)) {
- return helpers.message("Password must contain at least one number");
- }
- if (!/[!@#$%^&*]/.test(value)) {
- return helpers.message("Password must contain at least one special character");
- }
-
- return value;
+ uppercase: "Password must contain at least one uppercase letter",
+ lowercase: "Password must contain at least one lowercase letter",
+ number: "Password must contain at least one number",
+ special: "Password must contain at least one special character",
});
const credentials = joi.object({
@@ -60,15 +72,16 @@ const credentials = joi.object({
confirm: joi
.string()
.trim()
- .messages({
- "string.empty": "Password confirmation is required",
- })
.custom((value, helpers) => {
const { password } = helpers.prefs.context;
if (value !== password) {
- return helpers.message("Passwords do not match");
+ return helpers.error("different");
}
return value;
+ })
+ .messages({
+ "string.empty": "empty",
+ different: "Passwords do not match",
}),
role: joi.array(),
teamId: joi.string().allow("").optional(),
@@ -166,5 +179,5 @@ export {
monitorValidation,
settingsValidation,
maintenanceWindowValidation,
- advancedSettingsValidation
+ advancedSettingsValidation,
};
diff --git a/Server/validation/joi.js b/Server/validation/joi.js
index cd69a959d..06804975e 100644
--- a/Server/validation/joi.js
+++ b/Server/validation/joi.js
@@ -18,6 +18,9 @@ const roleValidatior = (role) => (value, helpers) => {
// Auth
//****************************************
+const passwordPattern =
+ /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!?@#$%^&*()\-_=+[\]{};:'",.<>~`|\\/])[A-Za-z0-9!?@#$%^&*()\-_=+[\]{};:'",.<>~`|\\/]+$/;
+
const loginValidation = joi.object({
email: joi
.string()
@@ -30,13 +33,7 @@ const loginValidation = joi.object({
}
return lowercasedValue;
}),
- password: joi
- .string()
- .min(8)
- .required()
- .pattern(
- /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*()])[A-Za-z0-9!@#$%^&*()]+$/
- ),
+ password: joi.string().min(8).required().pattern(passwordPattern),
});
const registrationBodyValidation = joi.object({
@@ -59,13 +56,7 @@ const registrationBodyValidation = joi.object({
}
return lowercasedValue;
}),
- password: joi
- .string()
- .min(8)
- .required()
- .pattern(
- /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*()])[A-Za-z0-9!@#$%^&*()]+$/
- ),
+ password: joi.string().min(8).required().pattern(passwordPattern),
profileImage: joi.any(),
role: joi
.array()
@@ -84,18 +75,8 @@ const editUserBodyValidation = joi.object({
firstName: joi.string().pattern(/^[A-Za-z]+$/),
lastName: joi.string().pattern(/^[A-Za-z]+$/),
profileImage: joi.any(),
- newPassword: joi
- .string()
- .min(8)
- .pattern(
- /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*()])[A-Za-z0-9!@#$%^&*()]+$/
- ),
- password: joi
- .string()
- .min(8)
- .pattern(
- /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*()])[A-Za-z0-9!@#$%^&*()]+$/
- ),
+ newPassword: joi.string().min(8).pattern(passwordPattern),
+ password: joi.string().min(8).pattern(passwordPattern),
deleteProfileImage: joi.boolean(),
role: joi.array(),
});
@@ -113,13 +94,7 @@ const recoveryTokenValidation = joi.object({
const newPasswordValidation = joi.object({
recoveryToken: joi.string().required(),
- password: joi
- .string()
- .min(8)
- .required()
- .pattern(
- /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*()])[A-Za-z0-9!@#$%^&*()]+$/
- ),
+ password: joi.string().min(8).required().pattern(passwordPattern),
confirm: joi.string(),
});