mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-29 08:21:00 +00:00
97 lines
3.8 KiB
TypeScript
97 lines
3.8 KiB
TypeScript
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 <origin>/_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/<path>) 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<HTMLIFrameElement>(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/<path>.
|
|
// 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 (
|
|
<div style={{ position: 'fixed', inset: 0 }}>
|
|
<iframe
|
|
ref={iframeRef}
|
|
title="User Management"
|
|
src={iframeSrc}
|
|
style={{ width: '100%', height: '100%', border: 0, display: 'block' }}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|