From 18ef81f6f15e0e37a23163a46c8e8e975e30c14b Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 11 Jun 2025 10:20:14 +0800 Subject: [PATCH 1/6] refactor login page --- .../Pages/Auth/Login/Components/EmailStep.jsx | 106 ------- .../Login/Components/ForgotPasswordLabel.jsx | 49 ---- .../Auth/Login/Components/PasswordStep.jsx | 141 ---------- client/src/Pages/Auth/Login/Login.jsx | 266 ------------------ client/src/Pages/Auth/Login/index.jsx | 162 +++++++++++ 5 files changed, 162 insertions(+), 562 deletions(-) delete mode 100644 client/src/Pages/Auth/Login/Components/EmailStep.jsx delete mode 100644 client/src/Pages/Auth/Login/Components/ForgotPasswordLabel.jsx delete mode 100644 client/src/Pages/Auth/Login/Components/PasswordStep.jsx delete mode 100644 client/src/Pages/Auth/Login/Login.jsx create mode 100644 client/src/Pages/Auth/Login/index.jsx 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..04155a9d6 --- /dev/null +++ b/client/src/Pages/Auth/Login/index.jsx @@ -0,0 +1,162 @@ +// 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"; + +// 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 ( + + + + + + } + /> + + + + + + + ); +}; + +export default Login; From f32892adaf9694a55c10f6e41d1fd39deeaa3e7c Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 11 Jun 2025 10:20:36 +0800 Subject: [PATCH 2/6] add components directory --- .../src/Pages/Auth/components/AuthHeader.jsx | 45 +++++++++++++++++++ client/src/Pages/Auth/components/TextLink.jsx | 34 ++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 client/src/Pages/Auth/components/AuthHeader.jsx create mode 100644 client/src/Pages/Auth/components/TextLink.jsx 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; From cd17ecdadbb81d8bb9a0d5677c62147addb6ba32 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 11 Jun 2025 10:21:33 +0800 Subject: [PATCH 3/6] fix import path --- client/src/Routes/index.jsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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"; From 9e6b826f6e8a03c5ab4cd6d423b3a1376dd8be55 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 11 Jun 2025 10:21:47 +0800 Subject: [PATCH 4/6] update translations --- client/src/locales/en.json | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) 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": { From 045b506e2ad395a9da6ed6e713aaa9d26473c876 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 11 Jun 2025 10:22:17 +0800 Subject: [PATCH 5/6] remove unused component --- .../ProtectedDistributedUptimeRoute/index.jsx | 30 ------------------- 1 file changed, 30 deletions(-) delete mode 100644 client/src/Components/ProtectedDistributedUptimeRoute/index.jsx 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; From 0e4b371925f4630096cdebec5e1470b40bf9dde2 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 11 Jun 2025 20:12:02 +0800 Subject: [PATCH 6/6] add login header --- client/src/Pages/Auth/Login/index.jsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/src/Pages/Auth/Login/index.jsx b/client/src/Pages/Auth/Login/index.jsx index 04155a9d6..ec0afd054 100644 --- a/client/src/Pages/Auth/Login/index.jsx +++ b/client/src/Pages/Auth/Login/index.jsx @@ -6,6 +6,7 @@ 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"; @@ -96,13 +97,14 @@ const Login = () => { alignItems="center" gap={theme.spacing(10)} > + {t("auth.login.heading")} +