diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 66521fdb1..5615e88a7 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -48,10 +48,10 @@ jobs: branch_slug=$(echo "${BRANCH}" | tr "[:upper:]" "[:lower:]" | sed -E "s/[^a-z0-9]+/-/g; s/^-+//; s/-+$//") echo "COMPOSE_PROJECT_NAME=${PROJECT}-${branch_slug}" >> "${GITHUB_ENV}" - # - name: Configure npm auth for Docker builds - # env: - # NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - # run: ./scripts/deploy/create-npmrc.sh + - name: Configure npm auth for Docker builds + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + run: ./scripts/deploy/create-npmrc.sh - name: Build ${{ matrix.service }} run: | diff --git a/.gitignore b/.gitignore index f6655fa10..0487d2783 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,11 @@ coverage/ .idea/ .vscode/ .npmrc +branch_structure.json +temp_auto_push.bat +temp_interactive_push.bat +.nx + +apps/backoffice/public/_um/ +apps/backoffice/public/tinymce/ + diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..850934ff5 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "user-management"] + path = user-management + url = git@github.com:Tria-plc/iamui.git diff --git a/Dockerfile b/Dockerfile index f29502bd4..d54102eb3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,8 @@ FROM node:24-alpine AS deps WORKDIR /app COPY package.json package-lock.json* ./ -RUN npm install --legacy-peer-deps +RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \ + npm install --legacy-peer-deps FROM deps AS base COPY . . diff --git a/apps/backoffice/src/app/features/user-management-host/UserManagementHostPage.tsx b/apps/backoffice/src/app/features/user-management-host/UserManagementHostPage.tsx new file mode 100644 index 000000000..542c37097 --- /dev/null +++ b/apps/backoffice/src/app/features/user-management-host/UserManagementHostPage.tsx @@ -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 /_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 { + 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(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 ( +
+