diff --git a/app/(admin)/_components/header.tsx b/app/(admin)/_components/header.tsx index ef68227..a5f5976 100644 --- a/app/(admin)/_components/header.tsx +++ b/app/(admin)/_components/header.tsx @@ -17,6 +17,8 @@ import { } from "lucide-react"; import { useAppTheme } from "@/components/ThemeProvider"; import { ThemeToggle } from "@/components/ThemeToggle"; +import { useAuth } from "@/hooks/useAuth"; +import { toast } from "sonner"; export function Header() { const pathname = usePathname(); @@ -25,6 +27,14 @@ export function Header() { const [userMenuOpen, setUserMenuOpen] = useState(false); const { theme } = useAppTheme(); const isDark = theme === "dark"; + const { logout } = useAuth(); + + const handleLogout = () => { + setUserMenuOpen(false); + logout(); + toast.success("Logged out successfully"); + router.push("/"); + }; // Mock notifications data const notifications = [ @@ -209,10 +219,7 @@ export function Header() { +

+ )} + {step === "verify" && registeredEmail && ( +

+ We've sent a verification code to {registeredEmail} +

+ )} + {step === "verify" && !registeredEmail && ( +

+ Enter the verification code sent to your email +

+ )} {/* Close Button */} + + + )} + + {/* Signup Form */} + {step === "signup" && ( +
+ {/* First Name Field */} +
+ + 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 && ( +

{errors.first_name}

+ )} +
+ + {/* Last Name Field */} +
+ + 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 && ( +

{errors.last_name}

+ )} +
+ + {/* Email Field */} +
+ + 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 && ( +

{errors.email}

+ )} +
+ + {/* Phone Field */} +
+ + 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'}`} + /> +
+ + {/* Password Field */} +
+ +
+ 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 + /> + +
+ {errors.password && ( +

{errors.password}

+ )} +
+ + {/* Confirm Password Field */} +
+ +
+ 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 + /> + +
+ {errors.password2 && ( +

{errors.password2}

+ )} +
+ + {/* Submit Button */} + +
+ )} + + {/* OTP Verification Form */} + {step === "verify" && ( +
+
+
+ +
+

+ Check your email +

+

+ We've sent a 6-digit verification code to your email address. +

+
+
+
+ + {/* Email Field (if not set) */} + {!registeredEmail && ( +
+ + 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 && ( +

{errors.email}

+ )} +
+ )} + + {/* OTP Field */} +
+ +
+ handleOtpChange("otp", value)} + aria-invalid={!!errors.otp} + > + + + + + + + + + +
+ {errors.otp && ( +

{errors.otp}

+ )} +
+ + {/* Resend OTP */} +
+
+ {/* Submit Button */} + + + {/* Back to signup */} +
+ +
+ )} ); -} \ No newline at end of file +} diff --git a/app/(auth)/signup/page.tsx b/app/(auth)/signup/page.tsx index 898cd16..ca29c90 100644 --- a/app/(auth)/signup/page.tsx +++ b/app/(auth)/signup/page.tsx @@ -69,13 +69,21 @@ export default function Signup() { try { 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."); - setRegisteredEmail(formData.email); - setOtpData({ email: formData.email, otp: "" }); - setStep("verify"); + // Redirect to login page with verify step + router.push(`/login?verify=true&email=${encodeURIComponent(formData.email)}`); } catch (error) { 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); // Don't set field errors for server errors, only show toast setErrors({}); @@ -105,9 +113,9 @@ export default function Signup() { // If verification is successful (no error thrown), show success and redirect 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(() => { - router.push("/login"); + router.push(`/login?email=${encodeURIComponent(otpData.email)}`); }, 1500); } catch (error) { const errorMessage = error instanceof Error ? error.message : "OTP verification failed. Please try again."; diff --git a/app/(pages)/book-now/page.tsx b/app/(pages)/book-now/page.tsx index 07e50e0..b8528af 100644 --- a/app/(pages)/book-now/page.tsx +++ b/app/(pages)/book-now/page.tsx @@ -23,11 +23,14 @@ import { CheckCircle2, CheckCircle, Loader2, + LogOut, } from "lucide-react"; import Link from "next/link"; import Image from "next/image"; import { useRouter } from "next/navigation"; import { LoginDialog } from "@/components/LoginDialog"; +import { useAuth } from "@/hooks/useAuth"; +import { toast } from "sonner"; interface User { ID: number; @@ -73,6 +76,7 @@ export default function BookNowPage() { const router = useRouter(); const { theme } = useAppTheme(); const isDark = theme === "dark"; + const { isAuthenticated, logout } = useAuth(); const [formData, setFormData] = useState({ firstName: "", lastName: "", @@ -87,6 +91,12 @@ export default function BookNowPage() { const [error, setError] = useState(null); const [showLoginDialog, setShowLoginDialog] = useState(false); + const handleLogout = () => { + logout(); + toast.success("Logged out successfully"); + router.push("/"); + }; + // Handle submit button click const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -660,6 +670,20 @@ export default function BookNowPage() {

+ + {/* Logout Button - Only show when authenticated */} + {isAuthenticated && ( +
+ +
+ )} )} diff --git a/components/Navbar.tsx b/components/Navbar.tsx index a3a55a2..dcd863a 100644 --- a/components/Navbar.tsx +++ b/components/Navbar.tsx @@ -2,13 +2,15 @@ import { motion, AnimatePresence } from "framer-motion"; 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 { useEffect, useState } from "react"; import { LoginDialog } from "@/components/LoginDialog"; import { useRouter, usePathname } from "next/navigation"; import Link from "next/link"; import { useAppTheme } from "@/components/ThemeProvider"; +import { useAuth } from "@/hooks/useAuth"; +import { toast } from "sonner"; export function Navbar() { const { theme } = useAppTheme(); @@ -18,6 +20,9 @@ export function Navbar() { const router = useRouter(); const pathname = usePathname(); 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 element = document.getElementById(id); @@ -33,6 +38,13 @@ export function Navbar() { setMobileMenuOpen(false); }; + const handleLogout = () => { + logout(); + toast.success("Logged out successfully"); + setMobileMenuOpen(false); + router.push("/"); + }; + // Close mobile menu when clicking outside useEffect(() => { if (mobileMenuOpen) { @@ -73,7 +85,7 @@ export function Navbar() { {/* Desktop Navigation */} - {!isUserDashboard && ( + {!isUserRoute && (
+ {!isUserDashboard && ( + + Book-Now + + )} + {isAuthenticated && ( + + )}
{/* Mobile Actions */} @@ -161,7 +195,7 @@ export function Navbar() { >
{/* Mobile Navigation Links */} - {!isUserDashboard && ( + {!isUserRoute && ( <> + )} + {isAuthenticated && ( + + )}
diff --git a/hooks/useAuth.ts b/hooks/useAuth.ts index 160e42c..cabc484 100644 --- a/hooks/useAuth.ts +++ b/hooks/useAuth.ts @@ -43,8 +43,14 @@ export function useAuth() { // Check if user is authenticated const isAuthenticated = !!user && !!getStoredTokens().access; - // Check if user is admin (check both is_admin and isAdmin) - const isAdmin = user?.is_admin === true || (user as any)?.isAdmin === true; + // Check if user is admin (check multiple possible field names) + 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 const loginMutation = useMutation({ @@ -109,8 +115,8 @@ export function useAuth() { const logout = useCallback(() => { clearAuthData(); queryClient.clear(); - router.push("/login"); - }, [queryClient, router]); + // Don't redirect here - let components handle redirect as needed + }, [queryClient]); // Login function const login = useCallback( diff --git a/lib/actions/auth.ts b/lib/actions/auth.ts index a97aa4f..93a0bf2 100644 --- a/lib/actions/auth.ts +++ b/lib/actions/auth.ts @@ -76,6 +76,28 @@ async function handleResponse(response: Response): Promise { 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 export async function registerUser(input: RegisterInput): Promise { const response = await fetch(API_ENDPOINTS.auth.register, { @@ -86,6 +108,29 @@ export async function registerUser(input: RegisterInput): Promise 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(response); } @@ -100,23 +145,7 @@ export async function verifyOtp(input: VerifyOtpInput): Promise { }); const data = await handleResponse(response); - - // 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; + return normalizeAuthResponse(data); } // Login user @@ -130,23 +159,7 @@ export async function loginUser(input: LoginInput): Promise { }); const data = await handleResponse(response); - - // 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; + return normalizeAuthResponse(data); } // Resend OTP @@ -245,9 +258,7 @@ export function storeUser(user: User): void { if (typeof window === "undefined") return; localStorage.setItem("auth_user", JSON.stringify(user)); - - // Also set cookie for middleware - document.cookie = `auth_user=${JSON.stringify(user)}; path=/; max-age=${7 * 24 * 60 * 60}; SameSite=Lax`; + document.cookie = `auth_user=${encodeURIComponent(JSON.stringify(user))}; path=/; max-age=${7 * 24 * 60 * 60}; SameSite=Lax`; } // Get stored user diff --git a/lib/models/auth.ts b/lib/models/auth.ts index 6c7921e..2af43b9 100644 --- a/lib/models/auth.ts +++ b/lib/models/auth.ts @@ -11,9 +11,17 @@ export interface User { last_name: string; phone_number?: string; 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; isVerified?: boolean; // API uses camelCase + is_active?: boolean; + isActive?: boolean; // API uses camelCase date_joined?: string; + last_login?: string; created_at?: string; updated_at?: string; } diff --git a/middleware.ts b/middleware.ts index 4c2bfa4..630a3ac 100644 --- a/middleware.ts +++ b/middleware.ts @@ -13,16 +13,31 @@ export function middleware(request: NextRequest) { if (userStr) { try { - const user = JSON.parse(userStr); - isAdmin = user.is_admin === true; + // Decode the user string if it's URL encoded + 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 { - // Invalid user data + // Invalid user data - silently fail and treat as non-admin } } // Protected routes const isProtectedRoute = pathname.startsWith("/user") || pathname.startsWith("/admin"); const isAdminRoute = pathname.startsWith("/admin"); + const isUserRoute = pathname.startsWith("/user"); const isAuthRoute = pathname.startsWith("/login") || pathname.startsWith("/signup"); // Redirect unauthenticated users away from protected routes @@ -34,12 +49,19 @@ export function middleware(request: NextRequest) { // Redirect authenticated users away from auth routes 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)); } // Redirect non-admin users away from admin routes - if (isAdminRoute && (!isAuthenticated || !isAdmin)) { - return NextResponse.redirect(new URL("/admin/dashboard", request.url)); + if (isAdminRoute && isAuthenticated && !isAdmin) { + return NextResponse.redirect(new URL("/user/dashboard", request.url)); } return NextResponse.next();