feat/authentication #21

Merged
Hammond merged 12 commits from feat/authentication into master 2025-11-24 22:09:51 +00:00
10 changed files with 829 additions and 127 deletions
Showing only changes of commit 041c36079d - Show all commits

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>
{/* 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>
)}
{/* 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> </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> </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
</Button> 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>
)}
</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)}
<Link 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'}`}
Book Now >
Book-Now
</Link> </Link>
</Button> )}
{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>
)}
</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();