diff --git a/client/src/Pages/Auth/Register/Register.jsx b/client/src/Pages/Auth/Register/Register.jsx
deleted file mode 100644
index 390aa6e36..000000000
--- a/client/src/Pages/Auth/Register/Register.jsx
+++ /dev/null
@@ -1,413 +0,0 @@
-import { useState, useEffect } from "react";
-import { useNavigate, useParams } from "react-router-dom";
-import { useDispatch } from "react-redux";
-import PropTypes from "prop-types";
-import { useTheme } from "@emotion/react";
-import { Box, Button, Stack, Typography } from "@mui/material";
-import { StepOne } from "./StepOne";
-import { StepTwo } from "./StepTwo";
-import { StepThree } from "./StepThree";
-import { networkService } from "../../../main";
-import { newOrChangedCredentials } from "../../../Validation/validation";
-import { createToast } from "../../../Utils/toastUtils";
-import { register } from "../../../Features/Auth/authSlice";
-import Background from "../../../assets/Images/background-grid.svg?react";
-import Logo from "../../../assets/icons/checkmate-icon.svg?react";
-import Mail from "../../../assets/icons/mail.svg?react";
-import "../index.css";
-import { useTranslation, Trans } from "react-i18next";
-/**
- * Displays the initial landing page.
- *
- * @param {Object} props
- * @param {boolean} props.isSuperAdmin - Whether the user is creating and admin account
- * @param {Function} props.onContinue - Callback function to handle "Continue with Email" button click.
- * @returns {JSX.Element}
- */
-const LandingPage = ({ isSuperAdmin, onSignup }) => {
- const theme = useTheme();
- const { t } = useTranslation();
- return (
- <>
-
-
-
- {isSuperAdmin
- ? t("auth.registration.heading.superAdmin")
- : t("auth.registration.heading.user")}
-
-
- {isSuperAdmin
- ? t("auth.registration.description.superAdmin")
- : t("auth.registration.description.user")}
-
-
-
-
-
-
-
- {
- window.open(
- "https://bluewavelabs.ca/terms-of-service-open-source",
- "_blank",
- "noreferrer"
- );
- }}
- />
- ),
- a2: (
- {
- window.open(
- "https://bluewavelabs.ca/privacy-policy-open-source",
- "_blank",
- "noreferrer"
- );
- }}
- />
- ),
- }}
- />
-
-
-
- >
- );
-};
-
-LandingPage.propTypes = {
- isSuperAdmin: PropTypes.bool,
- onSignup: PropTypes.func,
-};
-
-const Register = ({ isSuperAdmin }) => {
- const dispatch = useDispatch();
- const navigate = useNavigate();
- const { token } = useParams();
- const theme = useTheme();
- const { t } = useTranslation();
- // TODO If possible, change the IDs of these fields to match the backend
- const idMap = {
- "register-firstname-input": "firstName",
- "register-lastname-input": "lastName",
- "register-email-input": "email",
- "register-password-input": "password",
- "register-confirm-input": "confirm",
- };
-
- const [form, setForm] = useState({
- firstName: "",
- lastName: "",
- email: "",
- password: "",
- confirm: "",
- role: [],
- teamId: "",
- });
- const [errors, setErrors] = useState({});
- const [step, setStep] = useState(0);
-
- useEffect(() => {
- const fetchInvite = async () => {
- if (token !== undefined) {
- try {
- const res = await networkService.verifyInvitationToken(token);
- const invite = res.data.data;
- const { email } = invite;
- setForm({ ...form, email });
- } catch (error) {
- navigate("/register", { replace: true });
- }
- }
- };
- fetchInvite();
- }, []);
-
- /**
- * Validates the form data against the validation schema.
- *
- * @param {Object} data - The form data to validate.
- * @param {Object} [options] - Optional settings for validation.
- * @returns {Object | undefined} - Returns the validation error object if there are validation errors; otherwise, `undefined`.
- */
- const validateForm = (data, options = {}) => {
- const { error } = newOrChangedCredentials.validate(data, {
- abortEarly: false,
- ...options,
- });
- return error;
- };
-
- /**
- * Handles validation errors by setting the state with error messages and displaying a toast notification.
- *
- * @param {Object} error - The validation error object returned from the validation schema.
- */
- const handleError = (error) => {
- const newErrors = {};
- error.details.forEach((err) => {
- newErrors[err.path[0]] = err.message;
- });
- setErrors(newErrors);
- // Localization keys are in validation.js
- createToast({ body: t(error.details[0].message || "auth.common.errors.validation") });
- };
-
- const handleStepOne = async (e) => {
- e.preventDefault();
- let error = validateForm({
- firstName: form.firstName,
- lastName: form.lastName,
- });
-
- if (error) {
- handleError(error);
- return;
- }
-
- setStep(2);
- };
-
- const handleStepTwo = async (e) => {
- e.preventDefault();
-
- let error;
- error = validateForm({ email: form.email });
- if (error) {
- handleError(error);
- return;
- }
-
- setStep(3);
- };
-
- // Final step
- // 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: registerForm.password },
- });
- if (error) {
- handleError(error);
- return;
- }
-
- delete registerForm.confirm;
- const action = await dispatch(register(registerForm));
- if (action.payload.success) {
- const authToken = action.payload.data;
- 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"),
- });
- }
- }
- };
-
- const handleChange = (event) => {
- const { value, id } = event.target;
- const name = idMap[id];
- const lowerCasedValue =
- name === idMap["register-email-input"] ? value?.toLowerCase() || value : value;
- setForm((prev) => ({
- ...prev,
- [name]: lowerCasedValue,
- }));
-
- const { error } = newOrChangedCredentials.validate(
- { [name]: lowerCasedValue },
- { 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 (
-
-
-
-
-
-
- {t("common.appName")}
-
- .MuiStack-root": {
- border: 1,
- borderRadius: theme.spacing(5),
- borderColor: theme.palette.primary.lowContrast,
- backgroundColor: theme.palette.primary.main,
- padding: {
- xs: theme.spacing(12),
- sm: theme.spacing(20),
- },
- },
- }}
- >
- {step === 0 ? (
- setStep(1)}
- />
- ) : step === 1 ? (
- setStep(0)}
- />
- ) : step === 2 ? (
- setStep(1)}
- />
- ) : step === 3 ? (
- setStep(2)}
- />
- ) : (
- ""
- )}
-
-
-
- {
- navigate("/login");
- }}
- sx={{ userSelect: "none", color: theme.palette.accent.main }}
- />
- ),
- }}
- />
-
-
-
- );
-};
-Register.propTypes = {
- isSuperAdmin: PropTypes.bool,
-};
-export default Register;
diff --git a/client/src/Pages/Auth/Register/StepOne/index.jsx b/client/src/Pages/Auth/Register/StepOne/index.jsx
deleted file mode 100644
index 575a9132f..000000000
--- a/client/src/Pages/Auth/Register/StepOne/index.jsx
+++ /dev/null
@@ -1,142 +0,0 @@
-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 TextInput from "../../../../Components/Inputs/TextInput";
-import { useTranslation } from "react-i18next";
-
-StepOne.propTypes = {
- isSuperAdmin: PropTypes.bool,
- 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 {boolean} props.isSuperAdmin - Whether the user is creating and admin account
- * @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({ isSuperAdmin, form, errors, onSubmit, onChange, onBack }) {
- const theme = useTheme();
- const inputRef = useRef(null);
- const { t } = useTranslation();
-
- useEffect(() => {
- if (inputRef.current) {
- inputRef.current.focus();
- }
- }, []);
-
- return (
- <>
- {/* TODO this stack should be a component */}
-
-
-
- {isSuperAdmin
- ? t("auth.registration.heading.superAdmin")
- : t("auth.registration.heading.user")}
-
- {t("auth.registration.subheadings.stepOne")}
-
-
-
-
-
-
-
-
- {/* TODO buttons should be a component should be a component */}
-
-
-
-
-
- >
- );
-}
-
-export { StepOne };
diff --git a/client/src/Pages/Auth/Register/StepThree/index.jsx b/client/src/Pages/Auth/Register/StepThree/index.jsx
deleted file mode 100644
index c81ae08a7..000000000
--- a/client/src/Pages/Auth/Register/StepThree/index.jsx
+++ /dev/null
@@ -1,185 +0,0 @@
-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 TextInput from "../../../../Components/Inputs/TextInput";
-import Check from "../../../../Components/Check/Check";
-import { useValidatePassword } from "../../hooks/useValidatePassword";
-import { useTranslation } from "react-i18next";
-StepThree.propTypes = {
- isSuperAdmin: PropTypes.bool,
- onSubmit: PropTypes.func,
- onBack: PropTypes.func,
-};
-
-/**
- * Renders the third step of the sign up process.
- *
- * @param {Object} props
- * @param {boolean} props.isSuperAdmin - Whether the user is creating and admin account
- * @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({ isSuperAdmin, onSubmit, onBack }) {
- const theme = useTheme();
- const inputRef = useRef(null);
- const { t } = useTranslation();
-
- useEffect(() => {
- if (inputRef.current) {
- inputRef.current.focus();
- }
- }, []);
-
- const { handleChange, feedbacks, form, errors } = useValidatePassword();
- return (
- <>
-
-
-
- {isSuperAdmin
- ? t("auth.registration.heading.superAdmin")
- : t("auth.registration.heading.user")}
-
- {t("auth.registration.subheadings.stepThree")}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- >
- );
-}
-
-export { StepThree };
diff --git a/client/src/Pages/Auth/Register/StepTwo/index.jsx b/client/src/Pages/Auth/Register/StepTwo/index.jsx
deleted file mode 100644
index 306a2b750..000000000
--- a/client/src/Pages/Auth/Register/StepTwo/index.jsx
+++ /dev/null
@@ -1,125 +0,0 @@
-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 TextInput from "../../../../Components/Inputs/TextInput";
-import { useTranslation } from "react-i18next";
-
-StepTwo.propTypes = {
- isSuperAdmin: PropTypes.bool,
- 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 {boolean} props.isSuperAdmin - Whether the user is creating and admin account
- * @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({ isSuperAdmin, form, errors, onSubmit, onChange, onBack }) {
- const theme = useTheme();
- const inputRef = useRef(null);
- const { t } = useTranslation();
-
- useEffect(() => {
- if (inputRef.current) {
- inputRef.current.focus();
- }
- }, []);
-
- return (
- <>
-
-
-
- {isSuperAdmin
- ? t("auth.registration.heading.superAdmin")
- : t("auth.registration.heading.user")}
-
- {t("auth.registration.subheadings.stepTwo")}
-
-
-
- (e.target.value = e.target.value.toLowerCase())}
- onChange={onChange}
- error={errors.email ? true : false}
- helperText={t(errors.email)} // Localization keys are in validation.js
- ref={inputRef}
- />
-
-
-
-
-
-
- >
- );
-}
-
-export { StepTwo };
diff --git a/client/src/Pages/Auth/Register/index.jsx b/client/src/Pages/Auth/Register/index.jsx
new file mode 100644
index 000000000..2dd0951dd
--- /dev/null
+++ b/client/src/Pages/Auth/Register/index.jsx
@@ -0,0 +1,347 @@
+// Components
+import Stack from "@mui/material/Stack";
+import Typography from "@mui/material/Typography";
+import AuthHeader from "../components/AuthHeader";
+import TextInput from "../../../Components/Inputs/TextInput";
+import Check from "../../../Components/Check/Check";
+import Button from "@mui/material/Button";
+
+// Utils
+import { useTheme } from "@emotion/react";
+import { useTranslation } from "react-i18next";
+import { useState, useEffect } from "react";
+import { useNavigate } from "react-router-dom";
+import { useDispatch } from "react-redux";
+import { useParams } from "react-router-dom";
+import { networkService } from "../../../main";
+import { newOrChangedCredentials } from "../../../Validation/validation";
+import { register } from "../../../Features/Auth/authSlice";
+import { createToast } from "../../../Utils/toastUtils";
+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 }) => {
+ // 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(toSubmit));
+ 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 (
+
+
+
+
+
+ {superAdminExists
+ ? t("auth.registration.heading.user")
+ : t("auth.registration.heading.superAdmin")}
+
+
+ {superAdminExists
+ ? t("auth.registration.description.user")
+ : t("auth.registration.description.superAdmin")}
+
+ (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
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+Register.propTypes = {
+ superAdminExists: PropTypes.bool,
+};
+
+export default Register;