From 7a51cb2d16f46226a5313c1d90fab4bc3cd62e39 Mon Sep 17 00:00:00 2001
From: shamoo53
Date: Thu, 26 Mar 2026 10:02:19 +0100
Subject: [PATCH] Implement-Protected-Route-HO
Implement-Protected-Route-HO
---
.../forgot-password/ForgotPasswordForm.tsx | 211 ++++++++
app/auth/forgot-password/page.tsx | 12 +
app/auth/reset-password/ResetPasswordForm.tsx | 475 ++++++++++++++++++
app/auth/reset-password/page.tsx | 11 +
app/auth/signup/SignupForm.tsx | 459 +++++++++--------
components/passwordInput.tsx | 177 ++++---
components/ui/Toast.tsx | 12 +-
features/authThunk.ts | 115 -----
features/authValidation.ts | 63 +--
lib/api/auth.ts | 63 ++-
10 files changed, 1165 insertions(+), 433 deletions(-)
create mode 100644 app/auth/forgot-password/ForgotPasswordForm.tsx
create mode 100644 app/auth/forgot-password/page.tsx
create mode 100644 app/auth/reset-password/ResetPasswordForm.tsx
create mode 100644 app/auth/reset-password/page.tsx
delete mode 100644 features/authThunk.ts
diff --git a/app/auth/forgot-password/ForgotPasswordForm.tsx b/app/auth/forgot-password/ForgotPasswordForm.tsx
new file mode 100644
index 0000000..818eb40
--- /dev/null
+++ b/app/auth/forgot-password/ForgotPasswordForm.tsx
@@ -0,0 +1,211 @@
+'use client';
+
+import { useState } from 'react';
+import { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import Link from 'next/link';
+import { z } from 'zod';
+
+import { authApi } from '@/lib/api/auth';
+import { Button } from '@/components/ui/Button';
+import { Card } from '@/components/ui/Card';
+import type { ApiError } from '@/types/api';
+
+// Validation schema
+const forgotPasswordSchema = z.object({
+ email: z
+ .string()
+ .min(1, 'Email is required')
+ .email('Please enter a valid email address'),
+});
+
+type ForgotPasswordValues = z.infer;
+
+export default function ForgotPasswordForm() {
+ const [isSubmitted, setIsSubmitted] = useState(false);
+ const [isLoading, setIsLoading] = useState(false);
+
+ const {
+ register,
+ handleSubmit,
+ formState: { errors, isSubmitting },
+ } = useForm({
+ resolver: zodResolver(forgotPasswordSchema),
+ defaultValues: {
+ email: '',
+ },
+ });
+
+ const onSubmit = async (data: ForgotPasswordValues) => {
+ setIsLoading(true);
+ try {
+ await authApi.forgotPassword({
+ email: data.email,
+ });
+
+ // Show success message regardless of whether email exists
+ setIsSubmitted(true);
+ } catch (err) {
+ const apiError = err as ApiError;
+
+ // For security (avoiding user enumeration), always show success for 404
+ if (apiError.status === 404) {
+ setIsSubmitted(true);
+ } else {
+ // For other errors, show the error message
+ console.error('Forgot password error:', apiError);
+ }
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ if (isSubmitted) {
+ return (
+
+ {/* Success Icon */}
+
+
+ {/* Success Message */}
+
+
+ Check Your Email
+
+
+ We've sent password reset instructions to your email address.
+ Please check your inbox and follow the link to reset your password.
+
+
+
+ {/* Instructions */}
+
+
Next Steps:
+
+ - • Check your email inbox
+ - • Look for an email from StellarAid
+ - • Click the reset link in the email
+ - • Create a new password
+
+
+
+ {/* Action Buttons */}
+
+
+
+
+
+
+ Didn't receive the email? Check your spam folder or{' '}
+
+
+
+
+ );
+ }
+
+ return (
+
+ {/* Brand header */}
+
+
+ {/* Heading */}
+
+
Forgot Password?
+
+ Enter your email address and we'll send you a link to reset your password.
+
+
+
+ {/* Root-level error */}
+ {errors.root && (
+
+ {errors.root.message}
+
+ )}
+
+ {/* Form */}
+
+
+ {/* Back to login */}
+
+ Remember your password?{' '}
+
+ Sign In
+
+
+
+ );
+}
diff --git a/app/auth/forgot-password/page.tsx b/app/auth/forgot-password/page.tsx
new file mode 100644
index 0000000..320b36d
--- /dev/null
+++ b/app/auth/forgot-password/page.tsx
@@ -0,0 +1,12 @@
+import type { Metadata } from 'next';
+import ForgotPasswordForm from './ForgotPasswordForm';
+
+export const metadata: Metadata = {
+ title: 'Forgot Password | StellarAid',
+ description:
+ 'Reset your StellarAid password securely via email verification.',
+};
+
+export default function ForgotPasswordPage() {
+ return ;
+}
diff --git a/app/auth/reset-password/ResetPasswordForm.tsx b/app/auth/reset-password/ResetPasswordForm.tsx
new file mode 100644
index 0000000..3d7578e
--- /dev/null
+++ b/app/auth/reset-password/ResetPasswordForm.tsx
@@ -0,0 +1,475 @@
+'use client';
+
+import { useEffect, useState } from 'react';
+import { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { useRouter, useSearchParams } from 'next/navigation';
+import Link from 'next/link';
+import { z } from 'zod';
+
+import { authApi } from '@/lib/api/auth';
+import { Button } from '@/components/ui/Button';
+import { Card } from '@/components/ui/Card';
+import type { ApiError } from '@/types/api';
+
+// Password requirements
+const passwordRequirements = {
+ minLength: 8,
+ hasUpperCase: true,
+ hasLowerCase: true,
+ hasNumber: true,
+ hasSpecialChar: true,
+};
+
+// Validation schema
+const resetPasswordSchema = z
+ .object({
+ password: z
+ .string()
+ .min(passwordRequirements.minLength, `Password must be at least ${passwordRequirements.minLength} characters`)
+ .regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
+ .regex(/[a-z]/, 'Password must contain at least one lowercase letter')
+ .regex(/[0-9]/, 'Password must contain at least one number')
+ .regex(/[^A-Za-z0-9]/, 'Password must contain at least one special character'),
+ confirmPassword: z.string().min(1, 'Please confirm your password'),
+ token: z.string().min(1, 'Token is required'),
+ })
+ .refine((data) => data.password === data.confirmPassword, {
+ message: "Passwords must match",
+ path: ["confirmPassword"],
+ });
+
+type ResetPasswordValues = z.infer;
+
+export default function ResetPasswordForm() {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+ const [isLoading, setIsLoading] = useState(false);
+ const [isSuccess, setIsSuccess] = useState(false);
+ const [tokenError, setTokenError] = useState(null);
+
+ const token = searchParams.get('token');
+
+ const {
+ register,
+ handleSubmit,
+ formState: { errors, isSubmitting },
+ watch,
+ } = useForm({
+ resolver: zodResolver(resetPasswordSchema),
+ defaultValues: {
+ password: '',
+ confirmPassword: '',
+ token: token || '',
+ },
+ });
+
+ const password = watch('password');
+
+ // Check password requirements
+ const passwordChecks = {
+ length: password.length >= passwordRequirements.minLength,
+ uppercase: /[A-Z]/.test(password),
+ lowercase: /[a-z]/.test(password),
+ number: /[0-9]/.test(password),
+ special: /[^A-Za-z0-9]/.test(password),
+ };
+
+ // Validate token on mount
+ useEffect(() => {
+ if (!token) {
+ setTokenError('Invalid reset link. Please request a new password reset.');
+ }
+ }, [token]);
+
+ const onSubmit = async (data: ResetPasswordValues) => {
+ if (!token) {
+ setTokenError('Invalid reset link. Please request a new password reset.');
+ return;
+ }
+
+ setIsLoading(true);
+ try {
+ await authApi.resetPassword({
+ token,
+ password: data.password,
+ });
+
+ setIsSuccess(true);
+ } catch (err) {
+ const apiError = err as ApiError;
+
+ if (apiError.status === 400 && apiError.message?.includes('expired')) {
+ setTokenError('This reset link has expired. Please request a new password reset.');
+ } else if (apiError.status === 400 && apiError.message?.includes('invalid')) {
+ setTokenError('This reset link is invalid. Please request a new password reset.');
+ } else {
+ setTokenError(apiError.message || 'Unable to reset password. Please try again.');
+ }
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ // Success state
+ if (isSuccess) {
+ return (
+
+ {/* Success Icon */}
+
+
+ {/* Success Message */}
+
+
+ Password Reset Successful
+
+
+ Your password has been successfully reset. You can now sign in with your new password.
+
+
+
+ {/* Action Button */}
+
+
+
+
+ );
+ }
+
+ // Token error state
+ if (tokenError) {
+ return (
+
+ {/* Error Icon */}
+
+
+ {/* Error Message */}
+
+
+ Reset Link Invalid
+
+
+ {tokenError}
+
+
+
+ {/* Action Buttons */}
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+ return (
+
+ {/* Brand header */}
+
+
+ {/* Heading */}
+
+
Reset Password
+
+ Create a new password for your StellarAid account.
+
+
+
+ {/* Root-level error */}
+ {errors.root && (
+
+ {errors.root.message}
+
+ )}
+
+ {/* Form */}
+
+
+ {/* Back to login */}
+
+ Remember your password?{' '}
+
+ Sign In
+
+
+
+ );
+}
diff --git a/app/auth/reset-password/page.tsx b/app/auth/reset-password/page.tsx
new file mode 100644
index 0000000..8e1cf67
--- /dev/null
+++ b/app/auth/reset-password/page.tsx
@@ -0,0 +1,11 @@
+import type { Metadata } from "next";
+import ResetPasswordForm from "./ResetPasswordForm";
+
+export const metadata: Metadata = {
+ title: "Reset Password | StellarAid",
+ description: "Create a new password for your StellarAid account.",
+};
+
+export default function ResetPasswordPage() {
+ return ;
+}
diff --git a/app/auth/signup/SignupForm.tsx b/app/auth/signup/SignupForm.tsx
index 48d74a8..13fce31 100644
--- a/app/auth/signup/SignupForm.tsx
+++ b/app/auth/signup/SignupForm.tsx
@@ -1,22 +1,22 @@
"use client";
import { useCallback } from "react";
-import Link from 'next/link';
+import Link from "next/link";
import { Button, Input, useToast } from "@/components/ui";
-import PasswordInput from '@/components/passwordInput';
-import { FcGoogle } from "react-icons/fc";
-import { SiStellar } from "react-icons/si";
+import PasswordInput from "@/components/passwordInput";
+// import { FcGoogle } from "react-icons/fc";
+// import { SiStellar } from "react-icons/si";
// form & validation
-import { useForm, Controller } from 'react-hook-form';
-import { yupResolver } from '@hookform/resolvers/yup';
-import { useRouter } from 'next/navigation';
+import { useForm, Controller } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useRouter } from "next/navigation";
import { registerSchema } from "@/features/authValidation";
import { RadioOption } from "@/components/ui/radioOption";
interface RegisterFormData {
email: string;
- role: 'donor' | 'creator';
+ role: "donor" | "creator";
password: string;
confirmPassword: string;
}
@@ -31,13 +31,13 @@ const Signup = () => {
formState: { errors, isValid, isSubmitting },
watch,
} = useForm({
- resolver: yupResolver(registerSchema),
- mode: 'onChange',
+ resolver: zodResolver(registerSchema),
+ mode: "onChange",
defaultValues: {
- email: '',
- role: 'donor',
- password: '',
- confirmPassword: '',
+ email: "",
+ role: "donor",
+ password: "",
+ confirmPassword: "",
},
});
@@ -46,243 +46,288 @@ const Signup = () => {
try {
// TODO: Replace with actual API call when backend is ready
// await dispatch(registerUser(data)).unwrap();
-
+
// Simulate API call
- await new Promise(resolve => setTimeout(resolve, 1500));
-
- toast.success('Registration successful! Please check your email to verify your account.');
- router.push('/auth/verify-email');
+ await new Promise((resolve) => setTimeout(resolve, 1500));
+
+ toast.success(
+ "Registration successful! Please check your email to verify your account.",
+ );
+ router.push("/auth/verify-email");
} catch (err: any) {
- toast.error(err?.message || 'Registration failed. Please try again.');
+ toast.error(err?.message || "Registration failed. Please try again.");
}
},
- [toast, router]
+ [toast, router],
);
return (
-
+
+ {/* Sign-in link */}
+
- Already have an account?{" "}
- e.currentTarget.style.color = "#1e3a8a"}
- onMouseLeave={(e) => e.currentTarget.style.color = "#1d4ed8"}
- >
- Sign in
-
-
-
-
+ }}
+ >
+ Already have an account?{" "}
+ (e.currentTarget.style.color = "#1e3a8a")}
+ onMouseLeave={(e) => (e.currentTarget.style.color = "#1d4ed8")}
+ >
+ Sign in
+
+
+
+
);
};
diff --git a/components/passwordInput.tsx b/components/passwordInput.tsx
index 49d09de..e3140e8 100644
--- a/components/passwordInput.tsx
+++ b/components/passwordInput.tsx
@@ -1,5 +1,4 @@
import { useState, forwardRef, InputHTMLAttributes } from "react";
-import { FiEye, FiEyeOff } from "react-icons/fi";
import PasswordStrengthBar from "./passwordStrengthBar";
interface PasswordInputProps extends InputHTMLAttributes {
@@ -11,67 +10,129 @@ interface PasswordInputProps extends InputHTMLAttributes {
autoComplete?: string;
}
-const PasswordInput = forwardRef(function PasswordInput({ label, id, value, onChange, showStrength = false, autoComplete, ...props }, ref) {
- const [focused, setFocused] = useState(false);
- const [visible, setVisible] = useState(false);
- return (
-
-