diff --git a/client/src/Components/ProtectedDistributedUptimeRoute/index.jsx b/client/src/Components/ProtectedDistributedUptimeRoute/index.jsx
deleted file mode 100644
index 4a59859fe..000000000
--- a/client/src/Components/ProtectedDistributedUptimeRoute/index.jsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import { Navigate } from "react-router-dom";
-import { useSelector } from "react-redux";
-import PropTypes from "prop-types";
-
-/**
- * @param {Object} props - The props passed to the ProtectedDistributedUptimeRoute component.
- * @param {React.ReactNode} props.children - The children to render if the user is authenticated.
- * @returns {React.ReactElement} The children wrapped in a protected route or a redirect to the login page.
- */
-
-const ProtectedDistributedUptimeRoute = ({ children }) => {
- const distributedUptimeEnabled = useSelector(
- (state) => state.ui.distributedUptimeEnabled
- );
-
- return distributedUptimeEnabled === true ? (
- children
- ) : (
-
- );
-};
-
-ProtectedDistributedUptimeRoute.propTypes = {
- children: PropTypes.node.isRequired,
-};
-
-export default ProtectedDistributedUptimeRoute;
diff --git a/client/src/Pages/Auth/Login/Components/EmailStep.jsx b/client/src/Pages/Auth/Login/Components/EmailStep.jsx
deleted file mode 100644
index f3f56ec26..000000000
--- a/client/src/Pages/Auth/Login/Components/EmailStep.jsx
+++ /dev/null
@@ -1,106 +0,0 @@
-import { useRef, useEffect } from "react";
-import { Box, Button, Stack, Typography } from "@mui/material";
-import { useTheme } from "@emotion/react";
-import TextInput from "../../../../Components/Inputs/TextInput";
-import PropTypes from "prop-types";
-import { useTranslation } from "react-i18next";
-
-/**
- * Renders the email step of the login process which includes an email field.
- *
- * @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 EmailStep = ({ form, errors, onSubmit, onChange }) => {
- const theme = useTheme();
- const inputRef = useRef(null);
- const { t } = useTranslation();
-
- useEffect(() => {
- if (inputRef.current) {
- inputRef.current.focus();
- }
- }, []);
-
- return (
- <>
-
-
- {t("auth.login.heading")}
- {t("auth.login.subheadings.stepOne")}
-
-
-
-
-
-
-
-
- >
- );
-};
-
-EmailStep.propTypes = {
- form: PropTypes.object.isRequired,
- errors: PropTypes.object.isRequired,
- onSubmit: PropTypes.func.isRequired,
- onChange: PropTypes.func.isRequired,
-};
-
-export default EmailStep;
diff --git a/client/src/Pages/Auth/Login/Components/ForgotPasswordLabel.jsx b/client/src/Pages/Auth/Login/Components/ForgotPasswordLabel.jsx
deleted file mode 100644
index fe8c688ac..000000000
--- a/client/src/Pages/Auth/Login/Components/ForgotPasswordLabel.jsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import { Box, Typography, useTheme } from "@mui/material";
-import PropTypes from "prop-types";
-import { useNavigate } from "react-router";
-import { Trans, useTranslation } from "react-i18next";
-
-const ForgotPasswordLabel = ({ email, errorEmail }) => {
- const theme = useTheme();
- const navigate = useNavigate();
- const { t } = useTranslation();
-
- const handleNavigate = () => {
- if (email !== "" && !errorEmail) {
- sessionStorage.setItem("email", email);
- }
- navigate("/forgot-password");
- };
-
- return (
-
-
-
- ),
- }}
- />
-
-
- );
-};
-
-ForgotPasswordLabel.propTypes = {
- email: PropTypes.string,
- errorEmail: PropTypes.string,
-};
-
-export default ForgotPasswordLabel;
diff --git a/client/src/Pages/Auth/Login/Components/PasswordStep.jsx b/client/src/Pages/Auth/Login/Components/PasswordStep.jsx
deleted file mode 100644
index 5aa56fbb0..000000000
--- a/client/src/Pages/Auth/Login/Components/PasswordStep.jsx
+++ /dev/null
@@ -1,141 +0,0 @@
-import { useRef, useEffect } from "react";
-import { Box, Button, Stack, Typography } from "@mui/material";
-import { useTheme } from "@emotion/react";
-import { useSelector } from "react-redux";
-import TextInput from "../../../../Components/Inputs/TextInput";
-import { PasswordEndAdornment } from "../../../../Components/Inputs/TextInput/Adornments";
-import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
-import PropTypes from "prop-types";
-import { useTranslation } from "react-i18next";
-/**
- * Renders the password step of the login process, including a password input field.
- *
- * @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 PasswordStep = ({ form, errors, onSubmit, onChange, onBack }) => {
- const theme = useTheme();
- const inputRef = useRef(null);
- const authState = useSelector((state) => state.auth);
- const { t } = useTranslation();
-
- useEffect(() => {
- if (inputRef.current) {
- inputRef.current.focus();
- }
- }, []);
-
- return (
- <>
-
-
- {t("auth.login.heading")}
- {t("auth.login.subheadings.stepTwo")}
-
-
- }
- />
-
-
-
-
-
-
- >
- );
-};
-
-PasswordStep.propTypes = {
- form: PropTypes.object.isRequired,
- errors: PropTypes.object.isRequired,
- onSubmit: PropTypes.func.isRequired,
- onChange: PropTypes.func.isRequired,
- onBack: PropTypes.func.isRequired,
-};
-
-export default PasswordStep;
diff --git a/client/src/Pages/Auth/Login/Login.jsx b/client/src/Pages/Auth/Login/Login.jsx
deleted file mode 100644
index d2be3af35..000000000
--- a/client/src/Pages/Auth/Login/Login.jsx
+++ /dev/null
@@ -1,266 +0,0 @@
-import { useState, useEffect } from "react";
-import { useNavigate } from "react-router-dom";
-import { Box, Stack, Typography } from "@mui/material";
-import { useTheme } from "@emotion/react";
-import { loginCredentials } from "../../../Validation/validation";
-import { login } from "../../../Features/Auth/authSlice";
-import { useDispatch, useSelector } from "react-redux";
-import { createToast } from "../../../Utils/toastUtils";
-import { networkService } from "../../../main";
-import Background from "../../../assets/Images/background-grid.svg?react";
-import Logo from "../../../assets/icons/checkmate-icon.svg?react";
-import "../index.css";
-import EmailStep from "./Components/EmailStep";
-import PasswordStep from "./Components/PasswordStep";
-import ThemeSwitch from "../../../Components/ThemeSwitch";
-import ForgotPasswordLabel from "./Components/ForgotPasswordLabel";
-import LanguageSelector from "../../../Components/LanguageSelector";
-import { Trans, useTranslation } from "react-i18next";
-
-const DEMO = import.meta.env.VITE_APP_DEMO;
-
-/**
- * Displays the login page.
- */
-
-const Login = () => {
- const theme = useTheme();
- const dispatch = useDispatch();
- const { t } = useTranslation();
- const navigate = useNavigate();
- const authState = useSelector((state) => state.auth);
- const { authToken } = authState;
-
- const idMap = {
- "login-email-input": "email",
- "login-password-input": "password",
- };
-
- const [form, setForm] = useState({
- email: DEMO !== undefined ? "uptimedemo@demo.com" : "",
- password: DEMO !== undefined ? "Demouser1!" : "",
- });
- const [errors, setErrors] = useState({});
- const [step, setStep] = useState(0);
-
- useEffect(() => {
- if (authToken) {
- navigate("/uptime");
- }
- }, [authToken, navigate]);
-
- const handleChange = (event) => {
- const { value, id } = event.target;
- const name = idMap[id];
- const lowerCasedValue =
- name === idMap["login-email-input"] ? value?.toLowerCase() || value : value;
- setForm((prev) => ({
- ...prev,
- [name]: lowerCasedValue,
- }));
-
- const { error } = loginCredentials.validate(
- { [name]: lowerCasedValue },
- { abortEarly: false }
- );
-
- setErrors((prev) => {
- const prevErrors = { ...prev };
- if (error) prevErrors[name] = error.details[0].message;
- else delete prevErrors[name];
- return prevErrors;
- });
- };
-
- const handleSubmit = async (event) => {
- event.preventDefault();
-
- if (step === 0) {
- const { error } = loginCredentials.validate(
- { email: form.email },
- { abortEarly: false }
- );
- if (error) {
- const errorMessage = error.details[0].message;
- const translatedMessage = errorMessage.startsWith("auth")
- ? t(errorMessage) // Localization keys are in validation.js
- : errorMessage; // FIXME: Potential untranslated string
- setErrors((prev) => ({ ...prev, email: translatedMessage }));
- createToast({ body: translatedMessage });
- } else {
- setStep(1);
- }
- } else if (step === 1) {
- const { error } = loginCredentials.validate(form, { abortEarly: false });
-
- 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
- ? error.details[0].message.startsWith("auth")
- ? t(error.details[0].message) // Localization keys are in validation.js
- : error.details[0].message // FIXME: Potential untranslated string
- : t("auth.common.errors.validation"),
- });
- } else {
- const action = await dispatch(login(form));
- if (action.payload.success) {
- navigate("/uptime");
- createToast({
- body: t("auth.login.toasts.success"),
- });
- } else {
- if (action.payload) {
- if (action.payload.msg === "Incorrect password")
- setErrors({
- password: t("auth.common.fields.password.errors.incorrect"),
- });
- // dispatch errors
- createToast({
- body: t("auth.login.toasts.incorrectPassword"),
- });
- } else {
- // unknown errors
- createToast({
- body: t("common.toasts.unknownError"),
- });
- }
- }
- }
- }
- };
-
- 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 ? (
-
- ) : (
- step === 1 && (
- setStep(0)}
- />
- )
- )}
-
-
- {/* Registration link */}
-
-
- navigate("/register")}
- />
- ),
- }}
- />
-
-
-
-
- );
-};
-
-export default Login;
diff --git a/client/src/Pages/Auth/Login/index.jsx b/client/src/Pages/Auth/Login/index.jsx
new file mode 100644
index 000000000..ec0afd054
--- /dev/null
+++ b/client/src/Pages/Auth/Login/index.jsx
@@ -0,0 +1,164 @@
+// Components
+import Stack from "@mui/material/Stack";
+import AuthHeader from "../components/AuthHeader";
+import Button from "@mui/material/Button";
+import TextInput from "../../../Components/Inputs/TextInput";
+import { PasswordEndAdornment } from "../../../Components/Inputs/TextInput/Adornments";
+import { loginCredentials } from "../../../Validation/validation";
+import TextLink from "../components/TextLink";
+import Typography from "@mui/material/Typography";
+
+// Utils
+import { login } from "../../../Features/Auth/authSlice";
+import { useNavigate } from "react-router-dom";
+import { useDispatch } from "react-redux";
+import { useState } from "react";
+import { useTheme } from "@mui/material/styles";
+import { useTranslation } from "react-i18next";
+import { createToast } from "../../../Utils/toastUtils";
+
+const Login = () => {
+ // Local state
+ const [form, setForm] = useState({
+ email: "",
+ password: "",
+ });
+ const [errors, setErrors] = useState({
+ email: "",
+ password: "",
+ });
+
+ // Hooks
+ const { t } = useTranslation();
+ const theme = useTheme();
+ const dispatch = useDispatch();
+ const navigate = useNavigate();
+
+ // Handlers
+ const onChange = (e) => {
+ const { name, value } = e.target;
+ const updatedForm = { ...form, [name]: value };
+ const { error } = loginCredentials.validate({ [name]: value });
+ setForm(updatedForm);
+ setErrors((prev) => ({
+ ...prev,
+ [name]: error?.details?.[0]?.message || "",
+ }));
+ };
+
+ const onSubmit = async (e) => {
+ e.preventDefault();
+ const toSubmit = { ...form };
+ const { error } = loginCredentials.validate(toSubmit, { abortEarly: false });
+
+ if (error) {
+ const formErrors = {};
+ for (const err of error.details) {
+ formErrors[err.path[0]] = err.message;
+ }
+ setErrors(formErrors);
+ return;
+ }
+
+ const action = await dispatch(login(form));
+ if (action.payload.success) {
+ navigate("/uptime");
+ createToast({
+ body: t("auth.login.toasts.success"),
+ });
+ } else {
+ if (action.payload) {
+ if (action.payload.msg === "Incorrect password")
+ setErrors({
+ password: t("auth.login.errors.password.incorrect"),
+ });
+ // dispatch errors
+ createToast({
+ body: t("auth.login.toasts.incorrectPassword"),
+ });
+ } else {
+ // unknown errors
+ createToast({
+ body: t("common.toasts.unknownError"),
+ });
+ }
+ }
+ };
+
+ return (
+
+
+
+ {t("auth.login.heading")}
+
+
+
+ }
+ />
+
+
+
+
+
+
+ );
+};
+
+export default Login;
diff --git a/client/src/Pages/Auth/components/AuthHeader.jsx b/client/src/Pages/Auth/components/AuthHeader.jsx
new file mode 100644
index 000000000..c6ad46a47
--- /dev/null
+++ b/client/src/Pages/Auth/components/AuthHeader.jsx
@@ -0,0 +1,45 @@
+// 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";
+import ThemeSwitch from "../../../Components/ThemeSwitch";
+
+// Utils
+import { useTheme } from "@mui/material/styles";
+import { useTranslation } from "react-i18next";
+
+const AuthHeader = () => {
+ // Hooks
+ const theme = useTheme();
+ const { t } = useTranslation();
+
+ return (
+
+
+
+ {t("common.appName")}
+
+
+
+
+
+
+ );
+};
+
+export default AuthHeader;
diff --git a/client/src/Pages/Auth/components/TextLink.jsx b/client/src/Pages/Auth/components/TextLink.jsx
new file mode 100644
index 000000000..a10839e84
--- /dev/null
+++ b/client/src/Pages/Auth/components/TextLink.jsx
@@ -0,0 +1,34 @@
+import Typography from "@mui/material/Typography";
+import Stack from "@mui/material/Stack";
+import Link from "@mui/material/Link";
+import PropTypes from "prop-types";
+import { useTheme } from "@emotion/react";
+import { Link as RouterLink } from "react-router-dom";
+
+const TextLink = ({ text, linkText, href }) => {
+ const theme = useTheme();
+
+ return (
+
+ {text}
+
+ {linkText}
+
+
+ );
+};
+
+TextLink.propTypes = {
+ text: PropTypes.string,
+ linkText: PropTypes.string,
+ href: PropTypes.string,
+};
+
+export default TextLink;
diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx
index 3f815a665..85b8da82c 100644
--- a/client/src/Routes/index.jsx
+++ b/client/src/Routes/index.jsx
@@ -3,7 +3,7 @@ import HomeLayout from "../Components/Layouts/HomeLayout";
import NotFound from "../Pages/NotFound";
// Auth
-import AuthLogin from "../Pages/Auth/Login/Login";
+import AuthLogin from "../Pages/Auth/Login";
import AuthRegister from "../Pages/Auth/Register/Register";
import AuthForgotPassword from "../Pages/Auth/ForgotPassword";
import AuthCheckEmail from "../Pages/Auth/CheckEmail";
@@ -48,7 +48,6 @@ import Settings from "../Pages/Settings";
import Maintenance from "../Pages/Maintenance";
import ProtectedRoute from "../Components/ProtectedRoute";
-import ProtectedDistributedUptimeRoute from "../Components/ProtectedDistributedUptimeRoute";
import CreateNewMaintenanceWindow from "../Pages/Maintenance/CreateMaintenance";
import withAdminCheck from "../Components/HOC/withAdminCheck";
import BulkImport from "../Pages/Uptime/BulkImport";
diff --git a/client/src/locales/en.json b/client/src/locales/en.json
index 83ecf6b55..655f427bf 100644
--- a/client/src/locales/en.json
+++ b/client/src/locales/en.json
@@ -102,12 +102,19 @@
"stepTwo": "Enter your password"
},
"links": {
- "forgotPassword": "Forgot password? Reset password",
- "register": "Do not have an account? Register here"
+ "forgotPassword": "Forgot password?",
+ "forgotPasswordLink": "Reset password",
+ "register": "Do not have an account?",
+ "registerLink": "Register here"
},
"toasts": {
"success": "Welcome back! You're successfully logged in.",
"incorrectPassword": "Incorrect password"
+ },
+ "errors": {
+ "password": {
+ "incorrect": "The password you provided does not match our records"
+ }
}
},
"registration": {