Merge pull request 'mail-search: Akte-Kontext per Bearer' (#416) from feat/mailsearch-bearer into main
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (push) Successful in 39s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Successful in 2s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (push) Successful in 2s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (push) Failing after 13s
CI / Web (dms-browser) (push) Failing after 48s
CI / Web (pdf-viewer) (push) Failing after 38s
CI / Web (shl) (push) Failing after 8s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Failing after 5s

This commit was merged in pull request #416.
This commit is contained in:
patrick committed 2026-10-28 22:16:00 +01:00
commit ddda4b93a7
4 files changed
+53 -11

No files matched your search

@@ -13,9 +13,9 @@ afterEach(() => {
vi.restoreAllMocks();
});
function req(params: Record<string, string>) {
function req(params: Record<string, string>, headers?: Record<string, string>) {
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);
@@ -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
+3 -1
View File
@@ -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<CaseContextResult> {
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" };
}
+22
View File
@@ -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<Response> {
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<Response> {