Enhance authentication and middleware logic by improving user role checks, adding OTP verification steps, and refining redirection based on user roles. Update login and signup forms to handle multiple user attributes and streamline error handling. Integrate logout functionality across components for better user experience.

This commit is contained in:
iamkiddy 2025-11-23 21:13:18 +00:00
parent 7b5f57ea89
commit 041c36079d
10 changed files with 829 additions and 127 deletions

View File

@ -17,6 +17,8 @@ import {
} from "lucide-react"; } from "lucide-react";
import { useAppTheme } from "@/components/ThemeProvider"; import { useAppTheme } from "@/components/ThemeProvider";
import { ThemeToggle } from "@/components/ThemeToggle"; import { ThemeToggle } from "@/components/ThemeToggle";
import { useAuth } from "@/hooks/useAuth";
import { toast } from "sonner";
export function Header() { export function Header() {
const pathname = usePathname(); const pathname = usePathname();
@ -25,6 +27,14 @@ export function Header() {
const [userMenuOpen, setUserMenuOpen] = useState(false); const [userMenuOpen, setUserMenuOpen] = useState(false);
const { theme } = useAppTheme(); const { theme } = useAppTheme();
const isDark = theme === "dark"; const isDark = theme === "dark";
const { logout } = useAuth();
const handleLogout = () => {
setUserMenuOpen(false);
logout();
toast.success("Logged out successfully");
router.push("/");
};
// Mock notifications data // Mock notifications data
const notifications = [ const notifications = [
@ -209,10 +219,7 @@ export function Header() {
</Button> </Button>
<Button <Button
variant="ghost" variant="ghost"
onClick={() => { onClick={handleLogout}
setUserMenuOpen(false);
router.push("/");
}}
className={`w-full flex items-center gap-3 px-4 py-3 justify-start transition-colors cursor-pointer ${ className={`w-full flex items-center gap-3 px-4 py-3 justify-start transition-colors cursor-pointer ${
isDark ? "hover:bg-gray-800" : "hover:bg-gray-50" isDark ? "hover:bg-gray-800" : "hover:bg-gray-50"
}`} }`}

View File

@ -14,6 +14,8 @@ import {
Heart, Heart,
} from "lucide-react"; } from "lucide-react";
import { useAppTheme } from "@/components/ThemeProvider"; import { useAppTheme } from "@/components/ThemeProvider";
import { useAuth } from "@/hooks/useAuth";
import { toast } from "sonner";
const navItems = [ const navItems = [
{ label: "Dashboard", icon: LayoutGrid, href: "/admin/dashboard" }, { label: "Dashboard", icon: LayoutGrid, href: "/admin/dashboard" },
@ -26,6 +28,14 @@ export default function SideNav() {
const router = useRouter(); const router = useRouter();
const { theme } = useAppTheme(); const { theme } = useAppTheme();
const isDark = theme === "dark"; const isDark = theme === "dark";
const { logout } = useAuth();
const handleLogout = () => {
setOpen(false);
logout();
toast.success("Logged out successfully");
router.push("/");
};
const getActiveIndex = () => { const getActiveIndex = () => {
return navItems.findIndex((item) => pathname?.includes(item.href)) ?? -1; return navItems.findIndex((item) => pathname?.includes(item.href)) ?? -1;
@ -176,10 +186,7 @@ export default function SideNav() {
</div> </div>
<Button <Button
variant="ghost" variant="ghost"
onClick={() => { onClick={handleLogout}
setOpen(false);
router.push("/");
}}
className={`group flex items-center gap-2 py-2.5 pl-3 md:pl-3 pr-3 md:pr-3 transition-colors duration-200 w-[90%] md:w-[90%] ml-1 md:ml-2 cursor-pointer justify-start rounded-lg ${ className={`group flex items-center gap-2 py-2.5 pl-3 md:pl-3 pr-3 md:pr-3 transition-colors duration-200 w-[90%] md:w-[90%] ml-1 md:ml-2 cursor-pointer justify-start rounded-lg ${
isDark isDark
? "text-gray-300 hover:bg-gray-800 hover:text-rose-300" ? "text-gray-300 hover:bg-gray-800 hover:text-rose-300"

View File

@ -3,48 +3,127 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Heart, Eye, EyeOff, X, Loader2 } from "lucide-react"; import {
import Link from "next/link"; InputOTP,
InputOTPGroup,
InputOTPSlot,
} from "@/components/ui/input-otp";
import { Heart, Eye, EyeOff, X, Loader2, CheckCircle2 } from "lucide-react";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link";
import { useRouter, useSearchParams } from "next/navigation"; import { useRouter, useSearchParams } from "next/navigation";
import { useAppTheme } from "@/components/ThemeProvider"; import { useAppTheme } from "@/components/ThemeProvider";
import { useAuth } from "@/hooks/useAuth"; import { useAuth } from "@/hooks/useAuth";
import { loginSchema, type LoginInput } from "@/lib/schema/auth"; import {
loginSchema,
registerSchema,
verifyOtpSchema,
type LoginInput,
type RegisterInput,
type VerifyOtpInput
} from "@/lib/schema/auth";
import { toast } from "sonner"; import { toast } from "sonner";
type Step = "login" | "signup" | "verify";
export default function Login() { export default function Login() {
const { theme } = useAppTheme(); const { theme } = useAppTheme();
const isDark = theme === "dark"; const isDark = theme === "dark";
const [step, setStep] = useState<Step>("login");
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
const [showPassword2, setShowPassword2] = useState(false);
const [rememberMe, setRememberMe] = useState(false); const [rememberMe, setRememberMe] = useState(false);
const [formData, setFormData] = useState<LoginInput>({ const [registeredEmail, setRegisteredEmail] = useState("");
// Login form data
const [loginData, setLoginData] = useState<LoginInput>({
email: "", email: "",
password: "", password: "",
}); });
const [errors, setErrors] = useState<Partial<Record<keyof LoginInput, string>>>({});
// Signup form data
const [signupData, setSignupData] = useState<RegisterInput>({
first_name: "",
last_name: "",
email: "",
phone_number: "",
password: "",
password2: "",
});
// OTP verification data
const [otpData, setOtpData] = useState<VerifyOtpInput>({
email: "",
otp: "",
});
const [errors, setErrors] = useState<Partial<Record<string, string>>>({});
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const { login, isAuthenticated, loginMutation } = useAuth(); const {
login,
register,
verifyOtp,
isAuthenticated,
isAdmin,
loginMutation,
registerMutation,
verifyOtpMutation,
resendOtpMutation
} = useAuth();
// Check for verify step or email from query parameters
useEffect(() => {
const verifyEmail = searchParams.get("verify");
const emailParam = searchParams.get("email");
const errorParam = searchParams.get("error");
// Don't show verify step if there's an error indicating OTP sending failed
if (errorParam && errorParam.toLowerCase().includes("failed to send")) {
setStep("login");
return;
}
if (verifyEmail === "true" && emailParam) {
// Show verify step if verify=true
setStep("verify");
setRegisteredEmail(emailParam);
setOtpData({ email: emailParam, otp: "" });
} else if (emailParam && step === "login") {
// Pre-fill email in login form if email parameter is present
setLoginData(prev => ({ ...prev, email: emailParam }));
}
}, [searchParams, step]);
// Redirect if already authenticated // Redirect if already authenticated
useEffect(() => { useEffect(() => {
if (isAuthenticated) { if (isAuthenticated) {
const redirect = searchParams.get("redirect") || "/admin/dashboard"; // Use a small delay to ensure cookies are set and middleware has processed
router.push(redirect); const timer = setTimeout(() => {
} // Always redirect based on user role, ignore redirect parameter if user is admin
}, [isAuthenticated, router, searchParams]); const redirectParam = searchParams.get("redirect");
const defaultRedirect = isAdmin ? "/admin/dashboard" : "/user/dashboard";
const finalRedirect = isAdmin ? "/admin/dashboard" : (redirectParam || defaultRedirect);
const handleSubmit = async (e: React.FormEvent) => { // Use window.location.href to ensure full page reload and cookie reading
window.location.href = finalRedirect;
}, 200);
return () => clearTimeout(timer);
}
}, [isAuthenticated, isAdmin, searchParams]);
// Handle login
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
setErrors({}); setErrors({});
// Validate form // Validate form
const validation = loginSchema.safeParse(formData); const validation = loginSchema.safeParse(loginData);
if (!validation.success) { if (!validation.success) {
const fieldErrors: Partial<Record<keyof LoginInput, string>> = {}; const fieldErrors: Partial<Record<string, string>> = {};
validation.error.issues.forEach((err) => { validation.error.issues.forEach((err) => {
if (err.path[0]) { if (err.path[0]) {
fieldErrors[err.path[0] as keyof LoginInput] = err.message; fieldErrors[err.path[0] as string] = err.message;
} }
}); });
setErrors(fieldErrors); setErrors(fieldErrors);
@ -52,40 +131,214 @@ export default function Login() {
} }
try { try {
const result = await login(formData); const result = await login(loginData);
if (result.tokens && result.user) { if (result.tokens && result.user) {
toast.success("Login successful!"); toast.success("Login successful!");
// Wait a moment for cookies to be set, then redirect
// Normalize user data // Check if user is admin/staff/superuser - check all possible field names
const user = result.user; const user = result.user as any;
// Check for admin status - check multiple possible field names const userIsAdmin =
const isAdmin =
user.is_admin === true || user.is_admin === true ||
(user as any)?.isAdmin === true || user.isAdmin === true ||
(user as any)?.is_staff === true || user.is_staff === true ||
(user as any)?.isStaff === true; user.isStaff === true ||
user.is_superuser === true ||
user.isSuperuser === true;
// Redirect based on user role // Wait longer for cookies to be set and middleware to process
const redirect = searchParams.get("redirect"); setTimeout(() => {
if (redirect) { // Always redirect based on user role, ignore redirect parameter if user is admin
router.push(redirect); // This ensures admins always go to admin dashboard
} else { const defaultRedirect = userIsAdmin ? "/admin/dashboard" : "/user/dashboard";
// Default to admin dashboard
router.push("/admin/dashboard"); // Only use redirect parameter if user is NOT admin
} const redirectParam = searchParams.get("redirect");
const finalRedirect = userIsAdmin ? "/admin/dashboard" : (redirectParam || defaultRedirect);
// Use window.location.href instead of router.push to ensure full page reload
// This ensures cookies are read correctly by middleware
window.location.href = finalRedirect;
}, 300);
} }
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : "Login failed. Please try again."; const errorMessage = error instanceof Error ? error.message : "Login failed. Please try again.";
toast.error(errorMessage); toast.error(errorMessage);
// Don't set field errors for server errors, only show toast
setErrors({}); setErrors({});
} }
}; };
const handleChange = (field: keyof LoginInput, value: string) => { // Handle signup
setFormData((prev) => ({ ...prev, [field]: value })); const handleSignup = async (e: React.FormEvent) => {
// Clear error when user starts typing e.preventDefault();
setErrors({});
// Validate form
const validation = registerSchema.safeParse(signupData);
if (!validation.success) {
const fieldErrors: Partial<Record<string, string>> = {};
validation.error.issues.forEach((err) => {
if (err.path[0]) {
fieldErrors[err.path[0] as string] = err.message;
}
});
setErrors(fieldErrors);
return;
}
try {
const result = await register(signupData);
// Check if registration was successful (user created)
// Even if OTP sending failed, we should allow user to proceed to verification
// and use resend OTP feature
if (result && result.message) {
// Registration successful - proceed to OTP verification
toast.success("Registration successful! Please check your email for OTP verification.");
setRegisteredEmail(signupData.email);
setOtpData({ email: signupData.email, otp: "" });
setStep("verify");
} else {
// If no message but no error, still proceed (some APIs might not return message)
toast.success("Registration successful! Please check your email for OTP verification.");
setRegisteredEmail(signupData.email);
setOtpData({ email: signupData.email, otp: "" });
setStep("verify");
}
} catch (error) {
// Handle different types of errors
let errorMessage = "Registration failed. Please try again.";
if (error instanceof Error) {
errorMessage = error.message;
// If OTP sending failed, don't show OTP verification - just show error
if (errorMessage.toLowerCase().includes("failed to send") ||
errorMessage.toLowerCase().includes("failed to send otp")) {
toast.error("Registration failed: OTP could not be sent. Please try again later or contact support.");
setErrors({});
return;
}
// Check if it's an OTP sending error but registration might have succeeded
if (errorMessage.toLowerCase().includes("otp") ||
errorMessage.toLowerCase().includes("email") ||
errorMessage.toLowerCase().includes("send")) {
// If OTP sending failed but user might be created, allow proceeding to verification
// User can use resend OTP
toast.warning("Registration completed, but OTP email could not be sent. You can request a new OTP on the next screen.");
setRegisteredEmail(signupData.email);
setOtpData({ email: signupData.email, otp: "" });
setStep("verify");
return;
}
}
toast.error(errorMessage);
setErrors({});
}
};
// Handle OTP verification
const handleVerifyOtp = async (e: React.FormEvent) => {
e.preventDefault();
setErrors({});
// Use registeredEmail if available, otherwise use otpData.email
const emailToVerify = registeredEmail || otpData.email;
if (!emailToVerify) {
setErrors({ email: "Email address is required" });
return;
}
// Prepare OTP data with email
const otpToVerify = {
email: emailToVerify,
otp: otpData.otp,
};
// Validate OTP
const validation = verifyOtpSchema.safeParse(otpToVerify);
if (!validation.success) {
const fieldErrors: Partial<Record<string, string>> = {};
validation.error.issues.forEach((err) => {
if (err.path[0]) {
fieldErrors[err.path[0] as string] = err.message;
}
});
setErrors(fieldErrors);
return;
}
try {
const result = await verifyOtp(otpToVerify);
// If verification is successful, redirect to login page
toast.success("Email verified successfully! Redirecting to login...");
// Redirect to login page with email pre-filled
setTimeout(() => {
router.push(`/login?email=${encodeURIComponent(emailToVerify)}`);
}, 1000);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "OTP verification failed. Please try again.";
toast.error(errorMessage);
setErrors({});
}
};
// Handle resend OTP
const handleResendOtp = async () => {
const emailToUse = registeredEmail || otpData.email;
if (!emailToUse) {
toast.error("Email address is required to resend OTP.");
return;
}
try {
await resendOtpMutation.mutateAsync({ email: emailToUse, context: "registration" });
toast.success("OTP resent successfully! Please check your email.");
// Update registeredEmail if it wasn't set
if (!registeredEmail) {
setRegisteredEmail(emailToUse);
}
} catch (error) {
let errorMessage = "Failed to resend OTP. Please try again.";
if (error instanceof Error) {
errorMessage = error.message;
// Provide more helpful error messages
if (errorMessage.toLowerCase().includes("ssl") ||
errorMessage.toLowerCase().includes("certificate")) {
errorMessage = "Email service is currently unavailable. Please contact support or try again later.";
} else if (errorMessage.toLowerCase().includes("not found") ||
errorMessage.toLowerCase().includes("does not exist")) {
errorMessage = "Email address not found. Please check your email or register again.";
}
}
toast.error(errorMessage);
}
};
// Handle form field changes
const handleLoginChange = (field: keyof LoginInput, value: string) => {
setLoginData((prev) => ({ ...prev, [field]: value }));
if (errors[field]) {
setErrors((prev) => ({ ...prev, [field]: undefined }));
}
};
const handleSignupChange = (field: keyof RegisterInput, value: string) => {
setSignupData((prev) => ({ ...prev, [field]: value }));
if (errors[field]) {
setErrors((prev) => ({ ...prev, [field]: undefined }));
}
};
const handleOtpChange = (field: keyof VerifyOtpInput, value: string) => {
setOtpData((prev) => ({ ...prev, [field]: value }));
if (errors[field]) { if (errors[field]) {
setErrors((prev) => ({ ...prev, [field]: undefined })); setErrors((prev) => ({ ...prev, [field]: undefined }));
} }
@ -113,31 +366,58 @@ export default function Login() {
<span className="text-white text-xl font-semibold">Attune Heart Therapy</span> <span className="text-white text-xl font-semibold">Attune Heart Therapy</span>
</div> </div>
{/* Centered White Card */}
{/* Centered White Card - Login Form */}
<div className={`relative z-20 w-full max-w-md rounded-2xl shadow-2xl p-8 ${isDark ? 'bg-gray-800 border border-gray-700' : 'bg-white'}`}> <div className={`relative z-20 w-full max-w-md rounded-2xl shadow-2xl p-8 ${isDark ? 'bg-gray-800 border border-gray-700' : 'bg-white'}`}>
{/* Header with Close Button */} {/* Header with Close Button */}
<div className="flex items-start justify-between mb-2"> <div className="flex items-start justify-between mb-2">
<div className="flex-1"> <div className="flex-1">
{/* Heading */} {/* Heading */}
<h1 className="text-3xl font-bold bg-gradient-to-r from-rose-600 via-pink-600 to-rose-600 bg-clip-text text-transparent mb-2"> <h1 className="text-3xl font-bold bg-linear-to-r from-rose-600 via-pink-600 to-rose-600 bg-clip-text text-transparent mb-2">
Welcome back {step === "login" && "Welcome back"}
{step === "signup" && "Create an account"}
{step === "verify" && "Verify your email"}
</h1> </h1>
{/* Sign Up Prompt */} {/* Subtitle */}
{step === "login" && (
<p className={`mb-6 ${isDark ? 'text-gray-400' : 'text-gray-600'}`}> <p className={`mb-6 ${isDark ? 'text-gray-400' : 'text-gray-600'}`}>
New to Attune Heart Therapy?{" "} New to Attune Heart Therapy?{" "}
<Link href="/signup" className={`underline font-medium ${isDark ? 'text-blue-400 hover:text-blue-300' : 'text-blue-600 hover:text-blue-700'}`}> <Link
href="/signup"
className={`underline font-medium ${isDark ? 'text-blue-400 hover:text-blue-300' : 'text-blue-600 hover:text-blue-700'}`}
>
Sign up Sign up
</Link> </Link>
</p> </p>
)}
{step === "signup" && (
<p className={`mb-6 ${isDark ? 'text-gray-400' : 'text-gray-600'}`}>
Already have an account?{" "}
<button
type="button"
onClick={() => setStep("login")}
className={`underline font-medium ${isDark ? 'text-blue-400 hover:text-blue-300' : 'text-blue-600 hover:text-blue-700'}`}
>
Log in
</button>
</p>
)}
{step === "verify" && registeredEmail && (
<p className={`mb-6 ${isDark ? 'text-gray-400' : 'text-gray-600'}`}>
We've sent a verification code to <strong>{registeredEmail}</strong>
</p>
)}
{step === "verify" && !registeredEmail && (
<p className={`mb-6 ${isDark ? 'text-gray-400' : 'text-gray-600'}`}>
Enter the verification code sent to your email
</p>
)}
</div> </div>
{/* Close Button */} {/* Close Button */}
<Button <Button
onClick={() => router.back()} onClick={() => router.back()}
variant="ghost" variant="ghost"
size="icon" size="icon"
className={`flex-shrink-0 w-8 h-8 rounded-full ${isDark ? 'text-gray-400 hover:text-gray-300 hover:bg-gray-700' : 'text-gray-500 hover:text-gray-700 hover:bg-gray-100'}`} className={`shrink-0 w-8 h-8 rounded-full ${isDark ? 'text-gray-400 hover:text-gray-300 hover:bg-gray-700' : 'text-gray-500 hover:text-gray-700 hover:bg-gray-100'}`}
aria-label="Close" aria-label="Close"
> >
<X className="w-5 h-5" /> <X className="w-5 h-5" />
@ -145,7 +425,8 @@ export default function Login() {
</div> </div>
{/* Login Form */} {/* Login Form */}
<form className="space-y-6" onSubmit={handleSubmit}> {step === "login" && (
<form className="space-y-6" onSubmit={handleLogin}>
{/* Email Field */} {/* Email Field */}
<div className="space-y-2"> <div className="space-y-2">
<label htmlFor="email" className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}> <label htmlFor="email" className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}>
@ -155,11 +436,14 @@ export default function Login() {
id="email" id="email"
type="email" type="email"
placeholder="Email address" placeholder="Email address"
value={formData.email} value={loginData.email}
onChange={(e) => handleChange("email", e.target.value)} onChange={(e) => handleLoginChange("email", e.target.value)}
className={`h-12 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'} ${errors.email ? 'border-red-500' : ''}`} className={`h-12 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'} ${errors.email ? 'border-red-500' : ''}`}
required required
/> />
{errors.email && (
<p className="text-sm text-red-500">{errors.email}</p>
)}
</div> </div>
{/* Password Field */} {/* Password Field */}
@ -172,8 +456,8 @@ export default function Login() {
id="password" id="password"
type={showPassword ? "text" : "password"} type={showPassword ? "text" : "password"}
placeholder="Your password" placeholder="Your password"
value={formData.password} value={loginData.password}
onChange={(e) => handleChange("password", e.target.value)} onChange={(e) => handleLoginChange("password", e.target.value)}
className={`h-12 pr-12 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'} ${errors.password ? 'border-red-500' : ''}`} className={`h-12 pr-12 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'} ${errors.password ? 'border-red-500' : ''}`}
required required
/> />
@ -201,7 +485,7 @@ export default function Login() {
<Button <Button
type="submit" type="submit"
disabled={loginMutation.isPending} disabled={loginMutation.isPending}
className="w-full h-12 text-base font-semibold bg-gradient-to-r from-rose-500 to-pink-600 hover:from-rose-600 hover:to-pink-700 text-white shadow-lg hover:shadow-xl transition-all disabled:opacity-50 disabled:cursor-not-allowed" className="w-full h-12 text-base font-semibold bg-linear-to-r from-rose-500 to-pink-600 hover:from-rose-600 hover:to-pink-700 text-white shadow-lg hover:shadow-xl transition-all disabled:opacity-50 disabled:cursor-not-allowed"
> >
{loginMutation.isPending ? ( {loginMutation.isPending ? (
<> <>
@ -224,15 +508,287 @@ export default function Login() {
/> />
<span className={isDark ? 'text-gray-300' : 'text-black'}>Remember me</span> <span className={isDark ? 'text-gray-300' : 'text-black'}>Remember me</span>
</label> </label>
<Link <button
href="/forgot-password" type="button"
className={`font-medium ${isDark ? 'text-blue-400 hover:text-blue-300' : 'text-blue-600 hover:text-blue-700'}`} className={`font-medium ${isDark ? 'text-blue-400 hover:text-blue-300' : 'text-blue-600 hover:text-blue-700'}`}
> >
Forgot password? Forgot password?
</Link> </button>
</div>
</form>
)}
{/* Signup Form */}
{step === "signup" && (
<form className="space-y-4" onSubmit={handleSignup}>
{/* First Name Field */}
<div className="space-y-2">
<label htmlFor="firstName" className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}>
First Name *
</label>
<Input
id="firstName"
type="text"
placeholder="John"
value={signupData.first_name}
onChange={(e) => handleSignupChange("first_name", e.target.value)}
className={`h-11 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'} ${errors.first_name ? 'border-red-500' : ''}`}
required
/>
{errors.first_name && (
<p className="text-sm text-red-500">{errors.first_name}</p>
)}
</div> </div>
{/* Last Name Field */}
<div className="space-y-2">
<label htmlFor="lastName" className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}>
Last Name *
</label>
<Input
id="lastName"
type="text"
placeholder="Doe"
value={signupData.last_name}
onChange={(e) => handleSignupChange("last_name", e.target.value)}
className={`h-11 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'} ${errors.last_name ? 'border-red-500' : ''}`}
required
/>
{errors.last_name && (
<p className="text-sm text-red-500">{errors.last_name}</p>
)}
</div>
{/* Email Field */}
<div className="space-y-2">
<label htmlFor="signup-email" className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}>
Email address *
</label>
<Input
id="signup-email"
type="email"
placeholder="Email address"
value={signupData.email}
onChange={(e) => handleSignupChange("email", e.target.value)}
className={`h-11 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'} ${errors.email ? 'border-red-500' : ''}`}
required
/>
{errors.email && (
<p className="text-sm text-red-500">{errors.email}</p>
)}
</div>
{/* Phone Field */}
<div className="space-y-2">
<label htmlFor="phone" className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}>
Phone Number (Optional)
</label>
<Input
id="phone"
type="tel"
placeholder="+1 (555) 123-4567"
value={signupData.phone_number || ""}
onChange={(e) => handleSignupChange("phone_number", e.target.value)}
className={`h-11 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'}`}
/>
</div>
{/* Password Field */}
<div className="space-y-2">
<label htmlFor="signup-password" className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}>
Password *
</label>
<div className="relative">
<Input
id="signup-password"
type={showPassword ? "text" : "password"}
placeholder="Password (min 8 characters)"
value={signupData.password}
onChange={(e) => handleSignupChange("password", e.target.value)}
className={`h-11 pr-12 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'} ${errors.password ? 'border-red-500' : ''}`}
required
/>
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => setShowPassword(!showPassword)}
className={`absolute right-4 top-1/2 -translate-y-1/2 h-auto w-auto p-0 ${isDark ? 'text-gray-400 hover:text-gray-300' : 'text-gray-500 hover:text-gray-700'}`}
aria-label={showPassword ? "Hide password" : "Show password"}
>
{showPassword ? (
<EyeOff className="w-5 h-5" />
) : (
<Eye className="w-5 h-5" />
)}
</Button>
</div>
{errors.password && (
<p className="text-sm text-red-500">{errors.password}</p>
)}
</div>
{/* Confirm Password Field */}
<div className="space-y-2">
<label htmlFor="signup-password2" className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}>
Confirm Password *
</label>
<div className="relative">
<Input
id="signup-password2"
type={showPassword2 ? "text" : "password"}
placeholder="Confirm password"
value={signupData.password2}
onChange={(e) => handleSignupChange("password2", e.target.value)}
className={`h-11 pr-12 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'} ${errors.password2 ? 'border-red-500' : ''}`}
required
/>
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => setShowPassword2(!showPassword2)}
className={`absolute right-4 top-1/2 -translate-y-1/2 h-auto w-auto p-0 ${isDark ? 'text-gray-400 hover:text-gray-300' : 'text-gray-500 hover:text-gray-700'}`}
aria-label={showPassword2 ? "Hide password" : "Show password"}
>
{showPassword2 ? (
<EyeOff className="w-5 h-5" />
) : (
<Eye className="w-5 h-5" />
)}
</Button>
</div>
{errors.password2 && (
<p className="text-sm text-red-500">{errors.password2}</p>
)}
</div>
{/* Submit Button */}
<Button
type="submit"
disabled={registerMutation.isPending}
className="w-full h-12 text-base font-semibold bg-linear-to-r from-rose-500 to-pink-600 hover:from-rose-600 hover:to-pink-700 text-white shadow-lg hover:shadow-xl transition-all disabled:opacity-50 disabled:cursor-not-allowed mt-6"
>
{registerMutation.isPending ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Creating account...
</>
) : (
"Sign up"
)}
</Button>
</form> </form>
)}
{/* OTP Verification Form */}
{step === "verify" && (
<form className="space-y-6" onSubmit={handleVerifyOtp}>
<div className={`p-4 rounded-lg border ${isDark ? 'bg-blue-900/20 border-blue-800' : 'bg-blue-50 border-blue-200'}`}>
<div className="flex items-start gap-3">
<CheckCircle2 className={`w-5 h-5 mt-0.5 ${isDark ? 'text-blue-400' : 'text-blue-600'}`} />
<div>
<p className={`text-sm font-medium ${isDark ? 'text-blue-200' : 'text-blue-900'}`}>
Check your email
</p>
<p className={`text-sm mt-1 ${isDark ? 'text-blue-300' : 'text-blue-700'}`}>
We've sent a 6-digit verification code to your email address.
</p>
</div>
</div>
</div>
{/* Email Field (if not set) */}
{!registeredEmail && (
<div className="space-y-2">
<label htmlFor="verify-email" className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}>
Email address *
</label>
<Input
id="verify-email"
type="email"
placeholder="Email address"
value={otpData.email}
onChange={(e) => handleOtpChange("email", e.target.value)}
className={`h-12 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'} ${errors.email ? 'border-red-500' : ''}`}
required
/>
{errors.email && (
<p className="text-sm text-red-500">{errors.email}</p>
)}
</div>
)}
{/* OTP Field */}
<div className="space-y-2">
<label className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}>
Verification Code *
</label>
<div className="flex justify-center">
<InputOTP
maxLength={6}
value={otpData.otp}
onChange={(value) => handleOtpChange("otp", value)}
aria-invalid={!!errors.otp}
>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
</div>
{errors.otp && (
<p className="text-sm text-red-500 text-center">{errors.otp}</p>
)}
</div>
{/* Resend OTP */}
<div className="text-center">
<button
type="button"
onClick={handleResendOtp}
disabled={resendOtpMutation.isPending}
className={`text-sm font-medium ${isDark ? 'text-blue-400 hover:text-blue-300' : 'text-blue-600 hover:text-blue-700'} disabled:opacity-50 disabled:cursor-not-allowed`}
>
{resendOtpMutation.isPending ? "Sending..." : "Didn't receive the code? Resend"}
</button>
</div>
{/* Submit Button */}
<Button
type="submit"
disabled={verifyOtpMutation.isPending}
className="w-full h-12 text-base font-semibold bg-linear-to-r from-rose-500 to-pink-600 hover:from-rose-600 hover:to-pink-700 text-white shadow-lg hover:shadow-xl transition-all disabled:opacity-50 disabled:cursor-not-allowed"
>
{verifyOtpMutation.isPending ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Verifying...
</>
) : (
"Verify Email"
)}
</Button>
{/* Back to signup */}
<div className="text-center">
<button
type="button"
onClick={() => {
setStep("signup");
setOtpData({ email: "", otp: "" });
}}
className={`text-sm font-medium ${isDark ? 'text-gray-400 hover:text-gray-300' : 'text-gray-600 hover:text-gray-700'}`}
>
Back to signup
</button>
</div>
</form>
)}
</div> </div>
</div> </div>
); );

View File

@ -69,13 +69,21 @@ export default function Signup() {
try { try {
const result = await register(formData); const result = await register(formData);
// If registration is successful (no error thrown), show OTP verification // If registration is successful, redirect to login page with verify parameter
toast.success("Registration successful! Please check your email for OTP verification."); toast.success("Registration successful! Please check your email for OTP verification.");
setRegisteredEmail(formData.email); // Redirect to login page with verify step
setOtpData({ email: formData.email, otp: "" }); router.push(`/login?verify=true&email=${encodeURIComponent(formData.email)}`);
setStep("verify");
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : "Registration failed. Please try again."; const errorMessage = error instanceof Error ? error.message : "Registration failed. Please try again.";
// If OTP sending failed, don't show OTP verification - just show error
if (errorMessage.toLowerCase().includes("failed to send") ||
errorMessage.toLowerCase().includes("failed to send otp")) {
toast.error("Registration failed: OTP could not be sent. Please try again later or contact support.");
setErrors({});
return;
}
toast.error(errorMessage); toast.error(errorMessage);
// Don't set field errors for server errors, only show toast // Don't set field errors for server errors, only show toast
setErrors({}); setErrors({});
@ -105,9 +113,9 @@ export default function Signup() {
// If verification is successful (no error thrown), show success and redirect // If verification is successful (no error thrown), show success and redirect
toast.success("Email verified successfully! Redirecting to login..."); toast.success("Email verified successfully! Redirecting to login...");
// Redirect to login page after OTP verification // Redirect to login page after OTP verification with email pre-filled
setTimeout(() => { setTimeout(() => {
router.push("/login"); router.push(`/login?email=${encodeURIComponent(otpData.email)}`);
}, 1500); }, 1500);
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : "OTP verification failed. Please try again."; const errorMessage = error instanceof Error ? error.message : "OTP verification failed. Please try again.";

View File

@ -23,11 +23,14 @@ import {
CheckCircle2, CheckCircle2,
CheckCircle, CheckCircle,
Loader2, Loader2,
LogOut,
} from "lucide-react"; } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import Image from "next/image"; import Image from "next/image";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { LoginDialog } from "@/components/LoginDialog"; import { LoginDialog } from "@/components/LoginDialog";
import { useAuth } from "@/hooks/useAuth";
import { toast } from "sonner";
interface User { interface User {
ID: number; ID: number;
@ -73,6 +76,7 @@ export default function BookNowPage() {
const router = useRouter(); const router = useRouter();
const { theme } = useAppTheme(); const { theme } = useAppTheme();
const isDark = theme === "dark"; const isDark = theme === "dark";
const { isAuthenticated, logout } = useAuth();
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
firstName: "", firstName: "",
lastName: "", lastName: "",
@ -87,6 +91,12 @@ export default function BookNowPage() {
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [showLoginDialog, setShowLoginDialog] = useState(false); const [showLoginDialog, setShowLoginDialog] = useState(false);
const handleLogout = () => {
logout();
toast.success("Logged out successfully");
router.push("/");
};
// Handle submit button click // Handle submit button click
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
@ -660,6 +670,20 @@ export default function BookNowPage() {
</a> </a>
</p> </p>
</div> </div>
{/* Logout Button - Only show when authenticated */}
{isAuthenticated && (
<div className="mt-6 flex justify-center">
<Button
onClick={handleLogout}
variant="outline"
className="bg-red-600 hover:bg-red-700 text-white border-red-600 hover:border-red-700"
>
<LogOut className="w-4 h-4 mr-2" />
Logout
</Button>
</div>
)}
</> </>
)} )}
</div> </div>

View File

@ -2,13 +2,15 @@
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Heart, Menu, X } from "lucide-react"; import { Heart, Menu, X, LogOut } from "lucide-react";
import { ThemeToggle } from "@/components/ThemeToggle"; import { ThemeToggle } from "@/components/ThemeToggle";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { LoginDialog } from "@/components/LoginDialog"; import { LoginDialog } from "@/components/LoginDialog";
import { useRouter, usePathname } from "next/navigation"; import { useRouter, usePathname } from "next/navigation";
import Link from "next/link"; import Link from "next/link";
import { useAppTheme } from "@/components/ThemeProvider"; import { useAppTheme } from "@/components/ThemeProvider";
import { useAuth } from "@/hooks/useAuth";
import { toast } from "sonner";
export function Navbar() { export function Navbar() {
const { theme } = useAppTheme(); const { theme } = useAppTheme();
@ -18,6 +20,9 @@ export function Navbar() {
const router = useRouter(); const router = useRouter();
const pathname = usePathname(); const pathname = usePathname();
const isUserDashboard = pathname?.startsWith("/user/dashboard"); const isUserDashboard = pathname?.startsWith("/user/dashboard");
const isUserSettings = pathname?.startsWith("/user/settings");
const isUserRoute = pathname?.startsWith("/user/");
const { isAuthenticated, logout } = useAuth();
const scrollToSection = (id: string) => { const scrollToSection = (id: string) => {
const element = document.getElementById(id); const element = document.getElementById(id);
@ -33,6 +38,13 @@ export function Navbar() {
setMobileMenuOpen(false); setMobileMenuOpen(false);
}; };
const handleLogout = () => {
logout();
toast.success("Logged out successfully");
setMobileMenuOpen(false);
router.push("/");
};
// Close mobile menu when clicking outside // Close mobile menu when clicking outside
useEffect(() => { useEffect(() => {
if (mobileMenuOpen) { if (mobileMenuOpen) {
@ -73,7 +85,7 @@ export function Navbar() {
</motion.div> </motion.div>
{/* Desktop Navigation */} {/* Desktop Navigation */}
{!isUserDashboard && ( {!isUserRoute && (
<div className="hidden lg:flex items-center gap-4 xl:gap-6"> <div className="hidden lg:flex items-center gap-4 xl:gap-6">
<button <button
onClick={() => scrollToSection("about")} onClick={() => scrollToSection("about")}
@ -98,7 +110,7 @@ export function Navbar() {
{/* Desktop Actions */} {/* Desktop Actions */}
<div className="hidden lg:flex items-center gap-2"> <div className="hidden lg:flex items-center gap-2">
{!isUserDashboard && ( {!isAuthenticated && !isUserDashboard && (
<Button <Button
size="sm" size="sm"
variant="outline" variant="outline"
@ -109,9 +121,31 @@ export function Navbar() {
</Button> </Button>
)} )}
<ThemeToggle /> <ThemeToggle />
<Button size="sm" className="hover:opacity-90 hover:scale-105 transition-all text-xs sm:text-sm" asChild> {!isUserDashboard && (
<a href="/book-now">Book Now</a> <Link
href="/book-now"
className={`text-sm font-medium transition-colors cursor-pointer px-3 py-2 rounded-lg hover:opacity-90 ${isDark ? 'text-gray-300 hover:text-white' : 'text-gray-700 hover:text-rose-600'}`}
>
Book-Now
</Link>
)}
{isAuthenticated && (
<Button
size="sm"
variant="outline"
className={`hover:opacity-90 hover:scale-105 transition-all text-xs sm:text-sm ${
isUserRoute
? 'bg-red-600 hover:bg-red-700 text-white border-red-600 hover:border-red-700'
: isDark
? 'border-gray-700 text-gray-300 hover:bg-gray-800'
: ''
}`}
onClick={handleLogout}
>
<LogOut className="w-4 h-4 mr-2" />
Logout
</Button> </Button>
)}
</div> </div>
{/* Mobile Actions */} {/* Mobile Actions */}
@ -161,7 +195,7 @@ export function Navbar() {
> >
<div className="flex flex-col p-4 sm:p-6 space-y-3 sm:space-y-4"> <div className="flex flex-col p-4 sm:p-6 space-y-3 sm:space-y-4">
{/* Mobile Navigation Links */} {/* Mobile Navigation Links */}
{!isUserDashboard && ( {!isUserRoute && (
<> <>
<button <button
onClick={() => scrollToSection("about")} onClick={() => scrollToSection("about")}
@ -185,7 +219,7 @@ export function Navbar() {
)} )}
<div className={`border-t pt-3 sm:pt-4 mt-3 sm:mt-4 space-y-2 sm:space-y-3 ${isDark ? 'border-gray-700' : 'border-gray-200'}`}> <div className={`border-t pt-3 sm:pt-4 mt-3 sm:mt-4 space-y-2 sm:space-y-3 ${isDark ? 'border-gray-700' : 'border-gray-200'}`}>
{!isUserDashboard && ( {!isAuthenticated && !isUserDashboard && (
<Button <Button
variant="outline" variant="outline"
className={`w-full justify-start text-sm sm:text-base ${isDark ? 'border-gray-700 text-gray-300 hover:bg-gray-800' : ''}`} className={`w-full justify-start text-sm sm:text-base ${isDark ? 'border-gray-700 text-gray-300 hover:bg-gray-800' : ''}`}
@ -197,14 +231,33 @@ export function Navbar() {
Sign In Sign In
</Button> </Button>
)} )}
<Button {!isUserDashboard && (
className="w-full justify-start bg-gradient-to-r from-rose-500 to-pink-600 hover:from-rose-600 hover:to-pink-700 text-white text-sm sm:text-base" <Link
asChild href="/book-now"
onClick={() => setMobileMenuOpen(false)}
className={`text-left text-sm sm:text-base font-medium py-2.5 sm:py-3 px-3 sm:px-4 rounded-lg transition-colors ${isDark ? 'text-gray-300 hover:bg-gray-800' : 'text-gray-700 hover:bg-gray-100'}`}
> >
<Link href="/book-now" onClick={() => setMobileMenuOpen(false)}> Book-Now
Book Now
</Link> </Link>
)}
{isAuthenticated && (
<Button
variant="outline"
className={`w-full justify-start text-sm sm:text-base ${
isUserRoute
? 'bg-red-600 hover:bg-red-700 text-white border-red-600 hover:border-red-700'
: isDark
? 'border-gray-700 text-gray-300 hover:bg-gray-800'
: ''
}`}
onClick={() => {
handleLogout();
}}
>
<LogOut className="w-4 h-4 mr-2" />
Logout
</Button> </Button>
)}
</div> </div>
</div> </div>
</motion.div> </motion.div>

View File

@ -43,8 +43,14 @@ export function useAuth() {
// Check if user is authenticated // Check if user is authenticated
const isAuthenticated = !!user && !!getStoredTokens().access; const isAuthenticated = !!user && !!getStoredTokens().access;
// Check if user is admin (check both is_admin and isAdmin) // Check if user is admin (check multiple possible field names)
const isAdmin = user?.is_admin === true || (user as any)?.isAdmin === true; const isAdmin =
user?.is_admin === true ||
(user as any)?.isAdmin === true ||
(user as any)?.is_staff === true ||
(user as any)?.isStaff === true ||
(user as any)?.is_superuser === true ||
(user as any)?.isSuperuser === true;
// Login mutation // Login mutation
const loginMutation = useMutation({ const loginMutation = useMutation({
@ -109,8 +115,8 @@ export function useAuth() {
const logout = useCallback(() => { const logout = useCallback(() => {
clearAuthData(); clearAuthData();
queryClient.clear(); queryClient.clear();
router.push("/login"); // Don't redirect here - let components handle redirect as needed
}, [queryClient, router]); }, [queryClient]);
// Login function // Login function
const login = useCallback( const login = useCallback(

View File

@ -76,6 +76,28 @@ async function handleResponse<T>(response: Response): Promise<T> {
return data as T; return data as T;
} }
// Helper function to normalize auth response
function normalizeAuthResponse(data: AuthResponse): AuthResponse {
// Normalize tokens: if tokens are at root level, move them to tokens object
if (data.access && data.refresh && !data.tokens) {
data.tokens = {
access: data.access,
refresh: data.refresh,
};
}
// Normalize user: only map isVerified to is_verified if needed
if (data.user) {
const user = data.user as any;
if (user.isVerified !== undefined && user.is_verified === undefined) {
user.is_verified = user.isVerified;
}
data.user = user;
}
return data;
}
// Register a new user // Register a new user
export async function registerUser(input: RegisterInput): Promise<AuthResponse> { export async function registerUser(input: RegisterInput): Promise<AuthResponse> {
const response = await fetch(API_ENDPOINTS.auth.register, { const response = await fetch(API_ENDPOINTS.auth.register, {
@ -86,6 +108,29 @@ export async function registerUser(input: RegisterInput): Promise<AuthResponse>
body: JSON.stringify(input), body: JSON.stringify(input),
}); });
// Handle response - check if it's a 500 error that might indicate OTP sending failure
// but user registration might have succeeded
if (!response.ok && response.status === 500) {
try {
const data = await response.json();
// If the error message mentions OTP or email sending, it might be a partial success
const errorMessage = extractErrorMessage(data);
if (errorMessage.toLowerCase().includes("otp") ||
errorMessage.toLowerCase().includes("email") ||
errorMessage.toLowerCase().includes("send") ||
errorMessage.toLowerCase().includes("ssl") ||
errorMessage.toLowerCase().includes("certificate")) {
// Return a partial success response - user might be created, allow OTP resend
// This allows the user to proceed to OTP verification and use resend OTP
return {
message: "User registered, but OTP email could not be sent. Please use resend OTP.",
} as AuthResponse;
}
} catch {
// If we can't parse the error, continue to normal error handling
}
}
return handleResponse<AuthResponse>(response); return handleResponse<AuthResponse>(response);
} }
@ -100,23 +145,7 @@ export async function verifyOtp(input: VerifyOtpInput): Promise<AuthResponse> {
}); });
const data = await handleResponse<AuthResponse>(response); const data = await handleResponse<AuthResponse>(response);
return normalizeAuthResponse(data);
// Normalize response: if tokens are at root level, move them to tokens object
if (data.access && data.refresh && !data.tokens) {
data.tokens = {
access: data.access,
refresh: data.refresh,
};
}
// Normalize user: map isVerified to is_verified if needed
if (data.user) {
if (data.user.isVerified !== undefined && data.user.is_verified === undefined) {
data.user.is_verified = data.user.isVerified;
}
}
return data;
} }
// Login user // Login user
@ -130,23 +159,7 @@ export async function loginUser(input: LoginInput): Promise<AuthResponse> {
}); });
const data = await handleResponse<AuthResponse>(response); const data = await handleResponse<AuthResponse>(response);
return normalizeAuthResponse(data);
// Normalize response: if tokens are at root level, move them to tokens object
if (data.access && data.refresh && !data.tokens) {
data.tokens = {
access: data.access,
refresh: data.refresh,
};
}
// Normalize user: map isVerified to is_verified if needed
if (data.user) {
if (data.user.isVerified !== undefined && data.user.is_verified === undefined) {
data.user.is_verified = data.user.isVerified;
}
}
return data;
} }
// Resend OTP // Resend OTP
@ -245,9 +258,7 @@ export function storeUser(user: User): void {
if (typeof window === "undefined") return; if (typeof window === "undefined") return;
localStorage.setItem("auth_user", JSON.stringify(user)); localStorage.setItem("auth_user", JSON.stringify(user));
document.cookie = `auth_user=${encodeURIComponent(JSON.stringify(user))}; path=/; max-age=${7 * 24 * 60 * 60}; SameSite=Lax`;
// Also set cookie for middleware
document.cookie = `auth_user=${JSON.stringify(user)}; path=/; max-age=${7 * 24 * 60 * 60}; SameSite=Lax`;
} }
// Get stored user // Get stored user

View File

@ -11,9 +11,17 @@ export interface User {
last_name: string; last_name: string;
phone_number?: string; phone_number?: string;
is_admin?: boolean; is_admin?: boolean;
isAdmin?: boolean; // API uses camelCase
is_staff?: boolean;
isStaff?: boolean; // API uses camelCase
is_superuser?: boolean;
isSuperuser?: boolean; // API uses camelCase
is_verified?: boolean; is_verified?: boolean;
isVerified?: boolean; // API uses camelCase isVerified?: boolean; // API uses camelCase
is_active?: boolean;
isActive?: boolean; // API uses camelCase
date_joined?: string; date_joined?: string;
last_login?: string;
created_at?: string; created_at?: string;
updated_at?: string; updated_at?: string;
} }

View File

@ -13,16 +13,31 @@ export function middleware(request: NextRequest) {
if (userStr) { if (userStr) {
try { try {
const user = JSON.parse(userStr); // Decode the user string if it's URL encoded
isAdmin = user.is_admin === true; const decodedUserStr = decodeURIComponent(userStr);
const user = JSON.parse(decodedUserStr);
// Check for admin status using multiple possible field names
// Admin users must be verified (is_verified or isVerified must be true)
const isVerified = user.is_verified === true || user.isVerified === true;
const hasAdminRole =
user.is_admin === true ||
user.isAdmin === true ||
user.is_staff === true ||
user.isStaff === true ||
user.is_superuser === true ||
user.isSuperuser === true;
// User is admin only if they have admin role AND are verified
isAdmin = hasAdminRole && isVerified;
} catch { } catch {
// Invalid user data // Invalid user data - silently fail and treat as non-admin
} }
} }
// Protected routes // Protected routes
const isProtectedRoute = pathname.startsWith("/user") || pathname.startsWith("/admin"); const isProtectedRoute = pathname.startsWith("/user") || pathname.startsWith("/admin");
const isAdminRoute = pathname.startsWith("/admin"); const isAdminRoute = pathname.startsWith("/admin");
const isUserRoute = pathname.startsWith("/user");
const isAuthRoute = pathname.startsWith("/login") || pathname.startsWith("/signup"); const isAuthRoute = pathname.startsWith("/login") || pathname.startsWith("/signup");
// Redirect unauthenticated users away from protected routes // Redirect unauthenticated users away from protected routes
@ -34,12 +49,19 @@ export function middleware(request: NextRequest) {
// Redirect authenticated users away from auth routes // Redirect authenticated users away from auth routes
if (isAuthRoute && isAuthenticated) { if (isAuthRoute && isAuthenticated) {
// Redirect based on user role
const redirectPath = isAdmin ? "/admin/dashboard" : "/user/dashboard";
return NextResponse.redirect(new URL(redirectPath, request.url));
}
// Redirect admin users away from user routes
if (isUserRoute && isAuthenticated && isAdmin) {
return NextResponse.redirect(new URL("/admin/dashboard", request.url)); return NextResponse.redirect(new URL("/admin/dashboard", request.url));
} }
// Redirect non-admin users away from admin routes // Redirect non-admin users away from admin routes
if (isAdminRoute && (!isAuthenticated || !isAdmin)) { if (isAdminRoute && isAuthenticated && !isAdmin) {
return NextResponse.redirect(new URL("/admin/dashboard", request.url)); return NextResponse.redirect(new URL("/user/dashboard", request.url));
} }
return NextResponse.next(); return NextResponse.next();