Integrated joi validation

This commit is contained in:
Daniel Cojocea
2024-06-23 17:41:40 -04:00
parent b85f1f6396
commit 75f28eaf45
5 changed files with 175 additions and 127 deletions

View File

@@ -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 (
<TabPanel value="1">
<form className="edit-password-form" noValidate spellCheck="false">
<form
onSubmit={handleSubmit}
className="edit-password-form"
noValidate
spellCheck="false"
>
<div className="edit-password-form__wrapper">
<AnnouncementsDualButtonWithIcon
icon={
@@ -138,7 +118,16 @@ const PasswordPanel = () => {
autoComplete="current-password"
visibility={showPassword}
setVisibility={setShowPassword}
onChange={handleChange}
error={errors[idToName["edit-current-password"]] ? true : false}
/>
{errors[idToName["edit-current-password"]] ? (
<Typography variant="h5" component="p" className="input-error">
{errors[idToName["edit-current-password"]]}
</Typography>
) : (
""
)}
</Stack>
</div>
<div className="edit-password-form__wrapper">
@@ -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"]] ? (
<Typography variant="h5" component="p" className="input-error">
{errors["edit-new-password"]}
{errors[idToName["edit-new-password"]]}
</Typography>
) : (
""
@@ -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"]] ? (
<Typography variant="h5" component="p" className="input-error">
{errors["edit-confirm-password"]}
{errors[idToName["edit-confirm-password"]]}
</Typography>
) : (
""
@@ -208,7 +197,7 @@ const PasswordPanel = () => {
<ButtonSpinner
level="primary"
label="Save"
onClick={handleSavePassword}
onClick={handleSubmit}
isLoading={isLoading}
loadingText="Saving..."
disabled={Object.keys(errors).length !== 0 && true}

View File

@@ -7,6 +7,7 @@ import Button from "../../Button";
import EmailTextField from "../../TextFields/Email/EmailTextField";
import StringTextField from "../../TextFields/Text/TextField";
import Avatar from "../../Avatar";
import { editProfileValidation } from "../../../Validation/validation";
/**
* ProfilePanel component displays a form for editing user profile information
@@ -23,63 +24,38 @@ const ProfilePanel = () => {
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"]] ? (
<Typography variant="h5" component="p" className="input-error">
{errors["edit-first-name"]}
{errors[idToName["edit-first-name"]]}
</Typography>
) : (
""
@@ -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"]] ? (
<Typography variant="h5" component="p" className="input-error">
{errors["edit-last-name"]}
{errors[idToName["edit-last-name"]]}
</Typography>
) : (
""
@@ -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"]] ? (
<Typography variant="h5" component="p" className="input-error">
{errors["edit-email"]}
{errors[idToName["edit-email"]]}
</Typography>
) : (
""
@@ -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",
}}
/>
<ButtonSpinner

View File

@@ -68,6 +68,7 @@ const PasswordTextField = ({
<IconButton
aria-label="toggle password visibility"
onClick={() => setVisibility((show) => !show)}
tabIndex={-1}
sx={{
width: "30px",
height: "30px",

View File

@@ -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 {

View File

@@ -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,
};