This commit is contained in:
Alex Holliday
2026-02-06 23:37:25 +00:00
parent 5152336591
commit 734ec41ca0
4 changed files with 1 additions and 493 deletions
-330
View File
@@ -1,330 +0,0 @@
// Components
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx";
import Button from "@mui/material/Button";
import Box from "@mui/material/Box";
import PasswordTooltip from "../components/PasswordTooltip.jsx";
// Utils
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { useDispatch, useSelector } from "react-redux";
import { useParams } from "react-router-dom";
import { networkService } from "../../../main.jsx";
import { newOrChangedCredentials } from "../../../Validation/validation.js";
import { register } from "../../../Features/Auth/authSlice.js";
import AuthPageWrapper from "../components/AuthPageWrapper.jsx";
import { createToast } from "../../../Utils/toastUtils.jsx";
import PropTypes from "prop-types";
const getFeedbackStatus = (form, errors, field, criteria) => {
const fieldErrors = errors?.[field];
const isFieldEmpty = form?.[field]?.length === 0;
const hasError = fieldErrors?.includes(criteria) || fieldErrors?.includes("empty");
const isCorrect = !isFieldEmpty && !hasError;
if (isCorrect) {
return "success";
} else if (hasError) {
return "error";
} else {
return "info";
}
};
const Register = ({ superAdminExists }) => {
// Redux
const { isLoading } = useSelector((state) => state.auth);
// Local state
const [form, setForm] = useState({
firstName: "",
lastName: "",
email: "",
password: "",
confirm: "",
role: [],
teamId: "",
});
const [errors, setErrors] = useState({});
const [feedback, setFeedback] = useState({});
// Hooks
const theme = useTheme();
const { t } = useTranslation();
const { token } = useParams();
const navigate = useNavigate();
const dispatch = useDispatch();
// Effects
useEffect(() => {
const fetchInvite = async () => {
if (token !== undefined) {
try {
const res = await networkService.verifyInvitationToken(token);
const invite = res.data.data;
const { email } = invite;
setForm((prevForm) => {
if (!prevForm.email) {
return { ...prevForm, email };
}
return prevForm;
});
} catch (error) {
navigate("/register", { replace: true });
}
}
};
fetchInvite();
}, [form, token, navigate]);
// Handlers
const onChange = (e) => {
let { name, value } = e.target;
if (name === "email") value = value.toLowerCase();
const updatedForm = { ...form, [name]: value };
const { error } = newOrChangedCredentials.validate(
{ [name]: value },
{ abortEarly: false, context: { password: form.password } }
);
setErrors((prev) => ({
...prev,
[name]: error?.details?.[0]?.message || "",
}));
setForm(updatedForm);
};
const onPasswordChange = (e) => {
const { name, value } = e.target;
const updatedForm = { ...form, [name]: value };
setForm(updatedForm);
const validateValue = { [name]: value };
const validateOptions = { abortEarly: false, context: { password: form.password } };
if (name === "password" && form.confirm.length > 0) {
validateValue.confirm = form.confirm;
validateOptions.context = { password: value };
} else if (name === "confirm") {
validateValue.password = form.password;
}
const { error } = newOrChangedCredentials.validate(validateValue, validateOptions);
const pwdErrors = error?.details.map((error) => ({
path: error.path[0],
type: error.type,
}));
const errorsByPath =
pwdErrors &&
pwdErrors.reduce((acc, { path, type }) => {
if (!acc[path]) {
acc[path] = [];
}
acc[path].push(type);
return acc;
}, {});
const oldErrors = { ...errors };
if (name === "password") {
oldErrors.password = undefined;
} else if (name === "confirm") {
oldErrors.confirm = undefined;
}
const newErrors = { ...oldErrors, ...errorsByPath };
setErrors(newErrors);
const newFeedback = {
length: getFeedbackStatus(updatedForm, errorsByPath, "password", "string.min"),
special: getFeedbackStatus(updatedForm, errorsByPath, "password", "special"),
number: getFeedbackStatus(updatedForm, errorsByPath, "password", "number"),
uppercase: getFeedbackStatus(updatedForm, errorsByPath, "password", "uppercase"),
lowercase: getFeedbackStatus(updatedForm, errorsByPath, "password", "lowercase"),
confirm: getFeedbackStatus(updatedForm, errorsByPath, "confirm", "different"),
};
setFeedback(newFeedback);
};
const onSubmit = async (e) => {
e.preventDefault();
const toSubmit = {
...form,
role: superAdminExists ? form.role : ["superadmin"],
inviteToken: token ? token : "",
};
const { error } = newOrChangedCredentials.validate(toSubmit, {
abortEarly: false,
context: { password: form.password },
});
if (error) {
const formErrors = {};
for (const err of error.details) {
formErrors[err.path[0]] = err.message;
}
setErrors(formErrors);
return;
}
delete toSubmit.confirm;
const action = await dispatch(register({ user: toSubmit, token }));
if (action.payload.success) {
navigate("/uptime");
createToast({
body: t("auth.registration.toasts.success"),
});
} else {
if (action.payload) {
createToast({
body: action.payload.msg,
});
} else {
createToast({
body: t("common.toasts.unknownError"),
});
}
}
};
return (
<AuthPageWrapper
heading={t("auth.registration.heading.user")}
welcome={t("auth.registration.welcome")}
>
<Stack
component="form"
width="100%"
padding={theme.spacing(8)}
gap={theme.spacing(8)}
onSubmit={onSubmit}
sx={{
width: {
sm: "80%",
},
}}
>
<Typography variant="h2">
{superAdminExists
? t("auth.registration.description.user")
: t("auth.registration.description.superAdmin")}
</Typography>
<Stack
direction={{ xs: "column", lg: "row" }}
justifyContent="space-between"
gap={theme.spacing(4)}
>
<TextInput
name="firstName"
sx={{ flex: 1 }}
label={t("auth.common.inputs.firstName.label")}
width="100%"
gap={theme.spacing(4)}
isRequired={true}
placeholder={t("auth.common.inputs.firstName.placeholder")}
autoComplete="given-name"
value={form.firstName}
onChange={onChange}
error={errors.firstName ? true : false}
helperText={errors.firstName ? t(errors.firstName) : ""}
/>
<TextInput
name="lastName"
label={t("auth.common.inputs.lastName.label")}
sx={{ flex: 1 }}
width="100%"
gap={theme.spacing(4)}
isRequired={true}
placeholder={t("auth.common.inputs.lastName.placeholder")}
autoComplete="family-name"
value={form.lastName}
onChange={onChange}
error={errors.lastName ? true : false}
helperText={errors.lastName ? t(errors.lastName) : ""} // Localization keys are in validation.js
/>
</Stack>
<TextInput
type="email"
name="email"
gap={theme.spacing(4)}
label={t("auth.common.inputs.email.label")}
isRequired={true}
placeholder={t("auth.common.inputs.email.placeholder")}
autoComplete="email"
value={form.email}
onInput={(e) => (e.target.value = e.target.value.toLowerCase())}
onChange={onChange}
error={errors.email ? true : false}
helperText={errors.email ? t(errors.email) : ""} // Localization keys are in validation.js
/>
<PasswordTooltip
feedback={feedback}
form={form}
>
<Box>
<TextInput
type="password"
id="register-password-input"
name="password"
label={t("auth.common.inputs.password.label")}
gap={theme.spacing(4)}
isRequired={true}
placeholder="••••••••••"
autoComplete="current-password"
value={form.password}
onChange={onPasswordChange}
error={errors.password && errors.password[0] ? true : false}
helperText={
errors.password === "auth.common.inputs.password.errors.empty"
? t(errors.password)
: ""
}
/>
</Box>
</PasswordTooltip>
<TextInput
type="password"
id="register-confirm-input"
name="confirm"
label={t("auth.common.inputs.passwordConfirm.label")}
gap={theme.spacing(4)}
isRequired={true}
placeholder={t("auth.common.inputs.passwordConfirm.placeholder")}
autoComplete="current-password"
value={form.confirm}
onChange={onPasswordChange}
marginBottom={theme.spacing(4)}
error={errors.confirm && errors.confirm[0] ? true : false}
/>
<Button
disabled={isLoading}
variant="contained"
color="accent"
type="submit"
sx={{
width: "100%",
alignSelf: "center",
fontWeight: 700,
mt: theme.spacing(10),
}}
>
{t("auth.common.navigation.continue")}
</Button>
</Stack>
</AuthPageWrapper>
);
};
Register.propTypes = {
superAdminExists: PropTypes.bool,
};
export default Register;
@@ -1,49 +0,0 @@
// Components
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import Logo from "@/assets/icons/checkmate-icon.svg?react";
import LanguageSelector from "@/Components/LanguageSelector.jsx";
import ThemeSwitch from "@/Components/v1/ThemeSwitch/index.jsx";
// Utils
import { useTheme } from "@mui/material/styles";
import { useTranslation } from "react-i18next";
const AuthHeader = ({ hideLogo = false }) => {
// Hooks
const theme = useTheme();
const { t } = useTranslation();
return (
<Stack
direction="row"
alignItems="center"
justifyContent="space-between"
px={theme.spacing(12)}
gap={theme.spacing(4)}
>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(4)}
>
{!hideLogo && (
<>
<Logo style={{ borderRadius: theme.shape.borderRadius }} />
<Typography sx={{ userSelect: "none" }}>{t("common.appName")}</Typography>
</>
)}
</Stack>
<Stack
direction="row"
spacing={theme.spacing(2)}
alignItems="center"
>
<LanguageSelector />
<ThemeSwitch />
</Stack>
</Stack>
);
};
export default AuthHeader;
@@ -1,107 +0,0 @@
import Background from "@/assets/Images/background-grid.svg?react";
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import AuthHeader from "./AuthHeader.jsx";
import { useTheme } from "@mui/material/styles";
import Logo from "@/assets/icons/checkmate-icon.svg?react";
import PropTypes from "prop-types";
const AuthPageWrapper = ({ children, heading, welcome }) => {
const theme = useTheme();
return (
<Stack
gap={theme.spacing(10)}
minHeight="100vh"
position="relative"
backgroundColor={theme.palette.primary.main}
sx={{ overflow: "hidden" }}
>
<AuthHeader hideLogo={true} />
<Box
sx={{
position: "absolute",
top: 0,
left: "0%",
transform: "translate(-40%, -40%)",
zIndex: 0,
width: "100%",
height: "100%",
"& svg g g:last-of-type path": {
stroke: theme.palette.primary.lowContrast,
},
}}
>
<Background style={{ width: "100%" }} />
</Box>
<Box
sx={{
position: "absolute",
bottom: 0,
right: 0,
transform: "translate(45%, 55%)",
zIndex: 0,
width: "100%",
height: "100%",
"& svg g g:last-of-type path": {
stroke: theme.palette.primary.lowContrast,
},
}}
>
<Background style={{ width: "100%" }} />
</Box>
<Stack
backgroundColor={theme.palette.primary.main}
sx={{
borderRadius: theme.spacing(8),
boxShadow: theme.palette.tertiary.cardShadow,
margin: "auto",
alignItems: "center",
gap: theme.spacing(10),
padding: theme.spacing(20),
zIndex: 1,
position: "relative",
width: {
sm: "60%",
md: "50%",
lg: "40%",
xl: "30%",
},
}}
>
<Box
mb={theme.spacing(10)}
mt={theme.spacing(5)}
>
<Box
sx={{
width: { xs: 60, sm: 70, md: 80 },
}}
/>
<Logo style={{ width: "100%", height: "100%" }} />
</Box>
<Stack
mb={theme.spacing(4)}
textAlign="center"
>
<Typography
variant="h1"
mb={theme.spacing(2)}
>
{welcome}
</Typography>
<Typography variant="h1">{heading}</Typography>
</Stack>
{children}
</Stack>
</Stack>
);
};
export default AuthPageWrapper;
AuthPageWrapper.propTypes = {
children: PropTypes.node,
heading: PropTypes.node,
welcome: PropTypes.node,
};
+1 -7
View File
@@ -10,7 +10,6 @@ import NotFound from "../Pages/NotFound/index.jsx";
// Auth
import AuthLogin from "../Pages/Auth/Login";
import AuthRegister from "@/Pages/Auth/Register";
import OldAuthRegister from "../Pages/Auth/Register/old.jsx";
import AuthForgotPassword from "@/Pages/Auth/Recovery";
import AuthCheckEmail from "../Pages/Auth/CheckEmail.jsx";
import AuthSetNewPassword from "../Pages/Auth/SetNewPassword.jsx";
@@ -63,8 +62,7 @@ import CreateMonitor from "@/Pages/CreateMonitor";
const Routes = () => {
const mode = useSelector((state) => state.ui.mode);
// const AdminCheckedRegister = withAdminCheck(AuthRegister);
const AdminCheckedRegister = AuthRegister;
const AdminCheckedRegister = withAdminCheck(AuthRegister);
const v2theme = mode === "light" ? lightTheme : darkTheme;
return (
<LibRoutes>
@@ -382,10 +380,6 @@ const Routes = () => {
</>
}
/>
<Route
path="/register-old"
element={<OldAuthRegister />}
/>
<Route
exact