Neah/app/loggedout/page.tsx
2025-05-02 12:48:01 +02:00

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>
);
}