diff --git a/Client/src/Features/Auth/authSlice.js b/Client/src/Features/Auth/authSlice.js index 0e241bbcf..7f2c316fe 100644 --- a/Client/src/Features/Auth/authSlice.js +++ b/Client/src/Features/Auth/authSlice.js @@ -111,6 +111,28 @@ export const forgotPassword = createAsyncThunk( } ); +export const setNewPassword = createAsyncThunk( + "auth/setNewPassword", + async (data, thunkApi) => { + const { token, form } = data; + try { + await axiosInstance.post("/auth/recovery/validate", { + recoveryToken: token, + }); + const res = await axiosInstance.post("/auth/recovery/reset", { + ...form, + recoveryToken: token, + }); + return res.data; + } catch (error) { + if (error.response.data) { + return thunkApi.rejectWithValue(error.response.data); + } + return thunkApi.rejectWithValue(error.message); + } + } +); + const handleAuthFulfilled = (state, action) => { state.isLoading = false; state.success = action.payload.success; @@ -148,12 +170,24 @@ const handleDeleteRejected = (state, action) => { state.success = false; state.msg = action.payload ? action.payload.msg : "Failed to delete account."; }; -const handleForgotPasswordFulfilled = (state, action) => { +const handleForgotFulfilled = (state, action) => { state.isLoading = false; state.success = action.payload.success; state.msg = action.payload.msg; }; -const handleForgotPasswordRejected = (state, action) => { +const handleForgotRejected = (state, action) => { + state.isLoading = false; + state.success = false; + state.msg = action.payload + ? action.payload.msg + : "Failed to send reset instructions."; +}; +const handleNewPasswordFulfilled = (state, action) => { + state.isLoading = false; + state.success = action.payload.success; + state.msg = action.payload.msg; +}; +const handleNewPasswordRejected = (state, action) => { state.isLoading = false; state.success = false; state.msg = action.payload @@ -211,8 +245,16 @@ const authSlice = createSlice({ .addCase(forgotPassword.pending, (state) => { state.isLoading = true; }) - .addCase(forgotPassword.fulfilled, handleForgotPasswordFulfilled) - .addCase(forgotPassword.rejected, handleForgotPasswordRejected); + .addCase(forgotPassword.fulfilled, handleForgotFulfilled) + .addCase(forgotPassword.rejected, handleForgotRejected); + + // Set new password thunk + builder + .addCase(setNewPassword.pending, (state) => { + state.isLoading = true; + }) + .addCase(setNewPassword.fulfilled, handleNewPasswordFulfilled) + .addCase(setNewPassword.rejected, handleNewPasswordRejected); }, }); diff --git a/Client/src/Pages/SetNewPassword/index.jsx b/Client/src/Pages/SetNewPassword/index.jsx index c06787b17..56a18fcff 100644 --- a/Client/src/Pages/SetNewPassword/index.jsx +++ b/Client/src/Pages/SetNewPassword/index.jsx @@ -2,72 +2,116 @@ import BackgroundPattern from "../../Components/BackgroundPattern/BackgroundPatt import "./index.css"; import LockIcon from "../../assets/Images/lock-icon.png"; import Check from "../../Components/Check/Check"; -import Button from "../../Components/Button"; +import ButtonSpinner from "../../Components/ButtonSpinner"; import LeftArrow from "../../assets/Images/arrow-left.png"; import { useParams } from "react-router-dom"; -import { useState, useEffect } from "react"; -import { newPasswordValidation } from "../../Validation/validation"; -import axiosInstance from "../../Utils/axiosConfig"; +import { useState } from "react"; +import { credentials } from "../../Validation/validation"; import { useNavigate } from "react-router-dom"; import Field from "../../Components/Inputs/Field"; +import { useDispatch, useSelector } from "react-redux"; +import { setNewPassword } from "../../Features/Auth/authSlice"; +import { createToast } from "../../Utils/toastUtils"; const SetNewPassword = () => { const navigate = useNavigate(); + const dispatch = useDispatch(); + const [errors, setErrors] = useState({}); - const [form, setForm] = useState({}); - const [isLoading, setIsLoading] = useState(false); + const [form, setForm] = useState({ + password: "", + confirm: "", + }); const idMap = { "register-password-input": "password", "confirm-password-input": "confirm", }; - const handleInput = (e) => { - const fieldName = idMap[e.target.id]; - setForm({ ...form, [fieldName]: e.target.value }); - console.log(errors); - }; + const { isLoading } = useSelector((state) => state.auth); + const { token } = useParams(); - const handleSubmit = async () => { - // TODO show loading spinner - setIsLoading(true); - try { - await axiosInstance.post("/auth/recovery/validate", { - recoveryToken: token, - }); - await axiosInstance.post("/auth/recovery/reset", { - ...form, - recoveryToken: token, - }); - navigate("/new-password-confirmed"); - } catch (error) { - // TODO display error (Toast?) - alert(error.response.data.msg); - } finally { - setIsLoading(false); - } - }; + const handleSubmit = async (e) => { + e.preventDefault(); - useEffect(() => { - const { error } = newPasswordValidation.validate(form, { + const passwordForm = { ...form }; + const { error } = credentials.validate(passwordForm, { abortEarly: false, + context: { password: form.password }, }); if (error) { - const validationErrors = error.details.reduce((acc, err) => { - return { ...acc, [err.path[0]]: err.message }; - }, {}); - setErrors(validationErrors); + // 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 { - setErrors({}); + delete passwordForm.confirm; + const action = await dispatch( + setNewPassword({ token: token, form: passwordForm }) + ); + if (action.payload.success) { + navigate("/new-password-confirmed"); + createToast({ + variant: "info", + body: "Your password was reset successfully.", + hasIcon: false, + }); + } else { + 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, + }); + } + } } - }, [form]); + }; + + const handleChange = (event) => { + const { value, id } = event.target; + const name = idMap[id]; + setForm((prev) => ({ + ...prev, + [name]: value, + })); + + const { error } = credentials.validate( + { [name]: value }, + { abortEarly: false, context: { password: form.password } } + ); + + setErrors((prev) => { + const prevErrors = { ...prev }; + if (error) prevErrors[name] = error.details[0].message; + else delete prevErrors[name]; + return prevErrors; + }); + }; - const { token } = useParams(); return (
-
+
{ label="Password" isRequired={true} placeholder="••••••••" - onChange={handleInput} + value={form.password} + onChange={handleChange} error={errors.password} />
@@ -99,7 +144,8 @@ const SetNewPassword = () => { label="Confirm password" isRequired={true} placeholder="••••••••" - onChange={handleInput} + value={form.confirm} + onChange={handleChange} error={errors.confirm} />
@@ -109,8 +155,9 @@ const SetNewPassword = () => {
-
- + ); };