@@ -59,7 +28,6 @@ export default function AuthLayout({
);
}
- // Only render auth pages for unauthenticated users (no tokens, no session)
return (
diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx
index 5d953b3..018216e 100644
--- a/src/app/(auth)/login/page.tsx
+++ b/src/app/(auth)/login/page.tsx
@@ -4,6 +4,7 @@ import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { signIn } from 'next-auth/react';
import Link from 'next/link';
+import { resetLogoutState } from '@/services/api';
export default function LoginPage() {
const router = useRouter();
@@ -13,9 +14,12 @@ export default function LoginPage() {
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
- // Safety net: clear logout flag when login page loads
+ // Safety net: clear logout flags when login page loads.
+ // This resets both the localStorage flag and the module-level flag in api.ts
+ // so the API interceptor stops blocking requests.
if (typeof window !== 'undefined') {
localStorage.removeItem('isLoggingOut');
+ resetLogoutState();
}
const handleSubmit = async (e: React.FormEvent) => {
diff --git a/src/app/(user)/layout.tsx b/src/app/(user)/layout.tsx
index 349b8a8..6cb7728 100644
--- a/src/app/(user)/layout.tsx
+++ b/src/app/(user)/layout.tsx
@@ -2,7 +2,7 @@
import { useSession } from 'next-auth/react';
import { useRouter, usePathname } from 'next/navigation';
-import { useEffect, useState } from 'react';
+import { useEffect } from 'react';
import { Footer } from '@/components/layout/Footer';
import { CommonHeader } from '@/components/layout/CommonHeader';
import { PresenceProvider } from '@/components/providers/presence-provider';
@@ -27,16 +27,9 @@ export default function UserLayout({
const { data: session, status } = useSession();
const router = useRouter();
const pathname = usePathname();
- const [hasTokens, setHasTokens] = useState(false);
-
const isDashboard = pathname === '/user/dashboard';
const isPublic = isPublicPath(pathname);
- // Fast check for existing tokens on mount
- useEffect(() => {
- setHasTokens(!!localStorage.getItem('accessToken'));
- }, []);
-
useEffect(() => {
if (status === 'loading') return;
@@ -52,16 +45,11 @@ export default function UserLayout({
return;
}
- // Protected pages require login
- if (!session) {
- // Check localStorage - if tokens exist, session might still be resolving
- // Don't redirect to /login prematurely (prevents login page flash)
- const tokensExist = !!localStorage.getItem('accessToken');
- if (!tokensExist) {
- router.replace('/login');
- }
- return;
- }
+ // Don't redirect to /login here for protected pages — the middleware handles that.
+ // Doing it here causes race conditions during logout (layout detects session loss
+ // and redirects to /login via client-side navigation, conflicting with server-side
+ // logout redirect).
+ if (!session) return;
// Redirect agents to agent dashboard for protected user pages
const userRole = (session.user as any)?.role;
diff --git a/src/app/logout/actions.ts b/src/app/logout/actions.ts
index 06510c5..bc09ca2 100644
--- a/src/app/logout/actions.ts
+++ b/src/app/logout/actions.ts
@@ -1,7 +1,21 @@
'use server';
-import { signOut } from '@/auth';
+import { cookies } from 'next/headers';
+import { redirect } from 'next/navigation';
export async function logoutAction() {
- await signOut({ redirectTo: '/login' });
+ // Force-delete all next-auth session cookies directly.
+ // signOut() from next-auth was NOT clearing the httpOnly cookie reliably,
+ // so we delete them explicitly using the cookies() API.
+ const cookieStore = await cookies();
+
+ // Delete all possible next-auth v5 cookie names
+ cookieStore.delete('authjs.session-token');
+ cookieStore.delete('authjs.csrf-token');
+ cookieStore.delete('authjs.callback-url');
+ cookieStore.delete('__Secure-authjs.session-token');
+ cookieStore.delete('__Secure-authjs.csrf-token');
+ cookieStore.delete('__Secure-authjs.callback-url');
+
+ redirect('/login');
}
diff --git a/src/app/logout/page.tsx b/src/app/logout/page.tsx
index 10cc0c3..28d8bb2 100644
--- a/src/app/logout/page.tsx
+++ b/src/app/logout/page.tsx
@@ -40,8 +40,7 @@ export default function LogoutPage() {
localStorage.removeItem('refreshToken');
localStorage.removeItem('user');
- // Manually clear next-auth cookies as fallback
- // next-auth v5 uses these cookie names:
+ // Manually clear non-httpOnly next-auth cookies as fallback
const cookieNames = [
'authjs.session-token',
'authjs.callback-url',
@@ -59,13 +58,27 @@ export default function LogoutPage() {
document.cookie = `${name}=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Secure`;
});
- // Use server action to properly clear session (redirects to /login)
+ // Use server action to properly clear the httpOnly session cookie.
+ // signOut() internally calls redirect() which throws NEXT_REDIRECT —
+ // Next.js handles this automatically. We should NOT catch that error.
+ // Only use fallback if the action truly fails (not a redirect throw).
try {
await logoutAction();
- } catch {
- // Server action redirect throws in Next.js — this is expected.
- // If it truly fails, the manual cookie clearing above ensures logout.
- // Fallback redirect:
+ } catch (error: unknown) {
+ // In Next.js App Router, redirect() throws an error with digest containing "NEXT_REDIRECT".
+ // This is normal behavior — let it propagate so Next.js handles the redirect.
+ const isRedirectError =
+ error instanceof Error &&
+ 'digest' in error &&
+ typeof (error as { digest?: string }).digest === 'string' &&
+ (error as { digest: string }).digest.includes('NEXT_REDIRECT');
+
+ if (isRedirectError) {
+ // Re-throw so Next.js handles the redirect properly
+ throw error;
+ }
+
+ // Genuine error — use full page navigation as fallback
window.location.href = '/login';
}
};
diff --git a/src/components/providers/session-provider.tsx b/src/components/providers/session-provider.tsx
index d5b1c26..572740f 100644
--- a/src/components/providers/session-provider.tsx
+++ b/src/components/providers/session-provider.tsx
@@ -9,10 +9,11 @@ function TokenSync() {
const hasInitialized = useRef(false);
useEffect(() => {
- // Never restore tokens if a logout is in progress
+ // Never restore tokens if a logout is in progress or we're on the logout page
const loggingOut = localStorage.getItem("isLoggingOut") === "true";
+ const onLogoutPage = window.location.pathname === "/logout";
- if (status === "authenticated" && session?.user && !loggingOut) {
+ if (status === "authenticated" && session?.user && !loggingOut && !onLogoutPage) {
const user = session.user as { accessToken?: string; refreshToken?: string };
// Only sync tokens from NextAuth to localStorage if:
diff --git a/src/middleware.ts b/src/middleware.ts
index 39552d6..62954b8 100644
--- a/src/middleware.ts
+++ b/src/middleware.ts
@@ -7,6 +7,9 @@ const authRoutes = ["/login", "/signup", "/forgot-password", "/reset-password",
// Public routes - accessible to everyone (logged in or not)
const publicRoutes = ["/", "/contact", "/about", "/faq", "/education", "/logout", "/user/dashboard", "/user/profiles", "/user/profile"];
+// Routes that should NEVER be redirected away from (even if logged in)
+const noRedirectRoutes = ["/logout"];
+
export default auth((req) => {
const { nextUrl } = req;
const isLoggedIn = !!req.auth;
@@ -28,11 +31,20 @@ export default auth((req) => {
nextUrl.pathname.startsWith("/assets") ||
nextUrl.pathname.includes(".");
+ const isNoRedirectRoute = noRedirectRoutes.some(
+ (route) => nextUrl.pathname === route || nextUrl.pathname.startsWith(route + "/")
+ );
+
// Skip middleware for API routes and static files
if (isApiRoute || isStaticRoute) {
return NextResponse.next();
}
+ // Never redirect away from no-redirect routes (e.g., /logout must always be accessible)
+ if (isNoRedirectRoute) {
+ return NextResponse.next();
+ }
+
// Allow access to public routes for everyone
if (isPublicRoute) {
return NextResponse.next();
diff --git a/src/services/api.ts b/src/services/api.ts
index 64cde76..6637e01 100644
--- a/src/services/api.ts
+++ b/src/services/api.ts
@@ -28,6 +28,12 @@ let failedQueue: Array<{
config: InternalAxiosRequestConfig;
}> = [];
+// Reset the logging-out state. Call this when starting a new login session
+// to ensure the API interceptor is no longer blocking requests.
+export const resetLogoutState = () => {
+ isLoggingOut = false;
+};
+
// Force logout: clear all tokens and redirect to logout page
const forceLogout = () => {
if (isLoggingOut) return; // Prevent multiple redirects