From a694b9d9ea2319a508995b3d851665f202362f06 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Wed, 4 Mar 2026 22:41:05 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20DSFA=20Modul=20=E2=80=94=20Backend,=20P?= =?UTF-8?q?roxy,=20Frontend-Migration,=20Tests=20+=20Mock-Daten=20entfernt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Migration 024: compliance_dsfas + compliance_dsfa_audit_log Tabellen - dsfa_routes.py: CRUD + stats + audit-log + PATCH status Endpoints - Proxy: /api/sdk/v1/dsfa/[[...path]] → backend-compliance:8002/api/v1/dsfa - dsfa/page.tsx: mockDSFAs entfernt → echte API (loadDSFAs, handleCreateDSFA, handleStatusChange, handleDeleteDSFA) - GeneratorWizard: kontrollierte Inputs + onSubmit-Handler - reporting/page.tsx: getMockReport() Fallback entfernt → Fehlerstate - dsr/[requestId]/page.tsx: mockCommunications entfernt → leeres Array (TODO: Backend fehlt) - 52 neue Tests (680 gesamt, alle grün) Co-Authored-By: Claude Sonnet 4.6 --- .../app/api/sdk/v1/dsfa/[[...path]]/route.ts | 123 +++++ admin-compliance/app/sdk/dsfa/page.tsx | 290 +++++++++--- .../app/sdk/dsr/[requestId]/page.tsx | 31 +- admin-compliance/app/sdk/reporting/page.tsx | 80 +--- backend-compliance/compliance/api/__init__.py | 3 + .../compliance/api/dsfa_routes.py | 437 ++++++++++++++++++ backend-compliance/migrations/024_dsfa.sql | 33 ++ backend-compliance/tests/test_dsfa_routes.py | 384 +++++++++++++++ 8 files changed, 1199 insertions(+), 182 deletions(-) create mode 100644 admin-compliance/app/api/sdk/v1/dsfa/[[...path]]/route.ts create mode 100644 backend-compliance/compliance/api/dsfa_routes.py create mode 100644 backend-compliance/migrations/024_dsfa.sql create mode 100644 backend-compliance/tests/test_dsfa_routes.py diff --git a/admin-compliance/app/api/sdk/v1/dsfa/[[...path]]/route.ts b/admin-compliance/app/api/sdk/v1/dsfa/[[...path]]/route.ts new file mode 100644 index 0000000..a4e080e --- /dev/null +++ b/admin-compliance/app/api/sdk/v1/dsfa/[[...path]]/route.ts @@ -0,0 +1,123 @@ +/** + * DSFA API Proxy — Datenschutz-Folgenabschaetzung (Art. 35 DSGVO) + * Proxies /api/sdk/v1/dsfa/* → backend-compliance:8002/api/v1/dsfa/* + */ + +import { NextRequest, NextResponse } from 'next/server' + +const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002' + +async function proxyRequest( + request: NextRequest, + pathSegments: string[] | undefined, + method: string +) { + const pathStr = pathSegments?.join('/') || '' + const searchParams = request.nextUrl.searchParams.toString() + const basePath = `${BACKEND_URL}/api/v1/dsfa` + 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('DSFA API proxy error:', error) + return NextResponse.json( + { error: 'Verbindung zum Compliance 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/dsfa/page.tsx b/admin-compliance/app/sdk/dsfa/page.tsx index 607d690..5526d58 100644 --- a/admin-compliance/app/sdk/dsfa/page.tsx +++ b/admin-compliance/app/sdk/dsfa/page.tsx @@ -1,6 +1,6 @@ 'use client' -import React, { useState, useCallback } from 'react' +import React, { useState, useCallback, useEffect } from 'react' import { useRouter } from 'next/navigation' import { useSDK } from '@/lib/sdk' import { StepHeader, STEP_EXPLANATIONS } from '@/components/sdk/StepHeader' @@ -15,8 +15,8 @@ interface DSFA { title: string description: string status: 'draft' | 'in-review' | 'approved' | 'needs-update' - createdAt: Date - updatedAt: Date + createdAt: string + updatedAt: string approvedBy: string | null riskLevel: 'low' | 'medium' | 'high' | 'critical' processingActivity: string @@ -25,60 +25,19 @@ interface DSFA { measures: string[] } -// ============================================================================= -// MOCK DATA -// ============================================================================= - -const mockDSFAs: DSFA[] = [ - { - id: 'dsfa-1', - title: 'DSFA - Bewerber-Management-System', - description: 'Datenschutz-Folgenabschaetzung fuer das KI-gestuetzte Bewerber-Screening', - status: 'in-review', - createdAt: new Date('2024-01-10'), - updatedAt: new Date('2024-01-20'), - approvedBy: null, - riskLevel: 'high', - processingActivity: 'Automatisierte Bewertung von Bewerbungsunterlagen', - dataCategories: ['Kontaktdaten', 'Beruflicher Werdegang', 'Qualifikationen'], - recipients: ['HR-Abteilung', 'Fachabteilungen'], - measures: ['Verschluesselung', 'Zugriffskontrolle', 'Menschliche Pruefung'], - }, - { - id: 'dsfa-2', - title: 'DSFA - Video-Ueberwachung Buero', - description: 'Datenschutz-Folgenabschaetzung fuer die Videoueberwachung im Buerogebaeude', - status: 'approved', - createdAt: new Date('2023-11-01'), - updatedAt: new Date('2023-12-15'), - approvedBy: 'DSB Mueller', - riskLevel: 'medium', - processingActivity: 'Videoueberwachung zu Sicherheitszwecken', - dataCategories: ['Bilddaten', 'Bewegungsdaten'], - recipients: ['Sicherheitsdienst'], - measures: ['Loeschfristen', 'Zugriffsbeschraenkung', 'Hinweisschilder'], - }, - { - id: 'dsfa-3', - title: 'DSFA - Kundenanalyse', - description: 'Datenschutz-Folgenabschaetzung fuer Big-Data-Kundenanalysen', - status: 'draft', - createdAt: new Date('2024-01-22'), - updatedAt: new Date('2024-01-22'), - approvedBy: null, - riskLevel: 'high', - processingActivity: 'Analyse von Kundenverhalten fuer Marketing', - dataCategories: ['Kaufhistorie', 'Nutzungsverhalten', 'Praeferenzen'], - recipients: ['Marketing', 'Vertrieb'], - measures: [], - }, -] - // ============================================================================= // COMPONENTS // ============================================================================= -function DSFACard({ dsfa }: { dsfa: DSFA }) { +function DSFACard({ + dsfa, + onStatusChange, + onDelete, +}: { + dsfa: DSFA + onStatusChange: (id: string, status: string) => void + onDelete: (id: string) => void +}) { const statusColors = { draft: 'bg-gray-100 text-gray-600 border-gray-200', 'in-review': 'bg-yellow-100 text-yellow-700 border-yellow-200', @@ -100,6 +59,10 @@ function DSFACard({ dsfa }: { dsfa: DSFA }) { critical: 'bg-red-100 text-red-700', } + const createdDate = dsfa.createdAt + ? new Date(dsfa.createdAt).toLocaleDateString('de-DE') + : '—' + return (
- Erstellt: {dsfa.createdAt.toLocaleDateString('de-DE')} + Erstellt: {createdDate} {dsfa.approvedBy && ( Genehmigt von: {dsfa.approvedBy} )}
- - + )} + {dsfa.status === 'in-review' && ( + + )} +
@@ -167,8 +146,37 @@ function DSFACard({ dsfa }: { dsfa: DSFA }) { ) } -function GeneratorWizard({ onClose }: { onClose: () => void }) { +function GeneratorWizard({ onClose, onSubmit }: { onClose: () => void; onSubmit: (data: Partial) => Promise }) { const [step, setStep] = useState(1) + const [saving, setSaving] = useState(false) + const [title, setTitle] = useState('') + const [description, setDescription] = useState('') + const [processingActivity, setProcessingActivity] = useState('') + const [selectedCategories, setSelectedCategories] = useState([]) + const [riskLevel, setRiskLevel] = useState<'low' | 'medium' | 'high' | 'critical'>('low') + const [selectedMeasures, setSelectedMeasures] = useState([]) + + const riskMap: Record = { + Niedrig: 'low', Mittel: 'medium', Hoch: 'high', Kritisch: 'critical', + } + + const handleSubmit = async () => { + setSaving(true) + try { + await onSubmit({ + title, + description, + processingActivity, + dataCategories: selectedCategories, + riskLevel, + measures: selectedMeasures, + status: 'draft', + }) + onClose() + } finally { + setSaving(false) + } + } return (
@@ -208,6 +216,8 @@ function GeneratorWizard({ onClose }: { onClose: () => void }) { setTitle(e.target.value)} placeholder="z.B. DSFA - Mitarbeiter-Monitoring" className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500" /> @@ -216,10 +226,22 @@ function GeneratorWizard({ onClose }: { onClose: () => void }) {