diff --git a/Client/package-lock.json b/Client/package-lock.json
index b9c92f3f7..ffe529997 100644
--- a/Client/package-lock.json
+++ b/Client/package-lock.json
@@ -15,8 +15,8 @@
"@mui/lab": "^5.0.0-alpha.170",
"@mui/material": "^5.15.16",
"@mui/x-charts": "^7.5.1",
- "@mui/x-data-grid": "7.22.0",
- "@mui/x-date-pickers": "7.22.0",
+ "@mui/x-data-grid": "7.22.1",
+ "@mui/x-date-pickers": "7.22.1",
"@reduxjs/toolkit": "2.3.0",
"axios": "^1.7.4",
"chart.js": "^4.4.3",
@@ -29,7 +29,7 @@
"react-router": "^6.23.0",
"react-router-dom": "^6.23.1",
"react-toastify": "^10.0.5",
- "recharts": "2.13.2",
+ "recharts": "2.13.3",
"redux-persist": "6.0.0",
"vite-plugin-svgr": "^4.2.0"
},
@@ -1400,9 +1400,9 @@
}
},
"node_modules/@mui/x-charts": {
- "version": "7.22.0",
- "resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-7.22.0.tgz",
- "integrity": "sha512-B70ix8keyww9CpfdwbsHygQGsgEySCXuHhGrDRiVyFgK+Be4edBWNswbL3ngIp37CHBbWegaYkPp/Q9GDas0AA==",
+ "version": "7.22.1",
+ "resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-7.22.1.tgz",
+ "integrity": "sha512-zgr8CN4yLen5puqaX7Haj5+AoVG7E13HHsIiDoEAuQvuFDF0gKTxTTdLSKXqhd1qJUIIzJaztZtrr3YCVrENqw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.25.7",
@@ -1458,9 +1458,9 @@
}
},
"node_modules/@mui/x-data-grid": {
- "version": "7.22.0",
- "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.22.0.tgz",
- "integrity": "sha512-gXl7+hG0YRNU3YODlPvz6Q/9+EeUsPAWn/u2YMQmYTgwAxeY5QE3lY224VRnwM5v9SfTFheo1kzAKmXPdjb9tQ==",
+ "version": "7.22.1",
+ "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.22.1.tgz",
+ "integrity": "sha512-YHF96MEv7ACG/VuiycZjEAPH7cZLNuV2+bi/MyR1t/e6E6LTolYFykvjSFq+Imz1mYbW4+9mEvrHZsIKL5KKIQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.25.7",
@@ -1495,9 +1495,9 @@
}
},
"node_modules/@mui/x-date-pickers": {
- "version": "7.22.0",
- "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.22.0.tgz",
- "integrity": "sha512-hopYo3ORP7ddYKnyBsqAtO2txEe2Zf6cehdikS5b1cqMTGOSL+18b11jfGVod9oipjb9L2JcT/WWkjoifs9Iww==",
+ "version": "7.22.1",
+ "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.22.1.tgz",
+ "integrity": "sha512-VBgicE+7PvJrdHSL6HyieHT6a/0dENH8RaMIM2VwUFrGoZzvik50WNwY5U+Hip1BwZLIEvlqtNRQIIj6kgBR6Q==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.25.7",
@@ -1734,14 +1734,14 @@
}
},
"node_modules/@rollup/pluginutils": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz",
- "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==",
+ "version": "5.1.3",
+ "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.3.tgz",
+ "integrity": "sha512-Pnsb6f32CD2W3uCaLZIzDmeFyQ2b8UWMFI7xtwUezpcGBDVDW6y9XgAWIlARiGAo6eNF5FK5aQTr0LFyNyqq5A==",
"license": "MIT",
"dependencies": {
"@types/estree": "^1.0.0",
"estree-walker": "^2.0.2",
- "picomatch": "^2.3.1"
+ "picomatch": "^4.0.2"
},
"engines": {
"node": ">=14.0.0"
@@ -5248,12 +5248,12 @@
"license": "ISC"
},
"node_modules/picomatch": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
- "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
+ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"license": "MIT",
"engines": {
- "node": ">=8.6"
+ "node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
@@ -5518,9 +5518,9 @@
}
},
"node_modules/recharts": {
- "version": "2.13.2",
- "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.13.2.tgz",
- "integrity": "sha512-UDLGFmnsBluDIPpQb9uty0ejb+jiVI71vkki8vVsR6ZCJdgjBfKQoQfft4re99CKlTy9qjQApxCLG6TrxJkeAg==",
+ "version": "2.13.3",
+ "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.13.3.tgz",
+ "integrity": "sha512-YDZ9dOfK9t3ycwxgKbrnDlRC4BHdjlY73fet3a0C1+qGMjXVZe6+VXmpOIIhzkje5MMEL8AN4hLIe4AMskBzlA==",
"license": "MIT",
"dependencies": {
"clsx": "^2.0.0",
@@ -6334,17 +6334,17 @@
}
},
"node_modules/vite-plugin-svgr": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/vite-plugin-svgr/-/vite-plugin-svgr-4.2.0.tgz",
- "integrity": "sha512-SC7+FfVtNQk7So0XMjrrtLAbEC8qjFPifyD7+fs/E6aaNdVde6umlVVh0QuwDLdOMu7vp5RiGFsB70nj5yo0XA==",
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/vite-plugin-svgr/-/vite-plugin-svgr-4.3.0.tgz",
+ "integrity": "sha512-Jy9qLB2/PyWklpYy0xk0UU3TlU0t2UMpJXZvf+hWII1lAmRHrOUKi11Uw8N3rxoNk7atZNYO3pR3vI1f7oi+6w==",
"license": "MIT",
"dependencies": {
- "@rollup/pluginutils": "^5.0.5",
+ "@rollup/pluginutils": "^5.1.3",
"@svgr/core": "^8.1.0",
"@svgr/plugin-jsx": "^8.1.0"
},
"peerDependencies": {
- "vite": "^2.6.0 || 3 || 4 || 5"
+ "vite": ">=2.6.0"
}
},
"node_modules/which": {
diff --git a/Client/package.json b/Client/package.json
index 03c1a77c2..1a7213c20 100644
--- a/Client/package.json
+++ b/Client/package.json
@@ -18,8 +18,8 @@
"@mui/lab": "^5.0.0-alpha.170",
"@mui/material": "^5.15.16",
"@mui/x-charts": "^7.5.1",
- "@mui/x-data-grid": "7.22.0",
- "@mui/x-date-pickers": "7.22.0",
+ "@mui/x-data-grid": "7.22.1",
+ "@mui/x-date-pickers": "7.22.1",
"@reduxjs/toolkit": "2.3.0",
"axios": "^1.7.4",
"chart.js": "^4.4.3",
@@ -32,7 +32,7 @@
"react-router": "^6.23.0",
"react-router-dom": "^6.23.1",
"react-toastify": "^10.0.5",
- "recharts": "2.13.2",
+ "recharts": "2.13.3",
"redux-persist": "6.0.0",
"vite-plugin-svgr": "^4.2.0"
},
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/package-lock.json b/Server/package-lock.json
index a3a678817..7ec737059 100644
--- a/Server/package-lock.json
+++ b/Server/package-lock.json
@@ -12,7 +12,7 @@
"@sendgrid/mail": "^8.1.3",
"axios": "^1.7.2",
"bcrypt": "^5.1.1",
- "bullmq": "5.22.0",
+ "bullmq": "5.23.0",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.19.2",
@@ -1164,9 +1164,9 @@
"license": "MIT"
},
"node_modules/bullmq": {
- "version": "5.22.0",
- "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.22.0.tgz",
- "integrity": "sha512-nwjJSQt/kpO4bIfAznyKKz3+m5OZ6YSaz2Vg7oNoZWTD5wCnJJJy6b9iWM5QIF0bADhDWyorLCO0hU3de+iKMA==",
+ "version": "5.23.0",
+ "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.23.0.tgz",
+ "integrity": "sha512-VILKTIOwo9AopMyVqvDhQ1qyLrOtBSfu+G2bntgauQfxYzT7ETj+h2HeUe7a9i9AU/+OXJGYYm49NHJedEz7VQ==",
"license": "MIT",
"dependencies": {
"cron-parser": "^4.6.0",
@@ -6672,9 +6672,9 @@
}
},
"node_modules/winston": {
- "version": "3.15.0",
- "resolved": "https://registry.npmjs.org/winston/-/winston-3.15.0.tgz",
- "integrity": "sha512-RhruH2Cj0bV0WgNL+lOfoUBI4DVfdUNjVnJGVovWZmrcKtrFTTRzgXYK2O9cymSGjrERCtaAeHwMNnUWXlwZow==",
+ "version": "3.16.0",
+ "resolved": "https://registry.npmjs.org/winston/-/winston-3.16.0.tgz",
+ "integrity": "sha512-xz7+cyGN5M+4CmmD4Npq1/4T+UZaz7HaeTlAruFUTjk79CNMq+P6H30vlE4z0qfqJ01VHYQwd7OZo03nYm/+lg==",
"license": "MIT",
"dependencies": {
"@colors/colors": "^1.6.0",
diff --git a/Server/package.json b/Server/package.json
index 12ceaa407..3b28602a9 100644
--- a/Server/package.json
+++ b/Server/package.json
@@ -15,7 +15,7 @@
"@sendgrid/mail": "^8.1.3",
"axios": "^1.7.2",
"bcrypt": "^5.1.1",
- "bullmq": "5.22.0",
+ "bullmq": "5.23.0",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.19.2",
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(),
});