diff --git a/web/portal/README.md b/web/portal/README.md index 7e579e6a..748cd719 100644 --- a/web/portal/README.md +++ b/web/portal/README.md @@ -11,6 +11,7 @@ Browser-seitig (werden beim Build eingebettet): |---|---|---| | `NEXT_PUBLIC_CORE_API_BASE` | ja | Basis-URL des Core (`/auth/module-token`) | | `NEXT_PUBLIC_LOGIN_URL` | nein | Ziel der Weiterleitung, wenn die Sitzung fehlt | +| `NEXT_PUBLIC_TOKEN_MODULE` | nein | Zielmodul beim Token-Abruf (`?module=portal`); nur setzen, wenn der Core `portal` in `NEXARCH_ACCOUNT_MODULE_AUDIENCES` führt | Server-seitig (nur Route `/api/visible-modules`, nie im Browser sichtbar): @@ -30,6 +31,14 @@ Server-seitig (nur Route `/api/visible-modules`, nie im Browser sichtbar): Fehlt `PORTAL_POLICY_API_URL`, `PORTAL_POLICY_SERVICE_TOKEN` oder die Core-URL, antwortet die Route mit 503. +## Zielmodul `portal` einführen + +Reihenfolge beim Ausrollen: (1) Core: `portal` in `NEXARCH_ACCOUNT_MODULE_AUDIENCES` aufnehmen; (2) Portal: `NEXT_PUBLIC_TOKEN_MODULE=portal` (Build) und `PORTAL_TOKEN_AUDIENCE=portal` (Server) setzen. Umgekehrt scheitert der Token-Abruf, weil der Core ein unbekanntes Zielmodul ablehnt. Danach akzeptiert das Portal keine für `dms` ausgestellten Tokens mehr. + +## Netz + +Die Statusseite (`PORTAL_STATUS_API_URL`) ist selbst nicht authentifiziert und darf nur vom Portal-Server aus erreichbar sein (Firewall); die Kennzahlen- und Workflow-APIs ebenfalls nur intern. + ## Verhalten der Route - `GET /api/visible-modules` verlangt `Authorization: Bearer `; Signatur (Ed25519, JWKS vom Core) und Ablaufzeit werden geprüft, sonst 401. diff --git a/web/portal/lib/http.ts b/web/portal/lib/http.ts index ecf03697..255a8c5a 100644 --- a/web/portal/lib/http.ts +++ b/web/portal/lib/http.ts @@ -10,6 +10,7 @@ export function getSession(): Session { } session = createSession({ coreBase, + module: process.env.NEXT_PUBLIC_TOKEN_MODULE || undefined, onLoginRequired: () => redirectToLogin(process.env.NEXT_PUBLIC_LOGIN_URL), }); } diff --git a/web/shl/__tests__/session.test.ts b/web/shl/__tests__/session.test.ts index cb88b7ed..63f3a5e9 100644 --- a/web/shl/__tests__/session.test.ts +++ b/web/shl/__tests__/session.test.ts @@ -156,3 +156,20 @@ describe("UX-10: Weiterleitung zur Anmeldung", () => { expect(assign).not.toHaveBeenCalled(); }); }); + +describe("Zielmodul im Token-Abruf", () => { + it("haengt ?module= an, wenn gesetzt, und laesst es sonst weg", async () => { + const urls: string[] = []; + const mk = (module?: string) => { + const fetchImpl = vi.fn(async (input: RequestInfo | URL) => { + urls.push(String(input)); + return tokenReply(claims); + }) as unknown as typeof fetch; + return createSession({ coreBase: "/api/core/v1", module, fetchImpl }); + }; + await mk("portal").token(); + await mk().token(); + expect(urls[0]).toBe("/api/core/v1/auth/module-token?module=portal"); + expect(urls[1]).toBe("/api/core/v1/auth/module-token"); + }); +}); diff --git a/web/shl/auth/session.ts b/web/shl/auth/session.ts index 629795b8..65a82b66 100644 --- a/web/shl/auth/session.ts +++ b/web/shl/auth/session.ts @@ -39,6 +39,7 @@ export type IdentityLoader = () => Promise; export interface SessionOptions { coreBase: string; + module?: string; fetchImpl?: typeof fetch; now?: () => number; onLoginRequired?: () => void; @@ -99,7 +100,7 @@ export function createSession(opts: SessionOptions): Session { async function refresh(): Promise { let res: Response; try { - res = await fetchImpl(`${opts.coreBase}/auth/module-token`, { + res = await fetchImpl(`${opts.coreBase}/auth/module-token${opts.module ? `?module=${encodeURIComponent(opts.module)}` : ""}`, { method: "POST", credentials: "include", headers: { Accept: "application/json" },