140 lines
5.3 KiB
TypeScript
140 lines
5.3 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect } from "react";
|
|
import { useSession, signOut } from "next-auth/react";
|
|
import { MainNav } from "@/components/main-nav";
|
|
import { Footer } from "@/components/footer";
|
|
import { AuthCheck } from "@/components/auth/auth-check";
|
|
import { Toaster } from "@/components/ui/toaster";
|
|
import { useBackgroundImage } from "@/components/background-switcher";
|
|
import { clearAuthCookies, clearKeycloakCookies } from "@/lib/session";
|
|
|
|
interface LayoutWrapperProps {
|
|
children: React.ReactNode;
|
|
isSignInPage: boolean;
|
|
isAuthenticated: boolean;
|
|
}
|
|
|
|
export function LayoutWrapper({ children, isSignInPage, isAuthenticated }: LayoutWrapperProps) {
|
|
const { currentBackground, changeBackground } = useBackgroundImage();
|
|
const { data: session } = useSession();
|
|
|
|
// Global listener for logout messages from iframe applications
|
|
useEffect(() => {
|
|
if (isSignInPage) return; // Don't listen on signin page
|
|
|
|
const handleMessage = async (event: MessageEvent) => {
|
|
// Security: Validate message origin (in production, check against known iframe URLs)
|
|
// For now, we accept messages from any origin but validate the message structure
|
|
|
|
// Check if message is a logout request from iframe
|
|
if (event.data && typeof event.data === 'object') {
|
|
if (event.data.type === 'KEYCLOAK_LOGOUT' || event.data.type === 'LOGOUT') {
|
|
console.log('Received logout request from iframe application, triggering dashboard logout');
|
|
|
|
try {
|
|
// Mark logout in progress
|
|
sessionStorage.setItem('just_logged_out', 'true');
|
|
document.cookie = 'logout_in_progress=true; path=/; max-age=60';
|
|
|
|
// Clear cookies
|
|
clearAuthCookies();
|
|
clearKeycloakCookies();
|
|
|
|
// End SSO session using Admin API before signing out
|
|
// This ensures the realm-wide SSO session is cleared,
|
|
// not just the client session
|
|
try {
|
|
const ssoLogoutResponse = await fetch('/api/auth/end-sso-session', {
|
|
method: 'POST',
|
|
credentials: 'include',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
});
|
|
|
|
if (ssoLogoutResponse.ok) {
|
|
console.log('SSO session ended successfully');
|
|
} else {
|
|
const errorData = await ssoLogoutResponse.json().catch(() => ({}));
|
|
console.warn('Failed to end SSO session via Admin API, continuing with standard logout:', errorData);
|
|
// Continue with logout even if SSO session termination fails
|
|
}
|
|
} catch (error) {
|
|
console.error('Error ending SSO session:', error);
|
|
// Continue with logout even if SSO session termination fails
|
|
}
|
|
|
|
// Sign out from NextAuth
|
|
await signOut({
|
|
callbackUrl: '/signin?logout=true',
|
|
redirect: false
|
|
});
|
|
|
|
// Call Keycloak logout if we have ID token
|
|
const keycloakIssuer = process.env.NEXT_PUBLIC_KEYCLOAK_ISSUER;
|
|
const idToken = session?.idToken;
|
|
|
|
if (keycloakIssuer && idToken) {
|
|
const keycloakLogoutUrl = new URL(
|
|
`${keycloakIssuer}/protocol/openid-connect/logout`
|
|
);
|
|
keycloakLogoutUrl.searchParams.append(
|
|
'post_logout_redirect_uri',
|
|
window.location.origin + '/signin?logout=true'
|
|
);
|
|
keycloakLogoutUrl.searchParams.append(
|
|
'id_token_hint',
|
|
idToken
|
|
);
|
|
// Add kc_action=LOGOUT to ensure SSO session is cleared
|
|
keycloakLogoutUrl.searchParams.append(
|
|
'kc_action',
|
|
'LOGOUT'
|
|
);
|
|
window.location.replace(keycloakLogoutUrl.toString());
|
|
} else {
|
|
// Fallback: redirect to signin
|
|
window.location.replace('/signin?logout=true');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error handling iframe logout:', error);
|
|
window.location.replace('/signin?logout=true');
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
window.addEventListener('message', handleMessage);
|
|
return () => {
|
|
window.removeEventListener('message', handleMessage);
|
|
};
|
|
}, [isSignInPage, session]);
|
|
|
|
return (
|
|
<AuthCheck>
|
|
{!isSignInPage && isAuthenticated && <MainNav />}
|
|
<div
|
|
className={isSignInPage ? "min-h-screen" : "min-h-screen"}
|
|
style={
|
|
isSignInPage
|
|
? {} // No background style for signin page - let the page component handle it
|
|
: {
|
|
backgroundImage: `url('${currentBackground}')`,
|
|
backgroundSize: 'cover',
|
|
backgroundPosition: 'center',
|
|
backgroundRepeat: 'no-repeat',
|
|
backgroundAttachment: 'fixed',
|
|
cursor: 'pointer',
|
|
transition: 'background-image 0.5s ease-in-out'
|
|
}
|
|
}
|
|
onClick={!isSignInPage ? changeBackground : undefined}
|
|
>
|
|
<main>{children}</main>
|
|
</div>
|
|
{!isSignInPage && isAuthenticated && <Footer />}
|
|
<Toaster />
|
|
</AuthCheck>
|
|
);
|
|
}
|