Merge branch 'develop' into 1067-fe-hardware-monitoring-create-hardware-monitor

This commit is contained in:
Shemy Gan committed 2024-11-04 10:08:56 -05:00
commit cabc7a75df
20 files changed
+847 -746

No files matched your search

+10 -8
View File
@@ -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 />}
+3 -1
View File
@@ -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 };
-1
View File
@@ -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,
+3 -3
View File
@@ -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
View File
@@ -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
+19 -399
View File
@@ -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 };
+55 -110
View File
@@ -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 };
+34 -21
View File
@@ -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,
};