From 75f28eaf45e57e936fc06525ff1013e5f178f4af Mon Sep 17 00:00:00 2001 From: Daniel Cojocea Date: Sun, 23 Jun 2024 17:41:40 -0400 Subject: [PATCH] Integrated joi validation --- .../ProfileSettings/PasswordPanel.jsx | 135 ++++++++---------- .../ProfileSettings/ProfilePanel.jsx | 87 +++++------ .../TextFields/Password/PasswordTextField.jsx | 1 + Client/src/Pages/Settings/index.css | 6 + Client/src/Validation/validation.js | 73 ++++++++++ 5 files changed, 175 insertions(+), 127 deletions(-) diff --git a/Client/src/Components/TabPanels/ProfileSettings/PasswordPanel.jsx b/Client/src/Components/TabPanels/ProfileSettings/PasswordPanel.jsx index 372417298..3646b5aed 100644 --- a/Client/src/Components/TabPanels/ProfileSettings/PasswordPanel.jsx +++ b/Client/src/Components/TabPanels/ProfileSettings/PasswordPanel.jsx @@ -2,22 +2,12 @@ import TabPanel from "@mui/lab/TabPanel"; import React, { useState } from "react"; import AnnouncementsDualButtonWithIcon from "../../Announcements/AnnouncementsDualButtonWithIcon/AnnouncementsDualButtonWithIcon"; import { useTheme } from "@emotion/react"; -import { - Box, - Divider, - FormControl, - IconButton, - InputAdornment, - OutlinedInput, - Stack, - Typography, -} from "@mui/material"; -import VisibilityOff from "@mui/icons-material/VisibilityOff"; -import Visibility from "@mui/icons-material/Visibility"; +import { Box, Divider, Stack, Typography } from "@mui/material"; import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; import WarningAmberOutlinedIcon from "@mui/icons-material/WarningAmberOutlined"; import ButtonSpinner from "../../ButtonSpinner"; import PasswordTextField from "../../TextFields/Password/PasswordTextField"; +import { editPasswordValidation } from "../../../Validation/validation"; /** * PasswordPanel component manages the form for editing password. @@ -33,86 +23,76 @@ const PasswordPanel = () => { const [showPassword, setShowPassword] = useState(false); //for testing, will tweak when I implement redux slice + const idToName = { + "edit-current-password": "currentPassword", + "edit-new-password": "password", + "edit-confirm-password": "confirm", + }; const [localData, setLocalData] = useState({ - "edit-current-password": { - value: "", - //TBD - type: "", - }, - "edit-new-password": { - value: "", - type: "password", - }, - "edit-confirm-password": { - value: "", - type: "confirm", - }, + currentPassword: "", + password: "", + confirm: "", }); const [errors, setErrors] = useState({}); const handleChange = (event) => { const { value, id } = event.target; + const name = idToName[id]; setLocalData((prev) => ({ ...prev, - [id]: { - ...prev[id], - value: value, - }, + [name]: value, })); - validateField(id, value); - }; - const validateField = (id, value) => { - let error = ""; - switch (localData[id].type) { - case "password": - error = - value.trim() === "" - ? "*This field is required." - : value.length < 8 - ? "*Password must be at least 8 characters long." - : !/[A-Z]/.test(value) - ? "*Password must contain at least one uppercase letter." - : !/\d/.test(value) - ? "*Password must contain at least one number." - : !/[!@#$%^&*]/.test(value) - ? "*Password must contain at least one symbol." - : ""; - break; - case "confirm": - error = - value.trim() === "" - ? "*This field is required." - : value !== localData["edit-new-password"].value - ? "*Passwords do not match." - : ""; - break; - default: - break; - } + const validation = editPasswordValidation.validate( + { [name]: value }, + { abortEarly: false, context: { password: localData.password } } + ); setErrors((prev) => { const updatedErrors = { ...prev }; - if (error === "") { - delete updatedErrors[id]; + + if (validation.error) { + updatedErrors[name] = validation.error.details[0].message; } else { - updatedErrors[id] = error; + delete updatedErrors[name]; } return updatedErrors; }); }; - //TODO - implement save password function - const handleSavePassword = () => { + const handleSubmit = (event) => { + event.preventDefault(); setIsLoading(true); + + const { error } = editPasswordValidation.validate(localData, { + abortEarly: false, + context: { password: localData.password }, + }); + + if (error) { + const newErrors = {}; + error.details.forEach((err) => { + newErrors[err.path[0]] = err.message; + }); + setErrors(newErrors); + setIsLoading(false); + } else { + //TODO - submit logic + } + setTimeout(() => { setIsLoading(false); - setIsOpen(false); }, 2000); }; + return ( -
+
{ autoComplete="current-password" visibility={showPassword} setVisibility={setShowPassword} + onChange={handleChange} + error={errors[idToName["edit-current-password"]] ? true : false} /> + {errors[idToName["edit-current-password"]] ? ( + + {errors[idToName["edit-current-password"]]} + + ) : ( + "" + )}
@@ -156,11 +145,11 @@ const PasswordPanel = () => { visibility={showPassword} setVisibility={setShowPassword} onChange={handleChange} - error={errors["edit-new-password"] ? true : false} + error={errors[idToName["edit-new-password"]] ? true : false} /> - {errors["edit-new-password"] ? ( + {errors[idToName["edit-new-password"]] ? ( - {errors["edit-new-password"]} + {errors[idToName["edit-new-password"]]} ) : ( "" @@ -182,11 +171,11 @@ const PasswordPanel = () => { visibility={showPassword} setVisibility={setShowPassword} onChange={handleChange} - error={errors["edit-confirm-password"] ? true : false} + error={errors[idToName["edit-confirm-password"]] ? true : false} /> - {errors["edit-confirm-password"] ? ( + {errors[idToName["edit-confirm-password"]] ? ( - {errors["edit-confirm-password"]} + {errors[idToName["edit-confirm-password"]]} ) : ( "" @@ -208,7 +197,7 @@ const PasswordPanel = () => { { const [isLoading, setIsLoading] = useState(false); //for testing, will tweak when I implement redux slice + const idToName = { + "edit-first-name": "firstName", + "edit-last-name": "lastName", + "edit-email": "email", + }; const [localData, setLocalData] = useState({ - "edit-first-name": { - value: "", - type: "name", - }, - "edit-last-name": { - value: "", - type: "name", - }, - "edit-email": { - value: "", - type: "email", - }, + firstName: "", + lastName: "", + email: "", }); const [errors, setErrors] = useState({}); const handleChange = (event) => { const { value, id } = event.target; + const name = idToName[id]; setLocalData((prev) => ({ ...prev, - [id]: { - ...prev[id], - value: value, - }, + [name]: value, })); - validateField(id, value); - }; - - const validateField = (id, value) => { - let error = ""; - switch (localData[id].type) { - case "name": - error = - value.trim() === "" - ? "*This field is required." - : /\d/.test(value) - ? "*Name is invalid." - : ""; - break; - case "email": - error = - value.trim() === "" - ? "*This field is required." - : !/^\S+@\S+\.\S+$/.test(value) - ? "*Email is invalid." - : ""; - break; - default: - break; - } + + const validation = editProfileValidation.validate( + { [name]: value }, + { abortEarly: false } + ); setErrors((prev) => { const updatedErrors = { ...prev }; - if (error === "") { - delete updatedErrors[id]; + + if (validation.error) { + updatedErrors[name] = validation.error.details[0].message; } else { - updatedErrors[id] = error; + delete updatedErrors[name]; } return updatedErrors; }); @@ -128,11 +104,11 @@ const ProfilePanel = () => { placeholder="Enter your first name" autoComplete="given-name" onChange={handleChange} - error={errors["edit-first-name"] ? true : false} + error={errors[idToName["edit-first-name"]] ? true : false} /> - {errors["edit-first-name"] ? ( + {errors[idToName["edit-first-name"]] ? ( - {errors["edit-first-name"]} + {errors[idToName["edit-first-name"]]} ) : ( "" @@ -152,11 +128,11 @@ const ProfilePanel = () => { placeholder="Enter your last name" autoComplete="family-name" onChange={handleChange} - error={errors["edit-last-name"] ? true : false} + error={errors[idToName["edit-last-name"]] ? true : false} /> - {errors["edit-last-name"] ? ( + {errors[idToName["edit-last-name"]] ? ( - {errors["edit-last-name"]} + {errors[idToName["edit-last-name"]]} ) : ( "" @@ -179,11 +155,11 @@ const ProfilePanel = () => { placeholder="Enter your email" autoComplete="email" onChange={handleChange} - error={errors["edit-email"] ? true : false} + error={errors[idToName["edit-email"]] ? true : false} /> - {errors["edit-email"] ? ( + {errors[idToName["edit-email"]] ? ( - {errors["edit-email"]} + {errors[idToName["edit-email"]]} ) : ( "" @@ -206,7 +182,10 @@ const ProfilePanel = () => { firstName="Jackie" lastName="Dawn" sx={{ - width: "64px", height: "64px", border: "none", mr: "8px" + width: "64px", + height: "64px", + border: "none", + mr: "8px", }} /> setVisibility((show) => !show)} + tabIndex={-1} sx={{ width: "30px", height: "30px", diff --git a/Client/src/Pages/Settings/index.css b/Client/src/Pages/Settings/index.css index 6748239fc..4a0854cb2 100644 --- a/Client/src/Pages/Settings/index.css +++ b/Client/src/Pages/Settings/index.css @@ -171,9 +171,15 @@ } } +.settings .MuiStack-root:has(p.MuiTypography-root.input-error){ + position: relative; + margin-bottom: 8px; +} .settings p.MuiTypography-root.input-error{ color: var(--env-var-color-24); opacity: 0.8; + position: absolute; + top: calc(100% + 5px); } .settings .email-text-field, .settings .password-text-field { diff --git a/Client/src/Validation/validation.js b/Client/src/Validation/validation.js index 74233a029..d80de04f5 100644 --- a/Client/src/Validation/validation.js +++ b/Client/src/Validation/validation.js @@ -63,9 +63,82 @@ const newPasswordValidation = joi.object({ }), }); +const editProfileValidation = joi.object({ + firstName: joi.string().trim().pattern(new RegExp("^[A-Za-z]+$")).messages({ + "string.empty": "*First name is required.", + "string.pattern.base": "*First name must contain only letters.", + }), + lastName: joi.string().trim().pattern(new RegExp("^[A-Za-z]+$")).messages({ + "string.empty": "*Last name is required.", + "string.pattern.base": "*Last name must contain only letters.", + }), + email: joi + .string() + .trim() + .email({ tlds: { allow: false } }) + .messages({ + "string.empty": "*Email is required.", + "string.email": "*Invalid email address.", + }), +}); + +const editPasswordValidation = joi.object({ + // TBD - validation for current password ? + currentPassword : joi + .string().trim() + .messages({ + "string.empty": "*Current password is required.", + }), + password: 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; + }), + confirm: joi + .string() + .trim() + .messages({ + "string.empty": "*Password confirmation is required.", + }) + .custom((value, helpers) => { + const { password } = helpers.prefs.context; + if (value !== password) { + return helpers.message("*Passwords do not match."); + } + return value; + }), +}); + export { registerValidation, loginValidation, recoveryValidation, newPasswordValidation, + editPasswordValidation, + editProfileValidation, };