feat: Centralize authentication redirects to middleware, remove client-side redirect logic, and refine logout process.

This commit is contained in:
pradeepkumar
2026-03-10 09:28:19 +05:30
parent 69eff70fbb
commit c7f1b89203
9 changed files with 77 additions and 70 deletions

View File

@@ -18,10 +18,11 @@ export default function AgentLayout({
useEffect(() => {
if (status === 'loading') return;
if (!session) {
router.replace('/login');
return;
}
// Don't redirect to /login here — the middleware handles that.
// Doing it here causes race conditions during logout (layout detects
// session loss and redirects to /login via client-side navigation,
// which can conflict with the server-side logout redirect).
if (!session) return;
// Redirect non-agents to user dashboard
const userRole = (session.user as any)?.role;

View File

@@ -1,49 +1,18 @@
'use client';
import { useSession } from 'next-auth/react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
export default function AuthLayout({
children,
}: {
children: React.ReactNode;
}) {
const { data: session, status } = useSession();
const router = useRouter();
const [hasTokens, setHasTokens] = useState(false);
const { status } = useSession();
// Fast synchronous check for existing tokens on mount
// If tokens exist, the user is likely authenticated and will be redirected
useEffect(() => {
setHasTokens(!!localStorage.getItem('accessToken'));
}, []);
// Middleware handles redirecting logged-in users away from auth pages.
// Do NOT add client-side redirect logic here — it races with logout
// and causes login/logout loops.
useEffect(() => {
if (status === 'loading') return;
// Redirect authenticated users away from auth pages
if (session) {
const userRole = (session.user as any)?.role;
if (userRole === 'AGENT') {
router.replace('/agent/dashboard');
} else {
router.replace('/user/dashboard');
}
}
}, [session, status, router]);
// If user has tokens in localStorage, they are likely authenticated
// Show a blank white screen to prevent flash of login page during redirect
if (hasTokens || session) {
return (
<div className="min-h-screen bg-white flex items-center justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-[#00293d]"></div>
</div>
);
}
// Show loading spinner while checking authentication (no tokens = genuinely unauthenticated)
if (status === 'loading') {
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-b from-[#c4d9d4] to-[#f0f5fc]">
@@ -59,7 +28,6 @@ export default function AuthLayout({
);
}
// Only render auth pages for unauthenticated users (no tokens, no session)
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-b from-[#c4d9d4] to-[#f0f5fc] py-12 px-4">
<div className="w-full max-w-[510px]">

View File

@@ -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) => {

View File

@@ -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;

View File

@@ -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');
}

View File

@@ -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';
}
};

View File

@@ -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:

View File

@@ -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();

View File

@@ -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