Neah/app/components/responsive-iframe.tsx
2025-05-02 18:05:28 +02:00

214 lines
7.0 KiB
TypeScript

'use client';
import { useEffect, useRef, useState } from 'react';
import { useSession } from 'next-auth/react';
export interface ResponsiveIframeProps {
src: string;
title?: string;
token?: string;
className?: string;
style?: React.CSSProperties;
hideUntilLoad?: boolean;
allowFullScreen?: boolean;
scrolling?: boolean;
heightOffset?: number;
}
// Map of service prefixes to their base URLs - keep in sync with proxy route.ts
const SERVICE_URLS: Record<string, string> = {
'parole': process.env.NEXT_PUBLIC_IFRAME_PAROLE_URL || '',
'alma': process.env.NEXT_PUBLIC_IFRAME_AI_ASSISTANT_URL || '',
'crm': process.env.NEXT_PUBLIC_IFRAME_MEDIATIONS_URL || '',
'vision': process.env.NEXT_PUBLIC_IFRAME_CONFERENCE_URL || '',
'showcase': process.env.NEXT_PUBLIC_IFRAME_SHOWCASE_URL || '',
'agilite': process.env.NEXT_PUBLIC_IFRAME_AGILITY_URL || '',
'dossiers': process.env.NEXT_PUBLIC_IFRAME_DRIVE_URL || '',
'the-message': process.env.NEXT_PUBLIC_IFRAME_THEMESSAGE_URL || '',
'qg': process.env.NEXT_PUBLIC_IFRAME_MISSIONVIEW_URL || '',
'design': process.env.NEXT_PUBLIC_IFRAME_DESIGN_URL || '',
};
export default function ResponsiveIframe({
src,
title = 'Embedded content',
token,
className = '',
style = {},
hideUntilLoad = false,
allowFullScreen = false,
scrolling = true,
heightOffset = 0,
}: ResponsiveIframeProps) {
const [height, setHeight] = useState<number>(0);
const [loaded, setLoaded] = useState<boolean>(false);
const [authError, setAuthError] = useState<boolean>(false);
const iframeRef = useRef<HTMLIFrameElement>(null);
const silentAuthRef = useRef<HTMLIFrameElement>(null);
const silentAuthTimerRef = useRef<NodeJS.Timeout | null>(null);
const { data: session } = useSession();
// Convert proxy URLs to direct URLs to avoid double proxying
const processedSrc = (() => {
if (src.startsWith('/api/proxy/')) {
// Extract the service name and path
const parts = src.replace('/api/proxy/', '').split('/');
const serviceName = parts[0];
const path = parts.slice(1).join('/');
// Look up the base URL for this service
const baseUrl = SERVICE_URLS[serviceName];
if (baseUrl) {
console.log(`Converting proxy URL to direct URL: ${src} -> ${baseUrl}/${path}`);
return `${baseUrl}/${path}`;
}
}
return src;
})();
// Append token to src if provided
const fullSrc = token ? `${processedSrc}${processedSrc.includes('?') ? '&' : '?'}token=${token}` : processedSrc;
// Handle silent authentication refresh
useEffect(() => {
// Setup silent authentication check every 15 minutes
const setupSilentAuth = () => {
if (silentAuthTimerRef.current) {
clearTimeout(silentAuthTimerRef.current);
}
// Create a hidden iframe to check authentication status
silentAuthTimerRef.current = setTimeout(() => {
console.log('Running silent authentication check');
// Create the silent auth iframe if it doesn't exist
if (silentAuthRef.current && !silentAuthRef.current.src) {
silentAuthRef.current.src = '/silent-refresh';
}
// Setup next check
setupSilentAuth();
}, 15 * 60 * 1000); // 15 minutes
};
// Handle messages from the silent auth iframe
const handleAuthMessage = (event: MessageEvent) => {
if (event.data && event.data.type === 'AUTH_STATUS') {
console.log('Received auth status:', event.data);
if (event.data.status === 'UNAUTHENTICATED') {
console.error('Silent authentication failed - user is not authenticated');
setAuthError(true);
// Force immediate refresh
if (silentAuthRef.current) {
silentAuthRef.current.src = '/silent-refresh';
}
} else if (event.data.status === 'AUTHENTICATED') {
console.log('Silent authentication successful');
setAuthError(false);
}
}
};
window.addEventListener('message', handleAuthMessage);
setupSilentAuth();
// Cleanup
return () => {
window.removeEventListener('message', handleAuthMessage);
if (silentAuthTimerRef.current) {
clearTimeout(silentAuthTimerRef.current);
}
};
}, []);
// Adjust iframe height based on window size
useEffect(() => {
const updateHeight = () => {
const viewportHeight = window.innerHeight;
const newHeight = viewportHeight - heightOffset;
setHeight(newHeight > 0 ? newHeight : 0);
};
updateHeight();
window.addEventListener('resize', updateHeight);
return () => {
window.removeEventListener('resize', updateHeight);
};
}, [heightOffset]);
// Handle hash changes by updating iframe source
useEffect(() => {
const handleHashChange = () => {
const iframe = iframeRef.current;
if (iframe && iframe.src) {
const url = new URL(iframe.src);
// If there's a hash in the parent window's URL
if (window.location.hash) {
url.hash = window.location.hash;
iframe.src = url.toString();
}
}
};
window.addEventListener('hashchange', handleHashChange);
return () => {
window.removeEventListener('hashchange', handleHashChange);
};
}, []);
return (
<>
{/* Hidden iframe for silent authentication */}
<iframe
ref={silentAuthRef}
style={{ display: 'none' }}
title="Silent Authentication"
/>
{/* Main content iframe */}
<div className="relative w-full h-full">
{authError && (
<div className="absolute inset-0 flex items-center justify-center bg-red-50 bg-opacity-90 z-10">
<div className="text-center p-4">
<p className="text-red-600 font-semibold">Session expired or authentication error</p>
<button
className="mt-4 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
onClick={() => window.location.href = '/api/auth/signin?callbackUrl=' + encodeURIComponent(window.location.href)}
>
Sign in again
</button>
</div>
</div>
)}
<iframe
ref={iframeRef}
src={fullSrc}
title={title}
className={`w-full ${className}`}
style={{
height: height > 0 ? `${height}px` : '100%',
border: 'none',
visibility: hideUntilLoad && !loaded ? 'hidden' : 'visible',
...style,
}}
onLoad={() => {
setLoaded(true);
}}
allowFullScreen={allowFullScreen}
scrolling={scrolling ? 'yes' : 'no'}
/>
{hideUntilLoad && !loaded && (
<div className="flex justify-center items-center w-full h-full absolute top-0 left-0">
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500"></div>
</div>
)}
</div>
</>
);
}