refactor: streamline logout process by integrating next-auth's signOut and a dedicated server action for cookie management.
This commit is contained in:
@@ -1,21 +1,23 @@
|
|||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
import { cookies } from 'next/headers';
|
import { cookies } from 'next/headers';
|
||||||
import { redirect } from 'next/navigation';
|
|
||||||
|
|
||||||
export async function logoutAction() {
|
export async function clearAuthCookies() {
|
||||||
// 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();
|
const cookieStore = await cookies();
|
||||||
|
|
||||||
// Delete all possible next-auth v5 cookie names
|
// Delete all next-auth v5 cookie variants
|
||||||
cookieStore.delete('authjs.session-token');
|
const cookieNames = [
|
||||||
cookieStore.delete('authjs.csrf-token');
|
'authjs.session-token',
|
||||||
cookieStore.delete('authjs.callback-url');
|
'authjs.csrf-token',
|
||||||
cookieStore.delete('__Secure-authjs.session-token');
|
'authjs.callback-url',
|
||||||
cookieStore.delete('__Secure-authjs.csrf-token');
|
'__Secure-authjs.session-token',
|
||||||
cookieStore.delete('__Secure-authjs.callback-url');
|
'__Secure-authjs.csrf-token',
|
||||||
|
'__Secure-authjs.callback-url',
|
||||||
|
];
|
||||||
|
|
||||||
redirect('/login');
|
for (const name of cookieNames) {
|
||||||
|
cookieStore.delete(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { logoutAction } from './actions';
|
import { clearAuthCookies } from './actions';
|
||||||
|
import { signOut } from 'next-auth/react';
|
||||||
|
|
||||||
export default function LogoutPage() {
|
export default function LogoutPage() {
|
||||||
const hasStarted = useRef(false);
|
const hasStarted = useRef(false);
|
||||||
@@ -11,79 +12,26 @@ export default function LogoutPage() {
|
|||||||
hasStarted.current = true;
|
hasStarted.current = true;
|
||||||
|
|
||||||
const performLogout = async () => {
|
const performLogout = async () => {
|
||||||
// Set logout flag to prevent TokenSync/PresenceProvider from restoring tokens
|
// Clear localStorage
|
||||||
localStorage.setItem('isLoggingOut', 'true');
|
localStorage.setItem('isLoggingOut', 'true');
|
||||||
|
|
||||||
// Invalidate refresh token on the backend (best-effort)
|
|
||||||
const refreshToken = localStorage.getItem('refreshToken');
|
|
||||||
const accessToken = localStorage.getItem('accessToken');
|
|
||||||
if (refreshToken) {
|
|
||||||
try {
|
|
||||||
await fetch(
|
|
||||||
`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001/api/v1'}/auth/logout`,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ refreshToken }),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
// Best-effort — don't block logout if backend call fails
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear all localStorage auth data
|
|
||||||
localStorage.removeItem('accessToken');
|
localStorage.removeItem('accessToken');
|
||||||
localStorage.removeItem('refreshToken');
|
localStorage.removeItem('refreshToken');
|
||||||
localStorage.removeItem('user');
|
localStorage.removeItem('user');
|
||||||
|
|
||||||
// Manually clear non-httpOnly next-auth cookies as fallback
|
await signOut({ redirect: false })
|
||||||
const cookieNames = [
|
// Delete httpOnly cookies via server action (Next.js cookies() API)
|
||||||
'authjs.session-token',
|
await clearAuthCookies();
|
||||||
'authjs.callback-url',
|
|
||||||
'authjs.csrf-token',
|
|
||||||
'__Secure-authjs.session-token',
|
|
||||||
'__Secure-authjs.callback-url',
|
|
||||||
'__Secure-authjs.csrf-token',
|
|
||||||
'next-auth.session-token',
|
|
||||||
'next-auth.callback-url',
|
|
||||||
'next-auth.csrf-token',
|
|
||||||
'__Secure-next-auth.session-token',
|
|
||||||
];
|
|
||||||
cookieNames.forEach((name) => {
|
|
||||||
document.cookie = `${name}=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT`;
|
|
||||||
document.cookie = `${name}=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Secure`;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Use server action to properly clear the httpOnly session cookie.
|
setTimeout(() => {
|
||||||
// signOut() internally calls redirect() which throws NEXT_REDIRECT —
|
// Redirect to login
|
||||||
// Next.js handles this automatically. We should NOT catch that error.
|
window.location.reload()
|
||||||
// Only use fallback if the action truly fails (not a redirect throw).
|
|
||||||
try {
|
|
||||||
await logoutAction();
|
|
||||||
} 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';
|
window.location.href = '/login';
|
||||||
}
|
}, 3000)
|
||||||
};
|
};
|
||||||
|
|
||||||
performLogout();
|
performLogout().catch(() => {
|
||||||
|
window.location.href = '/login';
|
||||||
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -280,12 +280,6 @@ export function CommonHeader() {
|
|||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowProfileMenu(false);
|
setShowProfileMenu(false);
|
||||||
// Set logout flag FIRST so TokenSync and PresenceProvider
|
|
||||||
// don't restore tokens during the logout navigation
|
|
||||||
localStorage.setItem('isLoggingOut', 'true');
|
|
||||||
localStorage.removeItem('accessToken');
|
|
||||||
localStorage.removeItem('refreshToken');
|
|
||||||
localStorage.removeItem('user');
|
|
||||||
window.location.href = '/logout';
|
window.location.href = '/logout';
|
||||||
}}
|
}}
|
||||||
className="w-full flex items-center gap-3 px-4 py-3 hover:bg-black/5 transition-colors"
|
className="w-full flex items-center gap-3 px-4 py-3 hover:bg-black/5 transition-colors"
|
||||||
|
|||||||
@@ -34,17 +34,12 @@ export const resetLogoutState = () => {
|
|||||||
isLoggingOut = false;
|
isLoggingOut = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Force logout: clear all tokens and redirect to logout page
|
// Force logout: redirect to /logout page which handles everything
|
||||||
const forceLogout = () => {
|
const forceLogout = () => {
|
||||||
if (isLoggingOut) return; // Prevent multiple redirects
|
if (isLoggingOut) return;
|
||||||
isLoggingOut = true;
|
isLoggingOut = true;
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
// Set flag first so TokenSync/PresenceProvider don't restore tokens
|
|
||||||
localStorage.setItem('isLoggingOut', 'true');
|
localStorage.setItem('isLoggingOut', 'true');
|
||||||
localStorage.removeItem('accessToken');
|
|
||||||
localStorage.removeItem('refreshToken');
|
|
||||||
localStorage.removeItem('user');
|
|
||||||
// Redirect to /logout page which properly clears NextAuth session
|
|
||||||
window.location.href = '/logout';
|
window.location.href = '/logout';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user