From 797eacee22d6facf78d6f18c63b996806274c345 Mon Sep 17 00:00:00 2001 From: Daniel Cojocea Date: Mon, 15 Jul 2024 16:32:24 -0400 Subject: [PATCH] Refactored login page validation --- Client/src/Pages/Login/index.jsx | 118 +++++++++++++++------------- Client/src/Validation/validation.js | 107 +++++++++++++++++-------- 2 files changed, 137 insertions(+), 88 deletions(-) diff --git a/Client/src/Pages/Login/index.jsx b/Client/src/Pages/Login/index.jsx index c06f3b343..e8a13ed15 100644 --- a/Client/src/Pages/Login/index.jsx +++ b/Client/src/Pages/Login/index.jsx @@ -8,7 +8,7 @@ import CheckBox from "../../Components/Checkbox/Checkbox"; import Button from "../../Components/Button"; import Google from "../../assets/Images/Google.png"; import axiosInstance from "../../Utils/axiosConfig"; -import { loginValidation } from "../../Validation/validation"; +import { loginValidation, credentials } from "../../Validation/validation"; import { login } from "../../Features/Auth/authSlice"; import { useDispatch } from "react-redux"; import { createToast } from "../../Utils/toastUtils"; @@ -23,12 +23,11 @@ const Login = () => { "login-password-input": "password", }; - const [errors, setErrors] = useState({}); - const [form, setForm] = useState({ email: "", password: "", }); + const [errors, setErrors] = useState({}); useEffect(() => { axiosInstance @@ -43,71 +42,74 @@ const Login = () => { }); }, [navigate]); - useEffect(() => { - const { error } = loginValidation.validate(form, { - abortEarly: false, - }); - - if (error) { - // Creates an error object in the format { field: message } - const validationErrors = error.details.reduce((acc, err) => { - return { ...acc, [err.path[0]]: err.message }; - }, {}); - setErrors(validationErrors); - } else { - setErrors({}); - } - }, [form]); - const handleSubmit = async (e) => { e.preventDefault(); - try { - await loginValidation.validateAsync(form, { abortEarly: false }); + + const { error } = credentials.validate(form, { abortEarly: false }); + + if (error) { + // validation errors + const newErrors = {}; + error.details.forEach((err) => { + newErrors[err.path[0]] = err.message; + }); + setErrors(newErrors); + createToast({ + variant: "info", + body: + error.details && error.details.length > 0 + ? error.details[0].message + : "Error validating data.", + hasIcon: false, + }); + } else { const action = await dispatch(login(form)); - if (action.meta.requestStatus === "fulfilled") { + if (action.payload.success) { navigate("/monitors"); - } - if (action.meta.requestStatus === "rejected") { - const error = new Error("Request rejected"); - error.response = action.payload; - throw error; - } - } catch (error) { - if (error.name === "ValidationError") { - // validation errors createToast({ variant: "info", - body: - error && error.details && error.details.length > 0 - ? error.details[0].message - : "Error validating data.", - hasIcon: false, - }); - } else if (error.response) { - // dispatch errors - createToast({ - variant: "info", - body: error.response.msg, + body: "Welcome back! You're successfully logged in.", hasIcon: false, }); } else { - // unknown errors - createToast({ - variant: "info", - body: "Unknown error.", - hasIcon: false, - }); + if (action.payload) { + // dispatch errors + createToast({ + variant: "info", + body: action.payload.msg, + hasIcon: false, + }); + } else { + // unknown errors + createToast({ + variant: "info", + body: "Unknown error.", + hasIcon: false, + }); + } } } }; - const handleInput = (e) => { - const newForm = { ...form, [idMap[e.target.id]]: e.target.value }; - setForm(newForm); - }; + const handleChange = (event) => { + const { value, id } = event.target; + const name = idMap[id]; + setForm((prev) => ({ + ...prev, + [name]: value, + })); - const handleSignupClick = () => { - navigate("/register"); + const { error } = credentials.validate( + { [name]: value }, + { abortEarly: false } + ); + + setErrors((prev) => { + const prevErrors = { ...prev }; + if (error) prevErrors[name] = error.details[0].message; + else delete prevErrors[name]; + return prevErrors; + }); }; return ( @@ -126,9 +128,11 @@ const Login = () => { type="email" id="login-email-input" label="Email" + isRequired={true} placeholder="Enter your email" autoComplete="email" - onChange={handleInput} + value={form.email} + onChange={handleChange} error={errors.email} />
@@ -136,9 +140,11 @@ const Login = () => { type="password" id="login-password-input" label="Password" + isRequired={true} placeholder="Enter your password" autoComplete="current-password" - onChange={handleInput} + value={form.password} + onChange={handleChange} error={errors.password} />
diff --git a/Client/src/Validation/validation.js b/Client/src/Validation/validation.js index f662d550e..ce735a554 100644 --- a/Client/src/Validation/validation.js +++ b/Client/src/Validation/validation.js @@ -1,5 +1,78 @@ import joi from "joi"; +const nameSchema = joi + .string() + .max(50) + .trim() + .pattern(new RegExp("^[A-Za-z]+$")) + .messages({ + "string.empty": "Name is required", + "string.max": "Name must be less than 50 characters long", + "string.pattern.base": "Name must contain only letters", + }); + +const passwordSchema = joi + .string() + .trim() + .min(8) + .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; + }); + +const credentials = joi.object({ + firstname: nameSchema, + lastname: nameSchema, + email: joi + .string() + .trim() + .email({ tlds: { allow: false } }) + .messages({ + "string.empty": "Email is required", + "string.email": "Must be a valid email address", + }), + password: passwordSchema, + newPassword: passwordSchema, + confirm: joi + .string() + .trim() + .messages({ + "string.empty": "Password confirmation is required", + }) + .custom((value, helpers) => { + const { newPassword } = helpers.prefs.context; + if (value !== newPassword) { + return helpers.message("Passwords do not match"); + } + return value; + }), + role: joi.string().messages({ + "string.empty": "Role is required", + }), +}); + const registerValidation = joi.object({ firstname: joi.string().required().messages({ "string.empty": "First name is required", @@ -85,37 +158,6 @@ const editProfileValidation = joi.object({ }), }); -const passwordSchema = joi - .string() - .trim() - .min(8) - .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; - }); - const editPasswordValidation = joi.object({ // TBD - validation for current password ? password: passwordSchema, @@ -124,7 +166,7 @@ const editPasswordValidation = joi.object({ .string() .trim() .messages({ - "string.empty": "*Password confirmation is required.", + "string.empty": "Password confirmation is required", }) .custom((value, helpers) => { const { newPassword } = helpers.prefs.context; @@ -169,6 +211,7 @@ const imageValidation = joi.object({ }); export { + credentials, imageValidation, createMonitorValidation, registerValidation,