From 37166c966fbac1d3e7385d6cfff01f03ecfccf9f Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Sat, 7 Mar 2026 09:45:56 +0100 Subject: [PATCH] feat(sdk): Audit-Dashboard + RBAC-Admin Frontends, UCCA/Go Cleanup - Remove 5 unused UCCA routes (wizard, stats, dsb-pool) from Go main.go - Delete 64 deprecated Go handlers (DSGVO, Vendors, Incidents, Drafting) - Delete legacy proxy routes (dsgvo, vendors) - Add LLM Audit Dashboard (3 tabs: Log, Nutzung, Compliance) with export - Add RBAC Admin UI (5 tabs: Mandanten, Namespaces, Rollen, Benutzer, LLM-Policies) - Add proxy routes for audit-llm and rbac to Go backend - Add Workshop, Portfolio, Roadmap proxy routes and frontends - Add LLM Audit + RBAC Admin to SDKSidebar Co-Authored-By: Claude Opus 4.6 --- .../api/sdk/v1/audit-llm/[[...path]]/route.ts | 108 ++ .../[[...path]]}/route.ts | 73 +- .../app/api/sdk/v1/rbac/[[...path]]/route.ts | 125 ++ .../sdk/v1/roadmap-items/[[...path]]/route.ts | 111 ++ .../{vendors => roadmap}/[[...path]]/route.ts | 60 +- .../api/sdk/v1/workshops/[[...path]]/route.ts | 119 ++ admin-compliance/app/sdk/audit-llm/page.tsx | 561 +++++++++ admin-compliance/app/sdk/portfolio/page.tsx | 658 ++++++++++ admin-compliance/app/sdk/rbac/page.tsx | 1023 +++++++++++++++ admin-compliance/app/sdk/roadmap/page.tsx | 882 +++++++++++++ admin-compliance/app/sdk/workshop/page.tsx | 616 +++++++++ .../components/sdk/Sidebar/SDKSidebar.tsx | 60 + ai-compliance-sdk/cmd/server/main.go | 178 +-- .../api/handlers/drafting_handlers.go | 336 ----- .../internal/api/handlers/dsgvo_handlers.go | 802 ------------ .../api/handlers/incidents_handlers.go | 672 ---------- .../internal/api/handlers/vendor_handlers.go | 855 ------------- ai-compliance-sdk/internal/dsgvo/models.go | 235 ---- ai-compliance-sdk/internal/dsgvo/store.go | 664 ---------- .../internal/incidents/models.go | 305 ----- ai-compliance-sdk/internal/incidents/store.go | 571 --------- ai-compliance-sdk/internal/vendor/models.go | 488 ------- ai-compliance-sdk/internal/vendor/store.go | 1116 ----------------- 23 files changed, 4323 insertions(+), 6295 deletions(-) create mode 100644 admin-compliance/app/api/sdk/v1/audit-llm/[[...path]]/route.ts rename admin-compliance/app/api/sdk/v1/{dsgvo/[...path] => portfolio/[[...path]]}/route.ts (50%) create mode 100644 admin-compliance/app/api/sdk/v1/rbac/[[...path]]/route.ts create mode 100644 admin-compliance/app/api/sdk/v1/roadmap-items/[[...path]]/route.ts rename admin-compliance/app/api/sdk/v1/{vendors => roadmap}/[[...path]]/route.ts (69%) create mode 100644 admin-compliance/app/api/sdk/v1/workshops/[[...path]]/route.ts create mode 100644 admin-compliance/app/sdk/audit-llm/page.tsx create mode 100644 admin-compliance/app/sdk/portfolio/page.tsx create mode 100644 admin-compliance/app/sdk/rbac/page.tsx create mode 100644 admin-compliance/app/sdk/roadmap/page.tsx create mode 100644 admin-compliance/app/sdk/workshop/page.tsx delete mode 100644 ai-compliance-sdk/internal/api/handlers/drafting_handlers.go delete mode 100644 ai-compliance-sdk/internal/api/handlers/dsgvo_handlers.go delete mode 100644 ai-compliance-sdk/internal/api/handlers/incidents_handlers.go delete mode 100644 ai-compliance-sdk/internal/api/handlers/vendor_handlers.go delete mode 100644 ai-compliance-sdk/internal/dsgvo/models.go delete mode 100644 ai-compliance-sdk/internal/dsgvo/store.go delete mode 100644 ai-compliance-sdk/internal/incidents/models.go delete mode 100644 ai-compliance-sdk/internal/incidents/store.go delete mode 100644 ai-compliance-sdk/internal/vendor/models.go delete mode 100644 ai-compliance-sdk/internal/vendor/store.go diff --git a/admin-compliance/app/api/sdk/v1/audit-llm/[[...path]]/route.ts b/admin-compliance/app/api/sdk/v1/audit-llm/[[...path]]/route.ts new file mode 100644 index 0000000..262b79f --- /dev/null +++ b/admin-compliance/app/api/sdk/v1/audit-llm/[[...path]]/route.ts @@ -0,0 +1,108 @@ +/** + * LLM Audit API Proxy - Catch-all route + * Proxies all /api/sdk/v1/audit-llm/* requests to ai-compliance-sdk /sdk/v1/audit/* + */ + +import { NextRequest, NextResponse } from 'next/server' + +const SDK_BACKEND_URL = process.env.SDK_API_URL || 'http://ai-compliance-sdk:8090' + +async function proxyRequest( + request: NextRequest, + pathSegments: string[] | undefined, + method: string +) { + const pathStr = pathSegments?.join('/') || '' + const searchParams = request.nextUrl.searchParams.toString() + const basePath = `${SDK_BACKEND_URL}/sdk/v1/audit` + const url = pathStr + ? `${basePath}/${pathStr}${searchParams ? `?${searchParams}` : ''}` + : `${basePath}${searchParams ? `?${searchParams}` : ''}` + + try { + const headers: HeadersInit = { + 'Content-Type': 'application/json', + } + + const headerNames = ['authorization', 'x-namespace-id', 'x-tenant-slug'] + for (const name of headerNames) { + const value = request.headers.get(name) + if (value) { + headers[name] = value + } + } + + const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i + const clientUserId = request.headers.get('x-user-id') + const clientTenantId = request.headers.get('x-tenant-id') + headers['X-User-ID'] = (clientUserId && uuidRegex.test(clientUserId)) ? clientUserId : '00000000-0000-0000-0000-000000000001' + headers['X-Tenant-ID'] = (clientTenantId && uuidRegex.test(clientTenantId)) ? clientTenantId : (process.env.DEFAULT_TENANT_ID || '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e') + + const fetchOptions: RequestInit = { + method, + headers, + signal: AbortSignal.timeout(60000), + } + + if (method === 'POST' || method === 'PUT' || method === 'PATCH') { + const body = await request.text() + if (body) { + fetchOptions.body = body + } + } + + const response = await fetch(url, fetchOptions) + + // Handle export endpoints that may return CSV + const contentType = response.headers.get('content-type') || '' + if (contentType.includes('text/csv') || contentType.includes('application/octet-stream')) { + const blob = await response.arrayBuffer() + return new NextResponse(blob, { + status: response.status, + headers: { + 'Content-Type': contentType, + 'Content-Disposition': response.headers.get('content-disposition') || 'attachment', + }, + }) + } + + if (!response.ok) { + const errorText = await response.text() + let errorJson + try { + errorJson = JSON.parse(errorText) + } catch { + errorJson = { error: errorText } + } + return NextResponse.json( + { error: `Backend Error: ${response.status}`, ...errorJson }, + { status: response.status } + ) + } + + const data = await response.json() + return NextResponse.json(data) + } catch (error) { + console.error('LLM Audit API proxy error:', error) + return NextResponse.json( + { error: 'Verbindung zum SDK Backend fehlgeschlagen' }, + { status: 503 } + ) + } +} + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ path?: string[] }> } +) { + const { path } = await params + return proxyRequest(request, path, 'GET') +} + +export async function POST( + request: NextRequest, + { params }: { params: Promise<{ path?: string[] }> } +) { + const { path } = await params + return proxyRequest(request, path, 'POST') +} diff --git a/admin-compliance/app/api/sdk/v1/dsgvo/[...path]/route.ts b/admin-compliance/app/api/sdk/v1/portfolio/[[...path]]/route.ts similarity index 50% rename from admin-compliance/app/api/sdk/v1/dsgvo/[...path]/route.ts rename to admin-compliance/app/api/sdk/v1/portfolio/[[...path]]/route.ts index 1cec9c8..8412de3 100644 --- a/admin-compliance/app/api/sdk/v1/dsgvo/[...path]/route.ts +++ b/admin-compliance/app/api/sdk/v1/portfolio/[[...path]]/route.ts @@ -1,6 +1,6 @@ /** - * DSGVO API Proxy - Catch-all route - * Proxies all /api/sdk/v1/dsgvo/* requests to ai-compliance-sdk backend + * Portfolio API Proxy - Catch-all route + * Proxies all /api/sdk/v1/portfolio/* requests to ai-compliance-sdk backend */ import { NextRequest, NextResponse } from 'next/server' @@ -9,61 +9,50 @@ const SDK_BACKEND_URL = process.env.SDK_API_URL || 'http://ai-compliance-sdk:809 async function proxyRequest( request: NextRequest, - pathSegments: string[], + pathSegments: string[] | undefined, method: string ) { - const pathStr = pathSegments.join('/') + const pathStr = pathSegments?.join('/') || '' const searchParams = request.nextUrl.searchParams.toString() - const url = `${SDK_BACKEND_URL}/sdk/v1/dsgvo/${pathStr}${searchParams ? `?${searchParams}` : ''}` + const basePath = `${SDK_BACKEND_URL}/sdk/v1/portfolios` + const url = pathStr + ? `${basePath}/${pathStr}${searchParams ? `?${searchParams}` : ''}` + : `${basePath}${searchParams ? `?${searchParams}` : ''}` try { const headers: HeadersInit = { 'Content-Type': 'application/json', } - // Forward auth headers if present - const authHeader = request.headers.get('authorization') - if (authHeader) { - headers['Authorization'] = authHeader + const headerNames = ['authorization', 'x-namespace-id', 'x-tenant-slug'] + for (const name of headerNames) { + const value = request.headers.get(name) + if (value) { + headers[name] = value + } } + const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i + const clientUserId = request.headers.get('x-user-id') + const clientTenantId = request.headers.get('x-tenant-id') + headers['X-User-ID'] = (clientUserId && uuidRegex.test(clientUserId)) ? clientUserId : '00000000-0000-0000-0000-000000000001' + headers['X-Tenant-ID'] = (clientTenantId && uuidRegex.test(clientTenantId)) ? clientTenantId : (process.env.DEFAULT_TENANT_ID || '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e') + const fetchOptions: RequestInit = { method, headers, - signal: AbortSignal.timeout(30000), + signal: AbortSignal.timeout(60000), } - // Add body for POST/PUT/PATCH methods - if (['POST', 'PUT', 'PATCH'].includes(method)) { - const contentType = request.headers.get('content-type') - if (contentType?.includes('application/json')) { - try { - const text = await request.text() - if (text && text.trim()) { - fetchOptions.body = text - } - } catch { - // Empty or invalid body - continue without - } + if (method === 'POST' || method === 'PUT' || method === 'PATCH') { + const body = await request.text() + if (body) { + fetchOptions.body = body } } const response = await fetch(url, fetchOptions) - // Handle non-JSON responses (e.g., PDF export) - const responseContentType = response.headers.get('content-type') - if (responseContentType?.includes('application/pdf') || - responseContentType?.includes('application/octet-stream')) { - const blob = await response.blob() - return new NextResponse(blob, { - status: response.status, - headers: { - 'Content-Type': responseContentType, - 'Content-Disposition': response.headers.get('content-disposition') || '', - }, - }) - } - if (!response.ok) { const errorText = await response.text() let errorJson @@ -81,7 +70,7 @@ async function proxyRequest( const data = await response.json() return NextResponse.json(data) } catch (error) { - console.error('DSGVO API proxy error:', error) + console.error('Portfolio API proxy error:', error) return NextResponse.json( { error: 'Verbindung zum SDK Backend fehlgeschlagen' }, { status: 503 } @@ -91,7 +80,7 @@ async function proxyRequest( export async function GET( request: NextRequest, - { params }: { params: Promise<{ path: string[] }> } + { params }: { params: Promise<{ path?: string[] }> } ) { const { path } = await params return proxyRequest(request, path, 'GET') @@ -99,7 +88,7 @@ export async function GET( export async function POST( request: NextRequest, - { params }: { params: Promise<{ path: string[] }> } + { params }: { params: Promise<{ path?: string[] }> } ) { const { path } = await params return proxyRequest(request, path, 'POST') @@ -107,7 +96,7 @@ export async function POST( export async function PUT( request: NextRequest, - { params }: { params: Promise<{ path: string[] }> } + { params }: { params: Promise<{ path?: string[] }> } ) { const { path } = await params return proxyRequest(request, path, 'PUT') @@ -115,7 +104,7 @@ export async function PUT( export async function PATCH( request: NextRequest, - { params }: { params: Promise<{ path: string[] }> } + { params }: { params: Promise<{ path?: string[] }> } ) { const { path } = await params return proxyRequest(request, path, 'PATCH') @@ -123,7 +112,7 @@ export async function PATCH( export async function DELETE( request: NextRequest, - { params }: { params: Promise<{ path: string[] }> } + { params }: { params: Promise<{ path?: string[] }> } ) { const { path } = await params return proxyRequest(request, path, 'DELETE') diff --git a/admin-compliance/app/api/sdk/v1/rbac/[[...path]]/route.ts b/admin-compliance/app/api/sdk/v1/rbac/[[...path]]/route.ts new file mode 100644 index 0000000..47ce8db --- /dev/null +++ b/admin-compliance/app/api/sdk/v1/rbac/[[...path]]/route.ts @@ -0,0 +1,125 @@ +/** + * RBAC Admin API Proxy - Catch-all route + * Proxies /api/sdk/v1/rbac//... to ai-compliance-sdk /sdk/v1//... + * + * Mapping: /rbac/tenants/... → /sdk/v1/tenants/... + * /rbac/namespaces/... → /sdk/v1/namespaces/... + * /rbac/roles/... → /sdk/v1/roles/... + * /rbac/user-roles/... → /sdk/v1/user-roles/... + * /rbac/permissions/... → /sdk/v1/permissions/... + * /rbac/llm/policies/... → /sdk/v1/llm/policies/... + */ + +import { NextRequest, NextResponse } from 'next/server' + +const SDK_BACKEND_URL = process.env.SDK_API_URL || 'http://ai-compliance-sdk:8090' + +async function proxyRequest( + request: NextRequest, + pathSegments: string[] | undefined, + method: string +) { + // Path segments come as the full sub-path after /rbac/ + // e.g. /rbac/tenants/123 → pathSegments = ['tenants', '123'] + const pathStr = pathSegments?.join('/') || '' + const searchParams = request.nextUrl.searchParams.toString() + const url = `${SDK_BACKEND_URL}/sdk/v1/${pathStr}${searchParams ? `?${searchParams}` : ''}` + + try { + const headers: HeadersInit = { + 'Content-Type': 'application/json', + } + + const headerNames = ['authorization', 'x-namespace-id', 'x-tenant-slug'] + for (const name of headerNames) { + const value = request.headers.get(name) + if (value) { + headers[name] = value + } + } + + const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i + const clientUserId = request.headers.get('x-user-id') + const clientTenantId = request.headers.get('x-tenant-id') + headers['X-User-ID'] = (clientUserId && uuidRegex.test(clientUserId)) ? clientUserId : '00000000-0000-0000-0000-000000000001' + headers['X-Tenant-ID'] = (clientTenantId && uuidRegex.test(clientTenantId)) ? clientTenantId : (process.env.DEFAULT_TENANT_ID || '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e') + + const fetchOptions: RequestInit = { + method, + headers, + signal: AbortSignal.timeout(60000), + } + + if (method === 'POST' || method === 'PUT' || method === 'PATCH') { + const body = await request.text() + if (body) { + fetchOptions.body = body + } + } + + const response = await fetch(url, fetchOptions) + + if (!response.ok) { + const errorText = await response.text() + let errorJson + try { + errorJson = JSON.parse(errorText) + } catch { + errorJson = { error: errorText } + } + return NextResponse.json( + { error: `Backend Error: ${response.status}`, ...errorJson }, + { status: response.status } + ) + } + + const data = await response.json() + return NextResponse.json(data) + } catch (error) { + console.error('RBAC API proxy error:', error) + return NextResponse.json( + { error: 'Verbindung zum SDK Backend fehlgeschlagen' }, + { status: 503 } + ) + } +} + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ path?: string[] }> } +) { + const { path } = await params + return proxyRequest(request, path, 'GET') +} + +export async function POST( + request: NextRequest, + { params }: { params: Promise<{ path?: string[] }> } +) { + const { path } = await params + return proxyRequest(request, path, 'POST') +} + +export async function PUT( + request: NextRequest, + { params }: { params: Promise<{ path?: string[] }> } +) { + const { path } = await params + return proxyRequest(request, path, 'PUT') +} + +export async function PATCH( + request: NextRequest, + { params }: { params: Promise<{ path?: string[] }> } +) { + const { path } = await params + return proxyRequest(request, path, 'PATCH') +} + +export async function DELETE( + request: NextRequest, + { params }: { params: Promise<{ path?: string[] }> } +) { + const { path } = await params + return proxyRequest(request, path, 'DELETE') +} diff --git a/admin-compliance/app/api/sdk/v1/roadmap-items/[[...path]]/route.ts b/admin-compliance/app/api/sdk/v1/roadmap-items/[[...path]]/route.ts new file mode 100644 index 0000000..c8cd150 --- /dev/null +++ b/admin-compliance/app/api/sdk/v1/roadmap-items/[[...path]]/route.ts @@ -0,0 +1,111 @@ +/** + * Roadmap Items API Proxy - Catch-all route + * Proxies /api/sdk/v1/roadmap-items/* to ai-compliance-sdk /sdk/v1/roadmap-items/* + */ + +import { NextRequest, NextResponse } from 'next/server' + +const SDK_BACKEND_URL = process.env.SDK_API_URL || 'http://ai-compliance-sdk:8090' + +async function proxyRequest( + request: NextRequest, + pathSegments: string[] | undefined, + method: string +) { + const pathStr = pathSegments?.join('/') || '' + const searchParams = request.nextUrl.searchParams.toString() + const basePath = `${SDK_BACKEND_URL}/sdk/v1/roadmap-items` + const url = pathStr + ? `${basePath}/${pathStr}${searchParams ? `?${searchParams}` : ''}` + : `${basePath}${searchParams ? `?${searchParams}` : ''}` + + try { + const headers: HeadersInit = { + 'Content-Type': 'application/json', + } + + const headerNames = ['authorization', 'x-namespace-id', 'x-tenant-slug'] + for (const name of headerNames) { + const value = request.headers.get(name) + if (value) { + headers[name] = value + } + } + + const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i + const clientUserId = request.headers.get('x-user-id') + const clientTenantId = request.headers.get('x-tenant-id') + headers['X-User-ID'] = (clientUserId && uuidRegex.test(clientUserId)) ? clientUserId : '00000000-0000-0000-0000-000000000001' + headers['X-Tenant-ID'] = (clientTenantId && uuidRegex.test(clientTenantId)) ? clientTenantId : (process.env.DEFAULT_TENANT_ID || '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e') + + const fetchOptions: RequestInit = { + method, + headers, + signal: AbortSignal.timeout(60000), + } + + if (method === 'POST' || method === 'PUT' || method === 'PATCH') { + const body = await request.text() + if (body) { + fetchOptions.body = body + } + } + + const response = await fetch(url, fetchOptions) + + if (!response.ok) { + const errorText = await response.text() + let errorJson + try { + errorJson = JSON.parse(errorText) + } catch { + errorJson = { error: errorText } + } + return NextResponse.json( + { error: `Backend Error: ${response.status}`, ...errorJson }, + { status: response.status } + ) + } + + const data = await response.json() + return NextResponse.json(data) + } catch (error) { + console.error('Roadmap Items API proxy error:', error) + return NextResponse.json( + { error: 'Verbindung zum SDK Backend fehlgeschlagen' }, + { status: 503 } + ) + } +} + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ path?: string[] }> } +) { + const { path } = await params + return proxyRequest(request, path, 'GET') +} + +export async function PUT( + request: NextRequest, + { params }: { params: Promise<{ path?: string[] }> } +) { + const { path } = await params + return proxyRequest(request, path, 'PUT') +} + +export async function PATCH( + request: NextRequest, + { params }: { params: Promise<{ path?: string[] }> } +) { + const { path } = await params + return proxyRequest(request, path, 'PATCH') +} + +export async function DELETE( + request: NextRequest, + { params }: { params: Promise<{ path?: string[] }> } +) { + const { path } = await params + return proxyRequest(request, path, 'DELETE') +} diff --git a/admin-compliance/app/api/sdk/v1/vendors/[[...path]]/route.ts b/admin-compliance/app/api/sdk/v1/roadmap/[[...path]]/route.ts similarity index 69% rename from admin-compliance/app/api/sdk/v1/vendors/[[...path]]/route.ts rename to admin-compliance/app/api/sdk/v1/roadmap/[[...path]]/route.ts index 08bd63f..c3a8ee6 100644 --- a/admin-compliance/app/api/sdk/v1/vendors/[[...path]]/route.ts +++ b/admin-compliance/app/api/sdk/v1/roadmap/[[...path]]/route.ts @@ -1,6 +1,6 @@ /** - * Vendor Compliance API Proxy - Catch-all route - * Proxies all /api/sdk/v1/vendors/* requests to ai-compliance-sdk backend + * Roadmap API Proxy - Catch-all route + * Proxies all /api/sdk/v1/roadmap/* requests to ai-compliance-sdk backend */ import { NextRequest, NextResponse } from 'next/server' @@ -14,7 +14,7 @@ async function proxyRequest( ) { const pathStr = pathSegments?.join('/') || '' const searchParams = request.nextUrl.searchParams.toString() - const basePath = `${SDK_BACKEND_URL}/sdk/v1/vendors` + const basePath = `${SDK_BACKEND_URL}/sdk/v1/roadmaps` const url = pathStr ? `${basePath}/${pathStr}${searchParams ? `?${searchParams}` : ''}` : `${basePath}${searchParams ? `?${searchParams}` : ''}` @@ -24,8 +24,7 @@ async function proxyRequest( 'Content-Type': 'application/json', } - // Forward all relevant headers - const headerNames = ['authorization', 'x-tenant-id', 'x-user-id', 'x-namespace-id', 'x-tenant-slug'] + const headerNames = ['authorization', 'x-namespace-id', 'x-tenant-slug'] for (const name of headerNames) { const value = request.headers.get(name) if (value) { @@ -33,42 +32,27 @@ async function proxyRequest( } } + const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i + const clientUserId = request.headers.get('x-user-id') + const clientTenantId = request.headers.get('x-tenant-id') + headers['X-User-ID'] = (clientUserId && uuidRegex.test(clientUserId)) ? clientUserId : '00000000-0000-0000-0000-000000000001' + headers['X-Tenant-ID'] = (clientTenantId && uuidRegex.test(clientTenantId)) ? clientTenantId : (process.env.DEFAULT_TENANT_ID || '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e') + const fetchOptions: RequestInit = { method, headers, - signal: AbortSignal.timeout(30000), + signal: AbortSignal.timeout(60000), } - if (['POST', 'PUT', 'PATCH'].includes(method)) { - const contentType = request.headers.get('content-type') - if (contentType?.includes('application/json')) { - try { - const text = await request.text() - if (text && text.trim()) { - fetchOptions.body = text - } - } catch { - // Empty or invalid body - continue without - } + if (method === 'POST' || method === 'PUT' || method === 'PATCH') { + const body = await request.text() + if (body) { + fetchOptions.body = body } } const response = await fetch(url, fetchOptions) - // Handle non-JSON responses (e.g., PDF exports) - const responseContentType = response.headers.get('content-type') - if (responseContentType?.includes('application/pdf') || - responseContentType?.includes('application/octet-stream')) { - const blob = await response.blob() - return new NextResponse(blob, { - status: response.status, - headers: { - 'Content-Type': responseContentType, - 'Content-Disposition': response.headers.get('content-disposition') || '', - }, - }) - } - if (!response.ok) { const errorText = await response.text() let errorJson @@ -83,10 +67,22 @@ async function proxyRequest( ) } + const contentType = response.headers.get('content-type') || '' + if (contentType.includes('application/octet-stream') || contentType.includes('text/csv')) { + const blob = await response.blob() + return new NextResponse(blob, { + status: response.status, + headers: { + 'Content-Type': contentType, + 'Content-Disposition': response.headers.get('content-disposition') || '', + }, + }) + } + const data = await response.json() return NextResponse.json(data) } catch (error) { - console.error('Vendor Compliance API proxy error:', error) + console.error('Roadmap API proxy error:', error) return NextResponse.json( { error: 'Verbindung zum SDK Backend fehlgeschlagen' }, { status: 503 } diff --git a/admin-compliance/app/api/sdk/v1/workshops/[[...path]]/route.ts b/admin-compliance/app/api/sdk/v1/workshops/[[...path]]/route.ts new file mode 100644 index 0000000..5326d54 --- /dev/null +++ b/admin-compliance/app/api/sdk/v1/workshops/[[...path]]/route.ts @@ -0,0 +1,119 @@ +/** + * Workshop API Proxy - Catch-all route + * Proxies all /api/sdk/v1/workshops/* requests to ai-compliance-sdk backend + */ + +import { NextRequest, NextResponse } from 'next/server' + +const SDK_BACKEND_URL = process.env.SDK_API_URL || 'http://ai-compliance-sdk:8090' + +async function proxyRequest( + request: NextRequest, + pathSegments: string[] | undefined, + method: string +) { + const pathStr = pathSegments?.join('/') || '' + const searchParams = request.nextUrl.searchParams.toString() + const basePath = `${SDK_BACKEND_URL}/sdk/v1/workshops` + const url = pathStr + ? `${basePath}/${pathStr}${searchParams ? `?${searchParams}` : ''}` + : `${basePath}${searchParams ? `?${searchParams}` : ''}` + + try { + const headers: HeadersInit = { + 'Content-Type': 'application/json', + } + + const headerNames = ['authorization', 'x-namespace-id', 'x-tenant-slug'] + for (const name of headerNames) { + const value = request.headers.get(name) + if (value) { + headers[name] = value + } + } + + const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i + const clientUserId = request.headers.get('x-user-id') + const clientTenantId = request.headers.get('x-tenant-id') + headers['X-User-ID'] = (clientUserId && uuidRegex.test(clientUserId)) ? clientUserId : '00000000-0000-0000-0000-000000000001' + headers['X-Tenant-ID'] = (clientTenantId && uuidRegex.test(clientTenantId)) ? clientTenantId : (process.env.DEFAULT_TENANT_ID || '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e') + + const fetchOptions: RequestInit = { + method, + headers, + signal: AbortSignal.timeout(60000), + } + + if (method === 'POST' || method === 'PUT' || method === 'PATCH') { + const body = await request.text() + if (body) { + fetchOptions.body = body + } + } + + const response = await fetch(url, fetchOptions) + + if (!response.ok) { + const errorText = await response.text() + let errorJson + try { + errorJson = JSON.parse(errorText) + } catch { + errorJson = { error: errorText } + } + return NextResponse.json( + { error: `Backend Error: ${response.status}`, ...errorJson }, + { status: response.status } + ) + } + + const data = await response.json() + return NextResponse.json(data) + } catch (error) { + console.error('Workshop API proxy error:', error) + return NextResponse.json( + { error: 'Verbindung zum SDK Backend fehlgeschlagen' }, + { status: 503 } + ) + } +} + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ path?: string[] }> } +) { + const { path } = await params + return proxyRequest(request, path, 'GET') +} + +export async function POST( + request: NextRequest, + { params }: { params: Promise<{ path?: string[] }> } +) { + const { path } = await params + return proxyRequest(request, path, 'POST') +} + +export async function PUT( + request: NextRequest, + { params }: { params: Promise<{ path?: string[] }> } +) { + const { path } = await params + return proxyRequest(request, path, 'PUT') +} + +export async function PATCH( + request: NextRequest, + { params }: { params: Promise<{ path?: string[] }> } +) { + const { path } = await params + return proxyRequest(request, path, 'PATCH') +} + +export async function DELETE( + request: NextRequest, + { params }: { params: Promise<{ path?: string[] }> } +) { + const { path } = await params + return proxyRequest(request, path, 'DELETE') +} diff --git a/admin-compliance/app/sdk/audit-llm/page.tsx b/admin-compliance/app/sdk/audit-llm/page.tsx new file mode 100644 index 0000000..ebef77e --- /dev/null +++ b/admin-compliance/app/sdk/audit-llm/page.tsx @@ -0,0 +1,561 @@ +'use client' + +import React, { useState, useEffect, useCallback } from 'react' +import { useSDK } from '@/lib/sdk' + +// ============================================================================= +// TYPES +// ============================================================================= + +interface LLMLogEntry { + id: string + user_id: string + namespace: string + model: string + provider: string + prompt_tokens: number + completion_tokens: number + total_tokens: number + pii_detected: boolean + pii_categories: string[] + redacted: boolean + duration_ms: number + status: string + created_at: string +} + +interface UsageStats { + total_requests: number + total_tokens: number + total_prompt_tokens: number + total_completion_tokens: number + models_used: Record + providers_used: Record + avg_duration_ms: number + pii_detection_rate: number + period_start: string + period_end: string +} + +interface ComplianceReport { + total_requests: number + pii_incidents: number + pii_rate: number + redaction_rate: number + policy_violations: number + top_pii_categories: Record + namespace_breakdown: Record + user_breakdown: Record + period_start: string + period_end: string +} + +type TabId = 'llm-log' | 'usage' | 'compliance' + +// ============================================================================= +// HELPERS +// ============================================================================= + +const API_BASE = '/api/sdk/v1/audit-llm' + +function formatDate(iso: string): string { + return new Date(iso).toLocaleString('de-DE', { + day: '2-digit', month: '2-digit', year: 'numeric', + hour: '2-digit', minute: '2-digit', + }) +} + +function formatNumber(n: number): string { + return n.toLocaleString('de-DE') +} + +function formatDuration(ms: number): string { + if (ms < 1000) return `${ms}ms` + return `${(ms / 1000).toFixed(1)}s` +} + +function getDateRange(period: string): { from: string; to: string } { + const now = new Date() + const to = now.toISOString().slice(0, 10) + const from = new Date(now) + switch (period) { + case '7d': from.setDate(from.getDate() - 7); break + case '30d': from.setDate(from.getDate() - 30); break + case '90d': from.setDate(from.getDate() - 90); break + default: from.setDate(from.getDate() - 7) + } + return { from: from.toISOString().slice(0, 10), to } +} + +// ============================================================================= +// MAIN PAGE +// ============================================================================= + +export default function AuditLLMPage() { + const { state } = useSDK() + const [activeTab, setActiveTab] = useState('llm-log') + const [period, setPeriod] = useState('7d') + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + + // LLM Log state + const [logEntries, setLogEntries] = useState([]) + const [logFilter, setLogFilter] = useState({ model: '', pii: '' }) + + // Usage state + const [usageStats, setUsageStats] = useState(null) + + // Compliance state + const [complianceReport, setComplianceReport] = useState(null) + + // ─── Load Data ─────────────────────────────────────────────────────── + + const loadLLMLog = useCallback(async () => { + setLoading(true) + setError(null) + try { + const { from, to } = getDateRange(period) + const params = new URLSearchParams({ from, to, limit: '100' }) + if (logFilter.model) params.set('model', logFilter.model) + if (logFilter.pii === 'true') params.set('pii_detected', 'true') + if (logFilter.pii === 'false') params.set('pii_detected', 'false') + + const res = await fetch(`${API_BASE}/llm?${params}`) + if (!res.ok) throw new Error(`HTTP ${res.status}`) + const data = await res.json() + setLogEntries(Array.isArray(data) ? data : data.entries || data.logs || []) + } catch (e) { + setError(e instanceof Error ? e.message : 'Fehler beim Laden') + } finally { + setLoading(false) + } + }, [period, logFilter]) + + const loadUsage = useCallback(async () => { + setLoading(true) + setError(null) + try { + const { from, to } = getDateRange(period) + const res = await fetch(`${API_BASE}/usage?from=${from}&to=${to}`) + if (!res.ok) throw new Error(`HTTP ${res.status}`) + const data = await res.json() + setUsageStats(data) + } catch (e) { + setError(e instanceof Error ? e.message : 'Fehler beim Laden') + } finally { + setLoading(false) + } + }, [period]) + + const loadCompliance = useCallback(async () => { + setLoading(true) + setError(null) + try { + const { from, to } = getDateRange(period) + const res = await fetch(`${API_BASE}/compliance-report?from=${from}&to=${to}`) + if (!res.ok) throw new Error(`HTTP ${res.status}`) + const data = await res.json() + setComplianceReport(data) + } catch (e) { + setError(e instanceof Error ? e.message : 'Fehler beim Laden') + } finally { + setLoading(false) + } + }, [period]) + + useEffect(() => { + if (activeTab === 'llm-log') loadLLMLog() + else if (activeTab === 'usage') loadUsage() + else if (activeTab === 'compliance') loadCompliance() + }, [activeTab, loadLLMLog, loadUsage, loadCompliance]) + + // ─── Export ────────────────────────────────────────────────────────── + + const handleExport = async (type: 'llm' | 'general' | 'compliance', format: 'json' | 'csv') => { + try { + const { from, to } = getDateRange(period) + const res = await fetch(`${API_BASE}/export/${type}?from=${from}&to=${to}&format=${format}`) + if (!res.ok) throw new Error(`Export fehlgeschlagen: ${res.status}`) + const blob = await res.blob() + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = `audit-${type}-${from}-${to}.${format}` + a.click() + URL.revokeObjectURL(url) + } catch (e) { + setError(e instanceof Error ? e.message : 'Export fehlgeschlagen') + } + } + + // ─── Tabs ──────────────────────────────────────────────────────────── + + const tabs: { id: TabId; label: string }[] = [ + { id: 'llm-log', label: 'LLM-Log' }, + { id: 'usage', label: 'Nutzung' }, + { id: 'compliance', label: 'Compliance' }, + ] + + return ( +
+ {/* Header */} +
+

LLM Audit Dashboard

+

Monitoring und Compliance-Analyse der LLM-Operationen

+
+ + {/* Period + Tabs */} +
+
+ {tabs.map(tab => ( + + ))} +
+
+ + +
+
+ + {error && ( +
{error}
+ )} + + {loading && ( +
+
+
+ )} + + {/* ── LLM-Log Tab ── */} + {!loading && activeTab === 'llm-log' && ( +
+ {/* Filters */} +
+ setLogFilter(f => ({ ...f, model: e.target.value }))} + className="border border-gray-300 rounded-lg px-3 py-2 text-sm w-48" + /> + +
+ + {/* Table */} +
+ + + + + + + + + + + + + + {logEntries.length === 0 ? ( + + + + ) : logEntries.map(entry => ( + + + + + + + + + + ))} + +
ZeitpunktUserModelTokensPIIDauerStatus
+ Keine Log-Eintraege im gewaehlten Zeitraum +
{formatDate(entry.created_at)}{entry.user_id?.slice(0, 8)}... + + {entry.model} + + {formatNumber(entry.total_tokens)} + {entry.pii_detected ? ( + + {entry.redacted ? 'Redacted' : 'Erkannt'} + + ) : ( + - + )} + {formatDuration(entry.duration_ms)} + + {entry.status} + +
+
+
{logEntries.length} Eintraege
+
+ )} + + {/* ── Nutzung Tab ── */} + {!loading && activeTab === 'usage' && usageStats && ( +
+ {/* Stats Cards */} +
+ + + + 0.1} + /> +
+ + {/* Token Breakdown */} +
+
+

Model-Nutzung

+ {Object.entries(usageStats.models_used || {}).length === 0 ? ( +

Keine Daten

+ ) : ( +
+ {Object.entries(usageStats.models_used).sort((a, b) => b[1] - a[1]).map(([model, count]) => ( +
+ {model} +
+
+
+
+ {formatNumber(count)} +
+
+ ))} +
+ )} +
+ +
+

Provider-Verteilung

+ {Object.entries(usageStats.providers_used || {}).length === 0 ? ( +

Keine Daten

+ ) : ( +
+ {Object.entries(usageStats.providers_used).sort((a, b) => b[1] - a[1]).map(([provider, count]) => ( +
+ {provider} + {formatNumber(count)} +
+ ))} +
+ )} +
+
+ + {/* Token Details */} +
+

Token-Aufschluesselung

+
+
+
{formatNumber(usageStats.total_prompt_tokens)}
+
Prompt Tokens
+
+
+
{formatNumber(usageStats.total_completion_tokens)}
+
Completion Tokens
+
+
+
{formatNumber(usageStats.total_tokens)}
+
Gesamt
+
+
+
+
+ )} + + {/* ── Compliance Tab ── */} + {!loading && activeTab === 'compliance' && complianceReport && ( +
+ {/* Summary Cards */} +
+ + 0} + /> + 0.05} + /> + +
+ + {complianceReport.policy_violations > 0 && ( +
+
+ + + + {complianceReport.policy_violations} Policy-Verletzungen im Zeitraum +
+
+ )} + +
+ {/* PII Categories */} +
+

PII-Kategorien

+ {Object.entries(complianceReport.top_pii_categories || {}).length === 0 ? ( +

Keine PII erkannt

+ ) : ( +
+ {Object.entries(complianceReport.top_pii_categories).sort((a, b) => b[1] - a[1]).map(([cat, count]) => ( +
+ {cat} + {count} +
+ ))} +
+ )} +
+ + {/* Namespace Breakdown */} +
+

Namespace-Analyse

+ {Object.entries(complianceReport.namespace_breakdown || {}).length === 0 ? ( +

Keine Namespace-Daten

+ ) : ( +
+ + + + + + + + + + {Object.entries(complianceReport.namespace_breakdown).map(([ns, data]) => ( + + + + + + ))} + +
NamespaceRequestsPII
{ns}{formatNumber(data.requests)} + {data.pii_incidents > 0 ? ( + {data.pii_incidents} + ) : ( + 0 + )} +
+
+ )} +
+
+ + {/* User Breakdown */} + {Object.entries(complianceReport.user_breakdown || {}).length > 0 && ( +
+

Top-Nutzer

+
+ + + + + + + + + + {Object.entries(complianceReport.user_breakdown) + .sort((a, b) => b[1].requests - a[1].requests) + .slice(0, 10) + .map(([userId, data]) => ( + + + + + + ))} + +
User-IDRequestsPII-Vorfaelle
{userId}{formatNumber(data.requests)} + {data.pii_incidents > 0 ? ( + {data.pii_incidents} + ) : ( + 0 + )} +
+
+
+ )} +
+ )} + + {/* Empty state for usage/compliance when no data */} + {!loading && activeTab === 'usage' && !usageStats && !error && ( +
Keine Nutzungsdaten verfuegbar
+ )} + {!loading && activeTab === 'compliance' && !complianceReport && !error && ( +
Kein Compliance-Report verfuegbar
+ )} +
+ ) +} + +// ============================================================================= +// STAT CARD +// ============================================================================= + +function StatCard({ label, value, highlight }: { label: string; value: string; highlight?: boolean }) { + return ( +
+
{label}
+
{value}
+
+ ) +} diff --git a/admin-compliance/app/sdk/portfolio/page.tsx b/admin-compliance/app/sdk/portfolio/page.tsx new file mode 100644 index 0000000..7ffd7da --- /dev/null +++ b/admin-compliance/app/sdk/portfolio/page.tsx @@ -0,0 +1,658 @@ +'use client' + +import React, { useState, useEffect, useCallback } from 'react' +import { useSDK } from '@/lib/sdk' + +// ============================================================================= +// TYPES +// ============================================================================= + +interface Portfolio { + id: string + name: string + description: string + status: 'DRAFT' | 'ACTIVE' | 'REVIEW' | 'APPROVED' | 'ARCHIVED' + department: string + business_unit: string + owner: string + owner_email: string + total_assessments: number + total_roadmaps: number + total_workshops: number + avg_risk_score: number + high_risk_count: number + compliance_score: number + auto_update_metrics: boolean + require_approval: boolean + created_at: string + updated_at: string + approved_at: string | null + approved_by: string | null +} + +interface PortfolioItem { + id: string + portfolio_id: string + item_type: 'ASSESSMENT' | 'ROADMAP' | 'WORKSHOP' | 'DOCUMENT' + item_id: string + title: string + status: string + risk_level: string + risk_score: number + feasibility: string + sort_order: number + tags: string[] + notes: string + created_at: string +} + +interface PortfolioStats { + total_items: number + items_by_type: Record + risk_distribution: Record + avg_risk_score: number + compliance_score: number +} + +interface ActivityEntry { + timestamp: string + action: string + item_type: string + item_id: string + item_title: string + user_id: string +} + +interface CompareResult { + portfolios: Portfolio[] + risk_scores: Record + compliance_scores: Record + item_counts: Record + common_items: string[] + unique_items: Record +} + +// ============================================================================= +// API +// ============================================================================= + +const API_BASE = '/api/sdk/v1/portfolio' + +async function api(path: string, options?: RequestInit): Promise { + const res = await fetch(`${API_BASE}${path}`, { + headers: { 'Content-Type': 'application/json' }, + ...options, + }) + if (!res.ok) { + const err = await res.json().catch(() => ({ error: res.statusText })) + throw new Error(err.error || err.message || `HTTP ${res.status}`) + } + return res.json() +} + +// ============================================================================= +// COMPONENTS +// ============================================================================= + +const statusColors: Record = { + DRAFT: 'bg-gray-100 text-gray-700', + ACTIVE: 'bg-green-100 text-green-700', + REVIEW: 'bg-yellow-100 text-yellow-700', + APPROVED: 'bg-purple-100 text-purple-700', + ARCHIVED: 'bg-red-100 text-red-700', +} + +const statusLabels: Record = { + DRAFT: 'Entwurf', + ACTIVE: 'Aktiv', + REVIEW: 'In Pruefung', + APPROVED: 'Genehmigt', + ARCHIVED: 'Archiviert', +} + +function PortfolioCard({ portfolio, onSelect, onDelete }: { + portfolio: Portfolio + onSelect: (p: Portfolio) => void + onDelete: (id: string) => void +}) { + const totalItems = portfolio.total_assessments + portfolio.total_roadmaps + portfolio.total_workshops + + return ( +
onSelect(portfolio)}> +
+
+

{portfolio.name}

+ {portfolio.department && {portfolio.department}} +
+ + {statusLabels[portfolio.status] || portfolio.status} + +
+ + {portfolio.description && ( +

{portfolio.description}

+ )} + +
+
+
{portfolio.compliance_score}%
+
Compliance
+
+
+
{portfolio.avg_risk_score.toFixed(1)}
+
Risiko
+
+
+
{totalItems}
+
Items
+
+
+ + {portfolio.high_risk_count > 0 && ( +
+ + + + {portfolio.high_risk_count} Hoch-Risiko +
+ )} + +
+ {portfolio.owner || 'Kein Owner'} + +
+
+ ) +} + +function CreatePortfolioModal({ onClose, onCreated }: { + onClose: () => void + onCreated: () => void +}) { + const [name, setName] = useState('') + const [description, setDescription] = useState('') + const [department, setDepartment] = useState('') + const [owner, setOwner] = useState('') + const [saving, setSaving] = useState(false) + + const handleCreate = async () => { + if (!name.trim()) return + setSaving(true) + try { + await api('', { + method: 'POST', + body: JSON.stringify({ + name: name.trim(), + description: description.trim(), + department: department.trim(), + owner: owner.trim(), + }), + }) + onCreated() + } catch (err) { + console.error('Create portfolio error:', err) + } finally { + setSaving(false) + } + } + + return ( +
+
e.stopPropagation()}> +

Neues Portfolio

+
+
+ + setName(e.target.value)} + className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" + placeholder="z.B. KI-Portfolio Q1 2026" /> +
+
+ +