Merge pull request #165 from Tria-plc/freight/feature/user_management

Freight/feature/user management
This commit is contained in:
yaschalew10
2026-06-16 09:20:08 +03:00
committed by GitHub
3 changed files with 236 additions and 3 deletions

View File

@@ -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 (
<Routes>
<Route path="/" element={<Navigate to="/dashboard/overview" replace />} />
<Route path="/dashboard" element={<Navigate to="/dashboard/overview" replace />} />,
<Route path='/um/*' element={<UserManagementHostPage />} />
<Route path="/dashboard" element={<Navigate to="/dashboard/overview" replace />} />
<Route path="/dashboard" element={<DashboardShell />}>
<Route path="overview" element={<OverviewPage />} />
@@ -298,6 +297,10 @@ const App = () => {
<Route path="containers" element={<FleetResourcePage />} />
<Route path="cargoes" element={<FleetResourcePage />} />
{/* iframe-based user management module */}
<Route path="um/*" element={<UserManagementHostPage />} />
{/* Legacy embedded user management routes */}
<Route path="user-management" element={<UserManagementPage />} />
<Route path="user-management/users" element={<UsersPage />} />
<Route path="user-management/position-types" element={<PositionTypesPage />} />

View File

@@ -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<string>;
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<string> {
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();

View File

@@ -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<HTMLIFrameElement>(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 (
<div style={{ position: 'fixed', inset: 0 }}>
<iframe
ref={iframeRef}
title="User Management"
src={iframeSrc}
onLoad={handleIframeLoad}
style={{ width: '100%', height: '100%', border: 0, display: 'block' }}
/>
</div>
);
}