NeahStable/components/layout/layout-wrapper.tsx
2026-01-16 01:18:05 +01:00

211 lines
8.1 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";
import { useRocketChatCalls } from "@/hooks/use-rocketchat-calls";
interface LayoutWrapperProps {
children: React.ReactNode;
isSignInPage: boolean;
isAuthenticated: boolean;
}
export function LayoutWrapper({ children, isSignInPage, isAuthenticated }: LayoutWrapperProps) {
const { currentBackground, changeBackground } = useBackgroundImage();
const { data: session } = useSession();
// Listen for incoming RocketChat calls via WebSocket
useRocketChatCalls();
// Also listen for RocketChat iframe events (fallback)
useEffect(() => {
if (isSignInPage) return;
const handleRocketChatMessage = (event: MessageEvent) => {
// Listen for RocketChat iframe events
if (event.data && typeof event.data === 'object') {
// Check for new messages that might be call invitations
if (event.data.event === 'new-message' || event.data.eventName === 'new-message') {
const message = event.data.message || event.data.data;
if (message) {
// Check if message contains call-related content
const text = message.text || message.msg || '';
const isCallMessage =
text.includes('video') ||
text.includes('audio') ||
text.includes('call') ||
message.attachments?.some((att: any) =>
att.actions?.some((action: any) =>
action.type === 'button' &&
(action.text?.toLowerCase().includes('join') || action.text?.toLowerCase().includes('appel'))
)
);
if (isCallMessage) {
console.log('[ROCKETCHAT_IFRAME] 📞 Possible call message detected from iframe', message);
}
}
}
// Check for notification events that might be calls
if (event.data.event === 'notification' || event.data.eventName === 'notification') {
const notification = event.data.notification || event.data.data;
if (notification) {
const title = notification.title || '';
const text = notification.text || '';
const isCallNotification =
title.toLowerCase().includes('call') ||
title.toLowerCase().includes('appel') ||
text.toLowerCase().includes('video') ||
text.toLowerCase().includes('audio');
if (isCallNotification) {
console.log('[ROCKETCHAT_IFRAME] 📞 Possible call notification from iframe', notification);
}
}
}
}
};
window.addEventListener('message', handleRocketChatMessage);
return () => {
window.removeEventListener('message', handleRocketChatMessage);
};
}, [isSignInPage]);
// 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();
// Mark logout on server to force login prompt on next sign-in
try {
await fetch('/api/auth/mark-logout', {
method: 'POST',
credentials: 'include',
});
} catch (error) {
console.error('Error marking logout:', error);
// Continue even if this fails
}
// 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>
);
}