Merge branch 'develop' into 1067-fe-hardware-monitoring-create-hardware-monitor
This commit is contained in:
commit
cabc7a75df
20 files changed
+847
-746
No files matched your search
Generated
+28
-28
@@ -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": {
|
||||
|
||||
+3
-3
@@ -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"
|
||||
},
|
||||
|
||||
+10
-8
@@ -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={<AdminCheckedRegister />}
|
||||
/>
|
||||
|
||||
<Route
|
||||
exact
|
||||
path="/register/:token"
|
||||
element={<Register />}
|
||||
/>
|
||||
{/* <Route path="/toast" element={<ToastComponent />} /> */}
|
||||
|
||||
<Route
|
||||
path="*"
|
||||
element={<NotFound />}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/forgot-password"
|
||||
element={<ForgotPassword />}
|
||||
|
||||
@@ -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 && <Typography component="span">{noHighlightText}</Typography>}{" "}
|
||||
{text}
|
||||
</Typography>
|
||||
</Stack>
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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()}
|
||||
>
|
||||
<Stack
|
||||
gap={theme.spacing(2)}
|
||||
@@ -62,7 +62,8 @@ GenericDialog.propTypes = {
|
||||
open: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
children: PropTypes.element.isRequired,
|
||||
children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node])
|
||||
.isRequired,
|
||||
};
|
||||
|
||||
export { GenericDialog };
|
||||
@@ -51,7 +51,6 @@ Dialog.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
description: PropTypes.string,
|
||||
open: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
confirmationButtonLabel: PropTypes.string.isRequired,
|
||||
|
||||
@@ -166,12 +166,12 @@ const Field = forwardRef(
|
||||
<IconButton
|
||||
aria-label="toggle password visibility"
|
||||
onClick={() => 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",
|
||||
|
||||
@@ -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"}
|
||||
>
|
||||
<Stack direction="row">
|
||||
<Box flex={0.9}>
|
||||
<Typography component="h1">Current password</Typography>
|
||||
</Box>
|
||||
<Field
|
||||
type="text"
|
||||
id="hidden-username"
|
||||
name="username"
|
||||
autoComplete="username"
|
||||
hidden={true}
|
||||
value=""
|
||||
/>
|
||||
<Field
|
||||
type="text"
|
||||
id="hidden-username"
|
||||
name="username"
|
||||
autoComplete="username"
|
||||
hidden={true}
|
||||
value=""
|
||||
/>
|
||||
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent={"flex-start"}
|
||||
alignItems={"center"}
|
||||
gap={theme.spacing(8)}
|
||||
flexWrap={"wrap"}
|
||||
>
|
||||
<Typography
|
||||
component="h1"
|
||||
width="20ch"
|
||||
>
|
||||
Current password
|
||||
</Typography>
|
||||
<Field
|
||||
type="password"
|
||||
id="edit-current-password"
|
||||
@@ -134,10 +145,19 @@ const PasswordPanel = () => {
|
||||
error={errors[idToName["edit-current-password"]]}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack direction="row">
|
||||
<Box flex={0.9}>
|
||||
<Typography component="h1">New password</Typography>
|
||||
</Box>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems={"center"}
|
||||
gap={theme.spacing(8)}
|
||||
flexWrap={"wrap"}
|
||||
>
|
||||
<Typography
|
||||
component="h1"
|
||||
width="20ch"
|
||||
>
|
||||
New password
|
||||
</Typography>
|
||||
|
||||
<Field
|
||||
type="password"
|
||||
id="edit-new-password"
|
||||
@@ -148,10 +168,19 @@ const PasswordPanel = () => {
|
||||
error={errors[idToName["edit-new-password"]]}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack direction="row">
|
||||
<Box flex={0.9}>
|
||||
<Typography component="h1">Confirm new password</Typography>
|
||||
</Box>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems={"center"}
|
||||
gap={theme.spacing(8)}
|
||||
flexWrap={"wrap"}
|
||||
>
|
||||
<Typography
|
||||
component="h1"
|
||||
width="20ch"
|
||||
>
|
||||
Confirm new password
|
||||
</Typography>
|
||||
|
||||
<Field
|
||||
type="password"
|
||||
id="edit-confirm-password"
|
||||
@@ -162,15 +191,14 @@ const PasswordPanel = () => {
|
||||
error={errors[idToName["edit-confirm-password"]]}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack direction="row">
|
||||
<Box flex={0.9}></Box>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
{Object.keys(errors).length > 0 && (
|
||||
<Box sx={{ maxWidth: "70ch" }}>
|
||||
<Alert
|
||||
variant="warning"
|
||||
body="New password must contain at least 8 characters and must have at least one uppercase letter, one number and one symbol."
|
||||
body="New password must contain at least 8 characters and must have at least one uppercase letter, one lowercase letter, one number and one special character."
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="flex-end"
|
||||
@@ -178,10 +206,13 @@ const PasswordPanel = () => {
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleSubmit}
|
||||
type="submit"
|
||||
loading={isLoading}
|
||||
loadingIndicator="Saving..."
|
||||
disabled={Object.keys(errors).length !== 0 && true}
|
||||
disabled={
|
||||
Object.keys(errors).length !== 0 ||
|
||||
Object.values(localData).filter((value) => value !== "").length === 0
|
||||
}
|
||||
sx={{
|
||||
px: theme.spacing(12),
|
||||
mt: theme.spacing(20),
|
||||
|
||||
@@ -370,8 +370,6 @@ const ProfilePanel = () => {
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
{/* TODO - Update ModalPopup Component with @mui for reusability */}
|
||||
|
||||
<Dialog
|
||||
open={isModalOpen("delete")}
|
||||
theme={theme}
|
||||
|
||||
+123
-98
@@ -51,6 +51,10 @@ const LandingPage = ({ onContinue }) => {
|
||||
stroke: theme.palette.other.icon,
|
||||
},
|
||||
},
|
||||
"&:focus-visible": {
|
||||
outline: `2px solid ${theme.palette.primary.main}`,
|
||||
outlineOffset: `2px`,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Mail />
|
||||
@@ -130,7 +134,7 @@ const StepOne = ({ form, errors, onSubmit, onChange, onBack }) => {
|
||||
return (
|
||||
<>
|
||||
<Stack
|
||||
gap={{ xs: theme.spacing(8), sm: theme.spacing(12) }}
|
||||
gap={{ xs: theme.spacing(12), sm: theme.spacing(16) }}
|
||||
textAlign="center"
|
||||
>
|
||||
<Box>
|
||||
@@ -139,57 +143,66 @@ const StepOne = ({ form, errors, onSubmit, onChange, onBack }) => {
|
||||
</Box>
|
||||
<Box
|
||||
textAlign="left"
|
||||
mb={theme.spacing(5)}
|
||||
component="form"
|
||||
noValidate
|
||||
spellCheck={false}
|
||||
onSubmit={onSubmit}
|
||||
display="grid"
|
||||
gap={{ xs: theme.spacing(12), sm: theme.spacing(16) }}
|
||||
>
|
||||
<form
|
||||
noValidate
|
||||
spellCheck={false}
|
||||
onSubmit={onSubmit}
|
||||
<Field
|
||||
type="email"
|
||||
id="login-email-input"
|
||||
label="Email"
|
||||
isRequired={true}
|
||||
placeholder="jordan.ellis@domain.com"
|
||||
autoComplete="email"
|
||||
value={form.email}
|
||||
onInput={(e) => (e.target.value = e.target.value.toLowerCase())}
|
||||
onChange={onChange}
|
||||
error={errors.email}
|
||||
ref={inputRef}
|
||||
/>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Field
|
||||
type="email"
|
||||
id="login-email-input"
|
||||
label="Email"
|
||||
isRequired={true}
|
||||
placeholder="jordan.ellis@domain.com"
|
||||
autoComplete="email"
|
||||
value={form.email}
|
||||
onInput={(e) => (e.target.value = e.target.value.toLowerCase())}
|
||||
onChange={onChange}
|
||||
error={errors.email}
|
||||
ref={inputRef}
|
||||
/>
|
||||
</form>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="info"
|
||||
onClick={onBack}
|
||||
sx={{
|
||||
px: theme.spacing(5),
|
||||
"& svg.MuiSvgIcon-root": {
|
||||
mr: theme.spacing(3),
|
||||
},
|
||||
"&:focus-visible": {
|
||||
outline: `2px solid ${theme.palette.primary.main}`,
|
||||
outlineOffset: `2px`,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ArrowBackRoundedIcon />
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
type="submit"
|
||||
disabled={errors.email && true}
|
||||
sx={{
|
||||
width: "30%",
|
||||
"&.Mui-focusVisible": {
|
||||
outline: `2px solid ${theme.palette.primary.main}`,
|
||||
outlineOffset: `2px`,
|
||||
boxShadow: `none`,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="info"
|
||||
onClick={onBack}
|
||||
sx={{
|
||||
px: theme.spacing(5),
|
||||
"& svg.MuiSvgIcon-root": {
|
||||
mr: theme.spacing(3),
|
||||
},
|
||||
}}
|
||||
props={{ tabIndex: -1 }}
|
||||
>
|
||||
<ArrowBackRoundedIcon />
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={onSubmit}
|
||||
disabled={errors.email && true}
|
||||
sx={{ width: "30%" }}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
@@ -235,7 +248,7 @@ const StepTwo = ({ form, errors, onSubmit, onChange, onBack }) => {
|
||||
return (
|
||||
<>
|
||||
<Stack
|
||||
gap={{ xs: theme.spacing(8), sm: theme.spacing(12) }}
|
||||
gap={{ xs: theme.spacing(12), sm: theme.spacing(16) }}
|
||||
position="relative"
|
||||
textAlign="center"
|
||||
>
|
||||
@@ -244,64 +257,76 @@ const StepTwo = ({ form, errors, onSubmit, onChange, onBack }) => {
|
||||
<Typography>Enter your password</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
component="form"
|
||||
noValidate
|
||||
spellCheck={false}
|
||||
onSubmit={onSubmit}
|
||||
textAlign="left"
|
||||
mb={theme.spacing(5)}
|
||||
sx={{
|
||||
display: "grid",
|
||||
gap: { xs: theme.spacing(12), sm: theme.spacing(16) },
|
||||
}}
|
||||
>
|
||||
<form
|
||||
noValidate
|
||||
spellCheck={false}
|
||||
onSubmit={onSubmit}
|
||||
<Field
|
||||
type="password"
|
||||
id="login-password-input"
|
||||
label="Password"
|
||||
isRequired={true}
|
||||
placeholder="••••••••••"
|
||||
autoComplete="current-password"
|
||||
value={form.password}
|
||||
onChange={onChange}
|
||||
error={errors.password}
|
||||
ref={inputRef}
|
||||
/>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Field
|
||||
type="password"
|
||||
id="login-password-input"
|
||||
label="Password"
|
||||
isRequired={true}
|
||||
placeholder="••••••••••"
|
||||
autoComplete="current-password"
|
||||
value={form.password}
|
||||
onChange={onChange}
|
||||
error={errors.password}
|
||||
ref={inputRef}
|
||||
/>
|
||||
</form>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="info"
|
||||
onClick={onBack}
|
||||
sx={{
|
||||
px: theme.spacing(5),
|
||||
"& svg.MuiSvgIcon-root": {
|
||||
mr: theme.spacing(3),
|
||||
},
|
||||
"&:focus-visible": {
|
||||
outline: `2px solid ${theme.palette.primary.main}`,
|
||||
outlineOffset: `2px`,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ArrowBackRoundedIcon />
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
type="submit"
|
||||
disabled={errors.password && true}
|
||||
sx={{
|
||||
width: "30%",
|
||||
"&.Mui-focusVisible": {
|
||||
outline: `2px solid ${theme.palette.primary.main}`,
|
||||
outlineOffset: `2px`,
|
||||
boxShadow: `none`,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="info"
|
||||
onClick={onBack}
|
||||
sx={{
|
||||
px: theme.spacing(5),
|
||||
"& svg.MuiSvgIcon-root": {
|
||||
mr: theme.spacing(3),
|
||||
},
|
||||
}}
|
||||
props={{ tabIndex: -1 }}
|
||||
>
|
||||
<ArrowBackRoundedIcon />
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={onSubmit}
|
||||
disabled={errors.password && true}
|
||||
sx={{ width: "30%" }}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</Stack>
|
||||
<Box
|
||||
textAlign="center"
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: "104%",
|
||||
bottom: 0,
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)",
|
||||
transform: `translate(-50%, 150%)`,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { useDispatch } from "react-redux";
|
||||
import PropTypes from "prop-types";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Box, Button, Stack, Typography } from "@mui/material";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { StepOne } from "./StepOne";
|
||||
import { StepTwo } from "./StepTwo";
|
||||
import { StepThree } from "./StepThree";
|
||||
import { networkService } from "../../../main";
|
||||
import { credentials } from "../../../Validation/validation";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import { register } from "../../../Features/Auth/authSlice";
|
||||
import { useParams } from "react-router-dom";
|
||||
import Background from "../../../assets/Images/background-grid.svg?react";
|
||||
import Logo from "../../../assets/icons/bwu-icon.svg?react";
|
||||
import Mail from "../../../assets/icons/mail.svg?react";
|
||||
import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
|
||||
import Check from "../../../Components/Check/Check";
|
||||
import Field from "../../../Components/Inputs/Field";
|
||||
import { networkService } from "../../../main";
|
||||
import "../index.css";
|
||||
|
||||
/**
|
||||
@@ -54,6 +53,10 @@ const LandingPage = ({ isSuperAdmin, onSignup }) => {
|
||||
stroke: theme.palette.other.icon,
|
||||
},
|
||||
},
|
||||
"&:focus-visible": {
|
||||
outline: `2px solid ${theme.palette.primary.main}`,
|
||||
outlineOffset: `2px`,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Mail />
|
||||
@@ -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 (
|
||||
<>
|
||||
<Stack
|
||||
gap={{ xs: theme.spacing(8), sm: theme.spacing(12) }}
|
||||
textAlign="center"
|
||||
>
|
||||
<Box>
|
||||
<Typography component="h1">Sign Up</Typography>
|
||||
<Typography>Enter your personal details</Typography>
|
||||
</Box>
|
||||
<Box textAlign="left">
|
||||
<Box
|
||||
component="form"
|
||||
noValidate
|
||||
spellCheck={false}
|
||||
onSubmit={onSubmit}
|
||||
mb={theme.spacing(10)}
|
||||
>
|
||||
<Field
|
||||
id="register-firstname-input"
|
||||
label="Name"
|
||||
isRequired={true}
|
||||
placeholder="Jordan"
|
||||
autoComplete="given-name"
|
||||
value={form.firstName}
|
||||
onChange={onChange}
|
||||
error={errors.firstName}
|
||||
ref={inputRef}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
component="form"
|
||||
noValidate
|
||||
spellCheck={false}
|
||||
onSubmit={onSubmit}
|
||||
mb={theme.spacing(5)}
|
||||
>
|
||||
<Field
|
||||
id="register-lastname-input"
|
||||
label="Surname"
|
||||
isRequired={true}
|
||||
placeholder="Ellis"
|
||||
autoComplete="family-name"
|
||||
value={form.lastName}
|
||||
onChange={onChange}
|
||||
error={errors.lastName}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="info"
|
||||
onClick={onBack}
|
||||
sx={{
|
||||
px: theme.spacing(5),
|
||||
"& svg.MuiSvgIcon-root": {
|
||||
mr: theme.spacing(3),
|
||||
},
|
||||
}}
|
||||
props={{ tabIndex: -1 }}
|
||||
>
|
||||
<ArrowBackRoundedIcon />
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={onSubmit}
|
||||
disabled={(errors.firstName || errors.lastName) && true}
|
||||
sx={{ width: "30%" }}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<>
|
||||
<Stack
|
||||
gap={{ xs: theme.spacing(8), sm: theme.spacing(12) }}
|
||||
textAlign="center"
|
||||
>
|
||||
<Box>
|
||||
<Typography component="h1">Sign Up</Typography>
|
||||
<Typography>Enter your email address</Typography>
|
||||
</Box>
|
||||
<Box textAlign="left">
|
||||
<Box
|
||||
component="form"
|
||||
noValidate
|
||||
spellCheck={false}
|
||||
onSubmit={onSubmit}
|
||||
mb={theme.spacing(5)}
|
||||
>
|
||||
<Field
|
||||
type="email"
|
||||
id="register-email-input"
|
||||
label="Email"
|
||||
isRequired={true}
|
||||
placeholder="jordan.ellis@domain.com"
|
||||
autoComplete="email"
|
||||
value={form.email}
|
||||
onInput={(e) => (e.target.value = e.target.value.toLowerCase())}
|
||||
onChange={onChange}
|
||||
error={errors.email}
|
||||
ref={inputRef}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="info"
|
||||
onClick={onBack}
|
||||
sx={{
|
||||
px: theme.spacing(5),
|
||||
"& svg.MuiSvgIcon-root": {
|
||||
mr: theme.spacing(3),
|
||||
},
|
||||
}}
|
||||
props={{ tabIndex: -1 }}
|
||||
>
|
||||
<ArrowBackRoundedIcon />
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={onSubmit}
|
||||
disabled={errors.email && true}
|
||||
sx={{ width: "30%" }}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<>
|
||||
<Stack
|
||||
gap={{ xs: theme.spacing(8), sm: theme.spacing(12) }}
|
||||
textAlign="center"
|
||||
>
|
||||
<Box>
|
||||
<Typography component="h1">Sign Up</Typography>
|
||||
<Typography>Create your password</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
textAlign="left"
|
||||
sx={{
|
||||
"& .input-error": {
|
||||
display: "none",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component="form"
|
||||
noValidate
|
||||
spellCheck={false}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<Field
|
||||
type="password"
|
||||
id="register-password-input"
|
||||
label="Password"
|
||||
isRequired={true}
|
||||
placeholder="Create a password"
|
||||
autoComplete="current-password"
|
||||
value={form.password}
|
||||
onChange={onChange}
|
||||
error={errors.password}
|
||||
ref={inputRef}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
component="form"
|
||||
noValidate
|
||||
spellCheck={false}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<Field
|
||||
type="password"
|
||||
id="register-confirm-input"
|
||||
label="Confirm password"
|
||||
isRequired={true}
|
||||
placeholder="Confirm your password"
|
||||
autoComplete="current-password"
|
||||
value={form.confirm}
|
||||
onChange={onChange}
|
||||
error={errors.confirm}
|
||||
/>
|
||||
</Box>
|
||||
<Stack
|
||||
gap={theme.spacing(4)}
|
||||
mb={{ xs: theme.spacing(6), sm: theme.spacing(8) }}
|
||||
>
|
||||
<Check
|
||||
text={
|
||||
<>
|
||||
<Typography component="span">Must be at least</Typography> 8 characters
|
||||
long
|
||||
</>
|
||||
}
|
||||
variant={
|
||||
errors?.password === "Password is required"
|
||||
? "error"
|
||||
: form.password === ""
|
||||
? "info"
|
||||
: form.password.length < 8
|
||||
? "error"
|
||||
: "success"
|
||||
}
|
||||
/>
|
||||
<Check
|
||||
text={
|
||||
<>
|
||||
<Typography component="span">Must contain</Typography> one special
|
||||
character and a number
|
||||
</>
|
||||
}
|
||||
variant={
|
||||
errors?.password === "Password is required"
|
||||
? "error"
|
||||
: form.password === ""
|
||||
? "info"
|
||||
: !/^(?=.*[!@#$%^&*(),.?":{}|])(?=.*\d).+$/.test(form.password)
|
||||
? "error"
|
||||
: "success"
|
||||
}
|
||||
/>
|
||||
<Check
|
||||
text={
|
||||
<>
|
||||
<Typography component="span">Must contain at least</Typography> one
|
||||
upper and lower character
|
||||
</>
|
||||
}
|
||||
variant={
|
||||
errors?.password === "Password is required"
|
||||
? "error"
|
||||
: form.password === ""
|
||||
? "info"
|
||||
: !/^(?=.*[A-Z])(?=.*[a-z]).+$/.test(form.password)
|
||||
? "error"
|
||||
: "success"
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="info"
|
||||
onClick={onBack}
|
||||
sx={{
|
||||
px: theme.spacing(5),
|
||||
"& svg.MuiSvgIcon-root": {
|
||||
mr: theme.spacing(3),
|
||||
},
|
||||
}}
|
||||
props={{ tabIndex: -1 }}
|
||||
>
|
||||
<ArrowBackRoundedIcon />
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={onSubmit}
|
||||
disabled={errors.email && true}
|
||||
sx={{ width: "30%" }}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
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 ? (
|
||||
<StepThree
|
||||
form={form}
|
||||
errors={errors}
|
||||
/* form={form}
|
||||
errors={errors} */
|
||||
onSubmit={handleStepThree}
|
||||
onChange={handleChange}
|
||||
/* onChange={handleChange} */
|
||||
onBack={() => setStep(2)}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -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 */}
|
||||
<Stack
|
||||
gap={{ xs: theme.spacing(12), sm: theme.spacing(16) }}
|
||||
textAlign="center"
|
||||
>
|
||||
<Box>
|
||||
<Typography component="h1">Sign Up</Typography>
|
||||
<Typography>Enter your personal details</Typography>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
textAlign="left"
|
||||
component="form"
|
||||
noValidate
|
||||
spellCheck={false}
|
||||
onSubmit={onSubmit}
|
||||
display="grid"
|
||||
gap={{ xs: theme.spacing(12), sm: theme.spacing(16) }}
|
||||
>
|
||||
<Box
|
||||
display="grid"
|
||||
gap={{ xs: theme.spacing(8), sm: theme.spacing(12) }}
|
||||
>
|
||||
<Field
|
||||
id="register-firstname-input"
|
||||
label="Name"
|
||||
isRequired={true}
|
||||
placeholder="Jordan"
|
||||
autoComplete="given-name"
|
||||
value={form.firstName}
|
||||
onChange={onChange}
|
||||
error={errors.firstName}
|
||||
ref={inputRef}
|
||||
/>
|
||||
<Field
|
||||
id="register-lastname-input"
|
||||
label="Surname"
|
||||
isRequired={true}
|
||||
placeholder="Ellis"
|
||||
autoComplete="family-name"
|
||||
value={form.lastName}
|
||||
onChange={onChange}
|
||||
error={errors.lastName}
|
||||
/>
|
||||
</Box>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
{/* TODO buttons should be a component should be a component */}
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="info"
|
||||
onClick={onBack}
|
||||
sx={{
|
||||
px: theme.spacing(5),
|
||||
"& svg.MuiSvgIcon-root": {
|
||||
mr: theme.spacing(3),
|
||||
},
|
||||
|
||||
"&:focus-visible": {
|
||||
outline: `2px solid ${theme.palette.primary.main}`,
|
||||
outlineOffset: `2px`,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ArrowBackRoundedIcon />
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
type="submit"
|
||||
disabled={(errors.firstName || errors.lastName) && true}
|
||||
sx={{
|
||||
width: "30%",
|
||||
"&.Mui-focusVisible": {
|
||||
outline: `2px solid ${theme.palette.primary.main}`,
|
||||
outlineOffset: `2px`,
|
||||
boxShadow: `none`,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { StepOne };
|
||||
@@ -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 (
|
||||
<>
|
||||
<Stack
|
||||
gap={{ xs: theme.spacing(8), sm: theme.spacing(12) }}
|
||||
textAlign="center"
|
||||
>
|
||||
<Box>
|
||||
<Typography component="h1">Sign Up</Typography>
|
||||
<Typography>Create your password</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
component="form"
|
||||
noValidate
|
||||
spellCheck={false}
|
||||
onSubmit={onSubmit}
|
||||
textAlign="left"
|
||||
display="grid"
|
||||
gap={{ xs: theme.spacing(12), sm: theme.spacing(16) }}
|
||||
sx={{
|
||||
"& .input-error": {
|
||||
display: "none",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
display="grid"
|
||||
gap={{ xs: theme.spacing(8), sm: theme.spacing(12) }}
|
||||
>
|
||||
<Field
|
||||
type="password"
|
||||
id="register-password-input"
|
||||
name="password"
|
||||
label="Password"
|
||||
isRequired={true}
|
||||
placeholder="Create a password"
|
||||
autoComplete="current-password"
|
||||
value={form.password}
|
||||
onChange={handleChange}
|
||||
error={errors.password && errors.password[0]}
|
||||
ref={inputRef}
|
||||
/>
|
||||
<Field
|
||||
type="password"
|
||||
id="register-confirm-input"
|
||||
name="confirm"
|
||||
label="Confirm password"
|
||||
isRequired={true}
|
||||
placeholder="Confirm your password"
|
||||
autoComplete="current-password"
|
||||
value={form.confirm}
|
||||
onChange={handleChange}
|
||||
error={errors.confirm && errors.confirm[0]}
|
||||
/>
|
||||
</Box>
|
||||
<Stack
|
||||
gap={theme.spacing(4)}
|
||||
mb={{ xs: theme.spacing(6), sm: theme.spacing(8) }}
|
||||
>
|
||||
<Check
|
||||
noHighlightText={"Must be at least"}
|
||||
text={"8 characters long"}
|
||||
variant={feedbacks.length}
|
||||
/>
|
||||
<Check
|
||||
noHighlightText={"Must contain at least"}
|
||||
text={"one special character"}
|
||||
variant={feedbacks.special}
|
||||
/>
|
||||
<Check
|
||||
noHighlightText={"Must contain at least"}
|
||||
text={"one number"}
|
||||
variant={feedbacks.number}
|
||||
/>
|
||||
<Check
|
||||
noHighlightText={"Must contain at least"}
|
||||
text={"one upper character"}
|
||||
variant={feedbacks.uppercase}
|
||||
/>
|
||||
<Check
|
||||
noHighlightText={"Must contain at least"}
|
||||
text={"one lower character"}
|
||||
variant={feedbacks.lowercase}
|
||||
/>
|
||||
<Check
|
||||
noHighlightText={"Confirm password and password"}
|
||||
text={"must match"}
|
||||
variant={feedbacks.confirm}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="info"
|
||||
onClick={onBack}
|
||||
sx={{
|
||||
px: theme.spacing(5),
|
||||
"& svg.MuiSvgIcon-root": {
|
||||
mr: theme.spacing(3),
|
||||
},
|
||||
":focus-visible": {
|
||||
outline: `2px solid ${theme.palette.primary.dark}`,
|
||||
outlineOffset: "4px",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ArrowBackRoundedIcon />
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={
|
||||
form.password.length === 0 ||
|
||||
form.confirm.length === 0 ||
|
||||
Object.keys(errors).length !== 0
|
||||
}
|
||||
sx={{
|
||||
width: "30%",
|
||||
"&.Mui-focusVisible": {
|
||||
outline: `2px solid ${theme.palette.primary.main}`,
|
||||
outlineOffset: `2px`,
|
||||
boxShadow: `none`,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { StepThree };
|
||||
@@ -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 (
|
||||
<>
|
||||
<Stack
|
||||
gap={{ xs: theme.spacing(8), sm: theme.spacing(12) }}
|
||||
textAlign="center"
|
||||
>
|
||||
<Box>
|
||||
<Typography component="h1">Sign Up</Typography>
|
||||
<Typography>Enter your email address</Typography>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
component="form"
|
||||
textAlign="left"
|
||||
noValidate
|
||||
spellCheck={false}
|
||||
onSubmit={onSubmit}
|
||||
mb={theme.spacing(5)}
|
||||
display="grid"
|
||||
gap={{ xs: theme.spacing(12), sm: theme.spacing(16) }}
|
||||
>
|
||||
<Field
|
||||
type="email"
|
||||
id="register-email-input"
|
||||
label="Email"
|
||||
isRequired={true}
|
||||
placeholder="jordan.ellis@domain.com"
|
||||
autoComplete="email"
|
||||
value={form.email}
|
||||
onInput={(e) => (e.target.value = e.target.value.toLowerCase())}
|
||||
onChange={onChange}
|
||||
error={errors.email}
|
||||
ref={inputRef}
|
||||
/>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="info"
|
||||
onClick={onBack}
|
||||
sx={{
|
||||
px: theme.spacing(5),
|
||||
"& svg.MuiSvgIcon-root": {
|
||||
mr: theme.spacing(3),
|
||||
},
|
||||
"&:focus-visible": {
|
||||
outline: `2px solid ${theme.palette.primary.main}`,
|
||||
outlineOffset: `2px`,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ArrowBackRoundedIcon />
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={onSubmit}
|
||||
disabled={errors.email && true}
|
||||
sx={{
|
||||
width: "30%",
|
||||
"&.Mui-focusVisible": {
|
||||
outline: `2px solid ${theme.palette.primary.main}`,
|
||||
outlineOffset: `2px`,
|
||||
boxShadow: `none`,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { StepTwo };
|
||||
@@ -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 (
|
||||
<Stack
|
||||
className="set-new-password-page auth"
|
||||
@@ -188,8 +148,9 @@ const SetNewPassword = () => {
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<Field
|
||||
id={passwordId}
|
||||
type="password"
|
||||
id="register-password-input"
|
||||
name="password"
|
||||
label="Password"
|
||||
isRequired={true}
|
||||
placeholder="••••••••"
|
||||
@@ -205,8 +166,9 @@ const SetNewPassword = () => {
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<Field
|
||||
id={confirmPasswordId}
|
||||
type="password"
|
||||
id="confirm-password-input"
|
||||
name="confirm"
|
||||
label="Confirm password"
|
||||
isRequired={true}
|
||||
placeholder="••••••••"
|
||||
@@ -220,55 +182,34 @@ const SetNewPassword = () => {
|
||||
mb={theme.spacing(12)}
|
||||
>
|
||||
<Check
|
||||
text={
|
||||
<>
|
||||
<Typography component="span">Must be at least</Typography> 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}
|
||||
/>
|
||||
<Check
|
||||
text={
|
||||
<>
|
||||
<Typography component="span">Must contain</Typography> 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}
|
||||
/>
|
||||
<Check
|
||||
text={
|
||||
<>
|
||||
<Typography component="span">Must contain at least</Typography> 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}
|
||||
/>
|
||||
<Check
|
||||
noHighlightText={"Must contain at least"}
|
||||
text={"one upper character"}
|
||||
variant={feedbacks.uppercase}
|
||||
/>
|
||||
<Check
|
||||
noHighlightText={"Must contain at least"}
|
||||
text={"one lower character"}
|
||||
variant={feedbacks.lowercase}
|
||||
/>
|
||||
<Check
|
||||
noHighlightText={"Confirm password and password"}
|
||||
text={"must match"}
|
||||
variant={feedbacks.confirm}
|
||||
/>
|
||||
</Stack>
|
||||
</Box>
|
||||
@@ -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
|
||||
|
||||
@@ -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 };
|
||||
@@ -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,
|
||||
};
|
||||
Generated
+7
-7
@@ -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",
|
||||
|
||||
+1
-1
@@ -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",
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user