diff --git a/connect/cmd/connect-app/main.go b/connect/cmd/connect-app/main.go index d8e806f4..7745b7b5 100644 --- a/connect/cmd/connect-app/main.go +++ b/connect/cmd/connect-app/main.go @@ -15,7 +15,9 @@ import ( "github.com/jackc/pgx/v5/pgxpool" + "gitea.perlbach24.de/scripte/nexarch/common/moduleauth" "gitea.perlbach24.de/scripte/nexarch/common/policyclient" + "gitea.perlbach24.de/scripte/nexarch/connect/internal/bearerauth" "gitea.perlbach24.de/scripte/nexarch/connect/internal/connectorsetupapi" "gitea.perlbach24.de/scripte/nexarch/connect/internal/credential" "gitea.perlbach24.de/scripte/nexarch/connect/internal/crypto" @@ -84,6 +86,8 @@ func main() { } defer pool.Close() + verifier, allowHeaderAuth := buildIdentity() + mappings := fieldmapping.NewStore(pool) credentials := credential.NewStore(pool, crypto.NewService(fixedKEK{kek: kek})) @@ -97,7 +101,7 @@ func main() { srv := &http.Server{ Addr: addr, - Handler: mux, + Handler: bearerauth.Middleware(verifier, allowHeaderAuth, mux), ReadHeaderTimeout: 5 * time.Second, ReadTimeout: 15 * time.Second, WriteTimeout: 15 * time.Second, @@ -119,3 +123,36 @@ func main() { defer cancel() _ = srv.Shutdown(shutdownCtx) } + +// buildIdentity richtet die Token-Pruefung ein. Ohne NEXARCH_CONNECT_APP_JWKS_URL +// muss der Uebergangsschalter NEXARCH_CONNECT_APP_ALLOW_HEADER_AUTH=1 gesetzt +// sein; sonst startet der Dienst nicht. Die Datenbank gehoert einem Mandanten, +// daher ist NEXARCH_CONNECT_APP_TENANT_SLUG mit JWKS Pflicht. +func buildIdentity() (*moduleauth.Verifier, bool) { + allowHeader := os.Getenv("NEXARCH_CONNECT_APP_ALLOW_HEADER_AUTH") == "1" + jwksURL := os.Getenv("NEXARCH_CONNECT_APP_JWKS_URL") + if allowHeader { + log.Print("connect-app: WARNUNG: X-User-Id/X-Tenant-Slug ohne Token zugelassen (NEXARCH_CONNECT_APP_ALLOW_HEADER_AUTH=1), nicht authentifiziert") + } + if jwksURL == "" { + if !allowHeader { + log.Fatal("connect-app: NEXARCH_CONNECT_APP_JWKS_URL fehlt (oder NEXARCH_CONNECT_APP_ALLOW_HEADER_AUTH=1 als Uebergang setzen)") + } + return nil, allowHeader + } + tenantSlug := os.Getenv("NEXARCH_CONNECT_APP_TENANT_SLUG") + if tenantSlug == "" { + log.Fatal("connect-app: NEXARCH_CONNECT_APP_TENANT_SLUG ist mit NEXARCH_CONNECT_APP_JWKS_URL erforderlich") + } + audience := os.Getenv("NEXARCH_CONNECT_APP_JWT_AUDIENCE") + if audience == "" { + audience = "connect" + } + issuer := os.Getenv("NEXARCH_CONNECT_APP_JWT_ISSUER") + if issuer == "" { + issuer = "nexarch-core" + } + enforce := os.Getenv("NEXARCH_CONNECT_APP_JWT_AUDIENCE_ENFORCE") != "false" + v := moduleauth.NewVerifier(5*time.Minute, tenantSlug, moduleauth.HTTPKeyFetcher(jwksURL, nil)).Expect(audience, issuer, enforce) + return v, allowHeader +} diff --git a/connect/go.mod b/connect/go.mod index 1a1ea13c..d6c45234 100644 --- a/connect/go.mod +++ b/connect/go.mod @@ -5,7 +5,7 @@ go 1.25.0 toolchain go1.25.13 require ( - gitea.perlbach24.de/scripte/nexarch/common v0.2.0 + gitea.perlbach24.de/scripte/nexarch/common v0.2.1 github.com/jackc/pgx/v5 v5.9.2 golang.org/x/text v0.29.0 ) diff --git a/connect/go.sum b/connect/go.sum index 9ca36777..c33711d0 100644 --- a/connect/go.sum +++ b/connect/go.sum @@ -1,5 +1,5 @@ -gitea.perlbach24.de/scripte/nexarch/common v0.2.0 h1:gcOjoOvUkT0Zmqq3m7WQp1MfXSgmzrGTF2E/+NkO2bw= -gitea.perlbach24.de/scripte/nexarch/common v0.2.0/go.mod h1:CiEImG++QMt1xZqSMeDJhPiB7sfWTLZOwjS6OY/Iguc= +gitea.perlbach24.de/scripte/nexarch/common v0.2.1 h1:vIreSocK/F4+uid+lZ7u0CSNlPFvA1N7A77k+RhNeAc= +gitea.perlbach24.de/scripte/nexarch/common v0.2.1/go.mod h1:CiEImG++QMt1xZqSMeDJhPiB7sfWTLZOwjS6OY/Iguc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/connect/internal/bearerauth/bearerauth.go b/connect/internal/bearerauth/bearerauth.go new file mode 100644 index 00000000..bea3e87b --- /dev/null +++ b/connect/internal/bearerauth/bearerauth.go @@ -0,0 +1,71 @@ +// Package bearerauth setzt die Identitaet der Connect-APIs aus einem +// verifizierten Core-Modul-Token. Die Handler lesen weiter X-User-Id und +// X-Tenant-Slug; die Middleware ueberschreibt beide aus den Claims, sodass ein +// Client sie nicht frei setzen kann. X-User-Role kommt nur aus dem Token. +package bearerauth + +import ( + "encoding/json" + "errors" + "net/http" + "strings" + + "gitea.perlbach24.de/scripte/nexarch/common/moduleauth" +) + +const ( + headerUser = "X-User-Id" + headerTenant = "X-Tenant-Slug" + headerRole = "X-User-Role" +) + +// Middleware verlangt fuer Anfragen mit Authorization: Bearer ein gueltiges +// Token. Ohne Bearer werden X-User-Id und X-Tenant-Slug entfernt, ausser +// allowHeaderAuth ist gesetzt (Uebergang, bis alle Clients Bearer senden). +func Middleware(v *moduleauth.Verifier, allowHeaderAuth bool, next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + r.Header.Del(headerRole) + token, ok := bearer(r) + if !ok { + if !allowHeaderAuth { + r.Header.Del(headerUser) + r.Header.Del(headerTenant) + } + next.ServeHTTP(w, r) + return + } + if v == nil { + writeError(w, http.StatusUnauthorized, "nicht angemeldet") + return + } + claims, err := v.Verify(r.Context(), token) + if err != nil { + if errors.Is(err, moduleauth.ErrInvalidToken) { + writeError(w, http.StatusUnauthorized, "nicht angemeldet") + } else { + writeError(w, http.StatusServiceUnavailable, "identitaetsdienst nicht erreichbar") + } + return + } + r.Header.Set(headerUser, claims.Subject) + r.Header.Set(headerTenant, claims.Tenant) + if claims.Role != "" { + r.Header.Set(headerRole, claims.Role) + } + next.ServeHTTP(w, r) + }) +} + +func bearer(r *http.Request) (string, bool) { + h := r.Header.Get("Authorization") + if len(h) > 7 && strings.EqualFold(h[:7], "bearer ") { + return strings.TrimSpace(h[7:]), true + } + return "", false +} + +func writeError(w http.ResponseWriter, status int, message string) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + _ = json.NewEncoder(w).Encode(map[string]string{"error": message}) +} diff --git a/connect/internal/bearerauth/bearerauth_test.go b/connect/internal/bearerauth/bearerauth_test.go new file mode 100644 index 00000000..685f2fbe --- /dev/null +++ b/connect/internal/bearerauth/bearerauth_test.go @@ -0,0 +1,75 @@ +package bearerauth + +import ( + "net/http" + "net/http/httptest" + "testing" + + "gitea.perlbach24.de/scripte/nexarch/common/moduleauth" +) + +func echo() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte(r.Header.Get("X-User-Id") + "|" + r.Header.Get("X-Tenant-Slug") + "|" + r.Header.Get("X-User-Role"))) + }) +} + +func do(h http.Handler, hdr map[string]string) *httptest.ResponseRecorder { + req := httptest.NewRequest(http.MethodGet, "/x", nil) + for k, v := range hdr { + req.Header.Set(k, v) + } + rec := httptest.NewRecorder() + h.ServeHTTP(rec, req) + return rec +} + +func TestBearerOverridesClientSuppliedHeaders(t *testing.T) { + v, iss := moduleauth.NewTestVerifierAndIssuer() + h := Middleware(v, false, echo()) + rec := do(h, map[string]string{ + "Authorization": "Bearer " + iss.Token("u1", "acme", "user", "", ""), + "X-User-Id": "fremd", "X-Tenant-Slug": "fremd", + }) + if rec.Code != 200 || rec.Body.String() != "u1|acme|user" { + t.Fatalf("code=%d body=%q", rec.Code, rec.Body.String()) + } +} + +func TestInvalidBearerIs401(t *testing.T) { + v, _ := moduleauth.NewTestVerifierAndIssuer() + _, other := moduleauth.NewTestVerifierAndIssuer() + h := Middleware(v, true, echo()) + if rec := do(h, map[string]string{"Authorization": "Bearer " + other.Token("u1", "acme", "user", "", ""), "X-User-Id": "evil"}); rec.Code != 401 { + t.Fatalf("code=%d", rec.Code) + } + if rec := do(Middleware(nil, true, echo()), map[string]string{"Authorization": "Bearer x.y.z"}); rec.Code != 401 { + t.Fatalf("ohne Verifier code=%d", rec.Code) + } +} + +func TestWithoutBearerHeadersStrippedUnlessAllowed(t *testing.T) { + v, _ := moduleauth.NewTestVerifierAndIssuer() + hdr := map[string]string{"X-User-Id": "u9", "X-Tenant-Slug": "t9"} + if rec := do(Middleware(v, false, echo()), hdr); rec.Body.String() != "||" { + t.Fatalf("Header muessen entfernt werden, bekam %q", rec.Body.String()) + } + if rec := do(Middleware(v, true, echo()), hdr); rec.Body.String() != "u9|t9|" { + t.Fatalf("Uebergangsmodus muss Header durchlassen, bekam %q", rec.Body.String()) + } +} + +func TestRoleOnlyFromToken(t *testing.T) { + v, iss := moduleauth.NewTestVerifierAndIssuer() + h := Middleware(v, true, echo()) + rec := do(h, map[string]string{ + "Authorization": "Bearer " + iss.Token("u1", "acme", "tenant_admin", "", ""), + "X-User-Role": "tenant_admin_fremd", + }) + if rec.Body.String() != "u1|acme|tenant_admin" { + t.Fatalf("Rolle muss aus dem Token kommen, bekam %q", rec.Body.String()) + } + if rec := do(h, map[string]string{"X-User-Role": "tenant_admin"}); rec.Body.String() != "||" { + t.Fatalf("Rolle ohne Token muss entfernt werden, bekam %q", rec.Body.String()) + } +} diff --git a/web/connect-setup/lib/api.ts b/web/connect-setup/lib/api.ts index 8e73860d..be1b5d21 100644 --- a/web/connect-setup/lib/api.ts +++ b/web/connect-setup/lib/api.ts @@ -4,6 +4,8 @@ // Core-JWT-Anbindung) aus Umgebungsvariablen, die als Header durchgereicht // werden. +import { apiFetch, bearerEnabled } from "./http"; + export class UnauthorizedError extends Error {} export class ApiError extends Error {} @@ -16,6 +18,7 @@ function apiBase(): string { } function identityHeaders(): Record { + if (bearerEnabled()) return {}; const userID = process.env.NEXT_PUBLIC_USER_ID; const tenant = process.env.NEXT_PUBLIC_TENANT_SLUG; const headers: Record = {}; @@ -25,7 +28,7 @@ function identityHeaders(): Record { } async function req(path: string, init?: RequestInit): Promise { - const res = await fetch(`${apiBase()}${path}`, { + const res = await apiFetch(`${apiBase()}${path}`, { ...init, headers: { ...identityHeaders(), diff --git a/web/connect-setup/lib/http.ts b/web/connect-setup/lib/http.ts new file mode 100644 index 00000000..fec19b2a --- /dev/null +++ b/web/connect-setup/lib/http.ts @@ -0,0 +1,6 @@ +import { createBearerClient } from "@nexarch/shl"; + +export const { bearerEnabled, getSession, apiFetch, sessionUserId } = createBearerClient("connect", () => ({ + coreBase: process.env.NEXT_PUBLIC_CORE_API_BASE, + loginUrl: process.env.NEXT_PUBLIC_LOGIN_URL, +})); diff --git a/web/connect-status/lib/api.ts b/web/connect-status/lib/api.ts index c9028255..03e668c6 100644 --- a/web/connect-status/lib/api.ts +++ b/web/connect-status/lib/api.ts @@ -3,6 +3,8 @@ // bei workflow/approvalapi vor der zentralen Core-JWT-Anbindung) aus // Umgebungsvariablen, die als Header durchgereicht werden. +import { apiFetch, bearerEnabled } from "./http"; + export class UnauthorizedError extends Error {} export class ApiError extends Error {} @@ -15,6 +17,7 @@ function apiBase(): string { } function identityHeaders(): Record { + if (bearerEnabled()) return {}; const userID = process.env.NEXT_PUBLIC_USER_ID; const tenant = process.env.NEXT_PUBLIC_TENANT_SLUG; const headers: Record = {}; @@ -24,7 +27,7 @@ function identityHeaders(): Record { } async function req(path: string, init?: RequestInit): Promise { - const res = await fetch(`${apiBase()}${path}`, { + const res = await apiFetch(`${apiBase()}${path}`, { ...init, headers: { ...identityHeaders(), diff --git a/web/connect-status/lib/http.ts b/web/connect-status/lib/http.ts new file mode 100644 index 00000000..fec19b2a --- /dev/null +++ b/web/connect-status/lib/http.ts @@ -0,0 +1,6 @@ +import { createBearerClient } from "@nexarch/shl"; + +export const { bearerEnabled, getSession, apiFetch, sessionUserId } = createBearerClient("connect", () => ({ + coreBase: process.env.NEXT_PUBLIC_CORE_API_BASE, + loginUrl: process.env.NEXT_PUBLIC_LOGIN_URL, +})); diff --git a/web/datev-export/lib/api.ts b/web/datev-export/lib/api.ts index b7a30ccd..34d91457 100644 --- a/web/datev-export/lib/api.ts +++ b/web/datev-export/lib/api.ts @@ -3,6 +3,8 @@ // uebergangsweise (wie bei web/connect-status) aus Umgebungsvariablen, // die als Header durchgereicht werden. +import { apiFetch, bearerEnabled } from "./http"; + export class UnauthorizedError extends Error {} export class ApiError extends Error {} @@ -15,6 +17,7 @@ function apiBase(): string { } function identityHeaders(): Record { + if (bearerEnabled()) return {}; const userID = process.env.NEXT_PUBLIC_USER_ID; const tenant = process.env.NEXT_PUBLIC_TENANT_SLUG; const headers: Record = {}; @@ -24,7 +27,7 @@ function identityHeaders(): Record { } async function req(path: string, init?: RequestInit): Promise { - const res = await fetch(`${apiBase()}${path}`, { + const res = await apiFetch(`${apiBase()}${path}`, { ...init, headers: { "Content-Type": "application/json", diff --git a/web/datev-export/lib/http.ts b/web/datev-export/lib/http.ts new file mode 100644 index 00000000..fec19b2a --- /dev/null +++ b/web/datev-export/lib/http.ts @@ -0,0 +1,6 @@ +import { createBearerClient } from "@nexarch/shl"; + +export const { bearerEnabled, getSession, apiFetch, sessionUserId } = createBearerClient("connect", () => ({ + coreBase: process.env.NEXT_PUBLIC_CORE_API_BASE, + loginUrl: process.env.NEXT_PUBLIC_LOGIN_URL, +})); diff --git a/web/dms2-import/app/api/dms2-import/bearer.route.test.ts b/web/dms2-import/app/api/dms2-import/bearer.route.test.ts new file mode 100644 index 00000000..cae51277 --- /dev/null +++ b/web/dms2-import/app/api/dms2-import/bearer.route.test.ts @@ -0,0 +1,63 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { NextRequest } from "next/server"; +import { POST as preview } from "./preview/route"; +import { POST as execute } from "./execute/route"; +import { GET as getRun } from "./runs/[id]/route"; + +const ORIGINAL_ENV = process.env; + +beforeEach(() => { + process.env = { ...ORIGINAL_ENV, DMS2_IMPORT_API_URL: "http://connect.internal:9500" }; + global.fetch = vi.fn().mockResolvedValue({ ok: true, status: 200, json: async () => ({}) }); +}); + +afterEach(() => { + process.env = ORIGINAL_ENV; + vi.restoreAllMocks(); +}); + +function post(path: string, query: string, headers: Record = {}) { + const form = new FormData(); + form.append("file", new Blob(["x"]), "x.json"); + return new NextRequest(`http://localhost/api/dms2-import/${path}${query}`, { method: "POST", body: form, headers }); +} + +function sentHeaders(): Record { + return (vi.mocked(global.fetch).mock.calls[0][1] as { headers: Record }).headers; +} + +describe.each([ + ["preview", preview], + ["execute", execute], +])("POST %s", (path, handler) => { + it("reicht mit Bearer nur das Token weiter und ignoriert userId/tenant aus der URL", async () => { + const res = await handler(post(path, "?userId=fremd&tenant=fremd", { authorization: "Bearer tok" })); + expect(res.status).toBe(200); + expect(sentHeaders()).toEqual({ Authorization: "Bearer tok" }); + }); + + it("verlangt ohne Bearer userId und tenant", async () => { + const res = await handler(post(path, "")); + expect(res.status).toBe(400); + }); + + it("sendet ohne Bearer die Kopfzeilen aus der URL", async () => { + await handler(post(path, "?userId=u1&tenant=acme")); + expect(sentHeaders()).toEqual({ "X-User-Id": "u1", "X-Tenant-Slug": "acme" }); + }); +}); + +describe("GET runs/[id]", () => { + const ctx = { params: Promise.resolve({ id: "r1" }) }; + + it("reicht mit Bearer nur das Token weiter", async () => { + const req = new NextRequest("http://localhost/api/dms2-import/runs/r1?userId=fremd&tenant=fremd", { headers: { authorization: "Bearer tok" } }); + expect((await getRun(req, ctx)).status).toBe(200); + expect(sentHeaders()).toEqual({ Authorization: "Bearer tok" }); + }); + + it("verlangt ohne Bearer userId und tenant", async () => { + const res = await getRun(new NextRequest("http://localhost/api/dms2-import/runs/r1"), ctx); + expect(res.status).toBe(400); + }); +}); diff --git a/web/dms2-import/app/api/dms2-import/execute/route.ts b/web/dms2-import/app/api/dms2-import/execute/route.ts index fdcb1613..1eed5956 100644 --- a/web/dms2-import/app/api/dms2-import/execute/route.ts +++ b/web/dms2-import/app/api/dms2-import/execute/route.ts @@ -1,4 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; +import { bearerHeader } from "@nexarch/shl/auth/bearer"; function backendURL(): string { const base = process.env.DMS2_IMPORT_API_URL; @@ -16,8 +17,9 @@ function requireIdentity(request: NextRequest): { userId: string; tenant: string } export async function POST(request: NextRequest) { - const identity = requireIdentity(request); - if (!identity) { + const bearer = bearerHeader(request); + const identity = bearer ? null : requireIdentity(request); + if (!bearer && !identity) { return NextResponse.json({ error: "'userId' und 'tenant' sind erforderlich" }, { status: 400 }); } const form = await request.formData(); @@ -25,7 +27,7 @@ export async function POST(request: NextRequest) { try { backendResponse = await fetch(`${backendURL()}/dms2/import/execute`, { method: "POST", - headers: { "X-User-Id": identity.userId, "X-Tenant-Slug": identity.tenant }, + headers: bearer ? { Authorization: bearer } : { "X-User-Id": (identity as { userId: string }).userId, "X-Tenant-Slug": (identity as { tenant: string }).tenant }, body: form, cache: "no-store", }); diff --git a/web/dms2-import/app/api/dms2-import/preview/route.ts b/web/dms2-import/app/api/dms2-import/preview/route.ts index b64929ab..6803d107 100644 --- a/web/dms2-import/app/api/dms2-import/preview/route.ts +++ b/web/dms2-import/app/api/dms2-import/preview/route.ts @@ -1,4 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; +import { bearerHeader } from "@nexarch/shl/auth/bearer"; function backendURL(): string { const base = process.env.DMS2_IMPORT_API_URL; @@ -16,8 +17,9 @@ function requireIdentity(request: NextRequest): { userId: string; tenant: string } export async function POST(request: NextRequest) { - const identity = requireIdentity(request); - if (!identity) { + const bearer = bearerHeader(request); + const identity = bearer ? null : requireIdentity(request); + if (!bearer && !identity) { return NextResponse.json({ error: "'userId' und 'tenant' sind erforderlich" }, { status: 400 }); } const form = await request.formData(); @@ -25,7 +27,7 @@ export async function POST(request: NextRequest) { try { backendResponse = await fetch(`${backendURL()}/dms2/import/preview`, { method: "POST", - headers: { "X-User-Id": identity.userId, "X-Tenant-Slug": identity.tenant }, + headers: bearer ? { Authorization: bearer } : { "X-User-Id": (identity as { userId: string }).userId, "X-Tenant-Slug": (identity as { tenant: string }).tenant }, body: form, cache: "no-store", }); diff --git a/web/dms2-import/app/api/dms2-import/runs/[id]/route.ts b/web/dms2-import/app/api/dms2-import/runs/[id]/route.ts index 7943b9b7..27cd3cb4 100644 --- a/web/dms2-import/app/api/dms2-import/runs/[id]/route.ts +++ b/web/dms2-import/app/api/dms2-import/runs/[id]/route.ts @@ -1,4 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; +import { bearerHeader } from "@nexarch/shl/auth/bearer"; function backendURL(): string { const base = process.env.DMS2_IMPORT_API_URL; @@ -12,13 +13,14 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ const { id } = await params; const userId = request.nextUrl.searchParams.get("userId"); const tenant = request.nextUrl.searchParams.get("tenant"); - if (!userId || !tenant) { + const bearer = bearerHeader(request); + if (!bearer && (!userId || !tenant)) { return NextResponse.json({ error: "'userId' und 'tenant' sind erforderlich" }, { status: 400 }); } let backendResponse: Response; try { backendResponse = await fetch(`${backendURL()}/dms2/import/runs/${encodeURIComponent(id)}`, { - headers: { "X-User-Id": userId, "X-Tenant-Slug": tenant }, + headers: bearer ? { Authorization: bearer } : { "X-User-Id": userId as string, "X-Tenant-Slug": tenant as string }, cache: "no-store", }); } catch (err) { diff --git a/web/dms2-import/app/page.tsx b/web/dms2-import/app/page.tsx index b26fcd1a..303ba4fa 100644 --- a/web/dms2-import/app/page.tsx +++ b/web/dms2-import/app/page.tsx @@ -1,14 +1,23 @@ "use client"; -import { Suspense } from "react"; +import { Suspense, useEffect, useState } from "react"; import { useSearchParams } from "next/navigation"; import { TextField } from "@nexarch/shl"; import { ImportAssistant } from "./ImportAssistant"; +import { bearerEnabled, getSession } from "../lib/http"; function ImportPage() { const params = useSearchParams(); - const userId = params.get("user") ?? ""; - const tenant = params.get("tenant") ?? ""; + const [sessionIdentity, setSessionIdentity] = useState({ userId: "", tenant: "" }); + useEffect(() => { + if (!bearerEnabled()) return; + const s = getSession(); + s.token() + .then(() => setSessionIdentity({ userId: s.identity()?.userId ?? "", tenant: s.identity()?.tenant ?? "" })) + .catch(() => {}); + }, []); + const userId = bearerEnabled() ? sessionIdentity.userId : (params.get("user") ?? ""); + const tenant = bearerEnabled() ? sessionIdentity.tenant : (params.get("tenant") ?? ""); return (
diff --git a/web/dms2-import/lib/api.ts b/web/dms2-import/lib/api.ts index 002016e1..c257b1af 100644 --- a/web/dms2-import/lib/api.ts +++ b/web/dms2-import/lib/api.ts @@ -2,6 +2,8 @@ // app/api — kein direkter Zugriff auf die Connect-API vom Client aus, // gleiches Muster wie web/routing-rules/web/workflow-cases. +import { apiFetch } from "./http"; + export class ApiError extends Error { status: number; constructor(message: string, status: number) { @@ -50,7 +52,7 @@ async function handle(res: Response): Promise { export async function previewImport(userId: string, tenant: string, file: File): Promise { const form = new FormData(); form.append("file", file); - const res = await fetch(`/api/dms2-import/preview?userId=${encodeURIComponent(userId)}&tenant=${encodeURIComponent(tenant)}`, { + const res = await apiFetch(`/api/dms2-import/preview?userId=${encodeURIComponent(userId)}&tenant=${encodeURIComponent(tenant)}`, { method: "POST", body: form, }); @@ -61,7 +63,7 @@ export async function executeImport(userId: string, tenant: string, file: File, const form = new FormData(); form.append("file", file); form.append("mapping", JSON.stringify(mapping)); - const res = await fetch(`/api/dms2-import/execute?userId=${encodeURIComponent(userId)}&tenant=${encodeURIComponent(tenant)}`, { + const res = await apiFetch(`/api/dms2-import/execute?userId=${encodeURIComponent(userId)}&tenant=${encodeURIComponent(tenant)}`, { method: "POST", body: form, }); @@ -69,7 +71,7 @@ export async function executeImport(userId: string, tenant: string, file: File, } export async function getRun(userId: string, tenant: string, runId: string): Promise { - const res = await fetch( + const res = await apiFetch( `/api/dms2-import/runs/${encodeURIComponent(runId)}?userId=${encodeURIComponent(userId)}&tenant=${encodeURIComponent(tenant)}`, { cache: "no-store" } ); diff --git a/web/dms2-import/lib/http.ts b/web/dms2-import/lib/http.ts new file mode 100644 index 00000000..fec19b2a --- /dev/null +++ b/web/dms2-import/lib/http.ts @@ -0,0 +1,6 @@ +import { createBearerClient } from "@nexarch/shl"; + +export const { bearerEnabled, getSession, apiFetch, sessionUserId } = createBearerClient("connect", () => ({ + coreBase: process.env.NEXT_PUBLIC_CORE_API_BASE, + loginUrl: process.env.NEXT_PUBLIC_LOGIN_URL, +}));