mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-30 00:38:12 +00:00
Merge branch 'feature/user-management' into feature/pencil-design
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
return (
|
||||
localStorage.getItem('fhc-backoffice-auth-token') ??
|
||||
(() => {
|
||||
const escaped = 'auth-token'.replace(/([.$?*|{}()[\]\\/+^])/g, '\\$1');
|
||||
const match = document.cookie.match(new RegExp('(?:^|; )' + escaped + '=([^;]*)'));
|
||||
return match ? decodeURIComponent(match[1]) : null;
|
||||
})()
|
||||
);
|
||||
}
|
||||
|
||||
function readRefreshToken(): string | null {
|
||||
return localStorage.getItem('fhc-backoffice-auth-refresh-token') ?? 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') {
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import { useNavigate, useLocation } from 'react-router-dom';
|
||||
const NAV_ITEMS = [
|
||||
{ label: 'Dashboard', icon: IconDashboard, path: '/dashboard' },
|
||||
{ label: 'Items', icon: IconBox, path: '/items' },
|
||||
{ label: 'User Management', icon: IconUsers, path: '/user-management' },
|
||||
{ label: 'User Management', icon: IconUsers, path: '/um' },
|
||||
{ label: 'Audit Log', icon: IconClipboardList, path: '/audit-log' },
|
||||
];
|
||||
|
||||
|
||||
@@ -2,57 +2,13 @@ import { createBrowserRouter, RouterProvider, Navigate } from 'react-router-dom'
|
||||
import {
|
||||
Login,
|
||||
SetPasswordPage,
|
||||
UserManagementLayout,
|
||||
UserManagementPage,
|
||||
BulkUploadPage,
|
||||
ArchivedUsersPage,
|
||||
PositionManagementPage,
|
||||
CreatePositionPage,
|
||||
EditPositionPage,
|
||||
} from '@tria-plc/iamui-common';
|
||||
import { ProtectedRoute } from './ProtectedRoute';
|
||||
import { BackofficeLayout } from '../layouts/BackofficeLayout';
|
||||
import { AuthLayout } from '../layouts/AuthLayout';
|
||||
import { DashboardPage } from '../features/dashboard/pages/DashboardPage';
|
||||
import { ItemPage } from '../features/item/pages/ItemPage';
|
||||
import { AuditLogPageWrapper } from '../iam/pages/AuditLogPage';
|
||||
import { IamProviders, AuthProviders } from '../iam/IamProviders';
|
||||
import { AuthProviders } from '../iam/IamProviders';
|
||||
import UserManagementHostPage from '../features/user-management-host/UserManagementHostPage';
|
||||
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
element: (
|
||||
<AuthProviders>
|
||||
<ProtectedRoute />
|
||||
</AuthProviders>
|
||||
),
|
||||
children: [
|
||||
{
|
||||
element: <BackofficeLayout />,
|
||||
children: [
|
||||
{ path: '/', element: <Navigate to="/dashboard" replace /> },
|
||||
{ path: '/dashboard', element: <DashboardPage /> },
|
||||
{ path: '/items', element: <ItemPage /> },
|
||||
{
|
||||
path: '/user-management',
|
||||
element: (
|
||||
<IamProviders>
|
||||
<UserManagementLayout />
|
||||
</IamProviders>
|
||||
),
|
||||
children: [
|
||||
{ index: true, element: <UserManagementPage /> },
|
||||
{ path: 'bulk-upload', element: <BulkUploadPage /> },
|
||||
{ path: 'archived-users', element: <ArchivedUsersPage /> },
|
||||
{ path: 'position-management', element: <PositionManagementPage /> },
|
||||
{ path: 'position-management/new', element: <CreatePositionPage /> },
|
||||
{ path: 'position-management/edit/:id', element: <EditPositionPage /> },
|
||||
],
|
||||
},
|
||||
{ path: '/audit-log', element: <AuditLogPageWrapper /> },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
element: (
|
||||
<AuthProviders>
|
||||
@@ -64,6 +20,7 @@ const router = createBrowserRouter([
|
||||
{ path: '/otp-verify', element: <SetPasswordPage /> },
|
||||
],
|
||||
},
|
||||
{ path: '/um/*', element: <UserManagementHostPage /> },
|
||||
{ path: '/404', element: <div>Page not found</div> },
|
||||
{ path: '*', element: <Navigate to="/404" replace /> },
|
||||
]);
|
||||
|
||||
@@ -2,6 +2,20 @@ import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
|
||||
|
||||
function userManagementSpaFallback() {
|
||||
const rewrite = (req) => {
|
||||
const url = req.url || '';
|
||||
if (!url.startsWith('/_um/') && url !== '/_um') return;
|
||||
if (/\.[a-zA-Z0-9]+$/.test(url.split('?')[0])) return; // real assets pass through
|
||||
req.url = '/_um/index.html';
|
||||
};
|
||||
return {
|
||||
name: 'user-management-spa-fallback',
|
||||
configureServer(s) { s.middlewares.use((req, _res, next) => { rewrite(req); next(); }); },
|
||||
configurePreviewServer(s) { s.middlewares.use((req, _res, next) => { rewrite(req); next(); }); },
|
||||
};
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
root: __dirname,
|
||||
cacheDir: '../../node_modules/.vite/apps/backoffice',
|
||||
@@ -10,7 +24,7 @@ export default defineConfig({
|
||||
host: 'localhost',
|
||||
},
|
||||
preview: { port: 4201, host: 'localhost' },
|
||||
plugins: [react(), nxViteTsPaths()],
|
||||
plugins: [react(), nxViteTsPaths(), userManagementSpaFallback()],
|
||||
resolve: {
|
||||
dedupe: ['react', 'react-dom', 'react-router-dom', '@tanstack/react-query'],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user