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 */}
-
})
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 */
+
!})
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,
});