feat: Centralize authentication redirects to middleware, remove client-side redirect logic, and refine logout process.
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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]">
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user