diff --git a/client/src/App.tsx b/client/src/App.tsx index 386f2b1..30811c4 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,6 +1,7 @@ import { Route, Routes } from "react-router-dom"; import { ForgotPasswordPage } from "@/pages/forgot-password/page"; +import { ResetPasswordPage } from "@/pages/reset-password/page"; import VerifyEmailPage from "@/pages/verify-email/page"; import PrivateGuard from "@/guards/private-guard"; import { AppRoutes } from "@/constants/routes"; @@ -25,6 +26,10 @@ function App() { path={AppRoutes.ForgotPassword} element={} /> + } + /> ); diff --git a/client/src/constants/collections.ts b/client/src/constants/collections.ts index 453f821..27a6ab2 100644 --- a/client/src/constants/collections.ts +++ b/client/src/constants/collections.ts @@ -7,7 +7,7 @@ import { ValidateUsername, } from "@/lib/utils/validations"; -export const SIGNUPINPUTS = [ +export const SIGNUP_INPUTS = [ { name: "username", label: "Username", @@ -35,7 +35,7 @@ export const SIGNUPINPUTS = [ name: "password", label: "Password", tooltip: - "Create a strong password with at least 8 characters. Include a mix of uppercase letters, lowercase letters, numbers, and special characters for better security.", + "Create a password with at least 8 characters, including uppercase, lowercase, numbers, and special characters for security.", placeholder: "e.g. m#P52s@ap$V", type: "password", autoComplete: "new-password", @@ -56,7 +56,7 @@ export const SIGNUPINPUTS = [ }, ]; -export const SIGNININPUTS = [ +export const SIGNIN_INPUTS = [ { name: "email", label: "Email Address", @@ -82,3 +82,40 @@ export const SIGNININPUTS = [ maxLength: 128, }, ]; + +export const RESET_PASSWORD_INPUTS = [ + { + name: "old-password", + label: "Old Password", + tooltip: "Enter your current password", + placeholder: "e.g. m#P52s@ap$V", + type: "password", + autoComplete: "new-password", + suffixIcon: Lock, + validation: ValidatePassword, + maxLength: 128, + }, + { + name: "new-password", + label: "New Password", + tooltip: + "Create a password with at least 8 characters, including uppercase, lowercase, numbers, and special characters for security", + placeholder: "e.g. m#P52s@ap$V", + type: "password", + autoComplete: "new-password", + suffixIcon: Lock, + validation: ValidatePassword, + maxLength: 128, + }, + { + label: "Confirm Password", + tooltip: + "Re-enter your password to confirm it matches the one you typed above. Ensure both passwords are identical.", + placeholder: "e.g. m#P52s@ap$V", + type: "password", + autoComplete: "new-password", + suffixIcon: Lock, + validation: ValidateConfirmPassword, + maxLength: 128, + }, +]; diff --git a/client/src/constants/keys.ts b/client/src/constants/keys.ts index f22fa6f..a7c242f 100644 --- a/client/src/constants/keys.ts +++ b/client/src/constants/keys.ts @@ -5,3 +5,4 @@ export const VERIFYEMAILKEY = "verify-email"; export const RESENDVERIFYKEY = "resend-verify"; export const VERIFYTOKENKEY = "verify-token"; export const FORGOTPASSWORDKEY = "forgot-password"; +export const RESETPASSWORDKEY = "reset-password"; diff --git a/client/src/lib/DTO/reset-dto.ts b/client/src/lib/DTO/reset-dto.ts new file mode 100644 index 0000000..9422bf1 --- /dev/null +++ b/client/src/lib/DTO/reset-dto.ts @@ -0,0 +1,4 @@ +export type ResetDTO = { + token: string; + password: string; +}; diff --git a/client/src/lib/services/auth-service.ts b/client/src/lib/services/auth-service.ts index 648bddb..a5a380c 100644 --- a/client/src/lib/services/auth-service.ts +++ b/client/src/lib/services/auth-service.ts @@ -4,6 +4,7 @@ import { UserResponse } from "@/lib/classes/user-response-class"; import { SignUpDTO } from "@/lib/DTO/sign-up-dto"; import { SignInDTO } from "@/lib/DTO/sign-in-dto"; import { AppRoutes } from "@/constants/routes"; +import { ResetDTO } from "../DTO/reset-dto"; const baseURL = import.meta.env.VITE_ENV === "development" @@ -158,6 +159,33 @@ export const AuthService = { }); } + return new GenericResponse({ + message: data.message, + }); + }, + resetPassword: async (reset: ResetDTO) => { + const res = await fetch( + `${baseURL}${AppRoutes.ResetPassword}/${reset.token}`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + password: reset.password, + }), + } + ); + + const data = await res.json(); + + if (!res.ok) { + throw new ErrorResponse({ + status: res.status, + message: data.error, + }); + } + return new GenericResponse({ message: data.message, }); diff --git a/client/src/lib/stores/auth-store.ts b/client/src/lib/stores/auth-store.ts index 1f6860a..3581a56 100644 --- a/client/src/lib/stores/auth-store.ts +++ b/client/src/lib/stores/auth-store.ts @@ -1,5 +1,5 @@ -import { create } from "zustand"; import { createJSONStorage, persist } from "zustand/middleware"; +import { create } from "zustand"; interface AuthStoreState { email: string; diff --git a/client/src/pages/forgot-password/_components/forgot-password-form.tsx b/client/src/pages/forgot-password/_components/forgot-password-form.tsx index 3db235e..a841d7e 100644 --- a/client/src/pages/forgot-password/_components/forgot-password-form.tsx +++ b/client/src/pages/forgot-password/_components/forgot-password-form.tsx @@ -1,7 +1,6 @@ import { useMutation } from "@tanstack/react-query"; -import { useNavigate } from "react-router-dom"; +import { FormEvent, useState } from "react"; import { Mail } from "lucide-react"; -import { FormEvent } from "react"; import { toast } from "sonner"; import { GenericResponse } from "@/lib/classes/generic-response-class"; @@ -10,18 +9,17 @@ import { AuthService } from "@/lib/services/auth-service"; import { ValidateEmail } from "@/lib/utils/validations"; import { FORGOTPASSWORDKEY } from "@/constants/keys"; import { Button } from "@/components/text-button"; -import { AppRoutes } from "@/constants/routes"; import { Input } from "@/components/input"; const ForgotPasswordForm = () => { - const navigate = useNavigate(); - + const [submitted, setSubmitted] = useState(false); + const [email, setEmail] = useState(""); const { mutate, isPending } = useMutation({ mutationKey: [FORGOTPASSWORDKEY], mutationFn: AuthService.forgotPassword, onSuccess: (res: GenericResponse) => { toast.success(res.message); - navigate(AppRoutes.ResetPassword); + setSubmitted(true); }, onError: (error: ErrorResponse) => toast.error(error.message), }); @@ -33,10 +31,25 @@ const ForgotPasswordForm = () => { const forgotPasswordData = Object.fromEntries(formData.entries()); - mutate(forgotPasswordData["email"] as string); + const emailData = forgotPasswordData["email"] as string; + + setEmail(emailData); + + mutate(emailData); }; - return ( + return submitted ? ( +
+
+ +
+

+ If an account exists for{" "} + {email}, you will + receive a password reset link shortly. +

+
+ ) : (
{ + const navigate = useNavigate(); + const { token } = useParams(); + const [confirmPassword, setConfirmPassword] = useState(""); + const { mutate, isPending } = useMutation({ + mutationKey: [RESETPASSWORDKEY], + mutationFn: AuthService.resetPassword, + onSuccess: (res: GenericResponse) => { + toast.success(res.message); + navigate(AppRoutes.SignIn); + }, + onError: (error: ErrorResponse) => toast.error(error.message), + }); + + const onSubmit = (e: FormEvent) => { + e.preventDefault(); + + if (!token) return; + + const formData = new FormData(e.currentTarget); + + const resetPasswordData = Object.fromEntries( + formData.entries() + ) as ResetDTO; + + resetPasswordData.token = token; + + mutate(resetPasswordData); + }; + + return ( + + {RESET_PASSWORD_INPUTS.map((r) => ( + { + if (r.name === "new-password") { + setConfirmPassword(value); + } + if (r.name === undefined) { + return r.validation({ + pass1: value, + pass2: confirmPassword, + }); + } + + return r.validation(value); + }} + /> + ))} + +