From b7c1a5da1abb53245a5d6fbb4a1428e30c426ff5 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Thu, 5 Mar 2026 00:36:24 +0100 Subject: [PATCH] feat: Consent-Service Module nach Compliance migriert (DSR, E-Mail-Templates, Legal Docs, Banner) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 5-Phasen-Migration: Go consent-service Proxies durch native Python/FastAPI ersetzt. Phase 1 — DSR (Betroffenenrechte): 6 Tabellen, 30 Endpoints, Frontend-API umgestellt Phase 2 — E-Mail-Templates: 5 Tabellen, 20 Endpoints, neues Frontend, SDK_STEPS erweitert Phase 3 — Legal Documents Extension: User Consents, Audit Log, Cookie-Kategorien Phase 4 — Banner Consent: Device-Consents, Site-Configs, Kategorien, Vendors Phase 5 — Cleanup: DSR-Proxy aus main.py entfernt, Frontend-URLs aktualisiert 148 neue Tests (50 + 47 + 26 + 25), alle bestanden. Co-Authored-By: Claude Opus 4.6 --- .../app/sdk/consent-management/page.tsx | 4 +- admin-compliance/app/sdk/dsr/page.tsx | 2 +- .../app/sdk/email-templates/page.tsx | 825 ++++++++++++ admin-compliance/lib/sdk/dsr/api.ts | 643 ++-------- admin-compliance/lib/sdk/types.ts | 26 +- backend-compliance/compliance/api/__init__.py | 9 + .../compliance/api/banner_routes.py | 654 ++++++++++ .../compliance/api/dsr_routes.py | 1118 +++++++++++++++++ .../compliance/api/email_template_routes.py | 825 ++++++++++++ .../compliance/api/legal_document_routes.py | 525 +++++++- .../compliance/db/banner_models.py | 138 ++ .../compliance/db/dsr_models.py | 209 +++ .../compliance/db/email_template_models.py | 135 ++ .../db/legal_document_extend_models.py | 88 ++ backend-compliance/main.py | 12 +- backend-compliance/migrations/026_dsr.sql | 177 +++ .../migrations/027_email_templates.sql | 118 ++ .../migrations/028_legal_documents_extend.sql | 69 + .../migrations/029_banner_consent.sql | 98 ++ .../tests/test_banner_routes.py | 314 +++++ backend-compliance/tests/test_dsr_routes.py | 699 +++++++++++ .../tests/test_email_template_routes.py | 573 +++++++++ .../test_legal_document_routes_extended.py | 427 +++++++ 23 files changed, 7146 insertions(+), 542 deletions(-) create mode 100644 admin-compliance/app/sdk/email-templates/page.tsx create mode 100644 backend-compliance/compliance/api/banner_routes.py create mode 100644 backend-compliance/compliance/api/dsr_routes.py create mode 100644 backend-compliance/compliance/api/email_template_routes.py create mode 100644 backend-compliance/compliance/db/banner_models.py create mode 100644 backend-compliance/compliance/db/dsr_models.py create mode 100644 backend-compliance/compliance/db/email_template_models.py create mode 100644 backend-compliance/compliance/db/legal_document_extend_models.py create mode 100644 backend-compliance/migrations/026_dsr.sql create mode 100644 backend-compliance/migrations/027_email_templates.sql create mode 100644 backend-compliance/migrations/028_legal_documents_extend.sql create mode 100644 backend-compliance/migrations/029_banner_consent.sql create mode 100644 backend-compliance/tests/test_banner_routes.py create mode 100644 backend-compliance/tests/test_dsr_routes.py create mode 100644 backend-compliance/tests/test_email_template_routes.py create mode 100644 backend-compliance/tests/test_legal_document_routes_extended.py diff --git a/admin-compliance/app/sdk/consent-management/page.tsx b/admin-compliance/app/sdk/consent-management/page.tsx index 56778c2..d24da18 100644 --- a/admin-compliance/app/sdk/consent-management/page.tsx +++ b/admin-compliance/app/sdk/consent-management/page.tsx @@ -536,7 +536,7 @@ export default function ConsentManagementPage() { // Try to get DSR count try { - const dsrRes = await fetch('/api/sdk/v1/dsgvo/dsr', { + const dsrRes = await fetch('/api/sdk/v1/compliance/dsr', { headers: { 'X-Tenant-ID': localStorage.getItem('bp_tenant_id') || '', 'X-User-ID': localStorage.getItem('bp_user_id') || '', @@ -558,7 +558,7 @@ export default function ConsentManagementPage() { async function loadGDPRData() { try { - const res = await fetch('/api/sdk/v1/dsgvo/dsr', { + const res = await fetch('/api/sdk/v1/compliance/dsr', { headers: { 'X-Tenant-ID': localStorage.getItem('bp_tenant_id') || '', 'X-User-ID': localStorage.getItem('bp_user_id') || '', diff --git a/admin-compliance/app/sdk/dsr/page.tsx b/admin-compliance/app/sdk/dsr/page.tsx index 1c855c9..7d7ee6d 100644 --- a/admin-compliance/app/sdk/dsr/page.tsx +++ b/admin-compliance/app/sdk/dsr/page.tsx @@ -541,7 +541,7 @@ function DSRDetailPanel({ } const handleExportPDF = () => { - window.open(`/api/sdk/v1/dsgvo/dsr/${request.id}/export`, '_blank') + window.open(`/api/sdk/v1/compliance/dsr/${request.id}/export`, '_blank') } return ( diff --git a/admin-compliance/app/sdk/email-templates/page.tsx b/admin-compliance/app/sdk/email-templates/page.tsx new file mode 100644 index 0000000..552d2af --- /dev/null +++ b/admin-compliance/app/sdk/email-templates/page.tsx @@ -0,0 +1,825 @@ +'use client' + +import React, { useState, useEffect, useCallback } from 'react' +import { useSDK } from '@/lib/sdk' +import { StepHeader, STEP_EXPLANATIONS } from '@/components/sdk/StepHeader' + +// ============================================================================= +// TYPES +// ============================================================================= + +interface EmailTemplate { + id: string + template_type: string + name: string + description: string | null + category: string + is_active: boolean + sort_order: number + variables: string[] + latest_version?: TemplateVersion | null + created_at: string | null + updated_at: string | null +} + +interface TemplateVersion { + id: string + template_id: string + version: string + language: string + subject: string + body_html: string + body_text: string | null + status: string + submitted_at: string | null + published_at: string | null + created_at: string | null +} + +interface TemplateType { + type: string + name: string + category: string + variables: string[] +} + +interface SendLog { + id: string + template_type: string + recipient: string + subject: string + status: string + sent_at: string | null +} + +interface Settings { + sender_name: string + sender_email: string + reply_to: string | null + logo_url: string | null + primary_color: string + secondary_color: string + footer_text: string + company_name: string | null + company_address: string | null +} + +type TabId = 'templates' | 'editor' | 'settings' | 'logs' + +const API_BASE = '/api/sdk/v1/compliance/email-templates' + +function getHeaders(): HeadersInit { + if (typeof window === 'undefined') return { 'Content-Type': 'application/json' } + return { + 'Content-Type': 'application/json', + 'X-Tenant-ID': localStorage.getItem('bp_tenant_id') || '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e', + 'X-User-ID': localStorage.getItem('bp_user_id') || '', + } +} + +// ============================================================================= +// CATEGORY CONFIG +// ============================================================================= + +const CATEGORIES: Record = { + general: { label: 'Allgemein', color: 'text-gray-700', bgColor: 'bg-gray-100' }, + dsr: { label: 'Betroffenenrechte', color: 'text-blue-700', bgColor: 'bg-blue-100' }, + consent: { label: 'Einwilligung', color: 'text-green-700', bgColor: 'bg-green-100' }, + breach: { label: 'Datenpanne', color: 'text-red-700', bgColor: 'bg-red-100' }, + vendor: { label: 'Dienstleister', color: 'text-purple-700', bgColor: 'bg-purple-100' }, + training: { label: 'Schulung', color: 'text-orange-700', bgColor: 'bg-orange-100' }, +} + +const STATUS_BADGE: Record = { + draft: { label: 'Entwurf', color: 'bg-gray-100 text-gray-700' }, + review: { label: 'Pruefung', color: 'bg-yellow-100 text-yellow-700' }, + approved: { label: 'Genehmigt', color: 'bg-blue-100 text-blue-700' }, + published: { label: 'Publiziert', color: 'bg-green-100 text-green-700' }, +} + +// ============================================================================= +// PAGE +// ============================================================================= + +export default function EmailTemplatesPage() { + const sdk = useSDK() + const [activeTab, setActiveTab] = useState('templates') + const [templates, setTemplates] = useState([]) + const [templateTypes, setTemplateTypes] = useState([]) + const [settings, setSettings] = useState(null) + const [logs, setLogs] = useState([]) + const [logsTotal, setLogsTotal] = useState(0) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [selectedCategory, setSelectedCategory] = useState(null) + + // Editor state + const [selectedTemplate, setSelectedTemplate] = useState(null) + const [editorSubject, setEditorSubject] = useState('') + const [editorHtml, setEditorHtml] = useState('') + const [editorVersion, setEditorVersion] = useState(null) + const [saving, setSaving] = useState(false) + const [previewHtml, setPreviewHtml] = useState(null) + + // Settings form + const [settingsForm, setSettingsForm] = useState(null) + const [savingSettings, setSavingSettings] = useState(false) + + // ============================================================================= + // DATA LOADING + // ============================================================================= + + const loadTemplates = useCallback(async () => { + try { + const url = selectedCategory ? `${API_BASE}?category=${selectedCategory}` : API_BASE + const res = await fetch(url, { headers: getHeaders() }) + if (!res.ok) throw new Error(`HTTP ${res.status}`) + const data = await res.json() + setTemplates(Array.isArray(data) ? data : []) + } catch (e: any) { + setError(e.message) + } + }, [selectedCategory]) + + const loadTypes = useCallback(async () => { + try { + const res = await fetch(`${API_BASE}/types`, { headers: getHeaders() }) + if (res.ok) { + const data = await res.json() + setTemplateTypes(Array.isArray(data) ? data : []) + } + } catch { /* ignore */ } + }, []) + + const loadSettings = useCallback(async () => { + try { + const res = await fetch(`${API_BASE}/settings`, { headers: getHeaders() }) + if (res.ok) { + const data = await res.json() + setSettings(data) + setSettingsForm(data) + } + } catch { /* ignore */ } + }, []) + + const loadLogs = useCallback(async () => { + try { + const res = await fetch(`${API_BASE}/logs?limit=50`, { headers: getHeaders() }) + if (res.ok) { + const data = await res.json() + setLogs(data.logs || []) + setLogsTotal(data.total || 0) + } + } catch { /* ignore */ } + }, []) + + useEffect(() => { + setLoading(true) + Promise.all([loadTemplates(), loadTypes(), loadSettings()]) + .finally(() => setLoading(false)) + }, [loadTemplates, loadTypes, loadSettings]) + + useEffect(() => { + if (activeTab === 'logs') loadLogs() + }, [activeTab, loadLogs]) + + useEffect(() => { + loadTemplates() + }, [selectedCategory, loadTemplates]) + + // ============================================================================= + // EDITOR ACTIONS + // ============================================================================= + + const openEditor = useCallback(async (template: EmailTemplate) => { + setSelectedTemplate(template) + setActiveTab('editor') + setPreviewHtml(null) + + if (template.latest_version) { + setEditorSubject(template.latest_version.subject) + setEditorHtml(template.latest_version.body_html) + setEditorVersion(template.latest_version) + } else { + // Load default content + try { + const res = await fetch(`${API_BASE}/default/${template.template_type}`, { headers: getHeaders() }) + if (res.ok) { + const data = await res.json() + setEditorSubject(data.default_subject || '') + setEditorHtml(data.default_body_html || '') + } + } catch { /* ignore */ } + setEditorVersion(null) + } + }, []) + + const saveVersion = useCallback(async () => { + if (!selectedTemplate) return + setSaving(true) + try { + const res = await fetch(`${API_BASE}/${selectedTemplate.id}/versions`, { + method: 'POST', + headers: getHeaders(), + body: JSON.stringify({ + version: editorVersion ? `${parseFloat(editorVersion.version) + 0.1}` : '1.0', + language: 'de', + subject: editorSubject, + body_html: editorHtml, + }), + }) + if (!res.ok) throw new Error(`HTTP ${res.status}`) + const version = await res.json() + setEditorVersion(version) + await loadTemplates() + } catch (e: any) { + setError(e.message) + } finally { + setSaving(false) + } + }, [selectedTemplate, editorSubject, editorHtml, editorVersion, loadTemplates]) + + const publishVersion = useCallback(async () => { + if (!editorVersion) return + setSaving(true) + try { + const res = await fetch(`${API_BASE}/versions/${editorVersion.id}/publish`, { + method: 'POST', + headers: getHeaders(), + }) + if (!res.ok) throw new Error(`HTTP ${res.status}`) + const updated = await res.json() + setEditorVersion(updated) + await loadTemplates() + } catch (e: any) { + setError(e.message) + } finally { + setSaving(false) + } + }, [editorVersion, loadTemplates]) + + const loadPreview = useCallback(async () => { + if (!editorVersion) return + try { + const res = await fetch(`${API_BASE}/versions/${editorVersion.id}/preview`, { + method: 'POST', + headers: getHeaders(), + body: JSON.stringify({ variables: {} }), + }) + if (res.ok) { + const data = await res.json() + setPreviewHtml(data.body_html) + } + } catch { /* ignore */ } + }, [editorVersion]) + + const saveSettings2 = useCallback(async () => { + if (!settingsForm) return + setSavingSettings(true) + try { + const res = await fetch(`${API_BASE}/settings`, { + method: 'PUT', + headers: getHeaders(), + body: JSON.stringify(settingsForm), + }) + if (res.ok) { + const data = await res.json() + setSettings(data) + setSettingsForm(data) + } + } catch (e: any) { + setError(e.message) + } finally { + setSavingSettings(false) + } + }, [settingsForm]) + + const initializeDefaults = useCallback(async () => { + try { + const res = await fetch(`${API_BASE}/initialize`, { + method: 'POST', + headers: getHeaders(), + }) + if (res.ok) { + await loadTemplates() + } + } catch (e: any) { + setError(e.message) + } + }, [loadTemplates]) + + // ============================================================================= + // RENDER + // ============================================================================= + + const tabs: { id: TabId; label: string }[] = [ + { id: 'templates', label: 'Templates' }, + { id: 'editor', label: 'Editor' }, + { id: 'settings', label: 'Einstellungen' }, + { id: 'logs', label: 'Logs' }, + ] + + return ( +
+ + + {error && ( +
+ {error} + +
+ )} + + {/* Tab Navigation */} +
+ +
+ + {/* Tab Content */} + {activeTab === 'templates' && ( + + )} + + {activeTab === 'editor' && ( + setActiveTab('templates')} + /> + )} + + {activeTab === 'settings' && settingsForm && ( + + )} + + {activeTab === 'logs' && ( + + )} +
+ ) +} + +// ============================================================================= +// TAB COMPONENTS +// ============================================================================= + +function TemplatesTab({ + templates, loading, selectedCategory, onCategoryChange, onEdit, onInitialize, +}: { + templates: EmailTemplate[] + loading: boolean + selectedCategory: string | null + onCategoryChange: (cat: string | null) => void + onEdit: (t: EmailTemplate) => void + onInitialize: () => void +}) { + return ( +
+ {/* Category Pills */} +
+ + {Object.entries(CATEGORIES).map(([key, cat]) => ( + + ))} +
+ + {loading ? ( +
Lade Templates...
+ ) : templates.length === 0 ? ( +
+

Keine Templates vorhanden.

+ +
+ ) : ( +
+ {templates.map(t => ( + onEdit(t)} /> + ))} +
+ )} +
+ ) +} + +function TemplateCard({ template, onEdit }: { template: EmailTemplate; onEdit: () => void }) { + const cat = CATEGORIES[template.category] || CATEGORIES.general + const version = template.latest_version + const status = version ? (STATUS_BADGE[version.status] || STATUS_BADGE.draft) : STATUS_BADGE.draft + + return ( +
+
+
+

{template.name}

+ + {cat.label} + +
+ {status.label} +
+ {template.description && ( +

{template.description}

+ )} +
+ {(template.variables || []).slice(0, 4).map(v => ( + + {`{{${v}}}`} + + ))} + {(template.variables || []).length > 4 && ( + +{template.variables.length - 4} + )} +
+ {version && ( +
+ v{version.version} · {version.created_at ? new Date(version.created_at).toLocaleDateString('de-DE') : ''} +
+ )} +
+ ) +} + +function EditorTab({ + template, version, subject, html, previewHtml, saving, + onSubjectChange, onHtmlChange, onSave, onPublish, onPreview, onBack, +}: { + template: EmailTemplate | null + version: TemplateVersion | null + subject: string + html: string + previewHtml: string | null + saving: boolean + onSubjectChange: (v: string) => void + onHtmlChange: (v: string) => void + onSave: () => void + onPublish: () => void + onPreview: () => void + onBack: () => void +}) { + if (!template) { + return ( +
+ Waehlen Sie ein Template aus der Liste. +
+ +
+ ) + } + + const cat = CATEGORIES[template.category] || CATEGORIES.general + + return ( +
+
+
+ +

{template.name}

+ {cat.label} + {version && ( + + {(STATUS_BADGE[version.status] || STATUS_BADGE.draft).label} + + )} +
+
+ + {version && version.status !== 'published' && ( + + )} + {version && ( + + )} +
+
+ + {/* Variables */} +
+ Variablen: + {(template.variables || []).map(v => ( + + ))} +
+ + {/* Split View */} +
+ {/* Editor */} +
+
+ + onSubjectChange(e.target.value)} + className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-transparent" + placeholder="E-Mail Betreff..." + /> +
+
+ +