From c765ea9548c52921a80eceb5b407079391b1d821 Mon Sep 17 00:00:00 2001 From: pradeepkumar Date: Wed, 8 Apr 2026 11:04:29 +0530 Subject: [PATCH] refactor: standardize auth cookie configuration and improve logout reliability by explicitly expiring session tokens --- src/app/(agent)/agent/dashboard/page.tsx | 50 ++++++++++----------- src/app/logout/actions.ts | 57 +++++++++++++++++++----- src/app/logout/page.tsx | 29 +++++++----- src/auth.ts | 30 +++++++++++++ 4 files changed, 116 insertions(+), 50 deletions(-) diff --git a/src/app/(agent)/agent/dashboard/page.tsx b/src/app/(agent)/agent/dashboard/page.tsx index 875aec2..53f2342 100644 --- a/src/app/(agent)/agent/dashboard/page.tsx +++ b/src/app/(agent)/agent/dashboard/page.tsx @@ -84,8 +84,6 @@ export default function AgentDashboard() { const [testimonials, setTestimonials] = useState<{ id: string; text: string; author: string; role: string; rating: number }[]>([]); const [isSubmittingVerification, setIsSubmittingVerification] = useState(false); - const defaultImage = '/assets/demo_images/8f017153a7b4a239a4f0691234ef97dd55092282.jpg'; - // Helper to check if avatar is an S3 key (not a full URL or local path) const isS3Key = (avatar: string | null | undefined): boolean => { if (!avatar) return false; @@ -220,20 +218,15 @@ export default function AgentDashboard() { window.dispatchEvent(new Event('profile-updated')); }; - const getProfileImageUrl = () => { - // If image failed to load, return default - if (imageError) { - return defaultImage; - } - + const getProfileImageUrl = (): string | null => { // If we have a presigned URL from S3, use it if (avatarUrl) { return avatarUrl; } - // Check for null, undefined, or empty string + // No avatar set if (!agentProfile?.avatar || agentProfile.avatar.trim() === '') { - return defaultImage; + return null; } // For relative paths (local assets), return as-is @@ -241,8 +234,8 @@ export default function AgentDashboard() { return agentProfile.avatar; } - // Default fallback - return defaultImage; + // No URL resolvable + return null; }; if (loading) { @@ -371,22 +364,25 @@ export default function AgentDashboard() { {/* Profile Image */}
- {/* Shimmer while loading, initials only on error */} - {imageError ? ( -
- {agentProfile?.firstName?.[0]?.toUpperCase() || '?'} -
- ) : !imageLoaded ? ( + {/* Initials fallback as base layer */} +
+ {agentProfile?.firstName?.[0]?.toUpperCase() || '?'} +
+ {/* Shimmer while loading (only if we have a real image to load) */} + {getProfileImageUrl() && !imageLoaded && !imageError && (
- ) : null} - {/* eslint-disable-next-line @next/next/no-img-element */} - Profile setImageLoaded(true)} - onError={() => setImageError(true)} - /> + )} + {/* Image overlay — only render if we have a URL */} + {getProfileImageUrl() && !imageError && ( + /* eslint-disable-next-line @next/next/no-img-element */ + Profile setImageLoaded(true)} + onError={() => setImageError(true)} + /> + )} {/* Gradient Overlay */}
diff --git a/src/app/logout/actions.ts b/src/app/logout/actions.ts index 491a412..08a983c 100644 --- a/src/app/logout/actions.ts +++ b/src/app/logout/actions.ts @@ -5,8 +5,7 @@ import { cookies } from 'next/headers'; export async function clearAuthCookies() { const cookieStore = await cookies(); - // Delete all next-auth v5 cookie variants - // Must specify path and secure options to match how they were set + // All possible NextAuth cookie names (v4 + v5, secure + host prefixed) const cookieNames = [ 'authjs.session-token', 'authjs.csrf-token', @@ -14,27 +13,46 @@ export async function clearAuthCookies() { '__Secure-authjs.session-token', '__Secure-authjs.csrf-token', '__Secure-authjs.callback-url', - // next-auth v4 fallback names 'next-auth.session-token', 'next-auth.csrf-token', 'next-auth.callback-url', '__Secure-next-auth.session-token', '__Secure-next-auth.csrf-token', '__Secure-next-auth.callback-url', - // Host-prefixed variants (used when behind a proxy) '__Host-authjs.csrf-token', '__Host-next-auth.csrf-token', ]; + // Aggressively expire each cookie by setting empty value + maxAge: 0 + // This works better than .delete() in production behind load balancers for (const name of cookieNames) { - // Delete with explicit options to ensure cookie is actually removed - cookieStore.delete({ - name, - path: '/', - }); + try { + cookieStore.set({ + name, + value: '', + path: '/', + maxAge: 0, + httpOnly: true, + secure: true, + sameSite: 'lax', + }); + } catch { + // Some Secure cookies cannot be set in non-HTTPS environments + } + // Also try non-secure version + try { + cookieStore.set({ + name, + value: '', + path: '/', + maxAge: 0, + }); + } catch { + // ignore + } } - // Also try getting all cookies and deleting any auth-related ones + // Sweep: any remaining cookies that look auth-related → expire them const allCookies = cookieStore.getAll(); for (const cookie of allCookies) { if ( @@ -42,7 +60,24 @@ export async function clearAuthCookies() { cookie.name.includes('next-auth') || cookie.name.includes('session-token') ) { - cookieStore.delete({ name: cookie.name, path: '/' }); + try { + cookieStore.set({ + name: cookie.name, + value: '', + path: '/', + maxAge: 0, + httpOnly: true, + secure: true, + sameSite: 'lax', + }); + } catch { + // ignore + } + try { + cookieStore.set({ name: cookie.name, value: '', path: '/', maxAge: 0 }); + } catch { + // ignore + } } } diff --git a/src/app/logout/page.tsx b/src/app/logout/page.tsx index 9e090cb..3b43224 100644 --- a/src/app/logout/page.tsx +++ b/src/app/logout/page.tsx @@ -33,25 +33,30 @@ export default function LogoutPage() { localStorage.removeItem('refreshToken'); localStorage.removeItem('user'); - // Delete httpOnly cookies via server action FIRST - // This ensures the session cookie is gone before signOut triggers session checks - await clearAuthCookies(); + // Call NextAuth signOut — canonical way to clear session cookie + // (uses the exact cookie name/attributes from cookies config in auth.ts) + try { + await signOut({ redirect: false }); + } catch { + // Continue even if signOut fails + } - // Client-side signOut to clear NextAuth client state - await signOut({ redirect: false }); + // Belt-and-suspenders: server action to nuke any remaining auth cookies + try { + await clearAuthCookies(); + } catch { + // Continue + } - // Delete cookies again after signOut in case signOut recreated any - await clearAuthCookies(); - - // Clear the logout flag before redirecting so it doesn't block API calls on public pages + // Clear the logout flag before redirecting localStorage.removeItem('isLoggingOut'); - // Redirect to login immediately - window.location.href = '/'; + // Hard reload (replace, not push) so all in-memory state and cached responses are flushed + window.location.replace('/'); }; performLogout().catch(() => { - window.location.href = '/'; + window.location.replace('/'); }); }, []); diff --git a/src/auth.ts b/src/auth.ts index 99dac57..3289c95 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -240,5 +240,35 @@ export const { handlers, signIn, signOut, auth } = NextAuth({ maxAge: 7 * 24 * 60 * 60, // 7 days }, + // Explicit cookie configuration so we can reliably clear them on logout + cookies: { + sessionToken: { + name: process.env.NODE_ENV === 'production' ? '__Secure-authjs.session-token' : 'authjs.session-token', + options: { + httpOnly: true, + sameSite: 'lax', + path: '/', + secure: process.env.NODE_ENV === 'production', + }, + }, + callbackUrl: { + name: process.env.NODE_ENV === 'production' ? '__Secure-authjs.callback-url' : 'authjs.callback-url', + options: { + sameSite: 'lax', + path: '/', + secure: process.env.NODE_ENV === 'production', + }, + }, + csrfToken: { + name: process.env.NODE_ENV === 'production' ? '__Host-authjs.csrf-token' : 'authjs.csrf-token', + options: { + httpOnly: true, + sameSite: 'lax', + path: '/', + secure: process.env.NODE_ENV === 'production', + }, + }, + }, + secret: process.env.NEXTAUTH_SECRET, });