From 041c36079dbc2ff625d1a3d61d8e8fc1aef79315 Mon Sep 17 00:00:00 2001
From: iamkiddy
Date: Sun, 23 Nov 2025 21:13:18 +0000
Subject: [PATCH] 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.
---
app/(admin)/_components/header.tsx | 15 +-
app/(admin)/_components/side-nav.tsx | 15 +-
app/(auth)/login/page.tsx | 660 ++++++++++++++++++++++++---
app/(auth)/signup/page.tsx | 20 +-
app/(pages)/book-now/page.tsx | 24 +
components/Navbar.tsx | 83 +++-
hooks/useAuth.ts | 14 +-
lib/actions/auth.ts | 85 ++--
lib/models/auth.ts | 8 +
middleware.ts | 32 +-
10 files changed, 829 insertions(+), 127 deletions(-)
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() {
+
+ {/* Logout Button - Only show when authenticated */}
+ {isAuthenticated && (
+
+
+
+ Logout
+
+
+ )}
>
)}
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 && (
scrollToSection("about")}
@@ -98,7 +110,7 @@ export function Navbar() {
{/* Desktop Actions */}
- {!isUserDashboard && (
+ {!isAuthenticated && !isUserDashboard && (
)}
-
- Book Now
-
+ {!isUserDashboard && (
+
+ Book-Now
+
+ )}
+ {isAuthenticated && (
+
+
+ Logout
+
+ )}
{/* Mobile Actions */}
@@ -161,7 +195,7 @@ export function Navbar() {
>
{/* Mobile Navigation Links */}
- {!isUserDashboard && (
+ {!isUserRoute && (
<>
scrollToSection("about")}
@@ -185,7 +219,7 @@ export function Navbar() {
)}
- {!isUserDashboard && (
+ {!isAuthenticated && !isUserDashboard && (
)}
-
- setMobileMenuOpen(false)}>
- Book Now
+ {!isUserDashboard && (
+ 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
-
+ )}
+ {isAuthenticated && (
+ {
+ handleLogout();
+ }}
+ >
+
+ Logout
+
+ )}
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();