import { useEffect, useRef, useState } from 'react'; import { useNavigate, useLocation } from 'react-router-dom'; import { authStorage } from '@ema-platform/auth'; /** * Same-origin host for the user-management module. * * The host app (React 19 / Mantine 8 / Tailwind 3) embeds the module (React 18 / * Mantine 7 / Tailwind 4) via an iframe so the two never share a React tree, * router, or CSS — the version mismatch is fully isolated by the document * boundary. The module is built into apps/backoffice/public/_um and served by * THIS same server at /_um/, so there is no second server and no second * port. Override the mount path with VITE_USER_MANAGEMENT_BASE (default /_um). * * SSO: the module and host authenticate against the SAME backend, so the host's * token is valid in the module. The module posts `UM_REQUEST_AUTH`; we reply with * our stored token. Route-sync mirrors the module's internal route into the host * URL (/um/) so a refresh deep-links back to the selected menu. */ function readToken(): string | null { const escaped = 'auth-token'.replace(/([.$?*|{}()[\]\\/+^])/g, '\\$1'); const match = document.cookie.match(new RegExp('(?:^|; )' + escaped + '=([^;]*)')); return authStorage.getToken() ?? (match ? decodeURIComponent(match[1]) : null); } function readRefreshToken(): string | null { return authStorage.getRefreshToken() ?? null; } export default function UserManagementHostPage() { const navigate = useNavigate(); const location = useLocation(); const iframeRef = useRef(null); // Same-origin sub-path the module is served from (matches the module's Vite // `base` + the apps/backoffice/public/_um build). Same origin ⇒ no second port. const mountBase = ( (import.meta.env.VITE_USER_MANAGEMENT_BASE as string | undefined) ?? '/_um' ).replace(/\/$/, ''); const moduleOrigin = window.location.origin; // Deep-link: the host route is /um/*, so whatever follows /um is the module's // own route. Compute src ONCE (frozen) so later parent-URL updates don't reload. const [iframeSrc] = useState(() => { const sub = location.pathname.replace(/^\/um(?=\/|$)/, ''); return mountBase + sub + location.search; }); useEffect(() => { const onMessage = (event: MessageEvent) => { if (event.origin !== moduleOrigin) return; const data = event.data as { type?: string; path?: string } | undefined; if (!data) return; 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); } return; } if (data.type === 'UM_ROUTE_CHANGED' && typeof data.path === 'string') { // Allow the module to navigate the host away by using /return/. // Add a nav item with href: "/return/dashboard" in project.theme.ts // navItems to send the user back to the host app. const returnMatch = data.path.match(/^\/return\/(.+)/); if (returnMatch) { navigate('/' + returnMatch[1], { replace: true }); return; } const target = '/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 (