feat: Package 4 Rechtliche Texte — DB-Persistenz fuer Legal Documents, Einwilligungen und Cookie Banner
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 46s
CI / test-python-backend-compliance (push) Successful in 32s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 17s

- Migration 007: compliance_legal_documents, _versions, _approvals (Approval-Workflow)
- Migration 008: compliance_einwilligungen_catalog, _company, _cookies, _consents
- Backend: legal_document_routes.py (11 Endpoints + draft→review→approved→published Workflow)
- Backend: einwilligungen_routes.py (10 Endpoints inkl. Stats, Pagination, Revoke)
- Frontend: /api/admin/consent/[[...path]] Catch-All-Proxy fuer Legal Documents
- Frontend: catalog/consent/cookie-banner routes von In-Memory auf DB-Proxy umgestellt
- Frontend: einwilligungen/page.tsx + cookie-banner/page.tsx laden jetzt via API (kein Mock)
- Tests: 44/44 pass (test_legal_document_routes.py + test_einwilligungen_routes.py)
- Deploy-Scripts: apply_legal_docs_migration.sh + apply_einwilligungen_migration.sh

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-03 08:25:13 +01:00
parent 799668e472
commit 113ecdfa77
17 changed files with 2501 additions and 664 deletions

View File

@@ -1,215 +1,160 @@
/**
* API Route: Cookie Banner Configuration
*
* Proxies to backend-compliance for DB persistence.
* GET - Cookie Banner Konfiguration abrufen
* POST - Cookie Banner Konfiguration speichern
* PUT - Einzelne Kategorie aktualisieren
*/
import { NextRequest, NextResponse } from 'next/server'
import {
CookieBannerConfig,
CookieBannerStyling,
CookieBannerTexts,
DataPoint,
} from '@/lib/sdk/einwilligungen/types'
import {
generateCookieBannerConfig,
DEFAULT_COOKIE_BANNER_STYLING,
DEFAULT_COOKIE_BANNER_TEXTS,
} from '@/lib/sdk/einwilligungen/generator/cookie-banner'
import { PREDEFINED_DATA_POINTS } from '@/lib/sdk/einwilligungen/catalog/loader'
// In-Memory Storage fuer Cookie Banner Configs
const configStorage = new Map<string, CookieBannerConfig>()
const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002'
function getHeaders(request: NextRequest): HeadersInit {
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
const clientTenantId = request.headers.get('x-tenant-id') || request.headers.get('X-Tenant-ID')
const tenantId = (clientTenantId && uuidRegex.test(clientTenantId))
? clientTenantId
: (process.env.DEFAULT_TENANT_ID || '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e')
return {
'Content-Type': 'application/json',
'X-Tenant-ID': tenantId,
}
}
/**
* GET /api/sdk/v1/einwilligungen/cookie-banner/config
*
* Laedt die Cookie Banner Konfiguration fuer einen Tenant
*/
export async function GET(request: NextRequest) {
try {
const tenantId = request.headers.get('X-Tenant-ID')
const headers = getHeaders(request)
const response = await fetch(
`${BACKEND_URL}/api/compliance/einwilligungen/cookies`,
{ method: 'GET', headers, signal: AbortSignal.timeout(30000) }
)
if (!tenantId) {
return NextResponse.json(
{ error: 'Tenant ID required' },
{ status: 400 }
)
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json({ error: errorText }, { status: response.status })
}
let config = configStorage.get(tenantId)
if (!config) {
// Generiere Default-Konfiguration
config = generateCookieBannerConfig(tenantId, PREDEFINED_DATA_POINTS)
configStorage.set(tenantId, config)
}
return NextResponse.json(config)
const data = await response.json()
// Return in the format the frontend expects (CookieBannerConfig-like)
return NextResponse.json({
categories: data.categories || [],
config: data.config || {},
updatedAt: data.updated_at,
})
} catch (error) {
console.error('Error loading cookie banner config:', error)
return NextResponse.json(
{ error: 'Failed to load cookie banner config' },
{ status: 500 }
)
return NextResponse.json({ error: 'Failed to load cookie banner config' }, { status: 500 })
}
}
/**
* POST /api/sdk/v1/einwilligungen/cookie-banner/config
*
* Speichert oder aktualisiert die Cookie Banner Konfiguration
*
* Body:
* - dataPointIds?: string[] - IDs der Datenpunkte (fuer Neuberechnung)
* - styling?: Partial<CookieBannerStyling> - Styling-Optionen
* - texts?: Partial<CookieBannerTexts> - Text-Optionen
* - customDataPoints?: DataPoint[] - Kundenspezifische Datenpunkte
*/
export async function POST(request: NextRequest) {
try {
const tenantId = request.headers.get('X-Tenant-ID')
if (!tenantId) {
return NextResponse.json(
{ error: 'Tenant ID required' },
{ status: 400 }
)
}
const headers = getHeaders(request)
const body = await request.json()
const {
dataPointIds,
styling,
texts,
customDataPoints = [],
} = body
const { categories, config, styling, texts } = body
// Hole bestehende Konfiguration oder erstelle neue
let config = configStorage.get(tenantId)
if (dataPointIds && Array.isArray(dataPointIds)) {
// Neu berechnen basierend auf Datenpunkten
const allDataPoints: DataPoint[] = [
...PREDEFINED_DATA_POINTS,
...customDataPoints,
]
const selectedDataPoints = dataPointIds
.map((id: string) => allDataPoints.find((dp) => dp.id === id))
.filter((dp): dp is DataPoint => dp !== undefined)
config = generateCookieBannerConfig(
tenantId,
selectedDataPoints,
texts,
styling
)
} else if (config) {
// Nur Styling/Texts aktualisieren
if (styling) {
config.styling = {
...config.styling,
...styling,
}
}
if (texts) {
config.texts = {
...config.texts,
...texts,
}
}
config.updatedAt = new Date()
} else {
// Erstelle Default
config = generateCookieBannerConfig(
tenantId,
PREDEFINED_DATA_POINTS,
texts,
styling
)
const payload = {
categories: categories || [],
config: { ...(config || {}), styling: styling || {}, texts: texts || {} },
}
configStorage.set(tenantId, config)
const response = await fetch(
`${BACKEND_URL}/api/compliance/einwilligungen/cookies`,
{
method: 'PUT',
headers,
body: JSON.stringify(payload),
signal: AbortSignal.timeout(30000),
}
)
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json({ error: errorText }, { status: response.status })
}
const data = await response.json()
return NextResponse.json({
success: true,
config,
categories: data.categories || [],
config: data.config || {},
updatedAt: data.updated_at,
})
} catch (error) {
console.error('Error saving cookie banner config:', error)
return NextResponse.json(
{ error: 'Failed to save cookie banner config' },
{ status: 500 }
)
return NextResponse.json({ error: 'Failed to save cookie banner config' }, { status: 500 })
}
}
/**
* PUT /api/sdk/v1/einwilligungen/cookie-banner/config
*
* Aktualisiert einzelne Kategorien
*/
export async function PUT(request: NextRequest) {
try {
const tenantId = request.headers.get('X-Tenant-ID')
if (!tenantId) {
return NextResponse.json(
{ error: 'Tenant ID required' },
{ status: 400 }
)
}
const headers = getHeaders(request)
const body = await request.json()
const { categoryId, enabled } = body
const { categoryId, enabled, categories, config } = body
// If full categories array is provided, save it directly
if (categories !== undefined) {
const response = await fetch(
`${BACKEND_URL}/api/compliance/einwilligungen/cookies`,
{
method: 'PUT',
headers,
body: JSON.stringify({ categories, config: config || {} }),
signal: AbortSignal.timeout(30000),
}
)
const data = await response.json()
return NextResponse.json({ success: true, ...data })
}
// Single category toggle: fetch current, update, save back
if (!categoryId || typeof enabled !== 'boolean') {
return NextResponse.json(
{ error: 'categoryId and enabled required' },
{ status: 400 }
)
return NextResponse.json({ error: 'categoryId and enabled required' }, { status: 400 })
}
let config = configStorage.get(tenantId)
const currentResponse = await fetch(
`${BACKEND_URL}/api/compliance/einwilligungen/cookies`,
{ method: 'GET', headers, signal: AbortSignal.timeout(30000) }
)
const current = await currentResponse.json()
if (!config) {
config = generateCookieBannerConfig(tenantId, PREDEFINED_DATA_POINTS)
}
const updatedCategories = (current.categories || []).map((cat: { id: string; isRequired?: boolean; defaultEnabled?: boolean }) => {
if (cat.id !== categoryId) return cat
if (cat.isRequired && !enabled) return cat // Essential cookies cannot be disabled
return { ...cat, defaultEnabled: enabled }
})
// Finde und aktualisiere die Kategorie
const categoryIndex = config.categories.findIndex((c) => c.id === categoryId)
if (categoryIndex === -1) {
return NextResponse.json(
{ error: 'Category not found' },
{ status: 404 }
)
}
// Essenzielle Cookies koennen nicht deaktiviert werden
if (config.categories[categoryIndex].isRequired && !enabled) {
return NextResponse.json(
{ error: 'Essential cookies cannot be disabled' },
{ status: 400 }
)
}
config.categories[categoryIndex].defaultEnabled = enabled
config.updatedAt = new Date()
configStorage.set(tenantId, config)
const saveResponse = await fetch(
`${BACKEND_URL}/api/compliance/einwilligungen/cookies`,
{
method: 'PUT',
headers,
body: JSON.stringify({ categories: updatedCategories, config: current.config || {} }),
signal: AbortSignal.timeout(30000),
}
)
const data = await saveResponse.json()
return NextResponse.json({
success: true,
category: config.categories[categoryIndex],
category: updatedCategories.find((c: { id: string }) => c.id === categoryId),
...data,
})
} catch (error) {
console.error('Error updating cookie category:', error)
return NextResponse.json(
{ error: 'Failed to update cookie category' },
{ status: 500 }
)
return NextResponse.json({ error: 'Failed to update cookie category' }, { status: 500 })
}
}