diff --git a/apps/edr-freight-web/backoffice/src/App.tsx b/apps/edr-freight-web/backoffice/src/App.tsx index 8eaf0d4e1..8f89b6e37 100644 --- a/apps/edr-freight-web/backoffice/src/App.tsx +++ b/apps/edr-freight-web/backoffice/src/App.tsx @@ -24,6 +24,7 @@ import BookingContractPage from "./pages/bookings/BookingContractPage"; import BookingRequestDetailPage from "./pages/bookings/BookingRequestDetailPage"; import BookingRequestsPage from "./pages/bookings/BookingRequestsPage"; import NewBookingPage from "./pages/bookings/NewBookingPage"; +import UserManagementHostPage from "./pages/dashboard/user-management/UserManagementHostPage"; import DemoUser1Page from "./pages/dashboard/demo/DemoUser1Page"; import DemoUser2Page from "./pages/dashboard/demo/DemoUser2Page"; import OverviewPage from "./pages/dashboard/OverviewPage"; @@ -48,7 +49,6 @@ import FleetResourcePage from "./pages/fleet/FleetResourcePage"; import { getCategorySidebarChildren } from "./pages/ruleEngine/config/resources"; import TrainDetailPage from "./pages/trains/TrainDetailPage"; import RoutesPage from "./pages/fleet/RoutesPage"; -import UserManagementHostPage from './features/user-management-host/UserManagementHostPage'; const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[] => [ { @@ -260,8 +260,7 @@ const App = () => { return ( } /> - } />, - } /> + } /> }> } /> @@ -298,6 +297,10 @@ const App = () => { } /> } /> + {/* iframe-based user management module */} + } /> + + {/* Legacy embedded user management routes */} } /> } /> } /> diff --git a/apps/edr-freight-web/backoffice/src/auth/tokenManager.ts b/apps/edr-freight-web/backoffice/src/auth/tokenManager.ts new file mode 100644 index 000000000..3ee581eaf --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/auth/tokenManager.ts @@ -0,0 +1,117 @@ +export interface TokenMessage { + type: 'UM_AUTH_TOKEN'; + token: string; + refreshToken: string; +} + +/** + * Manages authentication tokens received from parent window (host) + * Used by iframe modules to receive and store auth tokens + */ +class TokenManager { + private token: string | null = null; + private refreshToken: string | null = null; + private tokenResolve: ((token: string) => void) | null = null; + private tokenPromise: Promise; + private isFramed: boolean; + + constructor() { + // Check if we're running in an iframe + this.isFramed = window.self !== window.top; + + // Create a promise that resolves when token is received + this.tokenPromise = new Promise((resolve) => { + this.tokenResolve = resolve; + }); + + if (this.isFramed) { + this.setupMessageListener(); + + // Request token after 2 seconds if not received + setTimeout(() => { + if (!this.token) { + this.requestTokenFromHost(); + } + }, 2000); + } + } + + private setupMessageListener() { + window.addEventListener('message', (event) => { + // Security: Only accept from parent window + if (event.source !== window.parent) { + return; + } + + const data = event.data as TokenMessage | undefined; + + if (data?.type === 'UM_AUTH_TOKEN') { + this.token = data.token; + this.refreshToken = data.refreshToken; + + // Store in localStorage for persistence + localStorage.setItem('um_auth_token', data.token); + localStorage.setItem('um_refresh_token', data.refreshToken); + + console.log('✅ Token received from host'); + + // Resolve the promise + if (this.tokenResolve) { + this.tokenResolve(data.token); + } + } + }); + } + + private requestTokenFromHost() { + console.log('📤 Requesting token from host...'); + window.parent.postMessage( + { type: 'UM_REQUEST_AUTH' }, + window.location.origin + ); + } + + /** + * Get token - waits for it if not yet received + */ + async getToken(): Promise { + if (this.token) { + return this.token; + } + + // Check localStorage as fallback + const stored = localStorage.getItem('um_auth_token'); + if (stored) { + this.token = stored; + return stored; + } + + // Wait for token to arrive from parent + return this.tokenPromise; + } + + /** + * Get token synchronously (returns null if not available) + */ + getTokenSync(): string | null { + return this.token || localStorage.getItem('um_auth_token'); + } + + getRefreshToken(): string | null { + return this.refreshToken || localStorage.getItem('um_refresh_token'); + } + + clearTokens() { + this.token = null; + this.refreshToken = null; + localStorage.removeItem('um_auth_token'); + localStorage.removeItem('um_refresh_token'); + } + + isInFrame(): boolean { + return this.isFramed; + } +} + +// Export singleton instance +export const tokenManager = new TokenManager(); diff --git a/apps/edr-freight-web/backoffice/src/pages/dashboard/user-management/UserManagementHostPage.tsx b/apps/edr-freight-web/backoffice/src/pages/dashboard/user-management/UserManagementHostPage.tsx new file mode 100644 index 000000000..526667a31 --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/pages/dashboard/user-management/UserManagementHostPage.tsx @@ -0,0 +1,113 @@ +import { useEffect, useRef, useState } from 'react'; +import { useNavigate, useLocation } from 'react-router-dom'; +import { getCookie } from '@/auth/cookies'; + +function readToken(): string | null { + return getCookie('auth-token') ?? null; +} + +function readRefreshToken(): string | null { + return getCookie('refresh-token') ?? null; +} + +export default function UserManagementHostPage() { + const navigate = useNavigate(); + const location = useLocation(); + const iframeRef = useRef(null); + + const mountBase = ( + (import.meta.env.VITE_USER_MANAGEMENT_BASE as string | undefined) ?? '/_um' + ).replace(/\/$/, ''); + + const moduleOrigin = window.location.origin; + + const [iframeSrc] = useState(() => { + const sub = location.pathname.replace(/^\/(?:dashboard\/)?um(?=\/|$)/, ''); + return mountBase + (sub || '/') + location.search; + }); + + // ✅ Send token when iframe loads + const handleIframeLoad = () => { + const token = readToken(); + const refreshToken = readRefreshToken(); + const target = iframeRef.current?.contentWindow; + + if (!token) { + console.warn('⚠️ No authentication token found'); + return; + } + + if (!target) { + console.warn('⚠️ No iframe reference'); + return; + } + + target.postMessage( + { + type: 'UM_AUTH_TOKEN', + token, + refreshToken, + }, + moduleOrigin + ); + + console.log('✅ Token sent to iframe module'); + }; + + // ✅ Listen for messages from iframe + useEffect(() => { + const onMessage = (event: MessageEvent) => { + // Security: Only accept from same origin + if (event.origin !== moduleOrigin) { + console.warn('🚫 Blocked message from different origin:', event.origin); + return; + } + + const data = event.data as { type?: string; path?: string } | undefined; + if (!data) return; + + // Handle auth request (if module asks for token again) + if (data.type === 'UM_REQUEST_AUTH') { + const token = readToken(); + const refreshToken = readRefreshToken(); + const target = iframeRef.current?.contentWindow; + + if (token && target) { + target.postMessage( + { + type: 'UM_AUTH_TOKEN', + token, + refreshToken, + }, + moduleOrigin + ); + console.log('✅ Token resent to iframe (on request)'); + } + return; + } + + // Handle route synchronization + if (data.type === 'UM_ROUTE_CHANGED' && typeof data.path === 'string') { + const target = '/dashboard/um' + data.path; + if (window.location.pathname + window.location.search !== target) { + navigate(target, { replace: true }); + } + } + }; + + window.addEventListener('message', onMessage); + return () => window.removeEventListener('message', onMessage); + }, [moduleOrigin, navigate]); + + return ( +
+