shl: gemeinsamer Bearer-Helfer fuer Frontends #417

Merged
patrick merged 1 commits from refactor/shl-bearer into main 2026-10-28 23:29:03 +01:00
25 changed files with 112 additions and 169 deletions

No files matched your search

@@ -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 });
}
+6 -19
View File
@@ -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<Response> {
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<Response> {
@@ -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;
@@ -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;
@@ -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;
@@ -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;
-6
View File
@@ -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;
}
+5 -28
View File
@@ -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<Response> {
return bearerEnabled() ? getSession().fetch(input, init) : fetch(input, init);
}
export async function sessionUserId(): Promise<string> {
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,
}));
+28
View File
@@ -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);
});
});
+42
View File
@@ -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<Response> =>
bearerEnabled() ? getSession().fetch(input, init) : fetch(input, init);
async function sessionUserId(): Promise<string> {
if (!bearerEnabled()) return "";
const s = getSession();
await s.token();
return s.identity()?.userId ?? "";
}
return { bearerEnabled, getSession, apiFetch, sessionUserId };
}
+2
View File
@@ -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";
@@ -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;
@@ -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;
-6
View File
@@ -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;
}
+5 -28
View File
@@ -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<Response> {
return bearerEnabled() ? getSession().fetch(input, init) : fetch(input, init);
}
export async function sessionUserId(): Promise<string> {
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,
}));
@@ -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;
+1 -1
View File
@@ -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;
@@ -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;
@@ -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;
-6
View File
@@ -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;
}
+5 -28
View File
@@ -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<Response> {
return bearerEnabled() ? getSession().fetch(input, init) : fetch(input, init);
}
export async function sessionUserId(): Promise<string> {
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,
}));
@@ -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;
+1 -1
View File
@@ -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;
-6
View File
@@ -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;
}
+5 -28
View File
@@ -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<Response> {
return bearerEnabled() ? getSession().fetch(input, init) : fetch(input, init);
}
export async function sessionUserId(): Promise<string> {
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,
}));