diff --git a/web/mail-search/app/api/case-context/route.ts b/web/mail-search/app/api/case-context/route.ts index 760a1c69..dbacfd5f 100644 --- a/web/mail-search/app/api/case-context/route.ts +++ b/web/mail-search/app/api/case-context/route.ts @@ -1,4 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; +import { bearerHeader } from "@nexarch/shl/auth/bearer"; // UX-09: Backend-for-Frontend-Route, die die externe Workflow- // Rueckkanal-API (VOR-04, GET /objects/{module}/{type}/{id}/context) @@ -23,8 +24,7 @@ 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"); - const auth = request.headers.get("authorization") ?? ""; - const bearer = auth.startsWith("Bearer ") && auth.length > 7 ? auth : null; + const bearer = bearerHeader(request); 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 }); } diff --git a/web/mail-search/lib/http.ts b/web/mail-search/lib/http.ts index 98ff17b2..a86e0481 100644 --- a/web/mail-search/lib/http.ts +++ b/web/mail-search/lib/http.ts @@ -1,7 +1,7 @@ // UX-03: Zugriff auf die Mail-API (mailapi, Bearer/Core-Modul-Token) mit // demselben Muster wie DOC-05/UX-10 im pdf-viewer (web/pdf-viewer/lib/http.ts) // -- zentral ueber @nexarch/shl (SHL-01), nicht neu erfunden. -import { createSession, redirectToLogin, type Session } from "@nexarch/shl"; +import { createBearerClient, createSession, redirectToLogin, type Session } from "@nexarch/shl"; export function authMode(): "token" | "caller" { return process.env.NEXT_PUBLIC_AUTH_MODE === "caller" ? "caller" : "token"; @@ -24,30 +24,17 @@ 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; -} +const workflow = createBearerClient("workflow", () => ({ + coreBase: process.env.NEXT_PUBLIC_CORE_API_BASE, + loginUrl: process.env.NEXT_PUBLIC_LOGIN_URL, +})); export function workflowFetch(input: string, init?: RequestInit): Promise { - return workflowBearerEnabled() ? getWorkflowSession().fetch(input, init) : fetch(input, init); + return authMode() === "token" ? workflow.apiFetch(input, init) : fetch(input, init); } export function resetSessionForTests(): void { session = undefined; - workflowSession = undefined; } export function apiFetch(input: string, init?: RequestInit): Promise { diff --git a/web/routing-rules/app/api/routing-rules/[id]/enabled/route.ts b/web/routing-rules/app/api/routing-rules/[id]/enabled/route.ts index b1e6e94c..e19d5ee4 100644 --- a/web/routing-rules/app/api/routing-rules/[id]/enabled/route.ts +++ b/web/routing-rules/app/api/routing-rules/[id]/enabled/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; -import { bearerHeader } from "../../../../../lib/forward"; +import { bearerHeader } from "@nexarch/shl/auth/bearer"; function backendURL(): string { const base = process.env.ROUTING_RULES_API_URL; diff --git a/web/routing-rules/app/api/routing-rules/[id]/priority/route.ts b/web/routing-rules/app/api/routing-rules/[id]/priority/route.ts index 7de54597..ab9c19f8 100644 --- a/web/routing-rules/app/api/routing-rules/[id]/priority/route.ts +++ b/web/routing-rules/app/api/routing-rules/[id]/priority/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; -import { bearerHeader } from "../../../../../lib/forward"; +import { bearerHeader } from "@nexarch/shl/auth/bearer"; function backendURL(): string { const base = process.env.ROUTING_RULES_API_URL; diff --git a/web/routing-rules/app/api/routing-rules/route.ts b/web/routing-rules/app/api/routing-rules/route.ts index da77f414..d89b525f 100644 --- a/web/routing-rules/app/api/routing-rules/route.ts +++ b/web/routing-rules/app/api/routing-rules/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; -import { bearerHeader } from "../../../lib/forward"; +import { bearerHeader } from "@nexarch/shl/auth/bearer"; function backendURL(): string { const base = process.env.ROUTING_RULES_API_URL; diff --git a/web/routing-rules/app/api/routing-rules/test/route.ts b/web/routing-rules/app/api/routing-rules/test/route.ts index 0045bc02..8dadd52a 100644 --- a/web/routing-rules/app/api/routing-rules/test/route.ts +++ b/web/routing-rules/app/api/routing-rules/test/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; -import { bearerHeader } from "../../../../lib/forward"; +import { bearerHeader } from "@nexarch/shl/auth/bearer"; function backendURL(): string { const base = process.env.ROUTING_RULES_API_URL; diff --git a/web/routing-rules/lib/forward.ts b/web/routing-rules/lib/forward.ts deleted file mode 100644 index 1a128bf9..00000000 --- a/web/routing-rules/lib/forward.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { NextRequest } from "next/server"; - -export function bearerHeader(request: NextRequest): string | null { - const auth = request.headers.get("authorization") ?? ""; - return auth.startsWith("Bearer ") && auth.length > 7 ? auth : null; -} diff --git a/web/routing-rules/lib/http.ts b/web/routing-rules/lib/http.ts index 46286a65..b709fafc 100644 --- a/web/routing-rules/lib/http.ts +++ b/web/routing-rules/lib/http.ts @@ -1,29 +1,6 @@ -import { createSession, redirectToLogin, type Session } from "@nexarch/shl"; +import { createBearerClient } from "@nexarch/shl"; -let session: Session | undefined; - -export function bearerEnabled(): boolean { - return Boolean(process.env.NEXT_PUBLIC_CORE_API_BASE); -} - -export function getSession(): Session { - if (!session) { - session = createSession({ - coreBase: process.env.NEXT_PUBLIC_CORE_API_BASE as string, - module: "workflow", - onLoginRequired: () => redirectToLogin(process.env.NEXT_PUBLIC_LOGIN_URL), - }); - } - return session; -} - -export function apiFetch(input: RequestInfo | URL, init?: RequestInit): Promise { - return bearerEnabled() ? getSession().fetch(input, init) : fetch(input, init); -} - -export async function sessionUserId(): Promise { - if (!bearerEnabled()) return ""; - const s = getSession(); - await s.token(); - return s.identity()?.userId ?? ""; -} +export const { bearerEnabled, getSession, apiFetch, sessionUserId } = createBearerClient("workflow", () => ({ + coreBase: process.env.NEXT_PUBLIC_CORE_API_BASE, + loginUrl: process.env.NEXT_PUBLIC_LOGIN_URL, +})); diff --git a/web/shl/__tests__/bearer.test.ts b/web/shl/__tests__/bearer.test.ts new file mode 100644 index 00000000..7b1f3280 --- /dev/null +++ b/web/shl/__tests__/bearer.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from "vitest"; +import { bearerHeader, createBearerClient } from "../auth/bearer"; + +const req = (auth?: string) => ({ headers: new Headers(auth ? { authorization: auth } : {}) }); + +describe("bearerHeader", () => { + it("liefert den Header bei Bearer mit Token", () => { + expect(bearerHeader(req("Bearer abc"))).toBe("Bearer abc"); + }); + it.each([undefined, "Bearer ", "Basic abc"])("liefert null bei %s", (h) => { + expect(bearerHeader(req(h))).toBeNull(); + }); +}); + +describe("createBearerClient", () => { + it("ist ohne coreBase aus und nutzt fetch ohne Sitzung", async () => { + const c = createBearerClient("workflow", () => ({})); + expect(c.bearerEnabled()).toBe(false); + expect(await c.sessionUserId()).toBe(""); + }); + it("ist mit coreBase an und liest die Konfiguration bei jedem Aufruf", () => { + let base: string | undefined; + const c = createBearerClient("workflow", () => ({ coreBase: base })); + expect(c.bearerEnabled()).toBe(false); + base = "https://core.example"; + expect(c.bearerEnabled()).toBe(true); + }); +}); diff --git a/web/shl/auth/bearer.ts b/web/shl/auth/bearer.ts new file mode 100644 index 00000000..252b49b6 --- /dev/null +++ b/web/shl/auth/bearer.ts @@ -0,0 +1,42 @@ +import { createSession, redirectToLogin, type Session } from "./session"; + +export function bearerHeader(request: { headers: { get(name: string): string | null } }): string | null { + const auth = request.headers.get("authorization") ?? ""; + return auth.startsWith("Bearer ") && auth.length > 7 ? auth : null; +} + +export interface BearerClientConfig { + coreBase?: string; + loginUrl?: string; +} + +// config ist eine Funktion, weil NEXT_PUBLIC_*-Werte erst beim Aufruf im Bundle der App stehen. +export function createBearerClient(module: string, config: () => BearerClientConfig) { + let session: Session | undefined; + + const bearerEnabled = (): boolean => Boolean(config().coreBase); + + function getSession(): Session { + if (!session) { + const { coreBase, loginUrl } = config(); + session = createSession({ + coreBase: coreBase as string, + module, + onLoginRequired: () => redirectToLogin(loginUrl), + }); + } + return session; + } + + const apiFetch = (input: RequestInfo | URL, init?: RequestInit): Promise => + bearerEnabled() ? getSession().fetch(input, init) : fetch(input, init); + + async function sessionUserId(): Promise { + if (!bearerEnabled()) return ""; + const s = getSession(); + await s.token(); + return s.identity()?.userId ?? ""; + } + + return { bearerEnabled, getSession, apiFetch, sessionUserId }; +} diff --git a/web/shl/index.ts b/web/shl/index.ts index 8e483df4..892700ee 100644 --- a/web/shl/index.ts +++ b/web/shl/index.ts @@ -31,6 +31,8 @@ export type { TextFieldProps, SelectFieldProps, CheckboxFieldProps } from "./com export { createSession, redirectToLogin, describeHttpError, authMessages, AuthError } from "./auth/session"; export type { Session, SessionOptions, Identity, IdentityLoader, AuthErrorKind } from "./auth/session"; +export { bearerHeader, createBearerClient } from "./auth/bearer"; +export type { BearerClientConfig } from "./auth/bearer"; export { LogoutButton } from "./auth/LogoutButton"; export type { LogoutButtonProps } from "./auth/LogoutButton"; diff --git a/web/workflow-approvals/app/api/approvals/[id]/decide/route.ts b/web/workflow-approvals/app/api/approvals/[id]/decide/route.ts index ea7d23d3..76b2af25 100644 --- a/web/workflow-approvals/app/api/approvals/[id]/decide/route.ts +++ b/web/workflow-approvals/app/api/approvals/[id]/decide/route.ts @@ -3,7 +3,7 @@ // unveraendert durch, damit die Oberflaeche 409 (bereits entschieden) und // 403 (nicht zustaendig) korrekt unterscheiden kann. import { NextRequest, NextResponse } from "next/server"; -import { bearerHeader } from "../../../../../lib/forward"; +import { bearerHeader } from "@nexarch/shl/auth/bearer"; function backendURL(): string { const base = process.env.WORKFLOW_APPROVALS_API_URL; diff --git a/web/workflow-approvals/app/api/approvals/route.ts b/web/workflow-approvals/app/api/approvals/route.ts index 9ffe98af..b723466c 100644 --- a/web/workflow-approvals/app/api/approvals/route.ts +++ b/web/workflow-approvals/app/api/approvals/route.ts @@ -4,7 +4,7 @@ // Session-Anbindung (auesserhalb dieser Kachel) gibt der Aufrufer sie // als Query-Parameter mit, wie tenantSlug in web/mail-search. import { NextRequest, NextResponse } from "next/server"; -import { bearerHeader } from "../../../lib/forward"; +import { bearerHeader } from "@nexarch/shl/auth/bearer"; function backendURL(): string { const base = process.env.WORKFLOW_APPROVALS_API_URL; diff --git a/web/workflow-approvals/lib/forward.ts b/web/workflow-approvals/lib/forward.ts deleted file mode 100644 index 1a128bf9..00000000 --- a/web/workflow-approvals/lib/forward.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { NextRequest } from "next/server"; - -export function bearerHeader(request: NextRequest): string | null { - const auth = request.headers.get("authorization") ?? ""; - return auth.startsWith("Bearer ") && auth.length > 7 ? auth : null; -} diff --git a/web/workflow-approvals/lib/http.ts b/web/workflow-approvals/lib/http.ts index 46286a65..b709fafc 100644 --- a/web/workflow-approvals/lib/http.ts +++ b/web/workflow-approvals/lib/http.ts @@ -1,29 +1,6 @@ -import { createSession, redirectToLogin, type Session } from "@nexarch/shl"; +import { createBearerClient } from "@nexarch/shl"; -let session: Session | undefined; - -export function bearerEnabled(): boolean { - return Boolean(process.env.NEXT_PUBLIC_CORE_API_BASE); -} - -export function getSession(): Session { - if (!session) { - session = createSession({ - coreBase: process.env.NEXT_PUBLIC_CORE_API_BASE as string, - module: "workflow", - onLoginRequired: () => redirectToLogin(process.env.NEXT_PUBLIC_LOGIN_URL), - }); - } - return session; -} - -export function apiFetch(input: RequestInfo | URL, init?: RequestInit): Promise { - return bearerEnabled() ? getSession().fetch(input, init) : fetch(input, init); -} - -export async function sessionUserId(): Promise { - if (!bearerEnabled()) return ""; - const s = getSession(); - await s.token(); - return s.identity()?.userId ?? ""; -} +export const { bearerEnabled, getSession, apiFetch, sessionUserId } = createBearerClient("workflow", () => ({ + coreBase: process.env.NEXT_PUBLIC_CORE_API_BASE, + loginUrl: process.env.NEXT_PUBLIC_LOGIN_URL, +})); diff --git a/web/workflow-cases/app/api/cases/[id]/overview/route.ts b/web/workflow-cases/app/api/cases/[id]/overview/route.ts index d4c4c494..b4e7872a 100644 --- a/web/workflow-cases/app/api/cases/[id]/overview/route.ts +++ b/web/workflow-cases/app/api/cases/[id]/overview/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; -import { bearerHeader } from "../../../../../lib/forward"; +import { bearerHeader } from "@nexarch/shl/auth/bearer"; function backendURL(): string { const base = process.env.WORKFLOW_CASES_API_URL; diff --git a/web/workflow-cases/app/api/cases/route.ts b/web/workflow-cases/app/api/cases/route.ts index 4ec10b6f..6931b8c6 100644 --- a/web/workflow-cases/app/api/cases/route.ts +++ b/web/workflow-cases/app/api/cases/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; -import { bearerHeader } from "../../../lib/forward"; +import { bearerHeader } from "@nexarch/shl/auth/bearer"; function backendURL(): string { const base = process.env.WORKFLOW_CASES_API_URL; diff --git a/web/workflow-cases/app/api/reminders/[id]/move/route.ts b/web/workflow-cases/app/api/reminders/[id]/move/route.ts index 22e03046..9ad2621b 100644 --- a/web/workflow-cases/app/api/reminders/[id]/move/route.ts +++ b/web/workflow-cases/app/api/reminders/[id]/move/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; -import { bearerHeader } from "../../../../../lib/forward"; +import { bearerHeader } from "@nexarch/shl/auth/bearer"; function backendURL(): string { const base = process.env.WORKFLOW_CASES_API_URL; diff --git a/web/workflow-cases/app/api/reminders/kanban/route.ts b/web/workflow-cases/app/api/reminders/kanban/route.ts index c6fcd7b7..36a3dc7d 100644 --- a/web/workflow-cases/app/api/reminders/kanban/route.ts +++ b/web/workflow-cases/app/api/reminders/kanban/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; -import { bearerHeader } from "../../../../lib/forward"; +import { bearerHeader } from "@nexarch/shl/auth/bearer"; function backendURL(): string { const base = process.env.WORKFLOW_CASES_API_URL; diff --git a/web/workflow-cases/lib/forward.ts b/web/workflow-cases/lib/forward.ts deleted file mode 100644 index 1a128bf9..00000000 --- a/web/workflow-cases/lib/forward.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { NextRequest } from "next/server"; - -export function bearerHeader(request: NextRequest): string | null { - const auth = request.headers.get("authorization") ?? ""; - return auth.startsWith("Bearer ") && auth.length > 7 ? auth : null; -} diff --git a/web/workflow-cases/lib/http.ts b/web/workflow-cases/lib/http.ts index 46286a65..b709fafc 100644 --- a/web/workflow-cases/lib/http.ts +++ b/web/workflow-cases/lib/http.ts @@ -1,29 +1,6 @@ -import { createSession, redirectToLogin, type Session } from "@nexarch/shl"; +import { createBearerClient } from "@nexarch/shl"; -let session: Session | undefined; - -export function bearerEnabled(): boolean { - return Boolean(process.env.NEXT_PUBLIC_CORE_API_BASE); -} - -export function getSession(): Session { - if (!session) { - session = createSession({ - coreBase: process.env.NEXT_PUBLIC_CORE_API_BASE as string, - module: "workflow", - onLoginRequired: () => redirectToLogin(process.env.NEXT_PUBLIC_LOGIN_URL), - }); - } - return session; -} - -export function apiFetch(input: RequestInfo | URL, init?: RequestInit): Promise { - return bearerEnabled() ? getSession().fetch(input, init) : fetch(input, init); -} - -export async function sessionUserId(): Promise { - if (!bearerEnabled()) return ""; - const s = getSession(); - await s.token(); - return s.identity()?.userId ?? ""; -} +export const { bearerEnabled, getSession, apiFetch, sessionUserId } = createBearerClient("workflow", () => ({ + coreBase: process.env.NEXT_PUBLIC_CORE_API_BASE, + loginUrl: process.env.NEXT_PUBLIC_LOGIN_URL, +})); diff --git a/web/workflow-tasks/app/api/tasks/[id]/status/route.ts b/web/workflow-tasks/app/api/tasks/[id]/status/route.ts index a93514c4..beeb3e52 100644 --- a/web/workflow-tasks/app/api/tasks/[id]/status/route.ts +++ b/web/workflow-tasks/app/api/tasks/[id]/status/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; -import { bearerHeader } from "../../../../../lib/forward"; +import { bearerHeader } from "@nexarch/shl/auth/bearer"; function backendURL(): string { const base = process.env.WORKFLOW_TASKS_API_URL; diff --git a/web/workflow-tasks/app/api/tasks/route.ts b/web/workflow-tasks/app/api/tasks/route.ts index 65a0570f..cd3c090f 100644 --- a/web/workflow-tasks/app/api/tasks/route.ts +++ b/web/workflow-tasks/app/api/tasks/route.ts @@ -3,7 +3,7 @@ // (app/api/case-context/route.ts) als Query-Parameter mitgegeben, bis zu // einer zentralen Session-Anbindung. import { NextRequest, NextResponse } from "next/server"; -import { bearerHeader } from "../../../lib/forward"; +import { bearerHeader } from "@nexarch/shl/auth/bearer"; function backendURL(): string { const base = process.env.WORKFLOW_TASKS_API_URL; diff --git a/web/workflow-tasks/lib/forward.ts b/web/workflow-tasks/lib/forward.ts deleted file mode 100644 index 1a128bf9..00000000 --- a/web/workflow-tasks/lib/forward.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { NextRequest } from "next/server"; - -export function bearerHeader(request: NextRequest): string | null { - const auth = request.headers.get("authorization") ?? ""; - return auth.startsWith("Bearer ") && auth.length > 7 ? auth : null; -} diff --git a/web/workflow-tasks/lib/http.ts b/web/workflow-tasks/lib/http.ts index 46286a65..b709fafc 100644 --- a/web/workflow-tasks/lib/http.ts +++ b/web/workflow-tasks/lib/http.ts @@ -1,29 +1,6 @@ -import { createSession, redirectToLogin, type Session } from "@nexarch/shl"; +import { createBearerClient } from "@nexarch/shl"; -let session: Session | undefined; - -export function bearerEnabled(): boolean { - return Boolean(process.env.NEXT_PUBLIC_CORE_API_BASE); -} - -export function getSession(): Session { - if (!session) { - session = createSession({ - coreBase: process.env.NEXT_PUBLIC_CORE_API_BASE as string, - module: "workflow", - onLoginRequired: () => redirectToLogin(process.env.NEXT_PUBLIC_LOGIN_URL), - }); - } - return session; -} - -export function apiFetch(input: RequestInfo | URL, init?: RequestInit): Promise { - return bearerEnabled() ? getSession().fetch(input, init) : fetch(input, init); -} - -export async function sessionUserId(): Promise { - if (!bearerEnabled()) return ""; - const s = getSession(); - await s.token(); - return s.identity()?.userId ?? ""; -} +export const { bearerEnabled, getSession, apiFetch, sessionUserId } = createBearerClient("workflow", () => ({ + coreBase: process.env.NEXT_PUBLIC_CORE_API_BASE, + loginUrl: process.env.NEXT_PUBLIC_LOGIN_URL, +}));