diff --git a/.chores.md b/.chores.md index b10e56264..6d45e9a11 100644 --- a/.chores.md +++ b/.chores.md @@ -1,8 +1,14 @@ - [x] Change schema to fix validations - [x] Refactor to apply Single Responsibility Principle to validation (validation is done on joi only) +- [ ] Validation on change +- [ ] Validation on submit - [ ] Take validation out from the component. - [x] SetNewPassword - [ ] Register + - [ ] Refactor on change to deal with step three specificity + - [ ] Fix Input enter not working + - [ ] PasswordPanel + - [ ] Login - [x] Update check component to accept not highlighted text - [x] Split some of these user feedbacks? Like: - [x] Must contain at least one special character, @@ -11,5 +17,13 @@ - [x] Must contain at least one lower character - [x] Pass mismatching password user feedback as the other messages, for consistency - [ ] Get rid of toast message -- [x] Check for confirm password if password changes and confirm is filled, and they do not match anymore -- [ ] Check for confirm password if password changes, and they now match +- [x] Tests + - [x] Valid Password and Matching Confirmation + - [x] Valid Password, Incorrect Confirmation, then Corrects confirmation + - [x] Password Update to Match Confirm Password + - [x] Short Password with Matching Confirmation + - [x] Invalid Password Format, Correct Confirmation + - [x] Empty Password Field with Confirmation Filled + - [x] Confirm Password Left Blank + - [x] Password Change After Successful Match +- [x] Avoid submission in new password if password or confirm empty, and on error diff --git a/Client/src/Pages/Auth/Register/Register.jsx b/Client/src/Pages/Auth/Register/Register.jsx index c5c6bd85a..b85cf89c0 100644 --- a/Client/src/Pages/Auth/Register/Register.jsx +++ b/Client/src/Pages/Auth/Register/Register.jsx @@ -1,20 +1,19 @@ +import { useState, useEffect } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { useDispatch } from "react-redux"; import PropTypes from "prop-types"; -import { useState, useEffect, useRef } from "react"; -import { useNavigate } from "react-router-dom"; import { useTheme } from "@emotion/react"; import { Box, Button, Stack, Typography } from "@mui/material"; -import { useDispatch } from "react-redux"; +import { StepOne } from "./StepOne"; +import { StepTwo } from "./StepTwo"; +import { StepThree } from "./StepThree"; +import { networkService } from "../../../main"; import { credentials } from "../../../Validation/validation"; import { createToast } from "../../../Utils/toastUtils"; import { register } from "../../../Features/Auth/authSlice"; -import { useParams } from "react-router-dom"; import Background from "../../../assets/Images/background-grid.svg?react"; import Logo from "../../../assets/icons/bwu-icon.svg?react"; import Mail from "../../../assets/icons/mail.svg?react"; -import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded"; -import Check from "../../../Components/Check/Check"; -import Field from "../../../Components/Inputs/Field"; -import { networkService } from "../../../main"; import "../index.css"; /** @@ -110,389 +109,6 @@ LandingPage.propTypes = { onSignup: PropTypes.func, }; -/** - * Renders the first step of the sign up process. - * - * @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 StepOne = ({ form, errors, onSubmit, onChange, onBack }) => { - const theme = useTheme(); - const inputRef = useRef(null); - - useEffect(() => { - if (inputRef.current) { - inputRef.current.focus(); - } - }, []); - - return ( - <> - - - Sign Up - Enter your personal details - - - - - - - - - - - - - - - - ); -}; - -StepOne.propTypes = { - form: PropTypes.object, - errors: PropTypes.object, - onSubmit: PropTypes.func, - onChange: PropTypes.func, - onBack: PropTypes.func, -}; - -/** - * Renders the second step of the sign up process. - * - * @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 StepTwo = ({ form, errors, onSubmit, onChange, onBack }) => { - const theme = useTheme(); - const inputRef = useRef(null); - - useEffect(() => { - if (inputRef.current) { - inputRef.current.focus(); - } - }, []); - - return ( - <> - - - Sign Up - Enter your email address - - - - (e.target.value = e.target.value.toLowerCase())} - onChange={onChange} - error={errors.email} - ref={inputRef} - /> - - - - - - - - - ); -}; - -StepTwo.propTypes = { - form: PropTypes.object, - errors: PropTypes.object, - onSubmit: PropTypes.func, - onChange: PropTypes.func, - onBack: PropTypes.func, -}; - -/** - * Renders the third step of the sign up process. - * - * @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 StepThree = ({ form, errors, onSubmit, onChange, onBack }) => { - const theme = useTheme(); - const inputRef = useRef(null); - - useEffect(() => { - if (inputRef.current) { - inputRef.current.focus(); - } - }, []); - - return ( - <> - - - Sign Up - Create your password - - - - - - - - - - - Must be at least 8 characters - long - - } - variant={ - errors?.password === "Password is required" - ? "error" - : form.password === "" - ? "info" - : form.password.length < 8 - ? "error" - : "success" - } - /> - - Must contain one special - character and a number - - } - variant={ - errors?.password === "Password is required" - ? "error" - : form.password === "" - ? "info" - : !/^(?=.*[!@#$%^&*(),.?":{}|])(?=.*\d).+$/.test(form.password) - ? "error" - : "success" - } - /> - - Must contain at least one - upper and lower character - - } - variant={ - errors?.password === "Password is required" - ? "error" - : form.password === "" - ? "info" - : !/^(?=.*[A-Z])(?=.*[a-z]).+$/.test(form.password) - ? "error" - : "success" - } - /> - - - - - - - - - ); -}; - -StepThree.propTypes = { - form: PropTypes.object, - errors: PropTypes.object, - onSubmit: PropTypes.func, - onChange: PropTypes.func, - onBack: PropTypes.func, -}; - const Register = ({ isSuperAdmin }) => { const dispatch = useDispatch(); const navigate = useNavigate(); @@ -596,42 +212,45 @@ const Register = ({ isSuperAdmin }) => { // Attempts account registration const handleStepThree = async (e) => { e.preventDefault(); + const { password, confirm } = e.target.elements; + console.log(password.value); + console.log(confirm.value); + // let registerForm = { + // ...form, - let registerForm = { - ...form, - role: isSuperAdmin ? ["superadmin"] : form.role, - inviteToken: token ? token : "", // Add the token to the request for verification - }; - let error = validateForm(registerForm, { - context: { password: form.password }, - }); - if (error) { - handleError(error); - return; - } + // role: isSuperAdmin ? ["superadmin"] : form.role, + // inviteToken: token ? token : "", // Add the token to the request for verification + // }; + // let error = validateForm(registerForm, { + // context: { password: form.password }, + // }); + // if (error) { + // handleError(error); + // return; + // } - delete registerForm.confirm; - const action = await dispatch(register(registerForm)); - if (action.payload.success) { - const authToken = action.payload.data; - localStorage.setItem("token", authToken); - navigate("/"); - createToast({ - body: "Welcome! Your account was created successfully.", - }); - } else { - if (action.payload) { - // dispatch errors - createToast({ - body: action.payload.msg, - }); - } else { - // unknown errors - createToast({ - body: "Unknown error.", - }); - } - } + // delete registerForm.confirm; + // const action = await dispatch(register(registerForm)); + // if (action.payload.success) { + // const authToken = action.payload.data; + // localStorage.setItem("token", authToken); + // navigate("/"); + // createToast({ + // body: "Welcome! Your account was created successfully.", + // }); + // } else { + // if (action.payload) { + // // dispatch errors + // createToast({ + // body: action.payload.msg, + // }); + // } else { + // // unknown errors + // createToast({ + // body: "Unknown error.", + // }); + // } + // } }; const handleChange = (event) => { @@ -732,10 +351,10 @@ const Register = ({ isSuperAdmin }) => { /> ) : step === 3 ? ( setStep(2)} /> ) : ( diff --git a/Client/src/Pages/Auth/Register/StepOne/index.jsx b/Client/src/Pages/Auth/Register/StepOne/index.jsx new file mode 100644 index 000000000..8867b2794 --- /dev/null +++ b/Client/src/Pages/Auth/Register/StepOne/index.jsx @@ -0,0 +1,121 @@ +import { useEffect, useRef } from "react"; +import PropTypes from "prop-types"; +import { useTheme } from "@emotion/react"; +import { Box, Button, Stack, Typography } from "@mui/material"; +import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded"; +import Field from "../../../../Components/Inputs/Field"; + +StepOne.propTypes = { + form: PropTypes.object, + errors: PropTypes.object, + onSubmit: PropTypes.func, + onChange: PropTypes.func, + onBack: PropTypes.func, +}; + +/** + * Renders the first step of the sign up process. + * + * @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} + */ + +function StepOne({ form, errors, onSubmit, onChange, onBack }) { + const theme = useTheme(); + const inputRef = useRef(null); + + useEffect(() => { + if (inputRef.current) { + inputRef.current.focus(); + } + }, []); + + return ( + <> + + + Sign Up + Enter your personal details + + + + + + + + + + + + + + + + ); +} + +export { StepOne }; diff --git a/Client/src/Pages/Auth/Register/StepThree/index.jsx b/Client/src/Pages/Auth/Register/StepThree/index.jsx new file mode 100644 index 000000000..cde86c2db --- /dev/null +++ b/Client/src/Pages/Auth/Register/StepThree/index.jsx @@ -0,0 +1,162 @@ +import { useEffect, useRef /* , useState */ } from "react"; +import PropTypes from "prop-types"; +import { useTheme } from "@emotion/react"; +import { Box, Button, Stack, Typography } from "@mui/material"; +import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded"; +import Field from "../../../../Components/Inputs/Field"; +import Check from "../../../../Components/Check/Check"; +/* import { credentials } from "../../../../Validation/validation"; */ +import { useValidatePassword } from "../../hooks/useValidatePassword"; + +StepThree.propTypes = { + form: PropTypes.object, + errors: PropTypes.object, + onSubmit: PropTypes.func, + /* onChange: PropTypes.func, */ + onBack: PropTypes.func, +}; + +/** + * Renders the third step of the sign up process. + * + * @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} + */ +function StepThree({ onSubmit, onBack }) { + const theme = useTheme(); + const inputRef = useRef(null); + + useEffect(() => { + if (inputRef.current) { + inputRef.current.focus(); + } + }, []); + + const { handleChange, feedbacks, form, errors } = useValidatePassword(); + + return ( + <> + + + Sign Up + Create your password + + + + + + + + + + + + + + + + + + + + ); +} + +export { StepThree }; diff --git a/Client/src/Pages/Auth/Register/StepTwo/index.jsx b/Client/src/Pages/Auth/Register/StepTwo/index.jsx new file mode 100644 index 000000000..567a82818 --- /dev/null +++ b/Client/src/Pages/Auth/Register/StepTwo/index.jsx @@ -0,0 +1,104 @@ +import { useEffect, useRef } from "react"; +import PropTypes from "prop-types"; +import { useTheme } from "@emotion/react"; +import { Box, Button, Stack, Typography } from "@mui/material"; +import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded"; +import Field from "../../../../Components/Inputs/Field"; + +StepTwo.propTypes = { + form: PropTypes.object, + errors: PropTypes.object, + onSubmit: PropTypes.func, + onChange: PropTypes.func, + onBack: PropTypes.func, +}; + +/** + * Renders the second step of the sign up process. + * + * @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} + */ +function StepTwo({ form, errors, onSubmit, onChange, onBack }) { + const theme = useTheme(); + const inputRef = useRef(null); + + useEffect(() => { + if (inputRef.current) { + inputRef.current.focus(); + } + }, []); + + return ( + <> + + + Sign Up + Enter your email address + + + + (e.target.value = e.target.value.toLowerCase())} + onChange={onChange} + error={errors.email} + ref={inputRef} + /> + + + + + + + + + ); +} + +export { StepTwo }; diff --git a/Client/src/Pages/Auth/SetNewPassword.jsx b/Client/src/Pages/Auth/SetNewPassword.jsx index 103dff4d5..1cb858e11 100644 --- a/Client/src/Pages/Auth/SetNewPassword.jsx +++ b/Client/src/Pages/Auth/SetNewPassword.jsx @@ -35,19 +35,12 @@ const SetNewPassword = () => { const handleSubmit = async (e) => { e.preventDefault(); - - const passwordForm = { ...form }; - const { error } = credentials.validate(passwordForm, { + const { error } = credentials.validate(form, { abortEarly: false, context: { password: form.password }, }); if (error) { - const newErrors = {}; - error.details.forEach((err) => { - newErrors[err.path[0]] = err.message; - }); - setErrors(newErrors); createToast({ body: error.details && error.details.length > 0 @@ -55,59 +48,49 @@ const SetNewPassword = () => { : "Error validating data.", }); } else { - delete passwordForm.confirm; - const action = await dispatch(setNewPassword({ token: token, form: passwordForm })); + const action = await dispatch(setNewPassword({ token, form })); if (action.payload.success) { navigate("/new-password-confirmed"); createToast({ body: "Your password was reset successfully.", }); } else { - if (action.payload) { - // dispatch errors - createToast({ - body: action.payload.msg, - }); - } else { - // unknown errors - createToast({ - body: "Unknown error.", - }); - } + const errorMessage = action.payload ? action.payload.msg : "Unknown error"; + createToast({ + body: errorMessage, + }); } } }; const handleChange = (event) => { - //update form state const { value, name } = event.target; setForm((prev) => ({ ...prev, [name]: value })); - //validate const validateValue = { [name]: value }; const validateOptions = { abortEarly: false, context: { password: form.password } }; if (name === "password" && form.confirm.length > 0) { validateValue.confirm = form.confirm; - validateOptions.context = value; + validateOptions.context = { password: value }; + } else if (name === "confirm") { + validateValue.password = form.password; } - const { error } = credentials.validate(validateValue, validateOptions); - //update errors - //fazer o map com path e message + const { error } = credentials.validate(validateValue, validateOptions); const errors = error?.details.map((error) => ({ - message: error.message, path: error.path[0], + message: error.message, })); - const errorsByPath = errors - ? errors.reduce((acc, { path, message }) => { - if (!acc[path]) { - acc[path] = []; - } - acc[path].push(message); - return acc; - }, {}) - : { [name]: [] }; - setErrors((prev) => ({ ...prev, ...errorsByPath })); + const errorsByPath = + errors && + errors.reduce((acc, { path, message }) => { + if (!acc[path]) { + acc[path] = []; + } + acc[path].push(message); + return acc; + }, {}); + setErrors(() => (errorsByPath ? { ...errorsByPath } : {})); }; const getFeedbackStatus = (field, criteria) => { @@ -228,7 +211,7 @@ const SetNewPassword = () => { placeholder="••••••••" value={form.password} onChange={handleChange} - /* error={errors.password} */ + error={errors.password} /> { placeholder="••••••••" value={form.confirm} onChange={handleChange} - /* error={errors.confirm} */ + error={errors.confirm} /> { color="primary" loading={isLoading} onClick={handleSubmit} - disabled={Object.keys(errors).length !== 0} + disabled={ + form.password.length === 0 || + form.confirm.length === 0 || + Object.keys(errors).length !== 0 + } sx={{ width: "100%", maxWidth: 400 }} > Reset password diff --git a/Client/src/Pages/Auth/hooks/useValidatePassword.jsx b/Client/src/Pages/Auth/hooks/useValidatePassword.jsx new file mode 100644 index 000000000..043fc6424 --- /dev/null +++ b/Client/src/Pages/Auth/hooks/useValidatePassword.jsx @@ -0,0 +1,67 @@ +import { useState } from "react"; +import { credentials } from "../../../Validation/validation"; + +function useValidatePassword() { + const [errors, setErrors] = useState({}); + const [form, setForm] = useState({ + password: "", + confirm: "", + }); + + const handleChange = (event) => { + const { value, name } = event.target; + setForm((prev) => ({ ...prev, [name]: value })); + + const validateValue = { [name]: value }; + const validateOptions = { abortEarly: false, context: { password: form.password } }; + if (name === "password" && form.confirm.length > 0) { + validateValue.confirm = form.confirm; + validateOptions.context = { password: value }; + } else if (name === "confirm") { + validateValue.password = form.password; + } + + const { error } = credentials.validate(validateValue, validateOptions); + const errors = error?.details.map((error) => ({ + path: error.path[0], + message: error.message, + })); + const errorsByPath = + errors && + errors.reduce((acc, { path, message }) => { + if (!acc[path]) { + acc[path] = []; + } + acc[path].push(message); + return acc; + }, {}); + setErrors(() => (errorsByPath ? { ...errorsByPath } : {})); + }; + + const getFeedbackStatus = (field, criteria) => { + const fieldErrors = errors[field]; + const isFieldEmpty = form[field].length === 0; + const hasError = fieldErrors?.includes(criteria) || fieldErrors?.includes("empty"); + const isCorrect = !isFieldEmpty && !hasError; + + if (isCorrect) { + return "success"; + } else if (hasError) { + return "error"; + } else { + return "info"; + } + }; + + const feedbacks = { + length: getFeedbackStatus("password", "length"), + special: getFeedbackStatus("password", "special"), + number: getFeedbackStatus("password", "number"), + uppercase: getFeedbackStatus("password", "uppercase"), + lowercase: getFeedbackStatus("password", "lowercase"), + confirm: getFeedbackStatus("confirm", "different"), + }; + + return { handleChange, feedbacks, form, errors }; +} +export { useValidatePassword };