From db11334b215744412500f6e924f1c5cafc5b9476 Mon Sep 17 00:00:00 2001 From: patrick Date: Wed, 28 Oct 2026 22:07:18 +0100 Subject: [PATCH] mail-search: Akte-Kontext per Bearer (Audience workflow) Die BFF-Route reicht mit Bearer nur das Token weiter; user/tenant aus der URL gelten nur noch ohne Token. Der Client sendet das Token ueber eine zweite Sitzung (module workflow), sobald NEXT_PUBLIC_CORE_API_BASE gesetzt ist. Co-Authored-By: Claude Sonnet 5.5 --- .../app/api/case-context/route.test.ts | 22 +++++++++++++++++-- web/mail-search/app/api/case-context/route.ts | 16 +++++++------- web/mail-search/lib/caseContext.ts | 4 +++- web/mail-search/lib/http.ts | 22 +++++++++++++++++++ 4 files changed, 53 insertions(+), 11 deletions(-) diff --git a/web/mail-search/app/api/case-context/route.test.ts b/web/mail-search/app/api/case-context/route.test.ts index 5f2cef9b..84bb4dd2 100644 --- a/web/mail-search/app/api/case-context/route.test.ts +++ b/web/mail-search/app/api/case-context/route.test.ts @@ -13,9 +13,9 @@ afterEach(() => { vi.restoreAllMocks(); }); -function req(params: Record) { +function req(params: Record, headers?: Record) { const search = new URLSearchParams(params).toString(); - return new NextRequest(`http://localhost/api/case-context?${search}`); + return new NextRequest(`http://localhost/api/case-context?${search}`, { headers }); } describe("GET /api/case-context", () => { @@ -45,6 +45,24 @@ describe("GET /api/case-context", () => { expect(res.status).toBe(502); }); + it("reicht mit Bearer nur das Token weiter und ignoriert user/tenant aus der URL", async () => { + global.fetch = vi.fn().mockResolvedValue({ ok: true, status: 200, json: async () => ({ Case: null, Notes: [], Reminders: [] }) }); + + const res = await GET(req({ module: "mail", type: "message", id: "m1", user: "fremd", tenant: "fremd" }, { authorization: "Bearer tok" })); + + expect(res.status).toBe(200); + expect(global.fetch).toHaveBeenCalledWith( + "http://workflow.internal:9700/objects/mail/message/m1/context", + { headers: { Authorization: "Bearer tok" } } + ); + }); + + it("akzeptiert mit Bearer fehlende user/tenant", async () => { + global.fetch = vi.fn().mockResolvedValue({ ok: true, status: 200, json: async () => ({ Case: null, Notes: [], Reminders: [] }) }); + const res = await GET(req({ module: "mail", type: "message", id: "m1" }, { authorization: "Bearer tok" })); + expect(res.status).toBe(200); + }); + it("liefert 400 bei fehlenden Pflichtparametern", async () => { const res = await GET(req({ module: "mail" })); expect(res.status).toBe(400); diff --git a/web/mail-search/app/api/case-context/route.ts b/web/mail-search/app/api/case-context/route.ts index 622b14e1..760a1c69 100644 --- a/web/mail-search/app/api/case-context/route.ts +++ b/web/mail-search/app/api/case-context/route.ts @@ -5,11 +5,9 @@ import { NextRequest, NextResponse } from "next/server"; // aufruft. Kein eigenes Akte-Datenmodell in Mail (Akzeptanzkriterium 2) // -- diese Route reicht die Antwort nur durch. // -// X-User-Id: uebergangsweise Identitaet wie bei caseapi/approvalapi/ -// backchannelapi selbst etabliert (kein zentrales IAM fuer Workflow- -// Zugriffe bisher). Bis zu einer zentralen Session-Anbindung traegt der -// Aufrufer die Nutzerkennung mit (gleiche Konvention wie tenantSlug in -// app/api/search/route.ts). +// Mit Authorization: Bearer (Core-Token, Audience workflow) wird nur das +// Token weitergereicht; X-User-Id/X-Tenant-Slug aus der URL gelten nur +// ohne Token (Uebergang). function backchannelBaseURL(): string { const base = process.env.WORKFLOW_BACKCHANNEL_URL; @@ -25,8 +23,10 @@ export async function GET(request: NextRequest) { const objectId = request.nextUrl.searchParams.get("id"); const userId = request.nextUrl.searchParams.get("user"); const tenantSlug = request.nextUrl.searchParams.get("tenant"); - if (!module || !objectType || !objectId || !userId || !tenantSlug) { - return NextResponse.json({ error: "'module', 'type', 'id', 'user' und 'tenant' sind erforderlich" }, { status: 400 }); + const auth = request.headers.get("authorization") ?? ""; + const bearer = auth.startsWith("Bearer ") && auth.length > 7 ? auth : null; + if (!module || !objectType || !objectId || (!bearer && (!userId || !tenantSlug))) { + return NextResponse.json({ error: "'module', 'type', 'id' sowie Bearer-Token oder 'user' und 'tenant' sind erforderlich" }, { status: 400 }); } let base: string; @@ -40,7 +40,7 @@ export async function GET(request: NextRequest) { try { upstream = await fetch( `${base}/objects/${encodeURIComponent(module)}/${encodeURIComponent(objectType)}/${encodeURIComponent(objectId)}/context`, - { headers: { "X-User-Id": userId, "X-Tenant-Slug": tenantSlug } } + { headers: bearer ? { Authorization: bearer } : { "X-User-Id": userId as string, "X-Tenant-Slug": tenantSlug as string } } ); } catch (err) { // Akzeptanzkriterium 3: Netzwerkfehler zum Workflow-Board wird als diff --git a/web/mail-search/lib/caseContext.ts b/web/mail-search/lib/caseContext.ts index 1496355d..9e91fb07 100644 --- a/web/mail-search/lib/caseContext.ts +++ b/web/mail-search/lib/caseContext.ts @@ -5,6 +5,8 @@ // (Akzeptanzkriterium 2) -- gibt die vom Workflow-Board gelieferte // Struktur unveraendert durch. +import { workflowFetch } from "./http"; + export interface CaseNote { ID: string; Text: string; @@ -42,7 +44,7 @@ export type CaseContextResult = export async function fetchCaseContext(module: string, objectType: string, objectId: string, tenant: string): Promise { let res: Response; try { - res = await fetch(`/api/case-context?module=${encodeURIComponent(module)}&type=${encodeURIComponent(objectType)}&id=${encodeURIComponent(objectId)}&tenant=${encodeURIComponent(tenant)}`); + res = await workflowFetch(`/api/case-context?module=${encodeURIComponent(module)}&type=${encodeURIComponent(objectType)}&id=${encodeURIComponent(objectId)}&tenant=${encodeURIComponent(tenant)}`); } catch { return { status: "unavailable" }; } diff --git a/web/mail-search/lib/http.ts b/web/mail-search/lib/http.ts index a37e3eab..98ff17b2 100644 --- a/web/mail-search/lib/http.ts +++ b/web/mail-search/lib/http.ts @@ -24,8 +24,30 @@ export function getSession(): Session { return session; } +let workflowSession: Session | undefined; + +export function workflowBearerEnabled(): boolean { + return authMode() === "token" && Boolean(process.env.NEXT_PUBLIC_CORE_API_BASE); +} + +export function getWorkflowSession(): Session { + if (!workflowSession) { + workflowSession = createSession({ + coreBase: process.env.NEXT_PUBLIC_CORE_API_BASE as string, + module: "workflow", + onLoginRequired: () => redirectToLogin(process.env.NEXT_PUBLIC_LOGIN_URL), + }); + } + return workflowSession; +} + +export function workflowFetch(input: string, init?: RequestInit): Promise { + return workflowBearerEnabled() ? getWorkflowSession().fetch(input, init) : fetch(input, init); +} + export function resetSessionForTests(): void { session = undefined; + workflowSession = undefined; } export function apiFetch(input: string, init?: RequestInit): Promise { -- 2.47.3