282 lines
10 KiB
TypeScript
282 lines
10 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useRef, useState } from 'react';
|
|
import { useSearchParams } from 'next/navigation';
|
|
import Link from 'next/link';
|
|
|
|
export default function LoggedOutPage() {
|
|
const [sessionStatus, setSessionStatus] = useState<'checking' | 'cleared' | 'error'>('checking');
|
|
const [message, setMessage] = useState<string>('');
|
|
const [keycloakLogoutUrl, setKeycloakLogoutUrl] = useState<string | null>(null);
|
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
const searchParams = useSearchParams();
|
|
const preserveSso = searchParams.get('preserveSso') === 'true';
|
|
|
|
useEffect(() => {
|
|
// Listen for messages from iframes
|
|
const handleMessage = (event: MessageEvent) => {
|
|
if (event.data && event.data.type === 'AUTH_STATUS') {
|
|
console.log('Received auth status message:', event.data);
|
|
}
|
|
};
|
|
|
|
window.addEventListener('message', handleMessage);
|
|
|
|
const clearSessions = async () => {
|
|
try {
|
|
console.log(`Clearing sessions (preserveSso: ${preserveSso})`);
|
|
|
|
// Try to get any stored user IDs for server-side cleanup
|
|
let userId = null;
|
|
try {
|
|
// Check localStorage first
|
|
userId = localStorage.getItem('userId') || sessionStorage.getItem('userId');
|
|
|
|
// Try to get from sessionStorage as fallback
|
|
if (!userId) {
|
|
const sessionData = sessionStorage.getItem('nextauth.session-token');
|
|
if (sessionData) {
|
|
try {
|
|
const parsed = JSON.parse(atob(sessionData.split('.')[1]));
|
|
userId = parsed.sub || parsed.id;
|
|
} catch (e) {
|
|
console.error('Failed to parse session data:', e);
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error('Error accessing storage:', e);
|
|
}
|
|
|
|
// If we found a user ID, call server-side cleanup
|
|
if (userId) {
|
|
console.log(`Found user ID: ${userId}, cleaning up server-side`);
|
|
try {
|
|
const response = await fetch('/api/auth/session-cleanup', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
userId,
|
|
preserveSso
|
|
}),
|
|
credentials: 'include'
|
|
});
|
|
|
|
const result = await response.json();
|
|
console.log('Server cleanup result:', result);
|
|
} catch (e) {
|
|
console.error('Error during server-side cleanup:', e);
|
|
}
|
|
}
|
|
|
|
// If not preserving SSO, get Keycloak logout URL
|
|
if (!preserveSso) {
|
|
try {
|
|
const response = await fetch('/api/auth/full-logout', {
|
|
credentials: 'include'
|
|
});
|
|
const data = await response.json();
|
|
if (data.success && data.logoutUrl) {
|
|
setKeycloakLogoutUrl(data.logoutUrl);
|
|
console.log('Keycloak logout URL:', data.logoutUrl);
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to get Keycloak logout URL:', e);
|
|
}
|
|
}
|
|
|
|
// Clear cookies appropriately based on preserve SSO setting
|
|
const cookies = document.cookie.split(';');
|
|
|
|
// Get all cookies names
|
|
const cookieNames = cookies.map(cookie => cookie.split('=')[0].trim());
|
|
|
|
// Find chunked cookies
|
|
const chunkedCookies = cookieNames
|
|
.filter(name => /next-auth.*\.\d+$/.test(name));
|
|
|
|
// Define cookies to clear
|
|
let cookiesToClear = [];
|
|
|
|
if (preserveSso) {
|
|
// Only clear app-specific cookies if preserving SSO
|
|
cookiesToClear = [
|
|
'next-auth.session-token',
|
|
'next-auth.csrf-token',
|
|
'next-auth.callback-url',
|
|
'__Secure-next-auth.session-token',
|
|
'__Host-next-auth.csrf-token',
|
|
...chunkedCookies
|
|
];
|
|
} else {
|
|
// Clear ALL auth-related cookies for full logout
|
|
const authCookies = cookieNames.filter(name =>
|
|
name.includes('auth') ||
|
|
name.includes('KEYCLOAK') ||
|
|
name.includes('KC_') ||
|
|
name.includes('session')
|
|
);
|
|
|
|
cookiesToClear = [
|
|
...authCookies,
|
|
...chunkedCookies,
|
|
'JSESSIONID',
|
|
'KEYCLOAK_SESSION',
|
|
'KEYCLOAK_IDENTITY',
|
|
'KC_RESTART'
|
|
];
|
|
}
|
|
|
|
// Clear the cookies with all possible path and domain combinations
|
|
const hostname = window.location.hostname;
|
|
const baseDomain = hostname.split('.').slice(-2).join('.');
|
|
|
|
cookiesToClear.forEach(cookieName => {
|
|
// Try various path and domain combinations to ensure complete cleanup
|
|
document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
|
|
document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=${hostname};`;
|
|
|
|
// Only try this for multi-part domains
|
|
if (hostname !== baseDomain) {
|
|
document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=.${baseDomain};`;
|
|
document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=.${hostname};`;
|
|
}
|
|
|
|
// Add secure and SameSite attributes
|
|
document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; SameSite=None; Secure;`;
|
|
});
|
|
|
|
// Clear session storage
|
|
try {
|
|
sessionStorage.clear();
|
|
} catch (e) {
|
|
console.error('Failed to clear sessionStorage:', e);
|
|
}
|
|
|
|
// Clear auth-related localStorage items
|
|
try {
|
|
localStorage.removeItem('userId');
|
|
localStorage.removeItem('userName');
|
|
localStorage.removeItem('userEmail');
|
|
localStorage.removeItem('token');
|
|
localStorage.removeItem('refreshToken');
|
|
|
|
// Rocket Chat items
|
|
localStorage.removeItem('Meteor.loginToken');
|
|
localStorage.removeItem('Meteor.userId');
|
|
} catch (e) {
|
|
console.error('Failed to clear localStorage items:', e);
|
|
}
|
|
|
|
// Notify parent window if we're in an iframe
|
|
if (window !== window.parent) {
|
|
try {
|
|
window.parent.postMessage({
|
|
type: 'AUTH_STATUS',
|
|
status: 'LOGGED_OUT',
|
|
preserveSso
|
|
}, '*');
|
|
} catch (e) {
|
|
console.error('Failed to send logout message to parent:', e);
|
|
}
|
|
}
|
|
|
|
setSessionStatus('cleared');
|
|
setMessage(preserveSso
|
|
? 'You have been logged out of this application, but your SSO session is still active for other services.'
|
|
: 'You have been completely logged out of all services.'
|
|
);
|
|
} catch (error) {
|
|
console.error('Error during logout cleanup:', error);
|
|
setSessionStatus('error');
|
|
setMessage('There was an error during the logout process.');
|
|
}
|
|
};
|
|
|
|
clearSessions();
|
|
|
|
return () => {
|
|
window.removeEventListener('message', handleMessage);
|
|
};
|
|
}, [preserveSso]);
|
|
|
|
return (
|
|
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-50 p-4">
|
|
<div className="w-full max-w-md p-8 space-y-6 bg-white rounded-lg shadow-md">
|
|
<div className="text-center">
|
|
<h1 className="text-2xl font-bold text-gray-900 mb-2">
|
|
{preserveSso ? 'Application Sign Out' : 'Complete Sign Out'}
|
|
</h1>
|
|
|
|
{sessionStatus === 'checking' && (
|
|
<p className="text-gray-600">Cleaning up your session...</p>
|
|
)}
|
|
|
|
{sessionStatus === 'cleared' && (
|
|
<>
|
|
<p className="text-gray-600 mb-4">{message}</p>
|
|
|
|
{preserveSso ? (
|
|
<>
|
|
<p className="text-sm text-gray-500 mb-4">
|
|
You can continue using other applications without signing in again.
|
|
</p>
|
|
<div className="flex flex-col space-y-4">
|
|
<Link
|
|
href="/"
|
|
className="inline-block px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors"
|
|
>
|
|
Return to Home Page
|
|
</Link>
|
|
<button
|
|
onClick={() => window.location.href = '/loggedout'}
|
|
className="inline-block px-4 py-2 bg-gray-200 text-gray-800 rounded hover:bg-gray-300 transition-colors"
|
|
>
|
|
Sign Out Completely
|
|
</button>
|
|
</div>
|
|
</>
|
|
) : (
|
|
<>
|
|
<p className="text-sm text-gray-500 mb-4">
|
|
You will need to sign in again to access any protected services.
|
|
</p>
|
|
|
|
{keycloakLogoutUrl && (
|
|
<iframe
|
|
ref={iframeRef}
|
|
src={keycloakLogoutUrl}
|
|
style={{ width: '1px', height: '1px', position: 'absolute', top: '-100px', left: '-100px' }}
|
|
title="Keycloak Logout"
|
|
/>
|
|
)}
|
|
|
|
<Link
|
|
href="/api/auth/signin?fresh=true"
|
|
className="inline-block px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors"
|
|
>
|
|
Sign In Again
|
|
</Link>
|
|
</>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{sessionStatus === 'error' && (
|
|
<>
|
|
<p className="text-red-600 mb-4">{message}</p>
|
|
<Link
|
|
href="/"
|
|
className="inline-block px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors"
|
|
>
|
|
Return to Home Page
|
|
</Link>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|