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