From 58e95d5e8e29ad47a89ad7fec86e41eaf6743475 Mon Sep 17 00:00:00 2001 From: Sharang Parnerkar <30073382+mighty840@users.noreply.github.com> Date: Fri, 10 Apr 2026 19:12:09 +0200 Subject: [PATCH] refactor(admin): split 9 more oversized lib/ files into focused modules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Files split by agents before rate limit: - dsr/api.ts (669 → barrel + helpers) - einwilligungen/context.tsx (669 → barrel + hooks/reducer) - export.ts (753 → barrel + domain exporters) - incidents/api.ts (845 → barrel + api-helpers) - tom-generator/context.tsx (720 → barrel + hooks/reducer) - vendor-compliance/context.tsx (1010 → 234 provider + hooks/reducer) - api-docs/endpoints.ts — partially split (3 domain files created) - academy/api.ts — partially split (helpers extracted) - whistleblower/api.ts — partially split (helpers extracted) next build passes. api-client.ts (885) deferred to next session. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../lib/sdk/academy/api-helpers.ts | 165 ++++ .../lib/sdk/api-docs/endpoints-go.ts | 383 ++++++++ .../lib/sdk/api-docs/endpoints-python-core.ts | 191 ++++ .../lib/sdk/api-docs/endpoints-python-gdpr.ts | 262 ++++++ .../lib/sdk/api-docs/endpoints-python-ops.ts | 449 +++++++++ admin-compliance/lib/sdk/dsr/api-crud.ts | 146 +++ admin-compliance/lib/sdk/dsr/api-mock.ts | 259 ++++++ admin-compliance/lib/sdk/dsr/api-types.ts | 133 +++ admin-compliance/lib/sdk/dsr/api-workflow.ts | 161 ++++ admin-compliance/lib/sdk/dsr/api.ts | 695 +------------- .../lib/sdk/einwilligungen/context.tsx | 671 +------------- .../lib/sdk/einwilligungen/hooks.tsx | 18 + .../lib/sdk/einwilligungen/provider.tsx | 384 ++++++++ .../lib/sdk/einwilligungen/reducer.ts | 237 +++++ admin-compliance/lib/sdk/export-pdf.ts | 361 ++++++++ admin-compliance/lib/sdk/export-zip.ts | 240 +++++ admin-compliance/lib/sdk/export.ts | 721 +-------------- .../lib/sdk/incidents/api-helpers.ts | 83 ++ .../lib/sdk/incidents/api-incidents.ts | 372 ++++++++ .../lib/sdk/incidents/api-mock.ts | 392 ++++++++ admin-compliance/lib/sdk/incidents/api.ts | 861 +----------------- .../lib/sdk/tom-generator/context.tsx | 723 +-------------- .../lib/sdk/tom-generator/hooks.tsx | 20 + .../lib/sdk/tom-generator/provider.tsx | 473 ++++++++++ .../lib/sdk/tom-generator/reducer.ts | 238 +++++ .../lib/sdk/vendor-compliance/context.tsx | 816 +---------------- .../lib/sdk/vendor-compliance/hooks.ts | 88 ++ .../lib/sdk/vendor-compliance/reducer.ts | 178 ++++ .../lib/sdk/vendor-compliance/use-actions.ts | 448 +++++++++ 29 files changed, 5785 insertions(+), 4383 deletions(-) create mode 100644 admin-compliance/lib/sdk/academy/api-helpers.ts create mode 100644 admin-compliance/lib/sdk/api-docs/endpoints-go.ts create mode 100644 admin-compliance/lib/sdk/api-docs/endpoints-python-core.ts create mode 100644 admin-compliance/lib/sdk/api-docs/endpoints-python-gdpr.ts create mode 100644 admin-compliance/lib/sdk/api-docs/endpoints-python-ops.ts create mode 100644 admin-compliance/lib/sdk/dsr/api-crud.ts create mode 100644 admin-compliance/lib/sdk/dsr/api-mock.ts create mode 100644 admin-compliance/lib/sdk/dsr/api-types.ts create mode 100644 admin-compliance/lib/sdk/dsr/api-workflow.ts create mode 100644 admin-compliance/lib/sdk/einwilligungen/hooks.tsx create mode 100644 admin-compliance/lib/sdk/einwilligungen/provider.tsx create mode 100644 admin-compliance/lib/sdk/einwilligungen/reducer.ts create mode 100644 admin-compliance/lib/sdk/export-pdf.ts create mode 100644 admin-compliance/lib/sdk/export-zip.ts create mode 100644 admin-compliance/lib/sdk/incidents/api-helpers.ts create mode 100644 admin-compliance/lib/sdk/incidents/api-incidents.ts create mode 100644 admin-compliance/lib/sdk/incidents/api-mock.ts create mode 100644 admin-compliance/lib/sdk/tom-generator/hooks.tsx create mode 100644 admin-compliance/lib/sdk/tom-generator/provider.tsx create mode 100644 admin-compliance/lib/sdk/tom-generator/reducer.ts create mode 100644 admin-compliance/lib/sdk/vendor-compliance/hooks.ts create mode 100644 admin-compliance/lib/sdk/vendor-compliance/reducer.ts create mode 100644 admin-compliance/lib/sdk/vendor-compliance/use-actions.ts diff --git a/admin-compliance/lib/sdk/academy/api-helpers.ts b/admin-compliance/lib/sdk/academy/api-helpers.ts new file mode 100644 index 0000000..ebc7477 --- /dev/null +++ b/admin-compliance/lib/sdk/academy/api-helpers.ts @@ -0,0 +1,165 @@ +/** + * Academy API - Shared configuration, helpers, and backend type mapping + */ + +import type { + Course, + CourseCategory, + LessonType, +} from './types' + +// ============================================================================= +// CONFIGURATION +// ============================================================================= + +export const ACADEMY_API_BASE = '/api/sdk/v1/academy' +export const API_TIMEOUT = 30000 // 30 seconds + +// ============================================================================= +// HELPER FUNCTIONS +// ============================================================================= + +function getTenantId(): string { + if (typeof window !== 'undefined') { + return localStorage.getItem('bp_tenant_id') || 'default-tenant' + } + return 'default-tenant' +} + +export function getAuthHeaders(): HeadersInit { + const headers: HeadersInit = { + 'Content-Type': 'application/json', + 'X-Tenant-ID': getTenantId() + } + + if (typeof window !== 'undefined') { + const token = localStorage.getItem('authToken') + if (token) { + headers['Authorization'] = `Bearer ${token}` + } + const userId = localStorage.getItem('bp_user_id') + if (userId) { + headers['X-User-ID'] = userId + } + } + + return headers +} + +export async function fetchWithTimeout( + url: string, + options: RequestInit = {}, + timeout: number = API_TIMEOUT +): Promise { + const controller = new AbortController() + const timeoutId = setTimeout(() => controller.abort(), timeout) + + try { + const response = await fetch(url, { + ...options, + signal: controller.signal, + headers: { + ...getAuthHeaders(), + ...options.headers + } + }) + + if (!response.ok) { + const errorBody = await response.text() + let errorMessage = `HTTP ${response.status}: ${response.statusText}` + try { + const errorJson = JSON.parse(errorBody) + errorMessage = errorJson.error || errorJson.message || errorMessage + } catch { + // Keep the HTTP status message + } + throw new Error(errorMessage) + } + + // Handle empty responses + const contentType = response.headers.get('content-type') + if (contentType && contentType.includes('application/json')) { + return response.json() + } + + return {} as T + } finally { + clearTimeout(timeoutId) + } +} + +// ============================================================================= +// BACKEND TYPE MAPPING (snake_case -> camelCase) +// ============================================================================= + +export interface BackendCourse { + id: string + title: string + description: string + category: CourseCategory + duration_minutes: number + required_for_roles: string[] + is_active: boolean + passing_score?: number + status?: string + lessons?: BackendLesson[] + created_at: string + updated_at: string +} + +interface BackendQuizQuestion { + id: string + question: string + options: string[] + correct_index: number + explanation: string +} + +interface BackendLesson { + id: string + course_id: string + title: string + description?: string + lesson_type: LessonType + content_url?: string + duration_minutes: number + order_index: number + quiz_questions?: BackendQuizQuestion[] +} + +export function mapCourseFromBackend(bc: BackendCourse): Course { + return { + id: bc.id, + title: bc.title, + description: bc.description || '', + category: bc.category, + durationMinutes: bc.duration_minutes || 0, + passingScore: bc.passing_score ?? 70, + isActive: bc.is_active ?? true, + status: (bc.status as 'draft' | 'published') ?? 'draft', + requiredForRoles: bc.required_for_roles || [], + lessons: (bc.lessons || []).map(l => ({ + id: l.id, + courseId: l.course_id, + title: l.title, + type: l.lesson_type, + contentMarkdown: l.content_url || '', + durationMinutes: l.duration_minutes || 0, + order: l.order_index, + quizQuestions: (l.quiz_questions || []).map(q => ({ + id: q.id || `q-${Math.random().toString(36).slice(2)}`, + lessonId: l.id, + question: q.question, + options: q.options, + correctOptionIndex: q.correct_index, + explanation: q.explanation, + })), + })), + createdAt: bc.created_at, + updatedAt: bc.updated_at, + } +} + +export function mapCoursesFromBackend(courses: BackendCourse[]): Course[] { + return courses.map(mapCourseFromBackend) +} diff --git a/admin-compliance/lib/sdk/api-docs/endpoints-go.ts b/admin-compliance/lib/sdk/api-docs/endpoints-go.ts new file mode 100644 index 0000000..bf864d6 --- /dev/null +++ b/admin-compliance/lib/sdk/api-docs/endpoints-go.ts @@ -0,0 +1,383 @@ +/** + * Go/Gin endpoints — AI Compliance SDK service modules + * (health, rbac, llm, go-audit, ucca, rag, roadmaps, roadmap-items, + * workshops, portfolios, academy, training, whistleblower, iace) + */ +import { ApiModule } from './types' + +export const goModules: ApiModule[] = [ + { + id: 'go-health', + name: 'Health — System-Status', + service: 'go', + basePath: '/sdk/v1', + exposure: 'admin', + endpoints: [ + { method: 'GET', path: '/health', description: 'API Health-Check', service: 'go', exposure: 'admin' }, + ], + }, + + { + id: 'rbac', + name: 'RBAC — Tenant, Rollen & Berechtigungen', + service: 'go', + basePath: '/sdk/v1', + exposure: 'internal', + endpoints: [ + { method: 'GET', path: '/tenants', description: 'Alle Tenants auflisten', service: 'go' }, + { method: 'GET', path: '/tenants/:id', description: 'Tenant laden', service: 'go' }, + { method: 'POST', path: '/tenants', description: 'Tenant erstellen', service: 'go' }, + { method: 'PUT', path: '/tenants/:id', description: 'Tenant aktualisieren', service: 'go' }, + { method: 'GET', path: '/tenants/:id/namespaces', description: 'Namespaces auflisten', service: 'go' }, + { method: 'POST', path: '/tenants/:id/namespaces', description: 'Namespace erstellen', service: 'go' }, + { method: 'GET', path: '/namespaces/:id', description: 'Namespace laden', service: 'go' }, + { method: 'GET', path: '/roles', description: 'Rollen auflisten', service: 'go' }, + { method: 'GET', path: '/roles/system', description: 'System-Rollen auflisten', service: 'go' }, + { method: 'GET', path: '/roles/:id', description: 'Rolle laden', service: 'go' }, + { method: 'POST', path: '/roles', description: 'Rolle erstellen', service: 'go' }, + { method: 'POST', path: '/user-roles', description: 'Rolle zuweisen', service: 'go' }, + { method: 'DELETE', path: '/user-roles/:userId/:roleId', description: 'Rolle entziehen', service: 'go' }, + { method: 'GET', path: '/user-roles/:userId', description: 'Benutzer-Rollen laden', service: 'go' }, + { method: 'GET', path: '/permissions/effective', description: 'Effektive Berechtigungen laden', service: 'go' }, + { method: 'GET', path: '/permissions/context', description: 'Benutzerkontext laden', service: 'go' }, + { method: 'GET', path: '/permissions/check', description: 'Berechtigung pruefen', service: 'go' }, + ], + }, + + { + id: 'llm', + name: 'LLM — KI-Textverarbeitung & Policies', + service: 'go', + basePath: '/sdk/v1/llm', + exposure: 'partner', + endpoints: [ + { method: 'GET', path: '/policies', description: 'LLM-Policies auflisten', service: 'go' }, + { method: 'GET', path: '/policies/:id', description: 'Policy laden', service: 'go' }, + { method: 'POST', path: '/policies', description: 'Policy erstellen', service: 'go' }, + { method: 'PUT', path: '/policies/:id', description: 'Policy aktualisieren', service: 'go' }, + { method: 'DELETE', path: '/policies/:id', description: 'Policy loeschen', service: 'go' }, + { method: 'POST', path: '/chat', description: 'Chat Completion', service: 'go' }, + { method: 'POST', path: '/complete', description: 'Text Completion', service: 'go' }, + { method: 'GET', path: '/models', description: 'Verfuegbare Modelle auflisten', service: 'go' }, + { method: 'GET', path: '/providers/status', description: 'Provider-Status laden', service: 'go' }, + { method: 'POST', path: '/analyze', description: 'Text analysieren', service: 'go' }, + { method: 'POST', path: '/redact', description: 'PII schwaerzen', service: 'go' }, + ], + }, + + { + id: 'go-audit', + name: 'Audit (Go) — LLM-Audit & Compliance-Reports', + service: 'go', + basePath: '/sdk/v1/audit', + exposure: 'internal', + endpoints: [ + { method: 'GET', path: '/llm', description: 'LLM-Audit-Logs laden', service: 'go' }, + { method: 'GET', path: '/general', description: 'Allgemeine Audit-Logs laden', service: 'go' }, + { method: 'GET', path: '/llm-operations', description: 'LLM-Operationen laden (Alias)', service: 'go' }, + { method: 'GET', path: '/trail', description: 'Audit-Trail laden (Alias)', service: 'go' }, + { method: 'GET', path: '/usage', description: 'Nutzungsstatistiken laden', service: 'go' }, + { method: 'GET', path: '/compliance-report', description: 'Compliance-Report laden', service: 'go' }, + { method: 'GET', path: '/export/llm', description: 'LLM-Audit exportieren', service: 'go' }, + { method: 'GET', path: '/export/general', description: 'Allgemeines Audit exportieren', service: 'go' }, + { method: 'GET', path: '/export/compliance', description: 'Compliance-Report exportieren', service: 'go' }, + ], + }, + + { + id: 'ucca', + name: 'UCCA — Use-Case Compliance Advisor', + service: 'go', + basePath: '/sdk/v1/ucca', + exposure: 'internal', + endpoints: [ + { method: 'POST', path: '/assess', description: 'Compliance-Bewertung durchfuehren', service: 'go' }, + { method: 'GET', path: '/assessments', description: 'Bewertungen auflisten', service: 'go' }, + { method: 'GET', path: '/assessments/:id', description: 'Bewertung laden', service: 'go' }, + { method: 'PUT', path: '/assessments/:id', description: 'Bewertung aktualisieren', service: 'go' }, + { method: 'DELETE', path: '/assessments/:id', description: 'Bewertung loeschen', service: 'go' }, + { method: 'POST', path: '/assessments/:id/explain', description: 'KI-Erklaerung generieren', service: 'go' }, + { method: 'GET', path: '/patterns', description: 'Compliance-Muster laden', service: 'go' }, + { method: 'GET', path: '/examples', description: 'Beispiele laden', service: 'go' }, + { method: 'GET', path: '/rules', description: 'Compliance-Regeln laden', service: 'go' }, + { method: 'GET', path: '/controls', description: 'Controls laden', service: 'go' }, + { method: 'GET', path: '/problem-solutions', description: 'Problem-Loesungs-Paare laden', service: 'go' }, + { method: 'GET', path: '/export/:id', description: 'Bewertung exportieren', service: 'go' }, + { method: 'GET', path: '/escalations', description: 'Eskalationen auflisten', service: 'go' }, + { method: 'GET', path: '/escalations/stats', description: 'Eskalations-Statistiken laden', service: 'go' }, + { method: 'GET', path: '/escalations/:id', description: 'Eskalation laden', service: 'go' }, + { method: 'POST', path: '/escalations', description: 'Eskalation erstellen', service: 'go' }, + { method: 'POST', path: '/escalations/:id/assign', description: 'Eskalation zuweisen', service: 'go' }, + { method: 'POST', path: '/escalations/:id/review', description: 'Review starten', service: 'go' }, + { method: 'POST', path: '/escalations/:id/decide', description: 'Entscheidung treffen', service: 'go' }, + { method: 'POST', path: '/obligations/assess', description: 'Pflichten bewerten', service: 'go' }, + { method: 'GET', path: '/obligations/:assessmentId', description: 'Bewertungsergebnis laden', service: 'go' }, + { method: 'GET', path: '/obligations/:assessmentId/by-regulation', description: 'Nach Regulierung gruppiert', service: 'go' }, + { method: 'GET', path: '/obligations/:assessmentId/by-deadline', description: 'Nach Frist gruppiert', service: 'go' }, + { method: 'GET', path: '/obligations/:assessmentId/by-responsible', description: 'Nach Verantwortlichem gruppiert', service: 'go' }, + { method: 'POST', path: '/obligations/export/memo', description: 'C-Level-Memo exportieren', service: 'go' }, + { method: 'POST', path: '/obligations/export/direct', description: 'Uebersicht direkt exportieren', service: 'go' }, + { method: 'GET', path: '/obligations/regulations', description: 'Regulierungen laden', service: 'go' }, + { method: 'GET', path: '/obligations/regulations/:regulationId/decision-tree', description: 'Entscheidungsbaum laden', service: 'go' }, + { method: 'POST', path: '/obligations/quick-check', description: 'Schnell-Check durchfuehren', service: 'go' }, + { method: 'POST', path: '/obligations/assess-from-scope', description: 'Aus Scope bewerten', service: 'go' }, + { method: 'GET', path: '/obligations/tom-controls/for-obligation/:obligationId', description: 'TOM-Controls fuer Pflicht laden', service: 'go' }, + { method: 'POST', path: '/obligations/gap-analysis', description: 'TOM-Gap-Analyse durchfuehren', service: 'go' }, + { method: 'GET', path: '/obligations/tom-controls/:controlId/obligations', description: 'Pflichten fuer TOM-Control laden', service: 'go' }, + ], + }, + + { + id: 'rag', + name: 'RAG — Legal Corpus & Vektorsuche', + service: 'go', + basePath: '/sdk/v1/rag', + exposure: 'partner', + endpoints: [ + { method: 'POST', path: '/search', description: 'Rechtskorpus durchsuchen', service: 'go' }, + { method: 'GET', path: '/regulations', description: 'Regulierungen auflisten', service: 'go' }, + { method: 'GET', path: '/corpus-status', description: 'Indexierungsstatus laden', service: 'go' }, + { method: 'GET', path: '/corpus-versions/:collection', description: 'Versionshistorie laden', service: 'go' }, + ], + }, + + { + id: 'roadmaps', + name: 'Roadmaps — Compliance-Implementierungsplaene', + service: 'go', + basePath: '/sdk/v1/roadmaps', + exposure: 'internal', + endpoints: [ + { method: 'POST', path: '/', description: 'Roadmap erstellen', service: 'go' }, + { method: 'GET', path: '/', description: 'Roadmaps auflisten', service: 'go' }, + { method: 'GET', path: '/:id', description: 'Roadmap laden', service: 'go' }, + { method: 'PUT', path: '/:id', description: 'Roadmap aktualisieren', service: 'go' }, + { method: 'DELETE', path: '/:id', description: 'Roadmap loeschen', service: 'go' }, + { method: 'GET', path: '/:id/stats', description: 'Roadmap-Statistiken laden', service: 'go' }, + { method: 'POST', path: '/:id/items', description: 'Item erstellen', service: 'go' }, + { method: 'GET', path: '/:id/items', description: 'Items auflisten', service: 'go' }, + { method: 'POST', path: '/import/upload', description: 'Import hochladen', service: 'go' }, + { method: 'GET', path: '/import/:jobId', description: 'Import-Status laden', service: 'go' }, + { method: 'POST', path: '/import/:jobId/confirm', description: 'Import bestaetigen', service: 'go' }, + ], + }, + + { + id: 'roadmap-items', + name: 'Roadmap Items — Einzelne Massnahmen', + service: 'go', + basePath: '/sdk/v1/roadmap-items', + exposure: 'internal', + endpoints: [ + { method: 'GET', path: '/:id', description: 'Item laden', service: 'go' }, + { method: 'PUT', path: '/:id', description: 'Item aktualisieren', service: 'go' }, + { method: 'PATCH', path: '/:id/status', description: 'Item-Status aendern', service: 'go' }, + { method: 'DELETE', path: '/:id', description: 'Item loeschen', service: 'go' }, + ], + }, + + { + id: 'workshops', + name: 'Workshops — Kollaborative Compliance-Workshops', + service: 'go', + basePath: '/sdk/v1/workshops', + exposure: 'internal', + endpoints: [ + { method: 'POST', path: '/', description: 'Workshop erstellen', service: 'go' }, + { method: 'GET', path: '/', description: 'Workshops auflisten', service: 'go' }, + { method: 'GET', path: '/:id', description: 'Workshop laden', service: 'go' }, + { method: 'PUT', path: '/:id', description: 'Workshop aktualisieren', service: 'go' }, + { method: 'DELETE', path: '/:id', description: 'Workshop loeschen', service: 'go' }, + { method: 'POST', path: '/:id/start', description: 'Workshop starten', service: 'go' }, + { method: 'POST', path: '/:id/pause', description: 'Workshop pausieren', service: 'go' }, + { method: 'POST', path: '/:id/complete', description: 'Workshop abschliessen', service: 'go' }, + { method: 'GET', path: '/:id/participants', description: 'Teilnehmer auflisten', service: 'go' }, + { method: 'PUT', path: '/:id/participants/:participantId', description: 'Teilnehmer aktualisieren', service: 'go' }, + { method: 'DELETE', path: '/:id/participants/:participantId', description: 'Teilnehmer entfernen', service: 'go' }, + { method: 'POST', path: '/:id/responses', description: 'Antwort einreichen', service: 'go' }, + { method: 'GET', path: '/:id/responses', description: 'Antworten laden', service: 'go' }, + { method: 'POST', path: '/:id/comments', description: 'Kommentar hinzufuegen', service: 'go' }, + { method: 'GET', path: '/:id/comments', description: 'Kommentare laden', service: 'go' }, + { method: 'POST', path: '/:id/advance', description: 'Zum naechsten Schritt', service: 'go' }, + { method: 'POST', path: '/:id/goto', description: 'Zu bestimmtem Schritt springen', service: 'go' }, + { method: 'GET', path: '/:id/stats', description: 'Workshop-Statistiken laden', service: 'go' }, + { method: 'GET', path: '/:id/summary', description: 'Zusammenfassung laden', service: 'go' }, + { method: 'GET', path: '/:id/export', description: 'Workshop exportieren', service: 'go' }, + { method: 'POST', path: '/join/:code', description: 'Per Zugangscode beitreten', service: 'go' }, + ], + }, + + { + id: 'portfolios', + name: 'Portfolios — KI-Use-Case-Portfolio', + service: 'go', + basePath: '/sdk/v1/portfolios', + exposure: 'internal', + endpoints: [ + { method: 'POST', path: '/', description: 'Portfolio erstellen', service: 'go' }, + { method: 'GET', path: '/', description: 'Portfolios auflisten', service: 'go' }, + { method: 'GET', path: '/:id', description: 'Portfolio laden', service: 'go' }, + { method: 'PUT', path: '/:id', description: 'Portfolio aktualisieren', service: 'go' }, + { method: 'DELETE', path: '/:id', description: 'Portfolio loeschen', service: 'go' }, + { method: 'POST', path: '/:id/items', description: 'Item hinzufuegen', service: 'go' }, + { method: 'GET', path: '/:id/items', description: 'Items auflisten', service: 'go' }, + { method: 'POST', path: '/:id/items/bulk', description: 'Items Bulk-Import', service: 'go' }, + { method: 'DELETE', path: '/:id/items/:itemId', description: 'Item entfernen', service: 'go' }, + { method: 'PUT', path: '/:id/items/order', description: 'Items sortieren', service: 'go' }, + { method: 'GET', path: '/:id/stats', description: 'Portfolio-Statistiken laden', service: 'go' }, + { method: 'GET', path: '/:id/activity', description: 'Aktivitaets-Log laden', service: 'go' }, + { method: 'POST', path: '/:id/recalculate', description: 'Metriken neu berechnen', service: 'go' }, + { method: 'POST', path: '/:id/submit-review', description: 'Zur Pruefung einreichen', service: 'go' }, + { method: 'POST', path: '/:id/approve', description: 'Portfolio genehmigen', service: 'go' }, + { method: 'POST', path: '/merge', description: 'Portfolios zusammenfuehren', service: 'go' }, + { method: 'POST', path: '/compare', description: 'Portfolios vergleichen', service: 'go' }, + ], + }, + + { + id: 'academy', + name: 'Academy — E-Learning & Zertifikate', + service: 'go', + basePath: '/sdk/v1/academy', + exposure: 'internal', + endpoints: [ + { method: 'POST', path: '/courses', description: 'Kurs erstellen', service: 'go' }, + { method: 'GET', path: '/courses', description: 'Kurse auflisten', service: 'go' }, + { method: 'GET', path: '/courses/:id', description: 'Kurs laden', service: 'go' }, + { method: 'PUT', path: '/courses/:id', description: 'Kurs aktualisieren', service: 'go' }, + { method: 'DELETE', path: '/courses/:id', description: 'Kurs loeschen', service: 'go' }, + { method: 'POST', path: '/enrollments', description: 'Einschreibung erstellen', service: 'go' }, + { method: 'GET', path: '/enrollments', description: 'Einschreibungen auflisten', service: 'go' }, + { method: 'PUT', path: '/enrollments/:id/progress', description: 'Fortschritt aktualisieren', service: 'go' }, + { method: 'POST', path: '/enrollments/:id/complete', description: 'Einschreibung abschliessen', service: 'go' }, + { method: 'GET', path: '/certificates/:id', description: 'Zertifikat laden', service: 'go' }, + { method: 'POST', path: '/enrollments/:id/certificate', description: 'Zertifikat generieren', service: 'go' }, + { method: 'GET', path: '/certificates/:id/pdf', description: 'Zertifikat-PDF herunterladen', service: 'go' }, + { method: 'POST', path: '/courses/:id/quiz', description: 'Quiz einreichen', service: 'go' }, + { method: 'PUT', path: '/lessons/:id', description: 'Lektion aktualisieren', service: 'go' }, + { method: 'POST', path: '/lessons/:id/quiz-test', description: 'Quiz testen', service: 'go' }, + { method: 'GET', path: '/stats', description: 'Academy-Statistiken laden', service: 'go' }, + { method: 'POST', path: '/courses/generate', description: 'Kurs aus Modul generieren', service: 'go' }, + { method: 'POST', path: '/courses/generate-all', description: 'Alle Kurse generieren', service: 'go' }, + ], + }, + + { + id: 'training', + name: 'Training — Schulungsmodule & Content-Pipeline', + service: 'go', + basePath: '/sdk/v1/training', + exposure: 'internal', + endpoints: [ + { method: 'GET', path: '/modules', description: 'Schulungsmodule auflisten', service: 'go' }, + { method: 'GET', path: '/modules/:id', description: 'Modul laden', service: 'go' }, + { method: 'POST', path: '/modules', description: 'Modul erstellen', service: 'go' }, + { method: 'PUT', path: '/modules/:id', description: 'Modul aktualisieren', service: 'go' }, + { method: 'GET', path: '/matrix', description: 'Schulungsmatrix laden', service: 'go' }, + { method: 'GET', path: '/matrix/:role', description: 'Matrix fuer Rolle laden', service: 'go' }, + { method: 'POST', path: '/matrix', description: 'Matrix-Eintrag setzen', service: 'go' }, + { method: 'DELETE', path: '/matrix/:role/:moduleId', description: 'Matrix-Eintrag loeschen', service: 'go' }, + { method: 'POST', path: '/assignments/compute', description: 'Zuweisungen berechnen', service: 'go' }, + { method: 'GET', path: '/assignments', description: 'Zuweisungen auflisten', service: 'go' }, + { method: 'GET', path: '/assignments/:id', description: 'Zuweisung laden', service: 'go' }, + { method: 'POST', path: '/assignments/:id/start', description: 'Zuweisung starten', service: 'go' }, + { method: 'POST', path: '/assignments/:id/progress', description: 'Fortschritt aktualisieren', service: 'go' }, + { method: 'POST', path: '/assignments/:id/complete', description: 'Zuweisung abschliessen', service: 'go' }, + { method: 'GET', path: '/quiz/:moduleId', description: 'Quiz laden', service: 'go' }, + { method: 'POST', path: '/quiz/:moduleId/submit', description: 'Quiz einreichen', service: 'go' }, + { method: 'GET', path: '/quiz/attempts/:assignmentId', description: 'Quiz-Versuche laden', service: 'go' }, + { method: 'POST', path: '/content/generate', description: 'Inhalt generieren', service: 'go' }, + { method: 'POST', path: '/content/generate-quiz', description: 'Quiz generieren', service: 'go' }, + { method: 'POST', path: '/content/generate-all', description: 'Alle Inhalte generieren', service: 'go' }, + { method: 'POST', path: '/content/generate-all-quiz', description: 'Alle Quizze generieren', service: 'go' }, + { method: 'GET', path: '/content/:moduleId', description: 'Modul-Inhalt laden', service: 'go' }, + { method: 'POST', path: '/content/:moduleId/publish', description: 'Inhalt veroeffentlichen', service: 'go' }, + { method: 'POST', path: '/content/:moduleId/generate-audio', description: 'Audio generieren', service: 'go' }, + { method: 'POST', path: '/content/:moduleId/generate-video', description: 'Video generieren', service: 'go' }, + { method: 'POST', path: '/content/:moduleId/preview-script', description: 'Video-Script Vorschau', service: 'go' }, + { method: 'GET', path: '/media/module/:moduleId', description: 'Medien fuer Modul laden', service: 'go' }, + { method: 'GET', path: '/media/:mediaId/url', description: 'Medien-URL laden', service: 'go' }, + { method: 'POST', path: '/media/:mediaId/publish', description: 'Medium veroeffentlichen', service: 'go' }, + { method: 'GET', path: '/deadlines', description: 'Fristen laden', service: 'go' }, + { method: 'GET', path: '/deadlines/overdue', description: 'Ueberfaellige Fristen laden', service: 'go' }, + { method: 'POST', path: '/escalation/check', description: 'Eskalation pruefen', service: 'go' }, + { method: 'GET', path: '/audit-log', description: 'Schulungs-Audit-Log laden', service: 'go' }, + { method: 'GET', path: '/stats', description: 'Schulungs-Statistiken laden', service: 'go' }, + { method: 'GET', path: '/certificates/:id/verify', description: 'Zertifikat verifizieren', service: 'go', exposure: 'partner' }, + ], + }, + + { + id: 'whistleblower', + name: 'Whistleblower — Hinweisgebersystem (HinSchG)', + service: 'go', + basePath: '/sdk/v1/whistleblower', + exposure: 'internal', + endpoints: [ + { method: 'POST', path: '/reports/submit', description: 'Anonymen Hinweis einreichen', service: 'go', exposure: 'public' }, + { method: 'GET', path: '/reports/access/:accessKey', description: 'Hinweis per Zugangscode laden', service: 'go', exposure: 'public' }, + { method: 'POST', path: '/reports/access/:accessKey/messages', description: 'Nachricht senden (anonym)', service: 'go', exposure: 'public' }, + { method: 'GET', path: '/reports', description: 'Alle Hinweise auflisten', service: 'go' }, + { method: 'GET', path: '/reports/:id', description: 'Hinweis laden', service: 'go' }, + { method: 'PUT', path: '/reports/:id', description: 'Hinweis aktualisieren', service: 'go' }, + { method: 'DELETE', path: '/reports/:id', description: 'Hinweis loeschen', service: 'go' }, + { method: 'POST', path: '/reports/:id/acknowledge', description: 'Eingangsbestaetigung senden', service: 'go' }, + { method: 'POST', path: '/reports/:id/investigate', description: 'Untersuchung starten', service: 'go' }, + { method: 'POST', path: '/reports/:id/measures', description: 'Abhilfemassnahme hinzufuegen', service: 'go' }, + { method: 'POST', path: '/reports/:id/close', description: 'Hinweis schliessen', service: 'go' }, + { method: 'POST', path: '/reports/:id/messages', description: 'Admin-Nachricht senden', service: 'go' }, + { method: 'GET', path: '/reports/:id/messages', description: 'Nachrichten laden', service: 'go' }, + { method: 'GET', path: '/stats', description: 'Whistleblower-Statistiken laden', service: 'go' }, + ], + }, + + { + id: 'iace', + name: 'IACE — Industrial AI / CE-Compliance Engine', + service: 'go', + basePath: '/sdk/v1/iace', + exposure: 'internal', + endpoints: [ + { method: 'GET', path: '/hazard-library', description: 'Gefahrenbibliothek laden', service: 'go' }, + { method: 'GET', path: '/controls-library', description: 'Controls-Bibliothek laden', service: 'go' }, + { method: 'POST', path: '/projects', description: 'Projekt erstellen', service: 'go' }, + { method: 'GET', path: '/projects', description: 'Projekte auflisten', service: 'go' }, + { method: 'GET', path: '/projects/:id', description: 'Projekt laden', service: 'go' }, + { method: 'PUT', path: '/projects/:id', description: 'Projekt aktualisieren', service: 'go' }, + { method: 'DELETE', path: '/projects/:id', description: 'Projekt archivieren', service: 'go' }, + { method: 'POST', path: '/projects/:id/init-from-profile', description: 'Aus Unternehmensprofil initialisieren', service: 'go' }, + { method: 'POST', path: '/projects/:id/completeness-check', description: 'Vollstaendigkeits-Check durchfuehren', service: 'go' }, + { method: 'POST', path: '/projects/:id/components', description: 'Komponente erstellen', service: 'go' }, + { method: 'GET', path: '/projects/:id/components', description: 'Komponenten auflisten', service: 'go' }, + { method: 'PUT', path: '/projects/:id/components/:cid', description: 'Komponente aktualisieren', service: 'go' }, + { method: 'DELETE', path: '/projects/:id/components/:cid', description: 'Komponente loeschen', service: 'go' }, + { method: 'POST', path: '/projects/:id/classify', description: 'Regulatorisch klassifizieren', service: 'go' }, + { method: 'GET', path: '/projects/:id/classifications', description: 'Klassifizierungen laden', service: 'go' }, + { method: 'POST', path: '/projects/:id/classify/:regulation', description: 'Fuer einzelne Regulierung klassifizieren', service: 'go' }, + { method: 'POST', path: '/projects/:id/hazards', description: 'Gefaehrdung erstellen', service: 'go' }, + { method: 'GET', path: '/projects/:id/hazards', description: 'Gefaehrdungen auflisten', service: 'go' }, + { method: 'PUT', path: '/projects/:id/hazards/:hid', description: 'Gefaehrdung aktualisieren', service: 'go' }, + { method: 'POST', path: '/projects/:id/hazards/suggest', description: 'KI-Gefaehrdungsvorschlaege generieren', service: 'go' }, + { method: 'POST', path: '/projects/:id/hazards/:hid/assess', description: 'Risiko bewerten', service: 'go' }, + { method: 'GET', path: '/projects/:id/risk-summary', description: 'Risiko-Zusammenfassung laden', service: 'go' }, + { method: 'POST', path: '/projects/:id/hazards/:hid/reassess', description: 'Risiko neu bewerten', service: 'go' }, + { method: 'POST', path: '/projects/:id/hazards/:hid/mitigations', description: 'Risikominderung erstellen', service: 'go' }, + { method: 'PUT', path: '/mitigations/:mid', description: 'Risikominderung aktualisieren', service: 'go' }, + { method: 'POST', path: '/mitigations/:mid/verify', description: 'Risikominderung verifizieren', service: 'go' }, + { method: 'POST', path: '/projects/:id/evidence', description: 'Nachweis hochladen', service: 'go' }, + { method: 'GET', path: '/projects/:id/evidence', description: 'Nachweise auflisten', service: 'go' }, + { method: 'POST', path: '/projects/:id/verification-plan', description: 'Verifizierungsplan erstellen', service: 'go' }, + { method: 'PUT', path: '/verification-plan/:vid', description: 'Plan aktualisieren', service: 'go' }, + { method: 'POST', path: '/verification-plan/:vid/complete', description: 'Verifizierung abschliessen', service: 'go' }, + { method: 'POST', path: '/projects/:id/tech-file/generate', description: 'Technische Akte generieren', service: 'go' }, + { method: 'GET', path: '/projects/:id/tech-file', description: 'Akte-Abschnitte laden', service: 'go' }, + { method: 'PUT', path: '/projects/:id/tech-file/:section', description: 'Abschnitt aktualisieren', service: 'go' }, + { method: 'POST', path: '/projects/:id/tech-file/:section/approve', description: 'Abschnitt genehmigen', service: 'go' }, + { method: 'GET', path: '/projects/:id/tech-file/export', description: 'Technische Akte exportieren', service: 'go' }, + { method: 'POST', path: '/projects/:id/monitoring', description: 'Monitoring-Event erstellen', service: 'go' }, + { method: 'GET', path: '/projects/:id/monitoring', description: 'Monitoring-Events laden', service: 'go' }, + { method: 'PUT', path: '/projects/:id/monitoring/:eid', description: 'Event aktualisieren', service: 'go' }, + { method: 'GET', path: '/projects/:id/audit-trail', description: 'Projekt-Audit-Trail laden', service: 'go' }, + ], + }, +] diff --git a/admin-compliance/lib/sdk/api-docs/endpoints-python-core.ts b/admin-compliance/lib/sdk/api-docs/endpoints-python-core.ts new file mode 100644 index 0000000..71842aa --- /dev/null +++ b/admin-compliance/lib/sdk/api-docs/endpoints-python-core.ts @@ -0,0 +1,191 @@ +/** + * Python/FastAPI endpoints — Core compliance modules + * (framework, audit, change-requests, company-profile, projects, + * compliance-scope, dashboard, generation, extraction, modules) + */ +import { ApiModule } from './types' + +export const pythonCoreModules: ApiModule[] = [ + { + id: 'compliance-framework', + name: 'Compliance Framework — Regulierungen, Anforderungen & Controls', + service: 'python', + basePath: '/api/compliance', + exposure: 'internal', + endpoints: [ + { method: 'GET', path: '/regulations', description: 'Alle Regulierungen auflisten', service: 'python' }, + { method: 'GET', path: '/regulations/{code}', description: 'Regulierung nach Code laden', service: 'python' }, + { method: 'GET', path: '/regulations/{code}/requirements', description: 'Anforderungen einer Regulierung', service: 'python' }, + { method: 'GET', path: '/requirements', description: 'Anforderungen auflisten (paginiert)', service: 'python' }, + { method: 'GET', path: '/requirements/{requirement_id}', description: 'Einzelne Anforderung laden', service: 'python' }, + { method: 'POST', path: '/requirements', description: 'Anforderung erstellen', service: 'python' }, + { method: 'PUT', path: '/requirements/{requirement_id}', description: 'Anforderung aktualisieren', service: 'python' }, + { method: 'DELETE', path: '/requirements/{requirement_id}', description: 'Anforderung loeschen', service: 'python' }, + { method: 'GET', path: '/controls', description: 'Alle Controls auflisten', service: 'python' }, + { method: 'GET', path: '/controls/paginated', description: 'Controls paginiert laden', service: 'python' }, + { method: 'GET', path: '/controls/{control_id}', description: 'Einzelnes Control laden', service: 'python' }, + { method: 'PUT', path: '/controls/{control_id}', description: 'Control aktualisieren', service: 'python' }, + { method: 'PUT', path: '/controls/{control_id}/review', description: 'Control-Review durchfuehren', service: 'python' }, + { method: 'GET', path: '/controls/by-domain/{domain}', description: 'Controls nach Domain filtern', service: 'python' }, + { method: 'POST', path: '/export', description: 'Audit-Export erstellen', service: 'python' }, + { method: 'GET', path: '/export/{export_id}', description: 'Export-Status abfragen', service: 'python' }, + { method: 'GET', path: '/export/{export_id}/download', description: 'Export-Datei herunterladen', service: 'python' }, + { method: 'GET', path: '/exports', description: 'Alle Exports auflisten', service: 'python' }, + { method: 'POST', path: '/init-tables', description: 'Datenbanktabellen initialisieren', service: 'python', exposure: 'admin' }, + { method: 'POST', path: '/create-indexes', description: 'Datenbank-Indizes erstellen', service: 'python', exposure: 'admin' }, + { method: 'POST', path: '/seed-risks', description: 'Risikodaten einspielen', service: 'python', exposure: 'admin' }, + { method: 'POST', path: '/seed', description: 'Systemdaten einspielen', service: 'python', exposure: 'admin' }, + ], + }, + + { + id: 'audit', + name: 'Audit — Sitzungen & Checklisten', + service: 'python', + basePath: '/api/compliance/audit', + exposure: 'internal', + endpoints: [ + { method: 'POST', path: '/sessions', description: 'Audit-Sitzung erstellen', service: 'python' }, + { method: 'GET', path: '/sessions', description: 'Alle Audit-Sitzungen auflisten', service: 'python' }, + { method: 'GET', path: '/sessions/{session_id}', description: 'Sitzung laden', service: 'python' }, + { method: 'PUT', path: '/sessions/{session_id}/start', description: 'Sitzung starten', service: 'python' }, + { method: 'PUT', path: '/sessions/{session_id}/complete', description: 'Sitzung abschliessen', service: 'python' }, + { method: 'PUT', path: '/sessions/{session_id}/archive', description: 'Sitzung archivieren', service: 'python' }, + { method: 'DELETE', path: '/sessions/{session_id}', description: 'Sitzung loeschen', service: 'python' }, + { method: 'GET', path: '/sessions/{session_id}/report/pdf', description: 'Sitzungsbericht als PDF exportieren', service: 'python' }, + { method: 'GET', path: '/checklist/{session_id}', description: 'Checkliste einer Sitzung laden', service: 'python' }, + { method: 'PUT', path: '/checklist/{session_id}/items/{requirement_id}/sign-off', description: 'Anforderung abzeichnen', service: 'python' }, + { method: 'GET', path: '/checklist/{session_id}/items/{requirement_id}', description: 'Abzeichnung-Details laden', service: 'python' }, + ], + }, + + { + id: 'ai-systems', + name: 'AI Act — KI-Systeme & Risikobewertung', + service: 'python', + basePath: '/api/compliance/ai', + exposure: 'internal', + endpoints: [ + { method: 'GET', path: '/systems', description: 'KI-Systeme auflisten', service: 'python' }, + { method: 'POST', path: '/systems', description: 'KI-System erstellen', service: 'python' }, + { method: 'GET', path: '/systems/{system_id}', description: 'KI-System laden', service: 'python' }, + { method: 'PUT', path: '/systems/{system_id}', description: 'KI-System aktualisieren', service: 'python' }, + { method: 'DELETE', path: '/systems/{system_id}', description: 'KI-System loeschen', service: 'python' }, + { method: 'POST', path: '/systems/{system_id}/assess', description: 'KI-Compliance bewerten', service: 'python' }, + ], + }, + + { + id: 'change-requests', + name: 'Change Requests — Aenderungsantraege', + service: 'python', + basePath: '/api/compliance/change-requests', + exposure: 'internal', + endpoints: [ + { method: 'GET', path: '/stats', description: 'CR-Statistiken laden', service: 'python' }, + { method: 'GET', path: '/{cr_id}', description: 'Einzelnen CR laden', service: 'python' }, + { method: 'POST', path: '/{cr_id}/accept', description: 'CR akzeptieren', service: 'python' }, + { method: 'POST', path: '/{cr_id}/reject', description: 'CR ablehnen', service: 'python' }, + { method: 'POST', path: '/{cr_id}/edit', description: 'CR bearbeiten', service: 'python' }, + { method: 'DELETE', path: '/{cr_id}', description: 'CR loeschen', service: 'python' }, + ], + }, + + { + id: 'company-profile', + name: 'Stammdaten — Unternehmensprofil', + service: 'python', + basePath: '/api/v1/company-profile', + exposure: 'internal', + endpoints: [ + { method: 'GET', path: '/', description: 'Unternehmensprofil laden', service: 'python' }, + { method: 'POST', path: '/', description: 'Profil erstellen/aktualisieren', service: 'python' }, + { method: 'DELETE', path: '/', description: 'Profil loeschen', service: 'python' }, + { method: 'GET', path: '/template-context', description: 'Profil als Template-Kontext (flach)', service: 'python' }, + { method: 'GET', path: '/audit', description: 'Profil-Aenderungsprotokoll laden', service: 'python' }, + ], + }, + + { + id: 'projects', + name: 'Projekte — Multi-Projekt-Verwaltung', + service: 'python', + basePath: '/api/compliance/v1/projects', + exposure: 'internal', + endpoints: [ + { method: 'GET', path: '/', description: 'Alle Projekte des Tenants auflisten', service: 'python' }, + { method: 'POST', path: '/', description: 'Neues Projekt erstellen (optional mit Stammdaten-Kopie)', service: 'python' }, + { method: 'GET', path: '/{project_id}', description: 'Einzelnes Projekt laden', service: 'python' }, + { method: 'PATCH', path: '/{project_id}', description: 'Projekt aktualisieren (Name, Beschreibung)', service: 'python' }, + { method: 'DELETE', path: '/{project_id}', description: 'Projekt archivieren (Soft Delete)', service: 'python' }, + ], + }, + + { + id: 'compliance-scope', + name: 'Compliance Scope — Geltungsbereich', + service: 'python', + basePath: '/api/v1/compliance-scope', + exposure: 'internal', + endpoints: [ + { method: 'GET', path: '/', description: 'Compliance-Scope laden', service: 'python' }, + { method: 'POST', path: '/', description: 'Compliance-Scope erstellen/aktualisieren', service: 'python' }, + ], + }, + + { + id: 'dashboard', + name: 'Dashboard — Compliance-Uebersicht & Reports', + service: 'python', + basePath: '/api/compliance/dashboard', + exposure: 'internal', + endpoints: [ + { method: 'GET', path: '/dashboard', description: 'Haupt-Dashboard laden', service: 'python' }, + { method: 'GET', path: '/score', description: 'Compliance-Score berechnen', service: 'python' }, + { method: 'GET', path: '/dashboard/executive', description: 'Executive-Dashboard laden', service: 'python' }, + { method: 'GET', path: '/dashboard/trend', description: 'Compliance-Trendverlauf laden', service: 'python' }, + { method: 'GET', path: '/reports/summary', description: 'Zusammenfassungsbericht laden', service: 'python' }, + { method: 'GET', path: '/reports/{period}', description: 'Periodenbericht generieren', service: 'python' }, + ], + }, + + { + id: 'generation', + name: 'Dokumentengenerierung — Automatische Erstellung', + service: 'python', + basePath: '/api/compliance/generation', + exposure: 'internal', + endpoints: [ + { method: 'GET', path: '/preview/{doc_type}', description: 'Generierungs-Vorschau laden', service: 'python' }, + { method: 'POST', path: '/apply/{doc_type}', description: 'Dokument generieren und anwenden', service: 'python' }, + ], + }, + + { + id: 'extraction', + name: 'Extraktion — Anforderungen aus RAG', + service: 'python', + basePath: '/api/compliance', + exposure: 'internal', + endpoints: [ + { method: 'POST', path: '/extract-requirements-from-rag', description: 'Anforderungen aus RAG-Korpus extrahieren', service: 'python' }, + ], + }, + + { + id: 'modules', + name: 'Module — Compliance-Modul-Verwaltung', + service: 'python', + basePath: '/api/compliance/modules', + exposure: 'internal', + endpoints: [ + { method: 'GET', path: '/modules', description: 'Module auflisten', service: 'python' }, + { method: 'GET', path: '/modules/overview', description: 'Modul-Uebersicht laden', service: 'python' }, + { method: 'GET', path: '/modules/{module_id}', description: 'Modul laden', service: 'python' }, + { method: 'POST', path: '/modules/seed', description: 'Module einspielen', service: 'python', exposure: 'admin' }, + { method: 'POST', path: '/modules/{module_id}/activate', description: 'Modul aktivieren', service: 'python' }, + { method: 'POST', path: '/modules/{module_id}/deactivate', description: 'Modul deaktivieren', service: 'python' }, + { method: 'POST', path: '/modules/{module_id}/regulations', description: 'Regulierungs-Zuordnung hinzufuegen', service: 'python' }, + ], + }, +] diff --git a/admin-compliance/lib/sdk/api-docs/endpoints-python-gdpr.ts b/admin-compliance/lib/sdk/api-docs/endpoints-python-gdpr.ts new file mode 100644 index 0000000..9001019 --- /dev/null +++ b/admin-compliance/lib/sdk/api-docs/endpoints-python-gdpr.ts @@ -0,0 +1,262 @@ +/** + * Python/FastAPI endpoints — GDPR, DSR, consent, and data-subject modules + * (banner, consent-templates, dsfa, dsr, einwilligungen, loeschfristen, + * consent-user, consent-admin, dsr-user, dsr-admin, gdpr) + */ +import { ApiModule } from './types' + +export const pythonGdprModules: ApiModule[] = [ + { + id: 'banner', + name: 'Cookie-Banner & Consent Management', + service: 'python', + basePath: '/api/compliance/consent', + exposure: 'internal', + endpoints: [ + { method: 'POST', path: '/consent', description: 'Einwilligung erfassen', service: 'python', exposure: 'public' }, + { method: 'GET', path: '/consent', description: 'Einwilligungen auflisten', service: 'python' }, + { method: 'DELETE', path: '/consent/{consent_id}', description: 'Einwilligung loeschen', service: 'python' }, + { method: 'GET', path: '/consent/export', description: 'Einwilligungsdaten exportieren', service: 'python' }, + { method: 'GET', path: '/config/{site_id}', description: 'Seitenkonfiguration laden', service: 'python', exposure: 'public' }, + { method: 'GET', path: '/admin/sites', description: 'Alle Seiten auflisten', service: 'python' }, + { method: 'POST', path: '/admin/sites', description: 'Seite erstellen', service: 'python' }, + { method: 'PUT', path: '/admin/sites/{site_id}', description: 'Seite aktualisieren', service: 'python' }, + { method: 'DELETE', path: '/admin/sites/{site_id}', description: 'Seite loeschen', service: 'python' }, + { method: 'GET', path: '/admin/sites/{site_id}/categories', description: 'Cookie-Kategorien auflisten', service: 'python' }, + { method: 'POST', path: '/admin/sites/{site_id}/categories', description: 'Cookie-Kategorie erstellen', service: 'python' }, + { method: 'DELETE', path: '/admin/categories/{category_id}', description: 'Cookie-Kategorie loeschen', service: 'python' }, + { method: 'GET', path: '/admin/sites/{site_id}/vendors', description: 'Anbieter auflisten', service: 'python' }, + { method: 'POST', path: '/admin/sites/{site_id}/vendors', description: 'Anbieter hinzufuegen', service: 'python' }, + { method: 'DELETE', path: '/admin/vendors/{vendor_id}', description: 'Anbieter loeschen', service: 'python' }, + { method: 'GET', path: '/admin/stats/{site_id}', description: 'Seiten-Statistiken laden', service: 'python' }, + ], + }, + + { + id: 'consent-templates', + name: 'Einwilligungsvorlagen — Consent Templates', + service: 'python', + basePath: '/api/compliance/consent-templates', + exposure: 'internal', + endpoints: [ + { method: 'GET', path: '/consent-templates', description: 'Vorlagen auflisten', service: 'python' }, + { method: 'POST', path: '/consent-templates', description: 'Vorlage erstellen', service: 'python' }, + { method: 'PUT', path: '/consent-templates/{template_id}', description: 'Vorlage aktualisieren', service: 'python' }, + { method: 'DELETE', path: '/consent-templates/{template_id}', description: 'Vorlage loeschen', service: 'python' }, + { method: 'GET', path: '/gdpr-processes', description: 'DSGVO-Prozesse auflisten', service: 'python' }, + { method: 'PUT', path: '/gdpr-processes/{process_id}', description: 'DSGVO-Prozess aktualisieren', service: 'python' }, + ], + }, + + { + id: 'dsfa', + name: 'DSFA — Datenschutz-Folgenabschaetzung', + service: 'python', + basePath: '/api/compliance/dsfa', + exposure: 'internal', + endpoints: [ + { method: 'GET', path: '/', description: 'DSFAs auflisten', service: 'python' }, + { method: 'POST', path: '/', description: 'DSFA erstellen', service: 'python' }, + { method: 'GET', path: '/{dsfa_id}', description: 'DSFA laden', service: 'python' }, + { method: 'PUT', path: '/{dsfa_id}', description: 'DSFA aktualisieren', service: 'python' }, + { method: 'DELETE', path: '/{dsfa_id}', description: 'DSFA loeschen', service: 'python' }, + { method: 'PATCH', path: '/{dsfa_id}/status', description: 'DSFA-Status aendern', service: 'python' }, + { method: 'PUT', path: '/{dsfa_id}/sections/{section_number}', description: 'DSFA-Abschnitt aktualisieren', service: 'python' }, + { method: 'POST', path: '/{dsfa_id}/submit-for-review', description: 'Zur Pruefung einreichen', service: 'python' }, + { method: 'POST', path: '/{dsfa_id}/approve', description: 'DSFA genehmigen', service: 'python' }, + { method: 'GET', path: '/{dsfa_id}/export', description: 'DSFA als JSON exportieren', service: 'python' }, + { method: 'GET', path: '/{dsfa_id}/versions', description: 'Versionshistorie laden', service: 'python' }, + { method: 'GET', path: '/{dsfa_id}/versions/{version_number}', description: 'Bestimmte Version laden', service: 'python' }, + { method: 'GET', path: '/stats', description: 'DSFA-Statistiken laden', service: 'python' }, + { method: 'GET', path: '/audit-log', description: 'DSFA-Audit-Log laden', service: 'python' }, + { method: 'GET', path: '/export/csv', description: 'Alle DSFAs als CSV exportieren', service: 'python' }, + ], + }, + + { + id: 'dsr', + name: 'DSR — Betroffenenrechte (Admin)', + service: 'python', + basePath: '/api/compliance/dsr', + exposure: 'internal', + endpoints: [ + { method: 'POST', path: '/', description: 'DSR erstellen', service: 'python' }, + { method: 'GET', path: '/', description: 'DSRs auflisten', service: 'python' }, + { method: 'GET', path: '/{dsr_id}', description: 'DSR laden', service: 'python' }, + { method: 'PUT', path: '/{dsr_id}', description: 'DSR aktualisieren', service: 'python' }, + { method: 'DELETE', path: '/{dsr_id}', description: 'DSR loeschen', service: 'python' }, + { method: 'POST', path: '/{dsr_id}/status', description: 'Status aendern', service: 'python' }, + { method: 'POST', path: '/{dsr_id}/verify-identity', description: 'Identitaet verifizieren', service: 'python' }, + { method: 'POST', path: '/{dsr_id}/assign', description: 'DSR zuweisen', service: 'python' }, + { method: 'POST', path: '/{dsr_id}/extend', description: 'Frist verlaengern', service: 'python' }, + { method: 'POST', path: '/{dsr_id}/complete', description: 'DSR abschliessen', service: 'python' }, + { method: 'POST', path: '/{dsr_id}/reject', description: 'DSR ablehnen', service: 'python' }, + { method: 'GET', path: '/{dsr_id}/history', description: 'Antragshistorie laden', service: 'python' }, + { method: 'GET', path: '/{dsr_id}/communications', description: 'Kommunikation laden', service: 'python' }, + { method: 'POST', path: '/{dsr_id}/communicate', description: 'Nachricht senden', service: 'python' }, + { method: 'GET', path: '/{dsr_id}/exception-checks', description: 'Ausnahme-Checks laden', service: 'python' }, + { method: 'POST', path: '/{dsr_id}/exception-checks/init', description: 'Ausnahme-Checks initialisieren', service: 'python' }, + { method: 'PUT', path: '/{dsr_id}/exception-checks/{check_id}', description: 'Ausnahme-Check aktualisieren', service: 'python' }, + { method: 'GET', path: '/stats', description: 'DSR-Statistiken laden', service: 'python' }, + { method: 'GET', path: '/export', description: 'DSRs exportieren', service: 'python' }, + { method: 'POST', path: '/deadlines/process', description: 'Fristen verarbeiten', service: 'python' }, + { method: 'GET', path: '/templates', description: 'DSR-Vorlagen laden', service: 'python' }, + { method: 'GET', path: '/templates/published', description: 'Veroeffentlichte Vorlagen laden', service: 'python' }, + { method: 'GET', path: '/templates/{template_id}/versions', description: 'Vorlagen-Versionen laden', service: 'python' }, + { method: 'POST', path: '/templates/{template_id}/versions', description: 'Vorlagen-Version erstellen', service: 'python' }, + { method: 'PUT', path: '/template-versions/{version_id}/publish', description: 'Vorlagen-Version veroeffentlichen', service: 'python' }, + ], + }, + + { + id: 'einwilligungen', + name: 'Einwilligungen — DSGVO-Einwilligungsverwaltung', + service: 'python', + basePath: '/api/compliance/einwilligungen', + exposure: 'internal', + endpoints: [ + { method: 'GET', path: '/catalog', description: 'Einwilligungskatalog laden', service: 'python' }, + { method: 'PUT', path: '/catalog', description: 'Katalog aktualisieren', service: 'python' }, + { method: 'GET', path: '/company', description: 'Unternehmens-Consent-Einstellungen laden', service: 'python' }, + { method: 'PUT', path: '/company', description: 'Einstellungen aktualisieren', service: 'python' }, + { method: 'GET', path: '/cookies', description: 'Cookie-Einwilligungen laden', service: 'python' }, + { method: 'PUT', path: '/cookies', description: 'Cookie-Einwilligungen aktualisieren', service: 'python' }, + { method: 'GET', path: '/consents/stats', description: 'Statistiken laden', service: 'python' }, + { method: 'GET', path: '/consents', description: 'Einwilligungen auflisten (paginiert)', service: 'python' }, + { method: 'POST', path: '/consents', description: 'Einwilligung erstellen', service: 'python' }, + { method: 'GET', path: '/consents/{consent_id}/history', description: 'Einwilligungshistorie laden', service: 'python' }, + { method: 'PUT', path: '/consents/{consent_id}/revoke', description: 'Einwilligung widerrufen', service: 'python' }, + ], + }, + + { + id: 'loeschfristen', + name: 'Loeschfristen — Aufbewahrung & Loeschung', + service: 'python', + basePath: '/api/compliance/loeschfristen', + exposure: 'internal', + endpoints: [ + { method: 'GET', path: '/', description: 'Loeschrichtlinien auflisten', service: 'python' }, + { method: 'POST', path: '/', description: 'Richtlinie erstellen', service: 'python' }, + { method: 'GET', path: '/stats', description: 'Loeschfristen-Statistiken laden', service: 'python' }, + { method: 'GET', path: '/{policy_id}', description: 'Richtlinie laden', service: 'python' }, + { method: 'PUT', path: '/{policy_id}', description: 'Richtlinie aktualisieren', service: 'python' }, + { method: 'PUT', path: '/{policy_id}/status', description: 'Richtlinien-Status aendern', service: 'python' }, + { method: 'DELETE', path: '/{policy_id}', description: 'Richtlinie loeschen', service: 'python' }, + { method: 'GET', path: '/{policy_id}/versions', description: 'Versionshistorie laden', service: 'python' }, + { method: 'GET', path: '/{policy_id}/versions/{version_number}', description: 'Bestimmte Version laden', service: 'python' }, + ], + }, + + { + id: 'consent-user', + name: 'Consent API — Nutzer-Einwilligungen', + service: 'python', + basePath: '/api/consents', + exposure: 'public', + endpoints: [ + { method: 'GET', path: '/token/demo', description: 'Demo-Token laden', service: 'python' }, + { method: 'GET', path: '/check/{document_type}', description: 'Einwilligungsstatus pruefen', service: 'python' }, + { method: 'GET', path: '/pending', description: 'Offene Einwilligungen laden', service: 'python' }, + { method: 'GET', path: '/documents/{document_type}/latest', description: 'Aktuellstes Dokument laden', service: 'python' }, + { method: 'POST', path: '/give', description: 'Einwilligung erteilen', service: 'python' }, + { method: 'GET', path: '/cookies/categories', description: 'Cookie-Kategorien laden', service: 'python' }, + { method: 'POST', path: '/cookies', description: 'Cookie-Einwilligung setzen', service: 'python' }, + { method: 'GET', path: '/privacy/my-data', description: 'Eigene Daten laden', service: 'python' }, + { method: 'POST', path: '/privacy/export', description: 'Datenexport anfordern', service: 'python' }, + { method: 'POST', path: '/privacy/delete', description: 'Datenlöschung anfordern', service: 'python' }, + { method: 'GET', path: '/health', description: 'Health-Check', service: 'python' }, + ], + }, + + { + id: 'consent-admin', + name: 'Consent Admin — Dokumenten- & Versionsverwaltung', + service: 'python', + basePath: '/api/admin/consents', + exposure: 'internal', + endpoints: [ + { method: 'GET', path: '/documents', description: 'Dokumente auflisten', service: 'python' }, + { method: 'POST', path: '/documents', description: 'Dokument erstellen', service: 'python' }, + { method: 'PUT', path: '/documents/{doc_id}', description: 'Dokument aktualisieren', service: 'python' }, + { method: 'DELETE', path: '/documents/{doc_id}', description: 'Dokument loeschen', service: 'python' }, + { method: 'GET', path: '/documents/{doc_id}/versions', description: 'Versionen laden', service: 'python' }, + { method: 'POST', path: '/versions', description: 'Version erstellen', service: 'python' }, + { method: 'PUT', path: '/versions/{version_id}', description: 'Version aktualisieren', service: 'python' }, + { method: 'POST', path: '/versions/{version_id}/publish', description: 'Version veroeffentlichen', service: 'python' }, + { method: 'POST', path: '/versions/{version_id}/archive', description: 'Version archivieren', service: 'python' }, + { method: 'DELETE', path: '/versions/{version_id}', description: 'Version loeschen', service: 'python' }, + { method: 'POST', path: '/versions/{version_id}/submit-review', description: 'Zur Pruefung einreichen', service: 'python' }, + { method: 'POST', path: '/versions/{version_id}/approve', description: 'Version genehmigen', service: 'python' }, + { method: 'POST', path: '/versions/{version_id}/reject', description: 'Version ablehnen', service: 'python' }, + { method: 'GET', path: '/versions/{version_id}/compare', description: 'Versionen vergleichen', service: 'python' }, + { method: 'GET', path: '/versions/{version_id}/approval-history', description: 'Genehmigungshistorie laden', service: 'python' }, + { method: 'POST', path: '/versions/upload-word', description: 'Word-Dokument hochladen', service: 'python' }, + { method: 'GET', path: '/scheduled-versions', description: 'Geplante Versionen laden', service: 'python' }, + { method: 'POST', path: '/scheduled-publishing/process', description: 'Geplante Veroeffentlichungen verarbeiten', service: 'python' }, + { method: 'GET', path: '/cookies/categories', description: 'Cookie-Kategorien laden', service: 'python' }, + { method: 'POST', path: '/cookies/categories', description: 'Kategorie erstellen', service: 'python' }, + { method: 'PUT', path: '/cookies/categories/{cat_id}', description: 'Kategorie aktualisieren', service: 'python' }, + { method: 'DELETE', path: '/cookies/categories/{cat_id}', description: 'Kategorie loeschen', service: 'python' }, + { method: 'GET', path: '/statistics', description: 'Admin-Statistiken laden', service: 'python' }, + { method: 'GET', path: '/audit-log', description: 'Audit-Log laden', service: 'python' }, + ], + }, + + { + id: 'dsr-user', + name: 'DSR API — Nutzer-Betroffenenrechte', + service: 'python', + basePath: '/api/dsr', + exposure: 'public', + endpoints: [ + { method: 'POST', path: '/', description: 'Antrag stellen', service: 'python' }, + { method: 'GET', path: '/', description: 'Eigene Antraege laden', service: 'python' }, + { method: 'GET', path: '/{dsr_id}', description: 'Antrag laden', service: 'python' }, + { method: 'POST', path: '/{dsr_id}/cancel', description: 'Antrag stornieren', service: 'python' }, + ], + }, + + { + id: 'dsr-admin', + name: 'DSR Admin — Antrags-Verwaltung', + service: 'python', + basePath: '/api/admin/dsr', + exposure: 'internal', + endpoints: [ + { method: 'GET', path: '/', description: 'Alle Antraege laden', service: 'python' }, + { method: 'GET', path: '/stats', description: 'DSR-Statistiken laden', service: 'python' }, + { method: 'GET', path: '/{dsr_id}', description: 'Antrag laden', service: 'python' }, + { method: 'POST', path: '/', description: 'Antrag erstellen', service: 'python' }, + { method: 'PUT', path: '/{dsr_id}', description: 'Antrag aktualisieren', service: 'python' }, + { method: 'POST', path: '/{dsr_id}/status', description: 'Status aendern', service: 'python' }, + { method: 'POST', path: '/{dsr_id}/verify-identity', description: 'Identitaet verifizieren', service: 'python' }, + { method: 'POST', path: '/{dsr_id}/assign', description: 'Zuweisen', service: 'python' }, + { method: 'POST', path: '/{dsr_id}/extend', description: 'Frist verlaengern', service: 'python' }, + { method: 'POST', path: '/{dsr_id}/complete', description: 'Abschliessen', service: 'python' }, + { method: 'POST', path: '/{dsr_id}/reject', description: 'Ablehnen', service: 'python' }, + { method: 'GET', path: '/{dsr_id}/history', description: 'Historie laden', service: 'python' }, + { method: 'GET', path: '/{dsr_id}/communications', description: 'Kommunikation laden', service: 'python' }, + { method: 'POST', path: '/{dsr_id}/communicate', description: 'Nachricht senden', service: 'python' }, + { method: 'GET', path: '/{dsr_id}/exception-checks', description: 'Ausnahme-Checks laden', service: 'python' }, + { method: 'POST', path: '/{dsr_id}/exception-checks/init', description: 'Checks initialisieren', service: 'python' }, + { method: 'PUT', path: '/{dsr_id}/exception-checks/{check_id}', description: 'Check aktualisieren', service: 'python' }, + { method: 'POST', path: '/deadlines/process', description: 'Fristen verarbeiten', service: 'python' }, + ], + }, + + { + id: 'gdpr', + name: 'GDPR / Datenschutz — Nutzerdaten & Export', + service: 'python', + basePath: '/api/gdpr', + exposure: 'public', + endpoints: [ + { method: 'POST', path: '/export-pdf', description: 'Nutzerdaten als PDF exportieren', service: 'python' }, + { method: 'GET', path: '/export-html', description: 'Nutzerdaten als HTML exportieren', service: 'python' }, + { method: 'GET', path: '/data-categories', description: 'Datenkategorien laden', service: 'python' }, + { method: 'GET', path: '/data-categories/{category}', description: 'Kategorie-Details laden', service: 'python' }, + { method: 'POST', path: '/request-deletion', description: 'Datenlöschung beantragen', service: 'python' }, + ], + }, +] diff --git a/admin-compliance/lib/sdk/api-docs/endpoints-python-ops.ts b/admin-compliance/lib/sdk/api-docs/endpoints-python-ops.ts new file mode 100644 index 0000000..f195276 --- /dev/null +++ b/admin-compliance/lib/sdk/api-docs/endpoints-python-ops.ts @@ -0,0 +1,449 @@ +/** + * Python/FastAPI endpoints — Operational compliance modules + * (tom, vvt, vendor-compliance, risks, evidence, incidents, escalations, + * email-templates, legal-documents, legal-templates, import, screening, + * scraper, source-policy, security-backlog, notfallplan, obligations, + * isms, quality) + */ +import { ApiModule } from './types' + +export const pythonOpsModules: ApiModule[] = [ + { + id: 'email-templates', + name: 'E-Mail-Vorlagen — Template-Verwaltung', + service: 'python', + basePath: '/api/compliance/email-templates', + exposure: 'internal', + endpoints: [ + { method: 'GET', path: '/types', description: 'Vorlagentypen laden', service: 'python' }, + { method: 'GET', path: '/stats', description: 'E-Mail-Statistiken laden', service: 'python' }, + { method: 'GET', path: '/settings', description: 'E-Mail-Einstellungen laden', service: 'python' }, + { method: 'PUT', path: '/settings', description: 'E-Mail-Einstellungen aktualisieren', service: 'python' }, + { method: 'GET', path: '/logs', description: 'Versandprotokoll laden', service: 'python' }, + { method: 'POST', path: '/initialize', description: 'Standard-Vorlagen initialisieren', service: 'python' }, + { method: 'GET', path: '/', description: 'Vorlagen auflisten', service: 'python' }, + { method: 'POST', path: '/', description: 'Vorlage erstellen', service: 'python' }, + { method: 'GET', path: '/{template_id}', description: 'Vorlage laden', service: 'python' }, + { method: 'GET', path: '/{template_id}/versions', description: 'Vorlagen-Versionen laden', service: 'python' }, + { method: 'POST', path: '/{template_id}/versions', description: 'Version erstellen', service: 'python' }, + { method: 'POST', path: '/versions', description: 'Version erstellen (alternativ)', service: 'python' }, + { method: 'GET', path: '/versions/{version_id}', description: 'Version laden', service: 'python' }, + { method: 'PUT', path: '/versions/{version_id}', description: 'Version aktualisieren', service: 'python' }, + { method: 'POST', path: '/versions/{version_id}/submit', description: 'Version einreichen', service: 'python' }, + { method: 'POST', path: '/versions/{version_id}/approve', description: 'Version genehmigen', service: 'python' }, + { method: 'POST', path: '/versions/{version_id}/reject', description: 'Version ablehnen', service: 'python' }, + { method: 'POST', path: '/versions/{version_id}/publish', description: 'Version veroeffentlichen', service: 'python' }, + { method: 'POST', path: '/versions/{version_id}/preview', description: 'Version-Vorschau generieren', service: 'python' }, + { method: 'POST', path: '/versions/{version_id}/send-test', description: 'Test-E-Mail senden', service: 'python' }, + { method: 'GET', path: '/default/{template_type}', description: 'Standard-Vorlage laden', service: 'python' }, + ], + }, + + { + id: 'escalations', + name: 'Eskalationen — Eskalationsmanagement', + service: 'python', + basePath: '/api/compliance/escalations', + exposure: 'internal', + endpoints: [ + { method: 'GET', path: '/', description: 'Eskalationen auflisten', service: 'python' }, + { method: 'POST', path: '/', description: 'Eskalation erstellen', service: 'python' }, + { method: 'GET', path: '/stats', description: 'Eskalations-Statistiken laden', service: 'python' }, + { method: 'GET', path: '/{escalation_id}', description: 'Eskalation laden', service: 'python' }, + { method: 'PUT', path: '/{escalation_id}', description: 'Eskalation aktualisieren', service: 'python' }, + { method: 'PUT', path: '/{escalation_id}/status', description: 'Eskalations-Status aendern', service: 'python' }, + { method: 'DELETE', path: '/{escalation_id}', description: 'Eskalation loeschen', service: 'python' }, + ], + }, + + { + id: 'evidence', + name: 'Nachweise — Evidence Management', + service: 'python', + basePath: '/api/compliance/evidence', + exposure: 'internal', + endpoints: [ + { method: 'GET', path: '/evidence', description: 'Nachweise auflisten', service: 'python' }, + { method: 'POST', path: '/evidence', description: 'Nachweis erstellen', service: 'python' }, + { method: 'DELETE', path: '/evidence/{evidence_id}', description: 'Nachweis loeschen', service: 'python' }, + { method: 'POST', path: '/evidence/upload', description: 'Nachweis-Datei hochladen', service: 'python' }, + { method: 'POST', path: '/evidence/collect', description: 'CI-Nachweis sammeln', service: 'python', exposure: 'partner' }, + { method: 'GET', path: '/evidence/ci-status', description: 'CI-Nachweis-Status laden', service: 'python', exposure: 'partner' }, + ], + }, + + { + id: 'import', + name: 'Dokument-Import & Gap-Analyse', + service: 'python', + basePath: '/api/import', + exposure: 'internal', + endpoints: [ + { method: 'POST', path: '/analyze', description: 'Dokument analysieren', service: 'python' }, + { method: 'GET', path: '/gap-analysis/{document_id}', description: 'Gap-Analyse laden', service: 'python' }, + { method: 'GET', path: '/documents', description: 'Importierte Dokumente auflisten', service: 'python' }, + { method: 'DELETE', path: '/{document_id}', description: 'Dokument loeschen', service: 'python' }, + ], + }, + + { + id: 'incidents', + name: 'Datenschutz-Vorfaelle — Incident Management', + service: 'python', + basePath: '/api/compliance/incidents', + exposure: 'internal', + endpoints: [ + { method: 'POST', path: '/', description: 'Vorfall erstellen', service: 'python' }, + { method: 'GET', path: '/', description: 'Vorfaelle auflisten', service: 'python' }, + { method: 'GET', path: '/stats', description: 'Vorfall-Statistiken laden', service: 'python' }, + { method: 'GET', path: '/{incident_id}', description: 'Vorfall laden', service: 'python' }, + { method: 'PUT', path: '/{incident_id}', description: 'Vorfall aktualisieren', service: 'python' }, + { method: 'DELETE', path: '/{incident_id}', description: 'Vorfall loeschen', service: 'python' }, + { method: 'PUT', path: '/{incident_id}/status', description: 'Vorfall-Status aendern', service: 'python' }, + { method: 'POST', path: '/{incident_id}/assess-risk', description: 'Risikobewertung durchfuehren', service: 'python' }, + { method: 'POST', path: '/{incident_id}/notify-authority', description: 'Behoerde benachrichtigen', service: 'python' }, + { method: 'POST', path: '/{incident_id}/notify-subjects', description: 'Betroffene benachrichtigen', service: 'python' }, + { method: 'POST', path: '/{incident_id}/measures', description: 'Massnahme hinzufuegen', service: 'python' }, + { method: 'PUT', path: '/{incident_id}/measures/{measure_id}', description: 'Massnahme aktualisieren', service: 'python' }, + { method: 'POST', path: '/{incident_id}/measures/{measure_id}/complete', description: 'Massnahme abschliessen', service: 'python' }, + { method: 'POST', path: '/{incident_id}/timeline', description: 'Zeitachsen-Eintrag hinzufuegen', service: 'python' }, + { method: 'POST', path: '/{incident_id}/close', description: 'Vorfall schliessen', service: 'python' }, + ], + }, + + { + id: 'isms', + name: 'ISMS — ISO 27001 Managementsystem', + service: 'python', + basePath: '/api/compliance/isms', + exposure: 'internal', + endpoints: [ + { method: 'GET', path: '/scope', description: 'ISMS-Scope laden', service: 'python' }, + { method: 'POST', path: '/scope', description: 'ISMS-Scope erstellen', service: 'python' }, + { method: 'PUT', path: '/scope/{scope_id}', description: 'ISMS-Scope aktualisieren', service: 'python' }, + { method: 'POST', path: '/scope/{scope_id}/approve', description: 'ISMS-Scope genehmigen', service: 'python' }, + { method: 'GET', path: '/context', description: 'ISMS-Kontext laden', service: 'python' }, + { method: 'POST', path: '/context', description: 'ISMS-Kontext erstellen', service: 'python' }, + { method: 'GET', path: '/policies', description: 'Richtlinien auflisten', service: 'python' }, + { method: 'POST', path: '/policies', description: 'Richtlinie erstellen', service: 'python' }, + { method: 'GET', path: '/policies/{policy_id}', description: 'Richtlinie laden', service: 'python' }, + { method: 'PUT', path: '/policies/{policy_id}', description: 'Richtlinie aktualisieren', service: 'python' }, + { method: 'POST', path: '/policies/{policy_id}/approve', description: 'Richtlinie genehmigen', service: 'python' }, + { method: 'GET', path: '/objectives', description: 'Sicherheitsziele laden', service: 'python' }, + { method: 'POST', path: '/objectives', description: 'Sicherheitsziel erstellen', service: 'python' }, + { method: 'PUT', path: '/objectives/{objective_id}', description: 'Sicherheitsziel aktualisieren', service: 'python' }, + { method: 'GET', path: '/soa', description: 'Statement of Applicability laden', service: 'python' }, + { method: 'POST', path: '/soa', description: 'SoA-Eintrag erstellen', service: 'python' }, + { method: 'PUT', path: '/soa/{entry_id}', description: 'SoA-Eintrag aktualisieren', service: 'python' }, + { method: 'POST', path: '/soa/{entry_id}/approve', description: 'SoA-Eintrag genehmigen', service: 'python' }, + { method: 'GET', path: '/findings', description: 'Audit-Feststellungen laden', service: 'python' }, + { method: 'POST', path: '/findings', description: 'Feststellung erstellen', service: 'python' }, + { method: 'PUT', path: '/findings/{finding_id}', description: 'Feststellung aktualisieren', service: 'python' }, + { method: 'POST', path: '/findings/{finding_id}/close', description: 'Feststellung schliessen', service: 'python' }, + { method: 'GET', path: '/capa', description: 'Korrekturmassnahmen laden', service: 'python' }, + { method: 'POST', path: '/capa', description: 'CAPA erstellen', service: 'python' }, + { method: 'PUT', path: '/capa/{capa_id}', description: 'CAPA aktualisieren', service: 'python' }, + { method: 'POST', path: '/capa/{capa_id}/verify', description: 'CAPA verifizieren', service: 'python' }, + { method: 'GET', path: '/management-reviews', description: 'Management-Reviews laden', service: 'python' }, + { method: 'POST', path: '/management-reviews', description: 'Review erstellen', service: 'python' }, + { method: 'GET', path: '/management-reviews/{review_id}', description: 'Review laden', service: 'python' }, + { method: 'PUT', path: '/management-reviews/{review_id}', description: 'Review aktualisieren', service: 'python' }, + { method: 'POST', path: '/management-reviews/{review_id}/approve', description: 'Review genehmigen', service: 'python' }, + { method: 'GET', path: '/internal-audits', description: 'Interne Audits laden', service: 'python' }, + { method: 'POST', path: '/internal-audits', description: 'Internes Audit erstellen', service: 'python' }, + { method: 'PUT', path: '/internal-audits/{audit_id}', description: 'Audit aktualisieren', service: 'python' }, + { method: 'POST', path: '/internal-audits/{audit_id}/complete', description: 'Audit abschliessen', service: 'python' }, + { method: 'POST', path: '/readiness-check', description: 'Bereitschafts-Check ausfuehren', service: 'python' }, + { method: 'GET', path: '/readiness-check/latest', description: 'Letzten Check laden', service: 'python' }, + { method: 'GET', path: '/audit-trail', description: 'Audit-Trail laden', service: 'python' }, + { method: 'GET', path: '/overview', description: 'ISO 27001 Uebersicht laden', service: 'python' }, + ], + }, + + { + id: 'legal-documents', + name: 'Rechtliche Dokumente — Verwaltung & Versionen', + service: 'python', + basePath: '/api/compliance/legal-documents', + exposure: 'internal', + endpoints: [ + { method: 'GET', path: '/documents', description: 'Dokumente auflisten', service: 'python' }, + { method: 'POST', path: '/documents', description: 'Dokument erstellen', service: 'python' }, + { method: 'GET', path: '/documents/{document_id}', description: 'Dokument laden', service: 'python' }, + { method: 'DELETE', path: '/documents/{document_id}', description: 'Dokument loeschen', service: 'python' }, + { method: 'GET', path: '/documents/{document_id}/versions', description: 'Versionen laden', service: 'python' }, + { method: 'POST', path: '/versions', description: 'Version erstellen', service: 'python' }, + { method: 'PUT', path: '/versions/{version_id}', description: 'Version aktualisieren', service: 'python' }, + { method: 'GET', path: '/versions/{version_id}', description: 'Version laden', service: 'python' }, + { method: 'POST', path: '/versions/upload-word', description: 'Word-Dokument hochladen', service: 'python' }, + { method: 'POST', path: '/versions/{version_id}/submit-review', description: 'Zur Pruefung einreichen', service: 'python' }, + { method: 'POST', path: '/versions/{version_id}/approve', description: 'Version genehmigen', service: 'python' }, + { method: 'POST', path: '/versions/{version_id}/reject', description: 'Version ablehnen', service: 'python' }, + { method: 'POST', path: '/versions/{version_id}/publish', description: 'Version veroeffentlichen', service: 'python' }, + { method: 'GET', path: '/versions/{version_id}/approval-history', description: 'Genehmigungshistorie laden', service: 'python' }, + { method: 'GET', path: '/public', description: 'Oeffentliche Dokumente laden', service: 'python', exposure: 'public' }, + { method: 'GET', path: '/public/{document_type}/latest', description: 'Aktuellstes Dokument laden', service: 'python', exposure: 'public' }, + { method: 'POST', path: '/consents', description: 'Einwilligung erfassen', service: 'python' }, + { method: 'GET', path: '/consents/my', description: 'Eigene Einwilligungen laden', service: 'python' }, + { method: 'GET', path: '/consents/check/{document_type}', description: 'Einwilligungsstatus pruefen', service: 'python' }, + { method: 'DELETE', path: '/consents/{consent_id}', description: 'Einwilligung widerrufen', service: 'python' }, + { method: 'GET', path: '/stats/consents', description: 'Einwilligungs-Statistiken laden', service: 'python' }, + { method: 'GET', path: '/audit-log', description: 'Audit-Log laden', service: 'python' }, + { method: 'GET', path: '/cookie-categories', description: 'Cookie-Kategorien auflisten', service: 'python' }, + { method: 'POST', path: '/cookie-categories', description: 'Cookie-Kategorie erstellen', service: 'python' }, + { method: 'PUT', path: '/cookie-categories/{category_id}', description: 'Kategorie aktualisieren', service: 'python' }, + { method: 'DELETE', path: '/cookie-categories/{category_id}', description: 'Kategorie loeschen', service: 'python' }, + ], + }, + + { + id: 'legal-templates', + name: 'Dokumentvorlagen — DSGVO-Generatoren', + service: 'python', + basePath: '/api/compliance/legal-templates', + exposure: 'internal', + endpoints: [ + { method: 'GET', path: '/', description: 'Vorlagen auflisten', service: 'python' }, + { method: 'GET', path: '/status', description: 'Vorlagenstatus laden', service: 'python' }, + { method: 'GET', path: '/sources', description: 'Vorlagenquellen laden', service: 'python' }, + { method: 'GET', path: '/{template_id}', description: 'Vorlage laden', service: 'python' }, + { method: 'POST', path: '/', description: 'Vorlage erstellen', service: 'python' }, + { method: 'PUT', path: '/{template_id}', description: 'Vorlage aktualisieren', service: 'python' }, + { method: 'DELETE', path: '/{template_id}', description: 'Vorlage loeschen', service: 'python' }, + ], + }, + + { + id: 'notfallplan', + name: 'Notfallplan — Kontakte, Szenarien & Uebungen', + service: 'python', + basePath: '/api/compliance/notfallplan', + exposure: 'internal', + endpoints: [ + { method: 'GET', path: '/contacts', description: 'Notfallkontakte laden', service: 'python' }, + { method: 'POST', path: '/contacts', description: 'Kontakt erstellen', service: 'python' }, + { method: 'PUT', path: '/contacts/{contact_id}', description: 'Kontakt aktualisieren', service: 'python' }, + { method: 'DELETE', path: '/contacts/{contact_id}', description: 'Kontakt loeschen', service: 'python' }, + { method: 'GET', path: '/scenarios', description: 'Notfallszenarien laden', service: 'python' }, + { method: 'POST', path: '/scenarios', description: 'Szenario erstellen', service: 'python' }, + { method: 'PUT', path: '/scenarios/{scenario_id}', description: 'Szenario aktualisieren', service: 'python' }, + { method: 'DELETE', path: '/scenarios/{scenario_id}', description: 'Szenario loeschen', service: 'python' }, + { method: 'GET', path: '/checklists', description: 'Checklisten laden', service: 'python' }, + { method: 'POST', path: '/checklists', description: 'Checkliste erstellen', service: 'python' }, + { method: 'PUT', path: '/checklists/{checklist_id}', description: 'Checkliste aktualisieren', service: 'python' }, + { method: 'DELETE', path: '/checklists/{checklist_id}', description: 'Checkliste loeschen', service: 'python' }, + { method: 'GET', path: '/exercises', description: 'Uebungen laden', service: 'python' }, + { method: 'POST', path: '/exercises', description: 'Uebung erstellen', service: 'python' }, + { method: 'GET', path: '/incidents', description: 'Notfall-Vorfaelle laden', service: 'python' }, + { method: 'POST', path: '/incidents', description: 'Vorfall erstellen', service: 'python' }, + { method: 'PUT', path: '/incidents/{incident_id}', description: 'Vorfall aktualisieren', service: 'python' }, + { method: 'DELETE', path: '/incidents/{incident_id}', description: 'Vorfall loeschen', service: 'python' }, + { method: 'GET', path: '/templates', description: 'Vorlagen laden', service: 'python' }, + { method: 'POST', path: '/templates', description: 'Vorlage erstellen', service: 'python' }, + { method: 'PUT', path: '/templates/{template_id}', description: 'Vorlage aktualisieren', service: 'python' }, + { method: 'DELETE', path: '/templates/{template_id}', description: 'Vorlage loeschen', service: 'python' }, + { method: 'GET', path: '/stats', description: 'Notfallplan-Statistiken laden', service: 'python' }, + ], + }, + + { + id: 'obligations', + name: 'Pflichten — Compliance-Obligations', + service: 'python', + basePath: '/api/compliance/obligations', + exposure: 'internal', + endpoints: [ + { method: 'GET', path: '/', description: 'Pflichten auflisten', service: 'python' }, + { method: 'POST', path: '/', description: 'Pflicht erstellen', service: 'python' }, + { method: 'GET', path: '/stats', description: 'Pflichten-Statistiken laden', service: 'python' }, + { method: 'GET', path: '/{obligation_id}', description: 'Pflicht laden', service: 'python' }, + { method: 'PUT', path: '/{obligation_id}', description: 'Pflicht aktualisieren', service: 'python' }, + { method: 'PUT', path: '/{obligation_id}/status', description: 'Pflicht-Status aendern', service: 'python' }, + { method: 'DELETE', path: '/{obligation_id}', description: 'Pflicht loeschen', service: 'python' }, + { method: 'GET', path: '/{obligation_id}/versions', description: 'Versionshistorie laden', service: 'python' }, + { method: 'GET', path: '/{obligation_id}/versions/{version_number}', description: 'Version laden', service: 'python' }, + ], + }, + + { + id: 'quality', + name: 'Quality — KI-Qualitaetsmetriken & Tests', + service: 'python', + basePath: '/api/compliance/quality', + exposure: 'internal', + endpoints: [ + { method: 'GET', path: '/stats', description: 'Qualitaets-Statistiken laden', service: 'python' }, + { method: 'GET', path: '/metrics', description: 'Metriken auflisten', service: 'python' }, + { method: 'POST', path: '/metrics', description: 'Metrik erstellen', service: 'python' }, + { method: 'PUT', path: '/metrics/{metric_id}', description: 'Metrik aktualisieren', service: 'python' }, + { method: 'DELETE', path: '/metrics/{metric_id}', description: 'Metrik loeschen', service: 'python' }, + { method: 'GET', path: '/tests', description: 'Tests auflisten', service: 'python' }, + { method: 'POST', path: '/tests', description: 'Test erstellen', service: 'python' }, + { method: 'PUT', path: '/tests/{test_id}', description: 'Test aktualisieren', service: 'python' }, + { method: 'DELETE', path: '/tests/{test_id}', description: 'Test loeschen', service: 'python' }, + ], + }, + + { + id: 'risks', + name: 'Risikomanagement — Bewertung & Matrix', + service: 'python', + basePath: '/api/compliance/risks', + exposure: 'internal', + endpoints: [ + { method: 'GET', path: '/risks', description: 'Risiken auflisten', service: 'python' }, + { method: 'POST', path: '/risks', description: 'Risiko erstellen', service: 'python' }, + { method: 'PUT', path: '/risks/{risk_id}', description: 'Risiko aktualisieren', service: 'python' }, + { method: 'DELETE', path: '/risks/{risk_id}', description: 'Risiko loeschen', service: 'python' }, + { method: 'GET', path: '/risks/matrix', description: 'Risikomatrix laden', service: 'python' }, + ], + }, + + { + id: 'screening', + name: 'Screening — Abhaengigkeiten-Pruefung', + service: 'python', + basePath: '/api/compliance/screening', + exposure: 'internal', + endpoints: [ + { method: 'POST', path: '/scan', description: 'Abhaengigkeiten scannen', service: 'python', exposure: 'partner' }, + { method: 'GET', path: '/{screening_id}', description: 'Screening-Ergebnis laden', service: 'python' }, + { method: 'GET', path: '/', description: 'Screenings auflisten', service: 'python' }, + ], + }, + + { + id: 'scraper', + name: 'Scraper — Rechtsquellen-Aktualisierung', + service: 'python', + basePath: '/api/compliance/scraper', + exposure: 'partner', + endpoints: [ + { method: 'GET', path: '/scraper/status', description: 'Scraper-Status laden', service: 'python' }, + { method: 'GET', path: '/scraper/sources', description: 'Quellen auflisten', service: 'python' }, + { method: 'POST', path: '/scraper/scrape-all', description: 'Alle Quellen scrapen', service: 'python' }, + { method: 'POST', path: '/scraper/scrape/{code}', description: 'Einzelne Quelle scrapen', service: 'python' }, + { method: 'POST', path: '/scraper/extract-bsi', description: 'BSI-Anforderungen extrahieren', service: 'python' }, + { method: 'POST', path: '/scraper/extract-pdf', description: 'PDF-Anforderungen extrahieren', service: 'python' }, + { method: 'GET', path: '/scraper/pdf-documents', description: 'PDF-Dokumente auflisten', service: 'python' }, + ], + }, + + { + id: 'security-backlog', + name: 'Security Backlog — Sicherheitsmassnahmen', + service: 'python', + basePath: '/api/compliance/security-backlog', + exposure: 'internal', + endpoints: [ + { method: 'GET', path: '/', description: 'Backlog-Eintraege auflisten', service: 'python' }, + { method: 'POST', path: '/', description: 'Eintrag erstellen', service: 'python' }, + { method: 'GET', path: '/stats', description: 'Backlog-Statistiken laden', service: 'python' }, + { method: 'PUT', path: '/{item_id}', description: 'Eintrag aktualisieren', service: 'python' }, + { method: 'DELETE', path: '/{item_id}', description: 'Eintrag loeschen', service: 'python' }, + ], + }, + + { + id: 'source-policy', + name: 'Source Policy — Datenquellen & PII-Regeln', + service: 'python', + basePath: '/api/compliance/source-policy', + exposure: 'internal', + endpoints: [ + { method: 'GET', path: '/sources', description: 'Datenquellen auflisten', service: 'python' }, + { method: 'POST', path: '/sources', description: 'Quelle erstellen', service: 'python' }, + { method: 'GET', path: '/sources/{source_id}', description: 'Quelle laden', service: 'python' }, + { method: 'PUT', path: '/sources/{source_id}', description: 'Quelle aktualisieren', service: 'python' }, + { method: 'DELETE', path: '/sources/{source_id}', description: 'Quelle loeschen', service: 'python' }, + { method: 'GET', path: '/operations-matrix', description: 'Operationsmatrix laden', service: 'python' }, + { method: 'PUT', path: '/operations/{operation_id}', description: 'Operation aktualisieren', service: 'python' }, + { method: 'GET', path: '/pii-rules', description: 'PII-Regeln auflisten', service: 'python' }, + { method: 'POST', path: '/pii-rules', description: 'PII-Regel erstellen', service: 'python' }, + { method: 'PUT', path: '/pii-rules/{rule_id}', description: 'PII-Regel aktualisieren', service: 'python' }, + { method: 'DELETE', path: '/pii-rules/{rule_id}', description: 'PII-Regel loeschen', service: 'python' }, + { method: 'GET', path: '/blocked-content', description: 'Gesperrte Inhalte laden', service: 'python' }, + { method: 'GET', path: '/policy-audit', description: 'Richtlinien-Audit-Log laden', service: 'python' }, + { method: 'GET', path: '/policy-stats', description: 'Richtlinien-Statistiken laden', service: 'python' }, + { method: 'GET', path: '/compliance-report', description: 'Compliance-Bericht laden', service: 'python' }, + ], + }, + + { + id: 'tom', + name: 'TOM — Technisch-Organisatorische Massnahmen', + service: 'python', + basePath: '/api/compliance/tom', + exposure: 'internal', + endpoints: [ + { method: 'GET', path: '/state', description: 'TOM-Zustand laden', service: 'python' }, + { method: 'POST', path: '/state', description: 'TOM-Zustand speichern', service: 'python' }, + { method: 'DELETE', path: '/state', description: 'TOM-Zustand loeschen', service: 'python' }, + { method: 'GET', path: '/measures', description: 'Massnahmen auflisten', service: 'python' }, + { method: 'POST', path: '/measures', description: 'Massnahme erstellen', service: 'python' }, + { method: 'PUT', path: '/measures/{measure_id}', description: 'Massnahme aktualisieren', service: 'python' }, + { method: 'POST', path: '/measures/bulk', description: 'Massnahmen Bulk-Upsert', service: 'python' }, + { method: 'GET', path: '/stats', description: 'TOM-Statistiken laden', service: 'python' }, + { method: 'GET', path: '/export', description: 'Massnahmen exportieren', service: 'python' }, + { method: 'GET', path: '/measures/{measure_id}/versions', description: 'Versionshistorie laden', service: 'python' }, + { method: 'GET', path: '/measures/{measure_id}/versions/{version_number}', description: 'Version laden', service: 'python' }, + ], + }, + + { + id: 'vendor-compliance', + name: 'Vendor Compliance — Auftragsverarbeitung', + service: 'python', + basePath: '/api/compliance/vendors', + exposure: 'internal', + endpoints: [ + { method: 'GET', path: '/vendors/stats', description: 'Anbieter-Statistiken laden', service: 'python' }, + { method: 'GET', path: '/vendors', description: 'Anbieter auflisten', service: 'python' }, + { method: 'GET', path: '/vendors/{vendor_id}', description: 'Anbieter laden', service: 'python' }, + { method: 'POST', path: '/vendors', description: 'Anbieter erstellen', service: 'python' }, + { method: 'PUT', path: '/vendors/{vendor_id}', description: 'Anbieter aktualisieren', service: 'python' }, + { method: 'DELETE', path: '/vendors/{vendor_id}', description: 'Anbieter loeschen', service: 'python' }, + { method: 'PATCH', path: '/vendors/{vendor_id}/status', description: 'Anbieter-Status aendern', service: 'python' }, + { method: 'GET', path: '/contracts', description: 'Vertraege auflisten', service: 'python' }, + { method: 'GET', path: '/contracts/{contract_id}', description: 'Vertrag laden', service: 'python' }, + { method: 'POST', path: '/contracts', description: 'Vertrag erstellen', service: 'python' }, + { method: 'PUT', path: '/contracts/{contract_id}', description: 'Vertrag aktualisieren', service: 'python' }, + { method: 'DELETE', path: '/contracts/{contract_id}', description: 'Vertrag loeschen', service: 'python' }, + { method: 'GET', path: '/findings', description: 'Feststellungen auflisten', service: 'python' }, + { method: 'GET', path: '/findings/{finding_id}', description: 'Feststellung laden', service: 'python' }, + { method: 'POST', path: '/findings', description: 'Feststellung erstellen', service: 'python' }, + { method: 'PUT', path: '/findings/{finding_id}', description: 'Feststellung aktualisieren', service: 'python' }, + { method: 'DELETE', path: '/findings/{finding_id}', description: 'Feststellung loeschen', service: 'python' }, + { method: 'GET', path: '/control-instances', description: 'Kontroll-Instanzen auflisten', service: 'python' }, + { method: 'GET', path: '/control-instances/{instance_id}', description: 'Instanz laden', service: 'python' }, + { method: 'POST', path: '/control-instances', description: 'Instanz erstellen', service: 'python' }, + { method: 'PUT', path: '/control-instances/{instance_id}', description: 'Instanz aktualisieren', service: 'python' }, + { method: 'DELETE', path: '/control-instances/{instance_id}', description: 'Instanz loeschen', service: 'python' }, + { method: 'GET', path: '/controls', description: 'Controls auflisten', service: 'python' }, + { method: 'POST', path: '/controls', description: 'Control erstellen', service: 'python' }, + { method: 'DELETE', path: '/controls/{control_id}', description: 'Control loeschen', service: 'python' }, + ], + }, + + { + id: 'vvt', + name: 'VVT — Verarbeitungsverzeichnis (Art. 30 DSGVO)', + service: 'python', + basePath: '/api/compliance/vvt', + exposure: 'internal', + endpoints: [ + { method: 'GET', path: '/organization', description: 'Organisationskopf laden', service: 'python' }, + { method: 'PUT', path: '/organization', description: 'Organisationskopf speichern', service: 'python' }, + { method: 'GET', path: '/activities', description: 'Verarbeitungstaetigkeiten auflisten', service: 'python' }, + { method: 'POST', path: '/activities', description: 'Taetigkeit erstellen', service: 'python' }, + { method: 'GET', path: '/activities/{activity_id}', description: 'Taetigkeit laden', service: 'python' }, + { method: 'PUT', path: '/activities/{activity_id}', description: 'Taetigkeit aktualisieren', service: 'python' }, + { method: 'DELETE', path: '/activities/{activity_id}', description: 'Taetigkeit loeschen', service: 'python' }, + { method: 'GET', path: '/audit-log', description: 'VVT-Audit-Log laden', service: 'python' }, + { method: 'GET', path: '/export', description: 'VVT exportieren', service: 'python' }, + { method: 'GET', path: '/stats', description: 'VVT-Statistiken laden', service: 'python' }, + { method: 'GET', path: '/activities/{activity_id}/versions', description: 'Versionshistorie laden', service: 'python' }, + { method: 'GET', path: '/activities/{activity_id}/versions/{version_number}', description: 'Version laden', service: 'python' }, + ], + }, +] diff --git a/admin-compliance/lib/sdk/dsr/api-crud.ts b/admin-compliance/lib/sdk/dsr/api-crud.ts new file mode 100644 index 0000000..50d19ef --- /dev/null +++ b/admin-compliance/lib/sdk/dsr/api-crud.ts @@ -0,0 +1,146 @@ +/** + * DSR API CRUD Operations + * + * List, create, read, update operations for DSR requests. + */ + +import { + DSRRequest, + DSRCreateRequest, + DSRStatistics, +} from './types' +import { BackendDSR, transformBackendDSR, getSdkHeaders } from './api-types' + +// ============================================================================= +// LIST & STATISTICS +// ============================================================================= + +/** + * Fetch DSR list from compliance backend via proxy + */ +export async function fetchSDKDSRList(): Promise<{ requests: DSRRequest[]; statistics: DSRStatistics }> { + const [listRes, statsRes] = await Promise.all([ + fetch('/api/sdk/v1/compliance/dsr?limit=100', { headers: getSdkHeaders() }), + fetch('/api/sdk/v1/compliance/dsr/stats', { headers: getSdkHeaders() }), + ]) + + if (!listRes.ok) { + throw new Error(`HTTP ${listRes.status}`) + } + + const listData = await listRes.json() + const backendDSRs: BackendDSR[] = listData.requests || [] + const requests = backendDSRs.map(transformBackendDSR) + + let statistics: DSRStatistics + if (statsRes.ok) { + const statsData = await statsRes.json() + statistics = { + total: statsData.total || 0, + byStatus: statsData.by_status || { intake: 0, identity_verification: 0, processing: 0, completed: 0, rejected: 0, cancelled: 0 }, + byType: statsData.by_type || { access: 0, rectification: 0, erasure: 0, restriction: 0, portability: 0, objection: 0 }, + overdue: statsData.overdue || 0, + dueThisWeek: statsData.due_this_week || 0, + averageProcessingDays: statsData.average_processing_days || 0, + completedThisMonth: statsData.completed_this_month || 0, + } + } else { + statistics = { + total: requests.length, + byStatus: { + intake: requests.filter(r => r.status === 'intake').length, + identity_verification: requests.filter(r => r.status === 'identity_verification').length, + processing: requests.filter(r => r.status === 'processing').length, + completed: requests.filter(r => r.status === 'completed').length, + rejected: requests.filter(r => r.status === 'rejected').length, + cancelled: requests.filter(r => r.status === 'cancelled').length, + }, + byType: { + access: requests.filter(r => r.type === 'access').length, + rectification: requests.filter(r => r.type === 'rectification').length, + erasure: requests.filter(r => r.type === 'erasure').length, + restriction: requests.filter(r => r.type === 'restriction').length, + portability: requests.filter(r => r.type === 'portability').length, + objection: requests.filter(r => r.type === 'objection').length, + }, + overdue: 0, + dueThisWeek: 0, + averageProcessingDays: 0, + completedThisMonth: 0, + } + } + + return { requests, statistics } +} + +// ============================================================================= +// SINGLE RESOURCE OPERATIONS +// ============================================================================= + +/** + * Create a new DSR via compliance backend + */ +export async function createSDKDSR(request: DSRCreateRequest): Promise { + const body = { + request_type: request.type, + requester_name: request.requester.name, + requester_email: request.requester.email, + requester_phone: request.requester.phone || null, + requester_address: request.requester.address || null, + requester_customer_id: request.requester.customerId || null, + source: request.source, + source_details: request.sourceDetails || null, + request_text: request.requestText || '', + priority: request.priority || 'normal', + } + const res = await fetch('/api/sdk/v1/compliance/dsr', { + method: 'POST', + headers: getSdkHeaders(), + body: JSON.stringify(body), + }) + if (!res.ok) { + throw new Error(`HTTP ${res.status}`) + } +} + +/** + * Fetch a single DSR by ID from compliance backend + */ +export async function fetchSDKDSR(id: string): Promise { + const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}`, { + headers: getSdkHeaders(), + }) + if (!res.ok) { + return null + } + const data = await res.json() + if (!data || !data.id) return null + return transformBackendDSR(data) +} + +/** + * Update DSR status via compliance backend + */ +export async function updateSDKDSRStatus(id: string, status: string): Promise { + const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/status`, { + method: 'POST', + headers: getSdkHeaders(), + body: JSON.stringify({ status }), + }) + if (!res.ok) { + throw new Error(`HTTP ${res.status}`) + } +} + +/** + * Update DSR fields (priority, notes, etc.) + */ +export async function updateDSR(id: string, data: Record): Promise { + const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}`, { + method: 'PUT', + headers: getSdkHeaders(), + body: JSON.stringify(data), + }) + if (!res.ok) throw new Error(`HTTP ${res.status}`) + return transformBackendDSR(await res.json()) +} diff --git a/admin-compliance/lib/sdk/dsr/api-mock.ts b/admin-compliance/lib/sdk/dsr/api-mock.ts new file mode 100644 index 0000000..7ead7b4 --- /dev/null +++ b/admin-compliance/lib/sdk/dsr/api-mock.ts @@ -0,0 +1,259 @@ +/** + * DSR Mock Data + * + * Mock DSR requests and statistics for development/testing fallback. + */ + +import { DSRRequest, DSRStatistics } from './types' + +// ============================================================================= +// MOCK DATA FUNCTIONS +// ============================================================================= + +export function createMockDSRList(): DSRRequest[] { + const now = new Date() + + return [ + { + id: 'dsr-001', + referenceNumber: 'DSR-2025-000001', + type: 'access', + status: 'intake', + priority: 'high', + requester: { + name: 'Max Mustermann', + email: 'max.mustermann@example.de' + }, + source: 'web_form', + sourceDetails: 'Kontaktformular auf breakpilot.de', + receivedAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(), + deadline: { + originalDeadline: new Date(now.getTime() + 28 * 24 * 60 * 60 * 1000).toISOString(), + currentDeadline: new Date(now.getTime() + 28 * 24 * 60 * 60 * 1000).toISOString(), + extended: false + }, + identityVerification: { verified: false }, + assignment: { assignedTo: null }, + createdAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(), + createdBy: 'system', + updatedAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(), + tenantId: 'default-tenant' + }, + { + id: 'dsr-002', + referenceNumber: 'DSR-2025-000002', + type: 'erasure', + status: 'identity_verification', + priority: 'high', + requester: { + name: 'Anna Schmidt', + email: 'anna.schmidt@example.de', + phone: '+49 170 1234567' + }, + source: 'email', + requestText: 'Ich moechte, dass alle meine Daten geloescht werden.', + receivedAt: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(), + deadline: { + originalDeadline: new Date(now.getTime() + 9 * 24 * 60 * 60 * 1000).toISOString(), + currentDeadline: new Date(now.getTime() + 9 * 24 * 60 * 60 * 1000).toISOString(), + extended: false + }, + identityVerification: { verified: false }, + assignment: { + assignedTo: 'DSB Mueller', + assignedAt: new Date(now.getTime() - 4 * 24 * 60 * 60 * 1000).toISOString() + }, + createdAt: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(), + createdBy: 'system', + updatedAt: new Date(now.getTime() - 4 * 24 * 60 * 60 * 1000).toISOString(), + tenantId: 'default-tenant' + }, + { + id: 'dsr-003', + referenceNumber: 'DSR-2025-000003', + type: 'rectification', + status: 'processing', + priority: 'normal', + requester: { + name: 'Peter Meier', + email: 'peter.meier@example.de' + }, + source: 'email', + requestText: 'Meine Adresse ist falsch gespeichert.', + receivedAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(), + deadline: { + originalDeadline: new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString(), + currentDeadline: new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString(), + extended: false + }, + identityVerification: { + verified: true, + method: 'existing_account', + verifiedAt: new Date(now.getTime() - 6 * 24 * 60 * 60 * 1000).toISOString(), + verifiedBy: 'DSB Mueller' + }, + assignment: { + assignedTo: 'DSB Mueller', + assignedAt: new Date(now.getTime() - 6 * 24 * 60 * 60 * 1000).toISOString() + }, + rectificationDetails: { + fieldsToCorrect: [ + { + field: 'Adresse', + currentValue: 'Musterstr. 1, 12345 Berlin', + requestedValue: 'Musterstr. 10, 12345 Berlin', + corrected: false + } + ] + }, + createdAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(), + createdBy: 'system', + updatedAt: new Date(now.getTime() - 1 * 24 * 60 * 60 * 1000).toISOString(), + tenantId: 'default-tenant' + }, + { + id: 'dsr-004', + referenceNumber: 'DSR-2025-000004', + type: 'portability', + status: 'processing', + priority: 'normal', + requester: { + name: 'Lisa Weber', + email: 'lisa.weber@example.de' + }, + source: 'web_form', + receivedAt: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString(), + deadline: { + originalDeadline: new Date(now.getTime() + 20 * 24 * 60 * 60 * 1000).toISOString(), + currentDeadline: new Date(now.getTime() + 20 * 24 * 60 * 60 * 1000).toISOString(), + extended: false + }, + identityVerification: { + verified: true, + method: 'id_document', + verifiedAt: new Date(now.getTime() - 8 * 24 * 60 * 60 * 1000).toISOString(), + verifiedBy: 'DSB Mueller' + }, + assignment: { + assignedTo: 'IT Team', + assignedAt: new Date(now.getTime() - 8 * 24 * 60 * 60 * 1000).toISOString() + }, + notes: 'JSON-Export wird vorbereitet', + createdAt: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString(), + createdBy: 'system', + updatedAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(), + tenantId: 'default-tenant' + }, + { + id: 'dsr-005', + referenceNumber: 'DSR-2025-000005', + type: 'objection', + status: 'rejected', + priority: 'low', + requester: { + name: 'Thomas Klein', + email: 'thomas.klein@example.de' + }, + source: 'letter', + requestText: 'Ich widerspreche der Verarbeitung meiner Daten fuer Marketingzwecke.', + receivedAt: new Date(now.getTime() - 35 * 24 * 60 * 60 * 1000).toISOString(), + deadline: { + originalDeadline: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(), + currentDeadline: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(), + extended: false + }, + completedAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(), + identityVerification: { + verified: true, + method: 'postal', + verifiedAt: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString(), + verifiedBy: 'DSB Mueller' + }, + assignment: { + assignedTo: 'Rechtsabteilung', + assignedAt: new Date(now.getTime() - 28 * 24 * 60 * 60 * 1000).toISOString() + }, + objectionDetails: { + processingPurpose: 'Marketing', + legalBasis: 'Berechtigtes Interesse (Art. 6(1)(f))', + objectionGrounds: 'Keine konkreten Gruende genannt', + decision: 'rejected', + decisionReason: 'Zwingende schutzwuerdige Gruende fuer die Verarbeitung ueberwiegen', + decisionBy: 'Rechtsabteilung', + decisionAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString() + }, + notes: 'Widerspruch unberechtigt - zwingende schutzwuerdige Gruende', + createdAt: new Date(now.getTime() - 35 * 24 * 60 * 60 * 1000).toISOString(), + createdBy: 'system', + updatedAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(), + tenantId: 'default-tenant' + }, + { + id: 'dsr-006', + referenceNumber: 'DSR-2025-000006', + type: 'access', + status: 'completed', + priority: 'normal', + requester: { + name: 'Sarah Braun', + email: 'sarah.braun@example.de' + }, + source: 'email', + receivedAt: new Date(now.getTime() - 45 * 24 * 60 * 60 * 1000).toISOString(), + deadline: { + originalDeadline: new Date(now.getTime() - 15 * 24 * 60 * 60 * 1000).toISOString(), + currentDeadline: new Date(now.getTime() - 15 * 24 * 60 * 60 * 1000).toISOString(), + extended: false + }, + completedAt: new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000).toISOString(), + identityVerification: { + verified: true, + method: 'id_document', + verifiedAt: new Date(now.getTime() - 42 * 24 * 60 * 60 * 1000).toISOString(), + verifiedBy: 'DSB Mueller' + }, + assignment: { + assignedTo: 'DSB Mueller', + assignedAt: new Date(now.getTime() - 42 * 24 * 60 * 60 * 1000).toISOString() + }, + dataExport: { + format: 'pdf', + generatedAt: new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000).toISOString(), + generatedBy: 'DSB Mueller', + fileName: 'datenauskunft_sarah_braun.pdf', + fileSize: 245000, + includesThirdPartyData: false + }, + createdAt: new Date(now.getTime() - 45 * 24 * 60 * 60 * 1000).toISOString(), + createdBy: 'system', + updatedAt: new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000).toISOString(), + tenantId: 'default-tenant' + } + ] +} + +export function createMockStatistics(): DSRStatistics { + return { + total: 6, + byStatus: { + intake: 1, + identity_verification: 1, + processing: 2, + completed: 1, + rejected: 1, + cancelled: 0 + }, + byType: { + access: 2, + rectification: 1, + erasure: 1, + restriction: 0, + portability: 1, + objection: 1 + }, + overdue: 0, + dueThisWeek: 2, + averageProcessingDays: 18, + completedThisMonth: 1 + } +} diff --git a/admin-compliance/lib/sdk/dsr/api-types.ts b/admin-compliance/lib/sdk/dsr/api-types.ts new file mode 100644 index 0000000..fcfcb3f --- /dev/null +++ b/admin-compliance/lib/sdk/dsr/api-types.ts @@ -0,0 +1,133 @@ +/** + * DSR API Types & Transform + * + * Backend DSR type definition and transformation to frontend DSRRequest format. + */ + +import { DSRRequest } from './types' + +// ============================================================================= +// BACKEND TYPE +// ============================================================================= + +export interface BackendDSR { + id: string + tenant_id: string + request_number: string + request_type: string + status: string + priority: string + requester_name: string + requester_email: string + requester_phone?: string + requester_address?: string + requester_customer_id?: string + source: string + source_details?: string + request_text?: string + notes?: string + internal_notes?: string + received_at: string + deadline_at: string + extended_deadline_at?: string + extension_reason?: string + extension_approved_by?: string + extension_approved_at?: string + identity_verified: boolean + verification_method?: string + verified_at?: string + verified_by?: string + verification_notes?: string + verification_document_ref?: string + assigned_to?: string + assigned_at?: string + assigned_by?: string + completed_at?: string + completion_notes?: string + rejection_reason?: string + rejection_legal_basis?: string + erasure_checklist?: any[] + data_export?: any + rectification_details?: any + objection_details?: any + affected_systems?: string[] + created_at: string + updated_at: string + created_by?: string + updated_by?: string +} + +// ============================================================================= +// TRANSFORM +// ============================================================================= + +/** + * Transform flat backend DSR to nested SDK DSRRequest format. + * New compliance backend already uses the same status names as frontend types. + */ +export function transformBackendDSR(b: BackendDSR): DSRRequest { + return { + id: b.id, + referenceNumber: b.request_number, + type: b.request_type as DSRRequest['type'], + status: (b.status as DSRRequest['status']) || 'intake', + priority: (b.priority as DSRRequest['priority']) || 'normal', + requester: { + name: b.requester_name, + email: b.requester_email, + phone: b.requester_phone, + address: b.requester_address, + customerId: b.requester_customer_id, + }, + source: (b.source as DSRRequest['source']) || 'email', + sourceDetails: b.source_details, + requestText: b.request_text, + receivedAt: b.received_at, + deadline: { + originalDeadline: b.deadline_at, + currentDeadline: b.extended_deadline_at || b.deadline_at, + extended: !!b.extended_deadline_at, + extensionReason: b.extension_reason, + extensionApprovedBy: b.extension_approved_by, + extensionApprovedAt: b.extension_approved_at, + }, + completedAt: b.completed_at, + identityVerification: { + verified: b.identity_verified, + method: b.verification_method as any, + verifiedAt: b.verified_at, + verifiedBy: b.verified_by, + notes: b.verification_notes, + documentRef: b.verification_document_ref, + }, + assignment: { + assignedTo: b.assigned_to || null, + assignedAt: b.assigned_at, + assignedBy: b.assigned_by, + }, + notes: b.notes, + internalNotes: b.internal_notes, + erasureChecklist: b.erasure_checklist ? { items: b.erasure_checklist, canProceedWithErasure: true } : undefined, + dataExport: b.data_export && Object.keys(b.data_export).length > 0 ? b.data_export : undefined, + rectificationDetails: b.rectification_details && Object.keys(b.rectification_details).length > 0 ? b.rectification_details : undefined, + objectionDetails: b.objection_details && Object.keys(b.objection_details).length > 0 ? b.objection_details : undefined, + createdAt: b.created_at, + createdBy: b.created_by || 'system', + updatedAt: b.updated_at, + updatedBy: b.updated_by, + tenantId: b.tenant_id, + } +} + +// ============================================================================= +// SHARED HELPERS +// ============================================================================= + +export function getSdkHeaders(): HeadersInit { + if (typeof window === 'undefined') return {} + return { + 'Content-Type': 'application/json', + 'X-Tenant-ID': localStorage.getItem('bp_tenant_id') || '', + 'X-User-ID': localStorage.getItem('bp_user_id') || '', + } +} diff --git a/admin-compliance/lib/sdk/dsr/api-workflow.ts b/admin-compliance/lib/sdk/dsr/api-workflow.ts new file mode 100644 index 0000000..8459508 --- /dev/null +++ b/admin-compliance/lib/sdk/dsr/api-workflow.ts @@ -0,0 +1,161 @@ +/** + * DSR API Workflow Actions + * + * Workflow operations: identity verification, assignment, deadline extension, + * completion, rejection, communications, exception checks, and history. + */ + +import { DSRRequest } from './types' +import { transformBackendDSR, getSdkHeaders } from './api-types' + +// ============================================================================= +// WORKFLOW ACTIONS +// ============================================================================= + +/** + * Verify identity of DSR requester + */ +export async function verifyDSRIdentity(id: string, data: { method: string; notes?: string; document_ref?: string }): Promise { + const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/verify-identity`, { + method: 'POST', + headers: getSdkHeaders(), + body: JSON.stringify(data), + }) + if (!res.ok) throw new Error(`HTTP ${res.status}`) + return transformBackendDSR(await res.json()) +} + +/** + * Assign DSR to a user + */ +export async function assignDSR(id: string, assigneeId: string): Promise { + const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/assign`, { + method: 'POST', + headers: getSdkHeaders(), + body: JSON.stringify({ assignee_id: assigneeId }), + }) + if (!res.ok) throw new Error(`HTTP ${res.status}`) + return transformBackendDSR(await res.json()) +} + +/** + * Extend DSR deadline (Art. 12 Abs. 3 DSGVO) + */ +export async function extendDSRDeadline(id: string, reason: string, days: number = 60): Promise { + const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/extend`, { + method: 'POST', + headers: getSdkHeaders(), + body: JSON.stringify({ reason, days }), + }) + if (!res.ok) throw new Error(`HTTP ${res.status}`) + return transformBackendDSR(await res.json()) +} + +/** + * Complete a DSR + */ +export async function completeDSR(id: string, summary?: string): Promise { + const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/complete`, { + method: 'POST', + headers: getSdkHeaders(), + body: JSON.stringify({ summary }), + }) + if (!res.ok) throw new Error(`HTTP ${res.status}`) + return transformBackendDSR(await res.json()) +} + +/** + * Reject a DSR with legal basis + */ +export async function rejectDSR(id: string, reason: string, legalBasis?: string): Promise { + const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/reject`, { + method: 'POST', + headers: getSdkHeaders(), + body: JSON.stringify({ reason, legal_basis: legalBasis }), + }) + if (!res.ok) throw new Error(`HTTP ${res.status}`) + return transformBackendDSR(await res.json()) +} + +// ============================================================================= +// COMMUNICATIONS +// ============================================================================= + +/** + * Fetch communications for a DSR + */ +export async function fetchDSRCommunications(id: string): Promise { + const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/communications`, { + headers: getSdkHeaders(), + }) + if (!res.ok) throw new Error(`HTTP ${res.status}`) + return res.json() +} + +/** + * Send a communication for a DSR + */ +export async function sendDSRCommunication(id: string, data: { communication_type?: string; channel?: string; subject?: string; content: string }): Promise { + const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/communicate`, { + method: 'POST', + headers: getSdkHeaders(), + body: JSON.stringify({ communication_type: 'outgoing', channel: 'email', ...data }), + }) + if (!res.ok) throw new Error(`HTTP ${res.status}`) + return res.json() +} + +// ============================================================================= +// EXCEPTION CHECKS (Art. 17) +// ============================================================================= + +/** + * Fetch exception checks for an erasure DSR + */ +export async function fetchDSRExceptionChecks(id: string): Promise { + const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/exception-checks`, { + headers: getSdkHeaders(), + }) + if (!res.ok) throw new Error(`HTTP ${res.status}`) + return res.json() +} + +/** + * Initialize Art. 17(3) exception checks for an erasure DSR + */ +export async function initDSRExceptionChecks(id: string): Promise { + const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/exception-checks/init`, { + method: 'POST', + headers: getSdkHeaders(), + }) + if (!res.ok) throw new Error(`HTTP ${res.status}`) + return res.json() +} + +/** + * Update a single exception check + */ +export async function updateDSRExceptionCheck(dsrId: string, checkId: string, data: { applies: boolean; notes?: string }): Promise { + const res = await fetch(`/api/sdk/v1/compliance/dsr/${dsrId}/exception-checks/${checkId}`, { + method: 'PUT', + headers: getSdkHeaders(), + body: JSON.stringify(data), + }) + if (!res.ok) throw new Error(`HTTP ${res.status}`) + return res.json() +} + +// ============================================================================= +// HISTORY +// ============================================================================= + +/** + * Fetch status change history for a DSR + */ +export async function fetchDSRHistory(id: string): Promise { + const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/history`, { + headers: getSdkHeaders(), + }) + if (!res.ok) throw new Error(`HTTP ${res.status}`) + return res.json() +} diff --git a/admin-compliance/lib/sdk/dsr/api.ts b/admin-compliance/lib/sdk/dsr/api.ts index cc58d46..1b34e57 100644 --- a/admin-compliance/lib/sdk/dsr/api.ts +++ b/admin-compliance/lib/sdk/dsr/api.ts @@ -1,669 +1,38 @@ /** - * DSR API Client - * - * API client for Data Subject Request management. - * Connects to the native compliance backend (Python/FastAPI). + * DSR API Client — Barrel re-exports + * Preserves the original public API so existing imports work unchanged. */ -import { - DSRRequest, - DSRCreateRequest, - DSRStatistics, -} from './types' +// Types & transform +export { transformBackendDSR, getSdkHeaders } from './api-types' +export type { BackendDSR } from './api-types' -// ============================================================================= -// SDK API FUNCTIONS (via Next.js proxy to compliance backend) -// ============================================================================= +// CRUD operations +export { + fetchSDKDSRList, + createSDKDSR, + fetchSDKDSR, + updateSDKDSRStatus, + updateDSR, +} from './api-crud' -interface BackendDSR { - id: string - tenant_id: string - request_number: string - request_type: string - status: string - priority: string - requester_name: string - requester_email: string - requester_phone?: string - requester_address?: string - requester_customer_id?: string - source: string - source_details?: string - request_text?: string - notes?: string - internal_notes?: string - received_at: string - deadline_at: string - extended_deadline_at?: string - extension_reason?: string - extension_approved_by?: string - extension_approved_at?: string - identity_verified: boolean - verification_method?: string - verified_at?: string - verified_by?: string - verification_notes?: string - verification_document_ref?: string - assigned_to?: string - assigned_at?: string - assigned_by?: string - completed_at?: string - completion_notes?: string - rejection_reason?: string - rejection_legal_basis?: string - erasure_checklist?: any[] - data_export?: any - rectification_details?: any - objection_details?: any - affected_systems?: string[] - created_at: string - updated_at: string - created_by?: string - updated_by?: string -} +// Workflow actions +export { + verifyDSRIdentity, + assignDSR, + extendDSRDeadline, + completeDSR, + rejectDSR, + fetchDSRCommunications, + sendDSRCommunication, + fetchDSRExceptionChecks, + initDSRExceptionChecks, + updateDSRExceptionCheck, + fetchDSRHistory, +} from './api-workflow' -/** - * Transform flat backend DSR to nested SDK DSRRequest format. - * New compliance backend already uses the same status names as frontend types. - */ -export function transformBackendDSR(b: BackendDSR): DSRRequest { - return { - id: b.id, - referenceNumber: b.request_number, - type: b.request_type as DSRRequest['type'], - status: (b.status as DSRRequest['status']) || 'intake', - priority: (b.priority as DSRRequest['priority']) || 'normal', - requester: { - name: b.requester_name, - email: b.requester_email, - phone: b.requester_phone, - address: b.requester_address, - customerId: b.requester_customer_id, - }, - source: (b.source as DSRRequest['source']) || 'email', - sourceDetails: b.source_details, - requestText: b.request_text, - receivedAt: b.received_at, - deadline: { - originalDeadline: b.deadline_at, - currentDeadline: b.extended_deadline_at || b.deadline_at, - extended: !!b.extended_deadline_at, - extensionReason: b.extension_reason, - extensionApprovedBy: b.extension_approved_by, - extensionApprovedAt: b.extension_approved_at, - }, - completedAt: b.completed_at, - identityVerification: { - verified: b.identity_verified, - method: b.verification_method as any, - verifiedAt: b.verified_at, - verifiedBy: b.verified_by, - notes: b.verification_notes, - documentRef: b.verification_document_ref, - }, - assignment: { - assignedTo: b.assigned_to || null, - assignedAt: b.assigned_at, - assignedBy: b.assigned_by, - }, - notes: b.notes, - internalNotes: b.internal_notes, - erasureChecklist: b.erasure_checklist ? { items: b.erasure_checklist, canProceedWithErasure: true } : undefined, - dataExport: b.data_export && Object.keys(b.data_export).length > 0 ? b.data_export : undefined, - rectificationDetails: b.rectification_details && Object.keys(b.rectification_details).length > 0 ? b.rectification_details : undefined, - objectionDetails: b.objection_details && Object.keys(b.objection_details).length > 0 ? b.objection_details : undefined, - createdAt: b.created_at, - createdBy: b.created_by || 'system', - updatedAt: b.updated_at, - updatedBy: b.updated_by, - tenantId: b.tenant_id, - } -} - -function getSdkHeaders(): HeadersInit { - if (typeof window === 'undefined') return {} - return { - 'Content-Type': 'application/json', - 'X-Tenant-ID': localStorage.getItem('bp_tenant_id') || '', - 'X-User-ID': localStorage.getItem('bp_user_id') || '', - } -} - -/** - * Fetch DSR list from compliance backend via proxy - */ -export async function fetchSDKDSRList(): Promise<{ requests: DSRRequest[]; statistics: DSRStatistics }> { - // Fetch list and stats in parallel - const [listRes, statsRes] = await Promise.all([ - fetch('/api/sdk/v1/compliance/dsr?limit=100', { headers: getSdkHeaders() }), - fetch('/api/sdk/v1/compliance/dsr/stats', { headers: getSdkHeaders() }), - ]) - - if (!listRes.ok) { - throw new Error(`HTTP ${listRes.status}`) - } - - const listData = await listRes.json() - const backendDSRs: BackendDSR[] = listData.requests || [] - const requests = backendDSRs.map(transformBackendDSR) - - let statistics: DSRStatistics - if (statsRes.ok) { - const statsData = await statsRes.json() - statistics = { - total: statsData.total || 0, - byStatus: statsData.by_status || { intake: 0, identity_verification: 0, processing: 0, completed: 0, rejected: 0, cancelled: 0 }, - byType: statsData.by_type || { access: 0, rectification: 0, erasure: 0, restriction: 0, portability: 0, objection: 0 }, - overdue: statsData.overdue || 0, - dueThisWeek: statsData.due_this_week || 0, - averageProcessingDays: statsData.average_processing_days || 0, - completedThisMonth: statsData.completed_this_month || 0, - } - } else { - // Fallback: calculate locally - const now = new Date() - statistics = { - total: requests.length, - byStatus: { - intake: requests.filter(r => r.status === 'intake').length, - identity_verification: requests.filter(r => r.status === 'identity_verification').length, - processing: requests.filter(r => r.status === 'processing').length, - completed: requests.filter(r => r.status === 'completed').length, - rejected: requests.filter(r => r.status === 'rejected').length, - cancelled: requests.filter(r => r.status === 'cancelled').length, - }, - byType: { - access: requests.filter(r => r.type === 'access').length, - rectification: requests.filter(r => r.type === 'rectification').length, - erasure: requests.filter(r => r.type === 'erasure').length, - restriction: requests.filter(r => r.type === 'restriction').length, - portability: requests.filter(r => r.type === 'portability').length, - objection: requests.filter(r => r.type === 'objection').length, - }, - overdue: 0, - dueThisWeek: 0, - averageProcessingDays: 0, - completedThisMonth: 0, - } - } - - return { requests, statistics } -} - -/** - * Create a new DSR via compliance backend - */ -export async function createSDKDSR(request: DSRCreateRequest): Promise { - const body = { - request_type: request.type, - requester_name: request.requester.name, - requester_email: request.requester.email, - requester_phone: request.requester.phone || null, - requester_address: request.requester.address || null, - requester_customer_id: request.requester.customerId || null, - source: request.source, - source_details: request.sourceDetails || null, - request_text: request.requestText || '', - priority: request.priority || 'normal', - } - const res = await fetch('/api/sdk/v1/compliance/dsr', { - method: 'POST', - headers: getSdkHeaders(), - body: JSON.stringify(body), - }) - if (!res.ok) { - throw new Error(`HTTP ${res.status}`) - } -} - -/** - * Fetch a single DSR by ID from compliance backend - */ -export async function fetchSDKDSR(id: string): Promise { - const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}`, { - headers: getSdkHeaders(), - }) - if (!res.ok) { - return null - } - const data = await res.json() - if (!data || !data.id) return null - return transformBackendDSR(data) -} - -/** - * Update DSR status via compliance backend - */ -export async function updateSDKDSRStatus(id: string, status: string): Promise { - const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/status`, { - method: 'POST', - headers: getSdkHeaders(), - body: JSON.stringify({ status }), - }) - if (!res.ok) { - throw new Error(`HTTP ${res.status}`) - } -} - -// ============================================================================= -// WORKFLOW ACTIONS -// ============================================================================= - -/** - * Verify identity of DSR requester - */ -export async function verifyDSRIdentity(id: string, data: { method: string; notes?: string; document_ref?: string }): Promise { - const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/verify-identity`, { - method: 'POST', - headers: getSdkHeaders(), - body: JSON.stringify(data), - }) - if (!res.ok) throw new Error(`HTTP ${res.status}`) - return transformBackendDSR(await res.json()) -} - -/** - * Assign DSR to a user - */ -export async function assignDSR(id: string, assigneeId: string): Promise { - const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/assign`, { - method: 'POST', - headers: getSdkHeaders(), - body: JSON.stringify({ assignee_id: assigneeId }), - }) - if (!res.ok) throw new Error(`HTTP ${res.status}`) - return transformBackendDSR(await res.json()) -} - -/** - * Extend DSR deadline (Art. 12 Abs. 3 DSGVO) - */ -export async function extendDSRDeadline(id: string, reason: string, days: number = 60): Promise { - const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/extend`, { - method: 'POST', - headers: getSdkHeaders(), - body: JSON.stringify({ reason, days }), - }) - if (!res.ok) throw new Error(`HTTP ${res.status}`) - return transformBackendDSR(await res.json()) -} - -/** - * Complete a DSR - */ -export async function completeDSR(id: string, summary?: string): Promise { - const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/complete`, { - method: 'POST', - headers: getSdkHeaders(), - body: JSON.stringify({ summary }), - }) - if (!res.ok) throw new Error(`HTTP ${res.status}`) - return transformBackendDSR(await res.json()) -} - -/** - * Reject a DSR with legal basis - */ -export async function rejectDSR(id: string, reason: string, legalBasis?: string): Promise { - const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/reject`, { - method: 'POST', - headers: getSdkHeaders(), - body: JSON.stringify({ reason, legal_basis: legalBasis }), - }) - if (!res.ok) throw new Error(`HTTP ${res.status}`) - return transformBackendDSR(await res.json()) -} - -// ============================================================================= -// COMMUNICATIONS -// ============================================================================= - -/** - * Fetch communications for a DSR - */ -export async function fetchDSRCommunications(id: string): Promise { - const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/communications`, { - headers: getSdkHeaders(), - }) - if (!res.ok) throw new Error(`HTTP ${res.status}`) - return res.json() -} - -/** - * Send a communication for a DSR - */ -export async function sendDSRCommunication(id: string, data: { communication_type?: string; channel?: string; subject?: string; content: string }): Promise { - const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/communicate`, { - method: 'POST', - headers: getSdkHeaders(), - body: JSON.stringify({ communication_type: 'outgoing', channel: 'email', ...data }), - }) - if (!res.ok) throw new Error(`HTTP ${res.status}`) - return res.json() -} - -// ============================================================================= -// EXCEPTION CHECKS (Art. 17) -// ============================================================================= - -/** - * Fetch exception checks for an erasure DSR - */ -export async function fetchDSRExceptionChecks(id: string): Promise { - const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/exception-checks`, { - headers: getSdkHeaders(), - }) - if (!res.ok) throw new Error(`HTTP ${res.status}`) - return res.json() -} - -/** - * Initialize Art. 17(3) exception checks for an erasure DSR - */ -export async function initDSRExceptionChecks(id: string): Promise { - const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/exception-checks/init`, { - method: 'POST', - headers: getSdkHeaders(), - }) - if (!res.ok) throw new Error(`HTTP ${res.status}`) - return res.json() -} - -/** - * Update a single exception check - */ -export async function updateDSRExceptionCheck(dsrId: string, checkId: string, data: { applies: boolean; notes?: string }): Promise { - const res = await fetch(`/api/sdk/v1/compliance/dsr/${dsrId}/exception-checks/${checkId}`, { - method: 'PUT', - headers: getSdkHeaders(), - body: JSON.stringify(data), - }) - if (!res.ok) throw new Error(`HTTP ${res.status}`) - return res.json() -} - -// ============================================================================= -// HISTORY -// ============================================================================= - -/** - * Fetch status change history for a DSR - */ -export async function fetchDSRHistory(id: string): Promise { - const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/history`, { - headers: getSdkHeaders(), - }) - if (!res.ok) throw new Error(`HTTP ${res.status}`) - return res.json() -} - -/** - * Update DSR fields (priority, notes, etc.) - */ -export async function updateDSR(id: string, data: Record): Promise { - const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}`, { - method: 'PUT', - headers: getSdkHeaders(), - body: JSON.stringify(data), - }) - if (!res.ok) throw new Error(`HTTP ${res.status}`) - return transformBackendDSR(await res.json()) -} - -// ============================================================================= -// MOCK DATA FUNCTIONS (kept as fallback) -// ============================================================================= - -export function createMockDSRList(): DSRRequest[] { - const now = new Date() - - return [ - { - id: 'dsr-001', - referenceNumber: 'DSR-2025-000001', - type: 'access', - status: 'intake', - priority: 'high', - requester: { - name: 'Max Mustermann', - email: 'max.mustermann@example.de' - }, - source: 'web_form', - sourceDetails: 'Kontaktformular auf breakpilot.de', - receivedAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(), - deadline: { - originalDeadline: new Date(now.getTime() + 28 * 24 * 60 * 60 * 1000).toISOString(), - currentDeadline: new Date(now.getTime() + 28 * 24 * 60 * 60 * 1000).toISOString(), - extended: false - }, - identityVerification: { - verified: false - }, - assignment: { - assignedTo: null - }, - createdAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(), - createdBy: 'system', - updatedAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(), - tenantId: 'default-tenant' - }, - { - id: 'dsr-002', - referenceNumber: 'DSR-2025-000002', - type: 'erasure', - status: 'identity_verification', - priority: 'high', - requester: { - name: 'Anna Schmidt', - email: 'anna.schmidt@example.de', - phone: '+49 170 1234567' - }, - source: 'email', - requestText: 'Ich moechte, dass alle meine Daten geloescht werden.', - receivedAt: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(), - deadline: { - originalDeadline: new Date(now.getTime() + 9 * 24 * 60 * 60 * 1000).toISOString(), - currentDeadline: new Date(now.getTime() + 9 * 24 * 60 * 60 * 1000).toISOString(), - extended: false - }, - identityVerification: { - verified: false - }, - assignment: { - assignedTo: 'DSB Mueller', - assignedAt: new Date(now.getTime() - 4 * 24 * 60 * 60 * 1000).toISOString() - }, - createdAt: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(), - createdBy: 'system', - updatedAt: new Date(now.getTime() - 4 * 24 * 60 * 60 * 1000).toISOString(), - tenantId: 'default-tenant' - }, - { - id: 'dsr-003', - referenceNumber: 'DSR-2025-000003', - type: 'rectification', - status: 'processing', - priority: 'normal', - requester: { - name: 'Peter Meier', - email: 'peter.meier@example.de' - }, - source: 'email', - requestText: 'Meine Adresse ist falsch gespeichert.', - receivedAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(), - deadline: { - originalDeadline: new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString(), - currentDeadline: new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString(), - extended: false - }, - identityVerification: { - verified: true, - method: 'existing_account', - verifiedAt: new Date(now.getTime() - 6 * 24 * 60 * 60 * 1000).toISOString(), - verifiedBy: 'DSB Mueller' - }, - assignment: { - assignedTo: 'DSB Mueller', - assignedAt: new Date(now.getTime() - 6 * 24 * 60 * 60 * 1000).toISOString() - }, - rectificationDetails: { - fieldsToCorrect: [ - { - field: 'Adresse', - currentValue: 'Musterstr. 1, 12345 Berlin', - requestedValue: 'Musterstr. 10, 12345 Berlin', - corrected: false - } - ] - }, - createdAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(), - createdBy: 'system', - updatedAt: new Date(now.getTime() - 1 * 24 * 60 * 60 * 1000).toISOString(), - tenantId: 'default-tenant' - }, - { - id: 'dsr-004', - referenceNumber: 'DSR-2025-000004', - type: 'portability', - status: 'processing', - priority: 'normal', - requester: { - name: 'Lisa Weber', - email: 'lisa.weber@example.de' - }, - source: 'web_form', - receivedAt: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString(), - deadline: { - originalDeadline: new Date(now.getTime() + 20 * 24 * 60 * 60 * 1000).toISOString(), - currentDeadline: new Date(now.getTime() + 20 * 24 * 60 * 60 * 1000).toISOString(), - extended: false - }, - identityVerification: { - verified: true, - method: 'id_document', - verifiedAt: new Date(now.getTime() - 8 * 24 * 60 * 60 * 1000).toISOString(), - verifiedBy: 'DSB Mueller' - }, - assignment: { - assignedTo: 'IT Team', - assignedAt: new Date(now.getTime() - 8 * 24 * 60 * 60 * 1000).toISOString() - }, - notes: 'JSON-Export wird vorbereitet', - createdAt: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString(), - createdBy: 'system', - updatedAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(), - tenantId: 'default-tenant' - }, - { - id: 'dsr-005', - referenceNumber: 'DSR-2025-000005', - type: 'objection', - status: 'rejected', - priority: 'low', - requester: { - name: 'Thomas Klein', - email: 'thomas.klein@example.de' - }, - source: 'letter', - requestText: 'Ich widerspreche der Verarbeitung meiner Daten fuer Marketingzwecke.', - receivedAt: new Date(now.getTime() - 35 * 24 * 60 * 60 * 1000).toISOString(), - deadline: { - originalDeadline: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(), - currentDeadline: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(), - extended: false - }, - completedAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(), - identityVerification: { - verified: true, - method: 'postal', - verifiedAt: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString(), - verifiedBy: 'DSB Mueller' - }, - assignment: { - assignedTo: 'Rechtsabteilung', - assignedAt: new Date(now.getTime() - 28 * 24 * 60 * 60 * 1000).toISOString() - }, - objectionDetails: { - processingPurpose: 'Marketing', - legalBasis: 'Berechtigtes Interesse (Art. 6(1)(f))', - objectionGrounds: 'Keine konkreten Gruende genannt', - decision: 'rejected', - decisionReason: 'Zwingende schutzwuerdige Gruende fuer die Verarbeitung ueberwiegen', - decisionBy: 'Rechtsabteilung', - decisionAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString() - }, - notes: 'Widerspruch unberechtigt - zwingende schutzwuerdige Gruende', - createdAt: new Date(now.getTime() - 35 * 24 * 60 * 60 * 1000).toISOString(), - createdBy: 'system', - updatedAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(), - tenantId: 'default-tenant' - }, - { - id: 'dsr-006', - referenceNumber: 'DSR-2025-000006', - type: 'access', - status: 'completed', - priority: 'normal', - requester: { - name: 'Sarah Braun', - email: 'sarah.braun@example.de' - }, - source: 'email', - receivedAt: new Date(now.getTime() - 45 * 24 * 60 * 60 * 1000).toISOString(), - deadline: { - originalDeadline: new Date(now.getTime() - 15 * 24 * 60 * 60 * 1000).toISOString(), - currentDeadline: new Date(now.getTime() - 15 * 24 * 60 * 60 * 1000).toISOString(), - extended: false - }, - completedAt: new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000).toISOString(), - identityVerification: { - verified: true, - method: 'id_document', - verifiedAt: new Date(now.getTime() - 42 * 24 * 60 * 60 * 1000).toISOString(), - verifiedBy: 'DSB Mueller' - }, - assignment: { - assignedTo: 'DSB Mueller', - assignedAt: new Date(now.getTime() - 42 * 24 * 60 * 60 * 1000).toISOString() - }, - dataExport: { - format: 'pdf', - generatedAt: new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000).toISOString(), - generatedBy: 'DSB Mueller', - fileName: 'datenauskunft_sarah_braun.pdf', - fileSize: 245000, - includesThirdPartyData: false - }, - createdAt: new Date(now.getTime() - 45 * 24 * 60 * 60 * 1000).toISOString(), - createdBy: 'system', - updatedAt: new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000).toISOString(), - tenantId: 'default-tenant' - } - ] -} - -export function createMockStatistics(): DSRStatistics { - return { - total: 6, - byStatus: { - intake: 1, - identity_verification: 1, - processing: 2, - completed: 1, - rejected: 1, - cancelled: 0 - }, - byType: { - access: 2, - rectification: 1, - erasure: 1, - restriction: 0, - portability: 1, - objection: 1 - }, - overdue: 0, - dueThisWeek: 2, - averageProcessingDays: 18, - completedThisMonth: 1 - } -} +// Mock data +export { + createMockDSRList, + createMockStatistics, +} from './api-mock' diff --git a/admin-compliance/lib/sdk/einwilligungen/context.tsx b/admin-compliance/lib/sdk/einwilligungen/context.tsx index 6474966..9d2517c 100644 --- a/admin-compliance/lib/sdk/einwilligungen/context.tsx +++ b/admin-compliance/lib/sdk/einwilligungen/context.tsx @@ -1,669 +1,12 @@ 'use client' -/** - * Einwilligungen Context & Reducer - * - * Zentrale State-Verwaltung fuer das Datenpunktkatalog & DSI-Generator Modul. - * Verwendet React Context + useReducer fuer vorhersehbare State-Updates. - */ - -import { - createContext, - useContext, - useReducer, - useCallback, - useMemo, - ReactNode, - Dispatch, -} from 'react' -import { - EinwilligungenState, - EinwilligungenAction, - EinwilligungenTab, - DataPoint, - DataPointCatalog, - GeneratedPrivacyPolicy, - CookieBannerConfig, - CompanyInfo, - ConsentStatistics, - PrivacyPolicySection, - SupportedLanguage, - ExportFormat, - DataPointCategory, - LegalBasis, - RiskLevel, -} from './types' -import { - PREDEFINED_DATA_POINTS, - RETENTION_MATRIX, - DEFAULT_COOKIE_CATEGORIES, - createDefaultCatalog, - getDataPointById, - getDataPointsByCategory, - countDataPointsByCategory, - countDataPointsByRiskLevel, -} from './catalog/loader' - // ============================================================================= -// INITIAL STATE +// Einwilligungen Context — Barrel re-exports +// Preserves the original public API so existing imports work unchanged. // ============================================================================= -const initialState: EinwilligungenState = { - // Data - catalog: null, - selectedDataPoints: [], - privacyPolicy: null, - cookieBannerConfig: null, - companyInfo: null, - consentStatistics: null, - - // UI State - activeTab: 'catalog', - isLoading: false, - isSaving: false, - error: null, - - // Editor State - editingDataPoint: null, - editingSection: null, - - // Preview - previewLanguage: 'de', - previewFormat: 'HTML', -} - -// ============================================================================= -// REDUCER -// ============================================================================= - -function einwilligungenReducer( - state: EinwilligungenState, - action: EinwilligungenAction -): EinwilligungenState { - switch (action.type) { - case 'SET_CATALOG': - return { - ...state, - catalog: action.payload, - // Automatisch alle aktiven Datenpunkte auswaehlen - selectedDataPoints: [ - ...action.payload.dataPoints.filter((dp) => dp.isActive !== false).map((dp) => dp.id), - ...action.payload.customDataPoints.filter((dp) => dp.isActive !== false).map((dp) => dp.id), - ], - } - - case 'SET_SELECTED_DATA_POINTS': - return { - ...state, - selectedDataPoints: action.payload, - } - - case 'TOGGLE_DATA_POINT': { - const id = action.payload - const isSelected = state.selectedDataPoints.includes(id) - return { - ...state, - selectedDataPoints: isSelected - ? state.selectedDataPoints.filter((dpId) => dpId !== id) - : [...state.selectedDataPoints, id], - } - } - - case 'ADD_CUSTOM_DATA_POINT': - if (!state.catalog) return state - return { - ...state, - catalog: { - ...state.catalog, - customDataPoints: [...state.catalog.customDataPoints, action.payload], - updatedAt: new Date(), - }, - selectedDataPoints: [...state.selectedDataPoints, action.payload.id], - } - - case 'UPDATE_DATA_POINT': { - if (!state.catalog) return state - const { id, data } = action.payload - - // Pruefe ob es ein vordefinierter oder kundenspezifischer Datenpunkt ist - const isCustom = state.catalog.customDataPoints.some((dp) => dp.id === id) - - if (isCustom) { - return { - ...state, - catalog: { - ...state.catalog, - customDataPoints: state.catalog.customDataPoints.map((dp) => - dp.id === id ? { ...dp, ...data } : dp - ), - updatedAt: new Date(), - }, - } - } else { - // Vordefinierte Datenpunkte: nur isActive aendern - return { - ...state, - catalog: { - ...state.catalog, - dataPoints: state.catalog.dataPoints.map((dp) => - dp.id === id ? { ...dp, ...data } : dp - ), - updatedAt: new Date(), - }, - } - } - } - - case 'DELETE_CUSTOM_DATA_POINT': - if (!state.catalog) return state - return { - ...state, - catalog: { - ...state.catalog, - customDataPoints: state.catalog.customDataPoints.filter((dp) => dp.id !== action.payload), - updatedAt: new Date(), - }, - selectedDataPoints: state.selectedDataPoints.filter((id) => id !== action.payload), - } - - case 'SET_PRIVACY_POLICY': - return { - ...state, - privacyPolicy: action.payload, - } - - case 'SET_COOKIE_BANNER_CONFIG': - return { - ...state, - cookieBannerConfig: action.payload, - } - - case 'UPDATE_COOKIE_BANNER_STYLING': - if (!state.cookieBannerConfig) return state - return { - ...state, - cookieBannerConfig: { - ...state.cookieBannerConfig, - styling: { - ...state.cookieBannerConfig.styling, - ...action.payload, - }, - updatedAt: new Date(), - }, - } - - case 'UPDATE_COOKIE_BANNER_TEXTS': - if (!state.cookieBannerConfig) return state - return { - ...state, - cookieBannerConfig: { - ...state.cookieBannerConfig, - texts: { - ...state.cookieBannerConfig.texts, - ...action.payload, - }, - updatedAt: new Date(), - }, - } - - case 'SET_COMPANY_INFO': - return { - ...state, - companyInfo: action.payload, - } - - case 'SET_CONSENT_STATISTICS': - return { - ...state, - consentStatistics: action.payload, - } - - case 'SET_ACTIVE_TAB': - return { - ...state, - activeTab: action.payload, - } - - case 'SET_LOADING': - return { - ...state, - isLoading: action.payload, - } - - case 'SET_SAVING': - return { - ...state, - isSaving: action.payload, - } - - case 'SET_ERROR': - return { - ...state, - error: action.payload, - } - - case 'SET_EDITING_DATA_POINT': - return { - ...state, - editingDataPoint: action.payload, - } - - case 'SET_EDITING_SECTION': - return { - ...state, - editingSection: action.payload, - } - - case 'SET_PREVIEW_LANGUAGE': - return { - ...state, - previewLanguage: action.payload, - } - - case 'SET_PREVIEW_FORMAT': - return { - ...state, - previewFormat: action.payload, - } - - case 'RESET_STATE': - return initialState - - default: - return state - } -} - -// ============================================================================= -// CONTEXT -// ============================================================================= - -interface EinwilligungenContextValue { - state: EinwilligungenState - dispatch: Dispatch - - // Computed Values - allDataPoints: DataPoint[] - selectedDataPointsData: DataPoint[] - dataPointsByCategory: Record - categoryStats: Record - riskStats: Record - legalBasisStats: Record - - // Actions - initializeCatalog: (tenantId: string) => void - loadCatalog: (tenantId: string) => Promise - saveCatalog: () => Promise - toggleDataPoint: (id: string) => void - addCustomDataPoint: (dataPoint: DataPoint) => void - updateDataPoint: (id: string, data: Partial) => void - deleteCustomDataPoint: (id: string) => void - setActiveTab: (tab: EinwilligungenTab) => void - setPreviewLanguage: (language: SupportedLanguage) => void - setPreviewFormat: (format: ExportFormat) => void - setCompanyInfo: (info: CompanyInfo) => void - generatePrivacyPolicy: () => Promise - generateCookieBannerConfig: () => void -} - -const EinwilligungenContext = createContext(null) - -// ============================================================================= -// PROVIDER -// ============================================================================= - -interface EinwilligungenProviderProps { - children: ReactNode - tenantId?: string -} - -export function EinwilligungenProvider({ children, tenantId }: EinwilligungenProviderProps) { - const [state, dispatch] = useReducer(einwilligungenReducer, initialState) - - // --------------------------------------------------------------------------- - // COMPUTED VALUES - // --------------------------------------------------------------------------- - - const allDataPoints = useMemo(() => { - if (!state.catalog) return PREDEFINED_DATA_POINTS - return [...state.catalog.dataPoints, ...state.catalog.customDataPoints] - }, [state.catalog]) - - const selectedDataPointsData = useMemo(() => { - return allDataPoints.filter((dp) => state.selectedDataPoints.includes(dp.id)) - }, [allDataPoints, state.selectedDataPoints]) - - const dataPointsByCategory = useMemo(() => { - const result: Partial> = {} - // 18 Kategorien (A-R) - const categories: DataPointCategory[] = [ - 'MASTER_DATA', // A - 'CONTACT_DATA', // B - 'AUTHENTICATION', // C - 'CONSENT', // D - 'COMMUNICATION', // E - 'PAYMENT', // F - 'USAGE_DATA', // G - 'LOCATION', // H - 'DEVICE_DATA', // I - 'MARKETING', // J - 'ANALYTICS', // K - 'SOCIAL_MEDIA', // L - 'HEALTH_DATA', // M - Art. 9 DSGVO - 'EMPLOYEE_DATA', // N - BDSG § 26 - 'CONTRACT_DATA', // O - 'LOG_DATA', // P - 'AI_DATA', // Q - AI Act - 'SECURITY', // R - ] - for (const cat of categories) { - result[cat] = selectedDataPointsData.filter((dp) => dp.category === cat) - } - return result as Record - }, [selectedDataPointsData]) - - const categoryStats = useMemo(() => { - const counts: Partial> = {} - for (const dp of selectedDataPointsData) { - counts[dp.category] = (counts[dp.category] || 0) + 1 - } - return counts as Record - }, [selectedDataPointsData]) - - const riskStats = useMemo(() => { - const counts: Record = { LOW: 0, MEDIUM: 0, HIGH: 0 } - for (const dp of selectedDataPointsData) { - counts[dp.riskLevel]++ - } - return counts - }, [selectedDataPointsData]) - - const legalBasisStats = useMemo(() => { - // Alle 7 Rechtsgrundlagen - const counts: Record = { - CONTRACT: 0, - CONSENT: 0, - EXPLICIT_CONSENT: 0, - LEGITIMATE_INTEREST: 0, - LEGAL_OBLIGATION: 0, - VITAL_INTERESTS: 0, - PUBLIC_INTEREST: 0, - } - for (const dp of selectedDataPointsData) { - counts[dp.legalBasis]++ - } - return counts - }, [selectedDataPointsData]) - - // --------------------------------------------------------------------------- - // ACTIONS - // --------------------------------------------------------------------------- - - const initializeCatalog = useCallback( - (tid: string) => { - const catalog = createDefaultCatalog(tid) - dispatch({ type: 'SET_CATALOG', payload: catalog }) - }, - [dispatch] - ) - - const loadCatalog = useCallback( - async (tid: string) => { - dispatch({ type: 'SET_LOADING', payload: true }) - dispatch({ type: 'SET_ERROR', payload: null }) - - try { - const response = await fetch(`/api/sdk/v1/einwilligungen/catalog`, { - headers: { - 'X-Tenant-ID': tid, - }, - }) - - if (response.ok) { - const data = await response.json() - dispatch({ type: 'SET_CATALOG', payload: data.catalog }) - if (data.companyInfo) { - dispatch({ type: 'SET_COMPANY_INFO', payload: data.companyInfo }) - } - if (data.cookieBannerConfig) { - dispatch({ type: 'SET_COOKIE_BANNER_CONFIG', payload: data.cookieBannerConfig }) - } - } else if (response.status === 404) { - // Katalog existiert noch nicht - erstelle Default - initializeCatalog(tid) - } else { - throw new Error('Failed to load catalog') - } - } catch (error) { - console.error('Error loading catalog:', error) - dispatch({ type: 'SET_ERROR', payload: 'Fehler beim Laden des Katalogs' }) - // Fallback zu Default - initializeCatalog(tid) - } finally { - dispatch({ type: 'SET_LOADING', payload: false }) - } - }, - [dispatch, initializeCatalog] - ) - - const saveCatalog = useCallback(async () => { - if (!state.catalog) return - - dispatch({ type: 'SET_SAVING', payload: true }) - dispatch({ type: 'SET_ERROR', payload: null }) - - try { - const response = await fetch(`/api/sdk/v1/einwilligungen/catalog`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Tenant-ID': state.catalog.tenantId, - }, - body: JSON.stringify({ - catalog: state.catalog, - companyInfo: state.companyInfo, - cookieBannerConfig: state.cookieBannerConfig, - }), - }) - - if (!response.ok) { - throw new Error('Failed to save catalog') - } - } catch (error) { - console.error('Error saving catalog:', error) - dispatch({ type: 'SET_ERROR', payload: 'Fehler beim Speichern des Katalogs' }) - } finally { - dispatch({ type: 'SET_SAVING', payload: false }) - } - }, [state.catalog, state.companyInfo, state.cookieBannerConfig, dispatch]) - - const toggleDataPoint = useCallback( - (id: string) => { - dispatch({ type: 'TOGGLE_DATA_POINT', payload: id }) - }, - [dispatch] - ) - - const addCustomDataPoint = useCallback( - (dataPoint: DataPoint) => { - dispatch({ type: 'ADD_CUSTOM_DATA_POINT', payload: { ...dataPoint, isCustom: true } }) - }, - [dispatch] - ) - - const updateDataPoint = useCallback( - (id: string, data: Partial) => { - dispatch({ type: 'UPDATE_DATA_POINT', payload: { id, data } }) - }, - [dispatch] - ) - - const deleteCustomDataPoint = useCallback( - (id: string) => { - dispatch({ type: 'DELETE_CUSTOM_DATA_POINT', payload: id }) - }, - [dispatch] - ) - - const setActiveTab = useCallback( - (tab: EinwilligungenTab) => { - dispatch({ type: 'SET_ACTIVE_TAB', payload: tab }) - }, - [dispatch] - ) - - const setPreviewLanguage = useCallback( - (language: SupportedLanguage) => { - dispatch({ type: 'SET_PREVIEW_LANGUAGE', payload: language }) - }, - [dispatch] - ) - - const setPreviewFormat = useCallback( - (format: ExportFormat) => { - dispatch({ type: 'SET_PREVIEW_FORMAT', payload: format }) - }, - [dispatch] - ) - - const setCompanyInfo = useCallback( - (info: CompanyInfo) => { - dispatch({ type: 'SET_COMPANY_INFO', payload: info }) - }, - [dispatch] - ) - - const generatePrivacyPolicy = useCallback(async () => { - if (!state.catalog || !state.companyInfo) { - dispatch({ type: 'SET_ERROR', payload: 'Bitte zuerst Firmendaten eingeben' }) - return - } - - dispatch({ type: 'SET_LOADING', payload: true }) - dispatch({ type: 'SET_ERROR', payload: null }) - - try { - const response = await fetch(`/api/sdk/v1/einwilligungen/privacy-policy/generate`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Tenant-ID': state.catalog.tenantId, - }, - body: JSON.stringify({ - dataPointIds: state.selectedDataPoints, - companyInfo: state.companyInfo, - language: state.previewLanguage, - format: state.previewFormat, - }), - }) - - if (response.ok) { - const policy = await response.json() - dispatch({ type: 'SET_PRIVACY_POLICY', payload: policy }) - } else { - throw new Error('Failed to generate privacy policy') - } - } catch (error) { - console.error('Error generating privacy policy:', error) - dispatch({ type: 'SET_ERROR', payload: 'Fehler bei der Generierung der Datenschutzerklaerung' }) - } finally { - dispatch({ type: 'SET_LOADING', payload: false }) - } - }, [ - state.catalog, - state.companyInfo, - state.selectedDataPoints, - state.previewLanguage, - state.previewFormat, - dispatch, - ]) - - const generateCookieBannerConfig = useCallback(() => { - if (!state.catalog) return - - const config: CookieBannerConfig = { - id: `cookie-banner-${state.catalog.tenantId}`, - tenantId: state.catalog.tenantId, - categories: DEFAULT_COOKIE_CATEGORIES.map((cat) => ({ - ...cat, - // Filtere nur die ausgewaehlten Datenpunkte - dataPointIds: cat.dataPointIds.filter((id) => state.selectedDataPoints.includes(id)), - })), - styling: { - position: 'BOTTOM', - theme: 'LIGHT', - primaryColor: '#6366f1', - borderRadius: 12, - }, - texts: { - title: { de: 'Cookie-Einstellungen', en: 'Cookie Settings' }, - description: { - de: 'Wir verwenden Cookies, um Ihnen die bestmoegliche Nutzung unserer Website zu ermoeglichen.', - en: 'We use cookies to provide you with the best possible experience on our website.', - }, - acceptAll: { de: 'Alle akzeptieren', en: 'Accept All' }, - rejectAll: { de: 'Alle ablehnen', en: 'Reject All' }, - customize: { de: 'Anpassen', en: 'Customize' }, - save: { de: 'Auswahl speichern', en: 'Save Selection' }, - privacyPolicyLink: { de: 'Datenschutzerklaerung', en: 'Privacy Policy' }, - }, - updatedAt: new Date(), - } - - dispatch({ type: 'SET_COOKIE_BANNER_CONFIG', payload: config }) - }, [state.catalog, state.selectedDataPoints, dispatch]) - - // --------------------------------------------------------------------------- - // CONTEXT VALUE - // --------------------------------------------------------------------------- - - const value: EinwilligungenContextValue = { - state, - dispatch, - - // Computed Values - allDataPoints, - selectedDataPointsData, - dataPointsByCategory, - categoryStats, - riskStats, - legalBasisStats, - - // Actions - initializeCatalog, - loadCatalog, - saveCatalog, - toggleDataPoint, - addCustomDataPoint, - updateDataPoint, - deleteCustomDataPoint, - setActiveTab, - setPreviewLanguage, - setPreviewFormat, - setCompanyInfo, - generatePrivacyPolicy, - generateCookieBannerConfig, - } - - return ( - {children} - ) -} - -// ============================================================================= -// HOOK -// ============================================================================= - -export function useEinwilligungen(): EinwilligungenContextValue { - const context = useContext(EinwilligungenContext) - if (!context) { - throw new Error('useEinwilligungen must be used within EinwilligungenProvider') - } - return context -} - -// ============================================================================= -// EXPORTS -// ============================================================================= - -export { initialState, einwilligungenReducer } +export { EinwilligungenProvider } from './provider' +export { EinwilligungenContext } from './provider' +export type { EinwilligungenContextValue } from './provider' +export { useEinwilligungen } from './hooks' +export { initialState, einwilligungenReducer } from './reducer' diff --git a/admin-compliance/lib/sdk/einwilligungen/hooks.tsx b/admin-compliance/lib/sdk/einwilligungen/hooks.tsx new file mode 100644 index 0000000..57f7373 --- /dev/null +++ b/admin-compliance/lib/sdk/einwilligungen/hooks.tsx @@ -0,0 +1,18 @@ +'use client' + +// ============================================================================= +// Einwilligungen Hook +// Custom hook for consuming the Einwilligungen context +// ============================================================================= + +import { useContext } from 'react' +import { EinwilligungenContext } from './provider' +import type { EinwilligungenContextValue } from './provider' + +export function useEinwilligungen(): EinwilligungenContextValue { + const context = useContext(EinwilligungenContext) + if (!context) { + throw new Error('useEinwilligungen must be used within EinwilligungenProvider') + } + return context +} diff --git a/admin-compliance/lib/sdk/einwilligungen/provider.tsx b/admin-compliance/lib/sdk/einwilligungen/provider.tsx new file mode 100644 index 0000000..d1bd56c --- /dev/null +++ b/admin-compliance/lib/sdk/einwilligungen/provider.tsx @@ -0,0 +1,384 @@ +'use client' + +/** + * Einwilligungen Provider + * + * React Context Provider fuer das Einwilligungen-Modul. + * Stellt State, computed values und Actions bereit. + */ + +import { + createContext, + useReducer, + useCallback, + useMemo, + ReactNode, + Dispatch, +} from 'react' +import { + EinwilligungenState, + EinwilligungenAction, + EinwilligungenTab, + DataPoint, + CookieBannerConfig, + CompanyInfo, + SupportedLanguage, + ExportFormat, + DataPointCategory, + LegalBasis, + RiskLevel, +} from './types' +import { + PREDEFINED_DATA_POINTS, + DEFAULT_COOKIE_CATEGORIES, + createDefaultCatalog, +} from './catalog/loader' +import { einwilligungenReducer, initialState } from './reducer' + +// ============================================================================= +// CONTEXT +// ============================================================================= + +export interface EinwilligungenContextValue { + state: EinwilligungenState + dispatch: Dispatch + + // Computed Values + allDataPoints: DataPoint[] + selectedDataPointsData: DataPoint[] + dataPointsByCategory: Record + categoryStats: Record + riskStats: Record + legalBasisStats: Record + + // Actions + initializeCatalog: (tenantId: string) => void + loadCatalog: (tenantId: string) => Promise + saveCatalog: () => Promise + toggleDataPoint: (id: string) => void + addCustomDataPoint: (dataPoint: DataPoint) => void + updateDataPoint: (id: string, data: Partial) => void + deleteCustomDataPoint: (id: string) => void + setActiveTab: (tab: EinwilligungenTab) => void + setPreviewLanguage: (language: SupportedLanguage) => void + setPreviewFormat: (format: ExportFormat) => void + setCompanyInfo: (info: CompanyInfo) => void + generatePrivacyPolicy: () => Promise + generateCookieBannerConfig: () => void +} + +export const EinwilligungenContext = createContext(null) + +// ============================================================================= +// PROVIDER +// ============================================================================= + +interface EinwilligungenProviderProps { + children: ReactNode + tenantId?: string +} + +export function EinwilligungenProvider({ children, tenantId }: EinwilligungenProviderProps) { + const [state, dispatch] = useReducer(einwilligungenReducer, initialState) + + // --------------------------------------------------------------------------- + // COMPUTED VALUES + // --------------------------------------------------------------------------- + + const allDataPoints = useMemo(() => { + if (!state.catalog) return PREDEFINED_DATA_POINTS + return [...state.catalog.dataPoints, ...state.catalog.customDataPoints] + }, [state.catalog]) + + const selectedDataPointsData = useMemo(() => { + return allDataPoints.filter((dp) => state.selectedDataPoints.includes(dp.id)) + }, [allDataPoints, state.selectedDataPoints]) + + const dataPointsByCategory = useMemo(() => { + const result: Partial> = {} + const categories: DataPointCategory[] = [ + 'MASTER_DATA', 'CONTACT_DATA', 'AUTHENTICATION', 'CONSENT', + 'COMMUNICATION', 'PAYMENT', 'USAGE_DATA', 'LOCATION', + 'DEVICE_DATA', 'MARKETING', 'ANALYTICS', 'SOCIAL_MEDIA', + 'HEALTH_DATA', 'EMPLOYEE_DATA', 'CONTRACT_DATA', 'LOG_DATA', + 'AI_DATA', 'SECURITY', + ] + for (const cat of categories) { + result[cat] = selectedDataPointsData.filter((dp) => dp.category === cat) + } + return result as Record + }, [selectedDataPointsData]) + + const categoryStats = useMemo(() => { + const counts: Partial> = {} + for (const dp of selectedDataPointsData) { + counts[dp.category] = (counts[dp.category] || 0) + 1 + } + return counts as Record + }, [selectedDataPointsData]) + + const riskStats = useMemo(() => { + const counts: Record = { LOW: 0, MEDIUM: 0, HIGH: 0 } + for (const dp of selectedDataPointsData) { + counts[dp.riskLevel]++ + } + return counts + }, [selectedDataPointsData]) + + const legalBasisStats = useMemo(() => { + const counts: Record = { + CONTRACT: 0, CONSENT: 0, EXPLICIT_CONSENT: 0, + LEGITIMATE_INTEREST: 0, LEGAL_OBLIGATION: 0, + VITAL_INTERESTS: 0, PUBLIC_INTEREST: 0, + } + for (const dp of selectedDataPointsData) { + counts[dp.legalBasis]++ + } + return counts + }, [selectedDataPointsData]) + + // --------------------------------------------------------------------------- + // ACTIONS + // --------------------------------------------------------------------------- + + const initializeCatalog = useCallback( + (tid: string) => { + const catalog = createDefaultCatalog(tid) + dispatch({ type: 'SET_CATALOG', payload: catalog }) + }, + [dispatch] + ) + + const loadCatalog = useCallback( + async (tid: string) => { + dispatch({ type: 'SET_LOADING', payload: true }) + dispatch({ type: 'SET_ERROR', payload: null }) + + try { + const response = await fetch(`/api/sdk/v1/einwilligungen/catalog`, { + headers: { 'X-Tenant-ID': tid }, + }) + + if (response.ok) { + const data = await response.json() + dispatch({ type: 'SET_CATALOG', payload: data.catalog }) + if (data.companyInfo) { + dispatch({ type: 'SET_COMPANY_INFO', payload: data.companyInfo }) + } + if (data.cookieBannerConfig) { + dispatch({ type: 'SET_COOKIE_BANNER_CONFIG', payload: data.cookieBannerConfig }) + } + } else if (response.status === 404) { + initializeCatalog(tid) + } else { + throw new Error('Failed to load catalog') + } + } catch (error) { + console.error('Error loading catalog:', error) + dispatch({ type: 'SET_ERROR', payload: 'Fehler beim Laden des Katalogs' }) + initializeCatalog(tid) + } finally { + dispatch({ type: 'SET_LOADING', payload: false }) + } + }, + [dispatch, initializeCatalog] + ) + + const saveCatalog = useCallback(async () => { + if (!state.catalog) return + + dispatch({ type: 'SET_SAVING', payload: true }) + dispatch({ type: 'SET_ERROR', payload: null }) + + try { + const response = await fetch(`/api/sdk/v1/einwilligungen/catalog`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Tenant-ID': state.catalog.tenantId, + }, + body: JSON.stringify({ + catalog: state.catalog, + companyInfo: state.companyInfo, + cookieBannerConfig: state.cookieBannerConfig, + }), + }) + + if (!response.ok) { + throw new Error('Failed to save catalog') + } + } catch (error) { + console.error('Error saving catalog:', error) + dispatch({ type: 'SET_ERROR', payload: 'Fehler beim Speichern des Katalogs' }) + } finally { + dispatch({ type: 'SET_SAVING', payload: false }) + } + }, [state.catalog, state.companyInfo, state.cookieBannerConfig, dispatch]) + + const toggleDataPoint = useCallback( + (id: string) => { + dispatch({ type: 'TOGGLE_DATA_POINT', payload: id }) + }, + [dispatch] + ) + + const addCustomDataPoint = useCallback( + (dataPoint: DataPoint) => { + dispatch({ type: 'ADD_CUSTOM_DATA_POINT', payload: { ...dataPoint, isCustom: true } }) + }, + [dispatch] + ) + + const updateDataPoint = useCallback( + (id: string, data: Partial) => { + dispatch({ type: 'UPDATE_DATA_POINT', payload: { id, data } }) + }, + [dispatch] + ) + + const deleteCustomDataPoint = useCallback( + (id: string) => { + dispatch({ type: 'DELETE_CUSTOM_DATA_POINT', payload: id }) + }, + [dispatch] + ) + + const setActiveTab = useCallback( + (tab: EinwilligungenTab) => { + dispatch({ type: 'SET_ACTIVE_TAB', payload: tab }) + }, + [dispatch] + ) + + const setPreviewLanguage = useCallback( + (language: SupportedLanguage) => { + dispatch({ type: 'SET_PREVIEW_LANGUAGE', payload: language }) + }, + [dispatch] + ) + + const setPreviewFormat = useCallback( + (format: ExportFormat) => { + dispatch({ type: 'SET_PREVIEW_FORMAT', payload: format }) + }, + [dispatch] + ) + + const setCompanyInfo = useCallback( + (info: CompanyInfo) => { + dispatch({ type: 'SET_COMPANY_INFO', payload: info }) + }, + [dispatch] + ) + + const generatePrivacyPolicy = useCallback(async () => { + if (!state.catalog || !state.companyInfo) { + dispatch({ type: 'SET_ERROR', payload: 'Bitte zuerst Firmendaten eingeben' }) + return + } + + dispatch({ type: 'SET_LOADING', payload: true }) + dispatch({ type: 'SET_ERROR', payload: null }) + + try { + const response = await fetch(`/api/sdk/v1/einwilligungen/privacy-policy/generate`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Tenant-ID': state.catalog.tenantId, + }, + body: JSON.stringify({ + dataPointIds: state.selectedDataPoints, + companyInfo: state.companyInfo, + language: state.previewLanguage, + format: state.previewFormat, + }), + }) + + if (response.ok) { + const policy = await response.json() + dispatch({ type: 'SET_PRIVACY_POLICY', payload: policy }) + } else { + throw new Error('Failed to generate privacy policy') + } + } catch (error) { + console.error('Error generating privacy policy:', error) + dispatch({ type: 'SET_ERROR', payload: 'Fehler bei der Generierung der Datenschutzerklaerung' }) + } finally { + dispatch({ type: 'SET_LOADING', payload: false }) + } + }, [ + state.catalog, + state.companyInfo, + state.selectedDataPoints, + state.previewLanguage, + state.previewFormat, + dispatch, + ]) + + const generateCookieBannerConfig = useCallback(() => { + if (!state.catalog) return + + const config: CookieBannerConfig = { + id: `cookie-banner-${state.catalog.tenantId}`, + tenantId: state.catalog.tenantId, + categories: DEFAULT_COOKIE_CATEGORIES.map((cat) => ({ + ...cat, + dataPointIds: cat.dataPointIds.filter((id) => state.selectedDataPoints.includes(id)), + })), + styling: { + position: 'BOTTOM', + theme: 'LIGHT', + primaryColor: '#6366f1', + borderRadius: 12, + }, + texts: { + title: { de: 'Cookie-Einstellungen', en: 'Cookie Settings' }, + description: { + de: 'Wir verwenden Cookies, um Ihnen die bestmoegliche Nutzung unserer Website zu ermoeglichen.', + en: 'We use cookies to provide you with the best possible experience on our website.', + }, + acceptAll: { de: 'Alle akzeptieren', en: 'Accept All' }, + rejectAll: { de: 'Alle ablehnen', en: 'Reject All' }, + customize: { de: 'Anpassen', en: 'Customize' }, + save: { de: 'Auswahl speichern', en: 'Save Selection' }, + privacyPolicyLink: { de: 'Datenschutzerklaerung', en: 'Privacy Policy' }, + }, + updatedAt: new Date(), + } + + dispatch({ type: 'SET_COOKIE_BANNER_CONFIG', payload: config }) + }, [state.catalog, state.selectedDataPoints, dispatch]) + + // --------------------------------------------------------------------------- + // CONTEXT VALUE + // --------------------------------------------------------------------------- + + const value: EinwilligungenContextValue = { + state, + dispatch, + allDataPoints, + selectedDataPointsData, + dataPointsByCategory, + categoryStats, + riskStats, + legalBasisStats, + initializeCatalog, + loadCatalog, + saveCatalog, + toggleDataPoint, + addCustomDataPoint, + updateDataPoint, + deleteCustomDataPoint, + setActiveTab, + setPreviewLanguage, + setPreviewFormat, + setCompanyInfo, + generatePrivacyPolicy, + generateCookieBannerConfig, + } + + return ( + {children} + ) +} diff --git a/admin-compliance/lib/sdk/einwilligungen/reducer.ts b/admin-compliance/lib/sdk/einwilligungen/reducer.ts new file mode 100644 index 0000000..884b60e --- /dev/null +++ b/admin-compliance/lib/sdk/einwilligungen/reducer.ts @@ -0,0 +1,237 @@ +/** + * Einwilligungen Reducer + * + * Action-Handling und State-Uebergaenge fuer das Einwilligungen-Modul. + */ + +import { + EinwilligungenState, + EinwilligungenAction, +} from './types' + +// ============================================================================= +// INITIAL STATE +// ============================================================================= + +export const initialState: EinwilligungenState = { + // Data + catalog: null, + selectedDataPoints: [], + privacyPolicy: null, + cookieBannerConfig: null, + companyInfo: null, + consentStatistics: null, + + // UI State + activeTab: 'catalog', + isLoading: false, + isSaving: false, + error: null, + + // Editor State + editingDataPoint: null, + editingSection: null, + + // Preview + previewLanguage: 'de', + previewFormat: 'HTML', +} + +// ============================================================================= +// REDUCER +// ============================================================================= + +export function einwilligungenReducer( + state: EinwilligungenState, + action: EinwilligungenAction +): EinwilligungenState { + switch (action.type) { + case 'SET_CATALOG': + return { + ...state, + catalog: action.payload, + selectedDataPoints: [ + ...action.payload.dataPoints.filter((dp) => dp.isActive !== false).map((dp) => dp.id), + ...action.payload.customDataPoints.filter((dp) => dp.isActive !== false).map((dp) => dp.id), + ], + } + + case 'SET_SELECTED_DATA_POINTS': + return { + ...state, + selectedDataPoints: action.payload, + } + + case 'TOGGLE_DATA_POINT': { + const id = action.payload + const isSelected = state.selectedDataPoints.includes(id) + return { + ...state, + selectedDataPoints: isSelected + ? state.selectedDataPoints.filter((dpId) => dpId !== id) + : [...state.selectedDataPoints, id], + } + } + + case 'ADD_CUSTOM_DATA_POINT': + if (!state.catalog) return state + return { + ...state, + catalog: { + ...state.catalog, + customDataPoints: [...state.catalog.customDataPoints, action.payload], + updatedAt: new Date(), + }, + selectedDataPoints: [...state.selectedDataPoints, action.payload.id], + } + + case 'UPDATE_DATA_POINT': { + if (!state.catalog) return state + const { id, data } = action.payload + + const isCustom = state.catalog.customDataPoints.some((dp) => dp.id === id) + + if (isCustom) { + return { + ...state, + catalog: { + ...state.catalog, + customDataPoints: state.catalog.customDataPoints.map((dp) => + dp.id === id ? { ...dp, ...data } : dp + ), + updatedAt: new Date(), + }, + } + } else { + return { + ...state, + catalog: { + ...state.catalog, + dataPoints: state.catalog.dataPoints.map((dp) => + dp.id === id ? { ...dp, ...data } : dp + ), + updatedAt: new Date(), + }, + } + } + } + + case 'DELETE_CUSTOM_DATA_POINT': + if (!state.catalog) return state + return { + ...state, + catalog: { + ...state.catalog, + customDataPoints: state.catalog.customDataPoints.filter((dp) => dp.id !== action.payload), + updatedAt: new Date(), + }, + selectedDataPoints: state.selectedDataPoints.filter((id) => id !== action.payload), + } + + case 'SET_PRIVACY_POLICY': + return { + ...state, + privacyPolicy: action.payload, + } + + case 'SET_COOKIE_BANNER_CONFIG': + return { + ...state, + cookieBannerConfig: action.payload, + } + + case 'UPDATE_COOKIE_BANNER_STYLING': + if (!state.cookieBannerConfig) return state + return { + ...state, + cookieBannerConfig: { + ...state.cookieBannerConfig, + styling: { + ...state.cookieBannerConfig.styling, + ...action.payload, + }, + updatedAt: new Date(), + }, + } + + case 'UPDATE_COOKIE_BANNER_TEXTS': + if (!state.cookieBannerConfig) return state + return { + ...state, + cookieBannerConfig: { + ...state.cookieBannerConfig, + texts: { + ...state.cookieBannerConfig.texts, + ...action.payload, + }, + updatedAt: new Date(), + }, + } + + case 'SET_COMPANY_INFO': + return { + ...state, + companyInfo: action.payload, + } + + case 'SET_CONSENT_STATISTICS': + return { + ...state, + consentStatistics: action.payload, + } + + case 'SET_ACTIVE_TAB': + return { + ...state, + activeTab: action.payload, + } + + case 'SET_LOADING': + return { + ...state, + isLoading: action.payload, + } + + case 'SET_SAVING': + return { + ...state, + isSaving: action.payload, + } + + case 'SET_ERROR': + return { + ...state, + error: action.payload, + } + + case 'SET_EDITING_DATA_POINT': + return { + ...state, + editingDataPoint: action.payload, + } + + case 'SET_EDITING_SECTION': + return { + ...state, + editingSection: action.payload, + } + + case 'SET_PREVIEW_LANGUAGE': + return { + ...state, + previewLanguage: action.payload, + } + + case 'SET_PREVIEW_FORMAT': + return { + ...state, + previewFormat: action.payload, + } + + case 'RESET_STATE': + return initialState + + default: + return state + } +} diff --git a/admin-compliance/lib/sdk/export-pdf.ts b/admin-compliance/lib/sdk/export-pdf.ts new file mode 100644 index 0000000..e7f44e0 --- /dev/null +++ b/admin-compliance/lib/sdk/export-pdf.ts @@ -0,0 +1,361 @@ +/** + * SDK PDF Export + * Generates PDF compliance reports from SDK state + */ + +import jsPDF from 'jspdf' +import { SDKState, SDK_STEPS } from './types' + +// ============================================================================= +// TYPES +// ============================================================================= + +export interface ExportOptions { + includeEvidence?: boolean + includeDocuments?: boolean + includeRawData?: boolean + language?: 'de' | 'en' +} + +export const DEFAULT_OPTIONS: ExportOptions = { + includeEvidence: true, + includeDocuments: true, + includeRawData: true, + language: 'de', +} + +// ============================================================================= +// LABELS (German) +// ============================================================================= + +export const LABELS_DE = { + title: 'AI Compliance SDK - Export', + subtitle: 'Compliance-Dokumentation', + generatedAt: 'Generiert am', + page: 'Seite', + summary: 'Zusammenfassung', + progress: 'Fortschritt', + phase1: 'Phase 1: Automatisches Compliance Assessment', + phase2: 'Phase 2: Dokumentengenerierung', + useCases: 'Use Cases', + risks: 'Risiken', + controls: 'Controls', + requirements: 'Anforderungen', + modules: 'Compliance-Module', + evidence: 'Nachweise', + checkpoints: 'Checkpoints', + noData: 'Keine Daten vorhanden', + status: 'Status', + completed: 'Abgeschlossen', + pending: 'Ausstehend', + inProgress: 'In Bearbeitung', + severity: 'Schweregrad', + mitigation: 'Mitigation', + description: 'Beschreibung', + category: 'Kategorie', + implementation: 'Implementierung', +} + +// ============================================================================= +// HELPERS +// ============================================================================= + +export function formatDate(date: Date | string | undefined): string { + if (!date) return '-' + const d = typeof date === 'string' ? new Date(date) : date + return d.toLocaleDateString('de-DE', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + }) +} + +function addHeader(doc: jsPDF, title: string, pageNum: number, totalPages: number): void { + const pageWidth = doc.internal.pageSize.getWidth() + doc.setDrawColor(147, 51, 234) + doc.setLineWidth(0.5) + doc.line(20, 15, pageWidth - 20, 15) + doc.setFontSize(10) + doc.setTextColor(100) + doc.text(title, 20, 12) + doc.text(`${LABELS_DE.page} ${pageNum}/${totalPages}`, pageWidth - 40, 12) +} + +function addFooter(doc: jsPDF, state: SDKState): void { + const pageWidth = doc.internal.pageSize.getWidth() + const pageHeight = doc.internal.pageSize.getHeight() + doc.setDrawColor(200) + doc.setLineWidth(0.3) + doc.line(20, pageHeight - 15, pageWidth - 20, pageHeight - 15) + doc.setFontSize(8) + doc.setTextColor(150) + doc.text(`Tenant: ${state.tenantId} | ${LABELS_DE.generatedAt}: ${formatDate(new Date())}`, 20, pageHeight - 10) +} + +function addSectionTitle(doc: jsPDF, title: string, y: number): number { + doc.setFontSize(14) + doc.setTextColor(147, 51, 234) + doc.setFont('helvetica', 'bold') + doc.text(title, 20, y) + doc.setFont('helvetica', 'normal') + return y + 10 +} + +function addSubsectionTitle(doc: jsPDF, title: string, y: number): number { + doc.setFontSize(11) + doc.setTextColor(60) + doc.setFont('helvetica', 'bold') + doc.text(title, 25, y) + doc.setFont('helvetica', 'normal') + return y + 7 +} + +function addText(doc: jsPDF, text: string, x: number, y: number, maxWidth: number = 170): number { + doc.setFontSize(10) + doc.setTextColor(60) + const lines = doc.splitTextToSize(text, maxWidth) + doc.text(lines, x, y) + return y + lines.length * 5 +} + +function checkPageBreak(doc: jsPDF, y: number, requiredSpace: number = 40): number { + const pageHeight = doc.internal.pageSize.getHeight() + if (y + requiredSpace > pageHeight - 25) { + doc.addPage() + return 30 + } + return y +} + +// ============================================================================= +// PDF EXPORT +// ============================================================================= + +export async function exportToPDF(state: SDKState, options: ExportOptions = {}): Promise { + const doc = new jsPDF() + + let y = 30 + const pageWidth = doc.internal.pageSize.getWidth() + + // Title Page + doc.setFillColor(147, 51, 234) + doc.rect(0, 0, pageWidth, 60, 'F') + doc.setFontSize(24) + doc.setTextColor(255) + doc.setFont('helvetica', 'bold') + doc.text(LABELS_DE.title, 20, 35) + doc.setFontSize(14) + doc.setFont('helvetica', 'normal') + doc.text(LABELS_DE.subtitle, 20, 48) + + y = 80 + doc.setDrawColor(200) + doc.setFillColor(249, 250, 251) + doc.roundedRect(20, y, pageWidth - 40, 50, 3, 3, 'FD') + + y += 15 + doc.setFontSize(12) + doc.setTextColor(60) + doc.text(`${LABELS_DE.generatedAt}: ${formatDate(new Date())}`, 30, y) + y += 10 + doc.text(`Tenant ID: ${state.tenantId}`, 30, y) + y += 10 + doc.text(`Version: ${state.version}`, 30, y) + y += 10 + const completedSteps = state.completedSteps.length + const totalSteps = SDK_STEPS.length + doc.text(`${LABELS_DE.progress}: ${completedSteps}/${totalSteps} Schritte (${Math.round(completedSteps / totalSteps * 100)}%)`, 30, y) + + y += 30 + y = addSectionTitle(doc, 'Inhaltsverzeichnis', y) + + const tocItems = [ + { title: 'Zusammenfassung', page: 2 }, + { title: 'Phase 1: Compliance Assessment', page: 3 }, + { title: 'Phase 2: Dokumentengenerierung', page: 4 }, + { title: 'Risiken & Controls', page: 5 }, + { title: 'Checkpoints', page: 6 }, + ] + + doc.setFontSize(10) + doc.setTextColor(80) + tocItems.forEach((item, idx) => { + doc.text(`${idx + 1}. ${item.title}`, 25, y) + doc.text(`${item.page}`, pageWidth - 30, y, { align: 'right' }) + y += 7 + }) + + // Summary Page + doc.addPage() + y = 30 + y = addSectionTitle(doc, LABELS_DE.summary, y) + + doc.setFillColor(249, 250, 251) + doc.roundedRect(20, y, pageWidth - 40, 40, 3, 3, 'F') + y += 15 + const phase1Steps = SDK_STEPS.filter(s => s.phase === 1) + const phase2Steps = SDK_STEPS.filter(s => s.phase === 2) + const phase1Completed = phase1Steps.filter(s => state.completedSteps.includes(s.id)).length + const phase2Completed = phase2Steps.filter(s => state.completedSteps.includes(s.id)).length + + doc.setFontSize(10) + doc.setTextColor(60) + doc.text(`${LABELS_DE.phase1}: ${phase1Completed}/${phase1Steps.length} ${LABELS_DE.completed}`, 30, y) + y += 8 + doc.text(`${LABELS_DE.phase2}: ${phase2Completed}/${phase2Steps.length} ${LABELS_DE.completed}`, 30, y) + y += 25 + + y = addSubsectionTitle(doc, 'Kennzahlen', y) + const metrics = [ + { label: 'Use Cases', value: state.useCases.length }, + { label: 'Risiken identifiziert', value: state.risks.length }, + { label: 'Controls definiert', value: state.controls.length }, + { label: 'Anforderungen', value: state.requirements.length }, + { label: 'Nachweise', value: state.evidence.length }, + ] + metrics.forEach(metric => { + doc.text(`${metric.label}: ${metric.value}`, 30, y) + y += 7 + }) + + // Use Cases + y += 10 + y = checkPageBreak(doc, y) + y = addSectionTitle(doc, LABELS_DE.useCases, y) + + if (state.useCases.length === 0) { + y = addText(doc, LABELS_DE.noData, 25, y) + } else { + state.useCases.forEach((uc, idx) => { + y = checkPageBreak(doc, y, 50) + doc.setFillColor(249, 250, 251) + doc.roundedRect(20, y - 5, pageWidth - 40, 35, 2, 2, 'F') + doc.setFontSize(11) + doc.setTextColor(40) + doc.setFont('helvetica', 'bold') + doc.text(`${idx + 1}. ${uc.name}`, 25, y + 5) + doc.setFont('helvetica', 'normal') + doc.setFontSize(9) + doc.setTextColor(100) + const ucStatus = uc.stepsCompleted === uc.steps.length ? LABELS_DE.completed : `${uc.stepsCompleted}/${uc.steps.length} Schritte` + doc.text(`ID: ${uc.id} | ${LABELS_DE.status}: ${ucStatus}`, 25, y + 13) + if (uc.description) { + y = addText(doc, uc.description, 25, y + 21, 160) + } + y += 40 + }) + } + + // Risks + doc.addPage() + y = 30 + y = addSectionTitle(doc, LABELS_DE.risks, y) + + if (state.risks.length === 0) { + y = addText(doc, LABELS_DE.noData, 25, y) + } else { + const sortedRisks = [...state.risks].sort((a, b) => { + const order = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3 } + return (order[a.severity] || 4) - (order[b.severity] || 4) + }) + sortedRisks.forEach((risk, idx) => { + y = checkPageBreak(doc, y, 45) + const severityColors: Record = { + CRITICAL: [220, 38, 38], HIGH: [234, 88, 12], + MEDIUM: [234, 179, 8], LOW: [34, 197, 94], + } + const color = severityColors[risk.severity] || [100, 100, 100] + doc.setFillColor(color[0], color[1], color[2]) + doc.rect(20, y - 3, 3, 30, 'F') + doc.setFontSize(11) + doc.setTextColor(40) + doc.setFont('helvetica', 'bold') + doc.text(`${idx + 1}. ${risk.title}`, 28, y + 5) + doc.setFont('helvetica', 'normal') + doc.setFontSize(9) + doc.setTextColor(100) + doc.text(`${LABELS_DE.severity}: ${risk.severity} | ${LABELS_DE.category}: ${risk.category}`, 28, y + 13) + if (risk.description) { + y = addText(doc, risk.description, 28, y + 21, 155) + } + if (risk.mitigation && risk.mitigation.length > 0) { + y += 5 + doc.setFontSize(9) + doc.setTextColor(34, 197, 94) + doc.text(`${LABELS_DE.mitigation}: ${risk.mitigation.join(', ')}`, 28, y) + } + y += 15 + }) + } + + // Controls + doc.addPage() + y = 30 + y = addSectionTitle(doc, LABELS_DE.controls, y) + + if (state.controls.length === 0) { + y = addText(doc, LABELS_DE.noData, 25, y) + } else { + state.controls.forEach((ctrl, idx) => { + y = checkPageBreak(doc, y, 35) + doc.setFillColor(249, 250, 251) + doc.roundedRect(20, y - 5, pageWidth - 40, 28, 2, 2, 'F') + doc.setFontSize(10) + doc.setTextColor(40) + doc.setFont('helvetica', 'bold') + doc.text(`${idx + 1}. ${ctrl.name}`, 25, y + 5) + doc.setFont('helvetica', 'normal') + doc.setFontSize(9) + doc.setTextColor(100) + doc.text(`${LABELS_DE.category}: ${ctrl.category} | ${LABELS_DE.implementation}: ${ctrl.implementationStatus || 'Nicht definiert'}`, 25, y + 13) + if (ctrl.description) { + y = addText(doc, ctrl.description.substring(0, 150) + (ctrl.description.length > 150 ? '...' : ''), 25, y + 20, 160) + } + y += 35 + }) + } + + // Checkpoints + doc.addPage() + y = 30 + y = addSectionTitle(doc, LABELS_DE.checkpoints, y) + + const checkpointIds = Object.keys(state.checkpoints) + if (checkpointIds.length === 0) { + y = addText(doc, LABELS_DE.noData, 25, y) + } else { + checkpointIds.forEach((cpId) => { + const cp = state.checkpoints[cpId] + y = checkPageBreak(doc, y, 25) + const statusColor = cp.passed ? [34, 197, 94] : [220, 38, 38] + doc.setFillColor(statusColor[0], statusColor[1], statusColor[2]) + doc.circle(25, y + 2, 3, 'F') + doc.setFontSize(10) + doc.setTextColor(40) + doc.text(cpId, 35, y + 5) + doc.setFontSize(9) + doc.setTextColor(100) + doc.text(`${LABELS_DE.status}: ${cp.passed ? LABELS_DE.completed : LABELS_DE.pending}`, 35, y + 12) + if (cp.errors && cp.errors.length > 0) { + doc.setTextColor(220, 38, 38) + doc.text(`Fehler: ${cp.errors.map(e => e.message).join(', ')}`, 35, y + 19) + y += 7 + } + y += 20 + }) + } + + // Add page numbers + const pageCount = doc.getNumberOfPages() + for (let i = 1; i <= pageCount; i++) { + doc.setPage(i) + if (i > 1) { + addHeader(doc, LABELS_DE.title, i, pageCount) + } + addFooter(doc, state) + } + + return doc.output('blob') +} diff --git a/admin-compliance/lib/sdk/export-zip.ts b/admin-compliance/lib/sdk/export-zip.ts new file mode 100644 index 0000000..deda2c9 --- /dev/null +++ b/admin-compliance/lib/sdk/export-zip.ts @@ -0,0 +1,240 @@ +/** + * SDK ZIP Export + * Packages SDK state, documents, and a PDF report into a ZIP archive + */ + +import JSZip from 'jszip' +import { SDKState, SDK_STEPS } from './types' +import { ExportOptions, DEFAULT_OPTIONS, formatDate, exportToPDF } from './export-pdf' + +// ============================================================================= +// ZIP EXPORT +// ============================================================================= + +export async function exportToZIP(state: SDKState, options: ExportOptions = {}): Promise { + const opts = { ...DEFAULT_OPTIONS, ...options } + const zip = new JSZip() + + const rootFolder = zip.folder('ai-compliance-sdk-export') + if (!rootFolder) throw new Error('Failed to create ZIP folder') + + const phase1Folder = rootFolder.folder('phase1-assessment') + const phase2Folder = rootFolder.folder('phase2-documents') + const dataFolder = rootFolder.folder('data') + + // Main State JSON + if (opts.includeRawData && dataFolder) { + dataFolder.file('state.json', JSON.stringify(state, null, 2)) + } + + // README + const readmeContent = `# AI Compliance SDK Export + +Generated: ${formatDate(new Date())} +Tenant: ${state.tenantId} +Version: ${state.version} + +## Folder Structure + +- **phase1-assessment/**: Compliance Assessment Ergebnisse + - use-cases.json: Alle Use Cases + - risks.json: Identifizierte Risiken + - controls.json: Definierte Controls + - requirements.json: Compliance-Anforderungen + +- **phase2-documents/**: Generierte Dokumente + - dsfa.json: Datenschutz-Folgenabschaetzung + - toms.json: Technische und organisatorische Massnahmen + - vvt.json: Verarbeitungsverzeichnis + - documents.json: Rechtliche Dokumente + +- **data/**: Rohdaten + - state.json: Kompletter SDK State + +## Progress + +Phase 1: ${SDK_STEPS.filter(s => s.phase === 1 && state.completedSteps.includes(s.id)).length}/${SDK_STEPS.filter(s => s.phase === 1).length} completed +Phase 2: ${SDK_STEPS.filter(s => s.phase === 2 && state.completedSteps.includes(s.id)).length}/${SDK_STEPS.filter(s => s.phase === 2).length} completed + +## Key Metrics + +- Use Cases: ${state.useCases.length} +- Risks: ${state.risks.length} +- Controls: ${state.controls.length} +- Requirements: ${state.requirements.length} +- Evidence: ${state.evidence.length} +` + + rootFolder.file('README.md', readmeContent) + + // Phase 1 Files + if (phase1Folder) { + phase1Folder.file('use-cases.json', JSON.stringify({ + exportedAt: new Date().toISOString(), + count: state.useCases.length, + useCases: state.useCases, + }, null, 2)) + + phase1Folder.file('risks.json', JSON.stringify({ + exportedAt: new Date().toISOString(), + count: state.risks.length, + risks: state.risks, + summary: { + critical: state.risks.filter(r => r.severity === 'CRITICAL').length, + high: state.risks.filter(r => r.severity === 'HIGH').length, + medium: state.risks.filter(r => r.severity === 'MEDIUM').length, + low: state.risks.filter(r => r.severity === 'LOW').length, + }, + }, null, 2)) + + phase1Folder.file('controls.json', JSON.stringify({ + exportedAt: new Date().toISOString(), + count: state.controls.length, + controls: state.controls, + }, null, 2)) + + phase1Folder.file('requirements.json', JSON.stringify({ + exportedAt: new Date().toISOString(), + count: state.requirements.length, + requirements: state.requirements, + }, null, 2)) + + phase1Folder.file('modules.json', JSON.stringify({ + exportedAt: new Date().toISOString(), + count: state.modules.length, + modules: state.modules, + }, null, 2)) + + if (opts.includeEvidence) { + phase1Folder.file('evidence.json', JSON.stringify({ + exportedAt: new Date().toISOString(), + count: state.evidence.length, + evidence: state.evidence, + }, null, 2)) + } + + phase1Folder.file('checkpoints.json', JSON.stringify({ + exportedAt: new Date().toISOString(), + checkpoints: state.checkpoints, + }, null, 2)) + + if (state.screening) { + phase1Folder.file('screening.json', JSON.stringify({ + exportedAt: new Date().toISOString(), + screening: state.screening, + }, null, 2)) + } + } + + // Phase 2 Files + if (phase2Folder) { + if (state.dsfa) { + phase2Folder.file('dsfa.json', JSON.stringify({ + exportedAt: new Date().toISOString(), + dsfa: state.dsfa, + }, null, 2)) + } + + phase2Folder.file('toms.json', JSON.stringify({ + exportedAt: new Date().toISOString(), + count: state.toms.length, + toms: state.toms, + }, null, 2)) + + phase2Folder.file('vvt.json', JSON.stringify({ + exportedAt: new Date().toISOString(), + count: state.vvt.length, + processingActivities: state.vvt, + }, null, 2)) + + if (opts.includeDocuments) { + phase2Folder.file('documents.json', JSON.stringify({ + exportedAt: new Date().toISOString(), + count: state.documents.length, + documents: state.documents, + }, null, 2)) + } + + if (state.cookieBanner) { + phase2Folder.file('cookie-banner.json', JSON.stringify({ + exportedAt: new Date().toISOString(), + config: state.cookieBanner, + }, null, 2)) + } + + phase2Folder.file('retention-policies.json', JSON.stringify({ + exportedAt: new Date().toISOString(), + count: state.retentionPolicies.length, + policies: state.retentionPolicies, + }, null, 2)) + + if (state.aiActClassification) { + phase2Folder.file('ai-act-classification.json', JSON.stringify({ + exportedAt: new Date().toISOString(), + classification: state.aiActClassification, + }, null, 2)) + } + + phase2Folder.file('obligations.json', JSON.stringify({ + exportedAt: new Date().toISOString(), + count: state.obligations.length, + obligations: state.obligations, + }, null, 2)) + + phase2Folder.file('consents.json', JSON.stringify({ + exportedAt: new Date().toISOString(), + count: state.consents.length, + consents: state.consents, + }, null, 2)) + + if (state.dsrConfig) { + phase2Folder.file('dsr-config.json', JSON.stringify({ + exportedAt: new Date().toISOString(), + config: state.dsrConfig, + }, null, 2)) + } + + phase2Folder.file('escalation-workflows.json', JSON.stringify({ + exportedAt: new Date().toISOString(), + count: state.escalationWorkflows.length, + workflows: state.escalationWorkflows, + }, null, 2)) + } + + // Security Data + if (dataFolder) { + if (state.sbom) { + dataFolder.file('sbom.json', JSON.stringify({ + exportedAt: new Date().toISOString(), + sbom: state.sbom, + }, null, 2)) + } + + if (state.securityIssues.length > 0) { + dataFolder.file('security-issues.json', JSON.stringify({ + exportedAt: new Date().toISOString(), + count: state.securityIssues.length, + issues: state.securityIssues, + }, null, 2)) + } + + if (state.securityBacklog.length > 0) { + dataFolder.file('security-backlog.json', JSON.stringify({ + exportedAt: new Date().toISOString(), + count: state.securityBacklog.length, + backlog: state.securityBacklog, + }, null, 2)) + } + } + + // Generate PDF and include in ZIP + try { + const pdfBlob = await exportToPDF(state, options) + const pdfArrayBuffer = await pdfBlob.arrayBuffer() + rootFolder.file('compliance-report.pdf', pdfArrayBuffer) + } catch (error) { + console.error('Failed to generate PDF for ZIP:', error) + } + + return zip.generateAsync({ type: 'blob', compression: 'DEFLATE' }) +} diff --git a/admin-compliance/lib/sdk/export.ts b/admin-compliance/lib/sdk/export.ts index a88c20b..aa6f6f7 100644 --- a/admin-compliance/lib/sdk/export.ts +++ b/admin-compliance/lib/sdk/export.ts @@ -1,711 +1,12 @@ /** - * SDK Export Utilities - * Handles PDF and ZIP export of SDK state and documents + * SDK Export Utilities — Barrel re-exports + * Preserves the original public API so existing imports work unchanged. */ -import jsPDF from 'jspdf' -import JSZip from 'jszip' -import { SDKState, SDK_STEPS, getStepById } from './types' - -// ============================================================================= -// TYPES -// ============================================================================= - -export interface ExportOptions { - includeEvidence?: boolean - includeDocuments?: boolean - includeRawData?: boolean - language?: 'de' | 'en' -} - -const DEFAULT_OPTIONS: ExportOptions = { - includeEvidence: true, - includeDocuments: true, - includeRawData: true, - language: 'de', -} - -// ============================================================================= -// LABELS (German) -// ============================================================================= - -const LABELS_DE = { - title: 'AI Compliance SDK - Export', - subtitle: 'Compliance-Dokumentation', - generatedAt: 'Generiert am', - page: 'Seite', - summary: 'Zusammenfassung', - progress: 'Fortschritt', - phase1: 'Phase 1: Automatisches Compliance Assessment', - phase2: 'Phase 2: Dokumentengenerierung', - useCases: 'Use Cases', - risks: 'Risiken', - controls: 'Controls', - requirements: 'Anforderungen', - modules: 'Compliance-Module', - evidence: 'Nachweise', - checkpoints: 'Checkpoints', - noData: 'Keine Daten vorhanden', - status: 'Status', - completed: 'Abgeschlossen', - pending: 'Ausstehend', - inProgress: 'In Bearbeitung', - severity: 'Schweregrad', - mitigation: 'Mitigation', - description: 'Beschreibung', - category: 'Kategorie', - implementation: 'Implementierung', -} - -// ============================================================================= -// PDF EXPORT -// ============================================================================= - -function formatDate(date: Date | string | undefined): string { - if (!date) return '-' - const d = typeof date === 'string' ? new Date(date) : date - return d.toLocaleDateString('de-DE', { - day: '2-digit', - month: '2-digit', - year: 'numeric', - hour: '2-digit', - minute: '2-digit', - }) -} - -function addHeader(doc: jsPDF, title: string, pageNum: number, totalPages: number): void { - const pageWidth = doc.internal.pageSize.getWidth() - - // Header line - doc.setDrawColor(147, 51, 234) // Purple - doc.setLineWidth(0.5) - doc.line(20, 15, pageWidth - 20, 15) - - // Title - doc.setFontSize(10) - doc.setTextColor(100) - doc.text(title, 20, 12) - - // Page number - doc.text(`${LABELS_DE.page} ${pageNum}/${totalPages}`, pageWidth - 40, 12) -} - -function addFooter(doc: jsPDF, state: SDKState): void { - const pageWidth = doc.internal.pageSize.getWidth() - const pageHeight = doc.internal.pageSize.getHeight() - - // Footer line - doc.setDrawColor(200) - doc.setLineWidth(0.3) - doc.line(20, pageHeight - 15, pageWidth - 20, pageHeight - 15) - - // Footer text - doc.setFontSize(8) - doc.setTextColor(150) - doc.text(`Tenant: ${state.tenantId} | ${LABELS_DE.generatedAt}: ${formatDate(new Date())}`, 20, pageHeight - 10) -} - -function addSectionTitle(doc: jsPDF, title: string, y: number): number { - doc.setFontSize(14) - doc.setTextColor(147, 51, 234) // Purple - doc.setFont('helvetica', 'bold') - doc.text(title, 20, y) - doc.setFont('helvetica', 'normal') - return y + 10 -} - -function addSubsectionTitle(doc: jsPDF, title: string, y: number): number { - doc.setFontSize(11) - doc.setTextColor(60) - doc.setFont('helvetica', 'bold') - doc.text(title, 25, y) - doc.setFont('helvetica', 'normal') - return y + 7 -} - -function addText(doc: jsPDF, text: string, x: number, y: number, maxWidth: number = 170): number { - doc.setFontSize(10) - doc.setTextColor(60) - const lines = doc.splitTextToSize(text, maxWidth) - doc.text(lines, x, y) - return y + lines.length * 5 -} - -function checkPageBreak(doc: jsPDF, y: number, requiredSpace: number = 40): number { - const pageHeight = doc.internal.pageSize.getHeight() - if (y + requiredSpace > pageHeight - 25) { - doc.addPage() - return 30 - } - return y -} - -export async function exportToPDF(state: SDKState, options: ExportOptions = {}): Promise { - const opts = { ...DEFAULT_OPTIONS, ...options } - const doc = new jsPDF() - - let y = 30 - const pageWidth = doc.internal.pageSize.getWidth() - - // ========================================================================== - // Title Page - // ========================================================================== - - // Logo/Title area - doc.setFillColor(147, 51, 234) - doc.rect(0, 0, pageWidth, 60, 'F') - - doc.setFontSize(24) - doc.setTextColor(255) - doc.setFont('helvetica', 'bold') - doc.text(LABELS_DE.title, 20, 35) - - doc.setFontSize(14) - doc.setFont('helvetica', 'normal') - doc.text(LABELS_DE.subtitle, 20, 48) - - // Reset for content - y = 80 - - // Summary box - doc.setDrawColor(200) - doc.setFillColor(249, 250, 251) - doc.roundedRect(20, y, pageWidth - 40, 50, 3, 3, 'FD') - - y += 15 - doc.setFontSize(12) - doc.setTextColor(60) - doc.text(`${LABELS_DE.generatedAt}: ${formatDate(new Date())}`, 30, y) - - y += 10 - doc.text(`Tenant ID: ${state.tenantId}`, 30, y) - - y += 10 - doc.text(`Version: ${state.version}`, 30, y) - - y += 10 - const completedSteps = state.completedSteps.length - const totalSteps = SDK_STEPS.length - doc.text(`${LABELS_DE.progress}: ${completedSteps}/${totalSteps} Schritte (${Math.round(completedSteps / totalSteps * 100)}%)`, 30, y) - - y += 30 - - // Table of Contents - y = addSectionTitle(doc, 'Inhaltsverzeichnis', y) - - const tocItems = [ - { title: 'Zusammenfassung', page: 2 }, - { title: 'Phase 1: Compliance Assessment', page: 3 }, - { title: 'Phase 2: Dokumentengenerierung', page: 4 }, - { title: 'Risiken & Controls', page: 5 }, - { title: 'Checkpoints', page: 6 }, - ] - - doc.setFontSize(10) - doc.setTextColor(80) - tocItems.forEach((item, idx) => { - doc.text(`${idx + 1}. ${item.title}`, 25, y) - doc.text(`${item.page}`, pageWidth - 30, y, { align: 'right' }) - y += 7 - }) - - // ========================================================================== - // Summary Page - // ========================================================================== - - doc.addPage() - y = 30 - - y = addSectionTitle(doc, LABELS_DE.summary, y) - - // Progress overview - doc.setFillColor(249, 250, 251) - doc.roundedRect(20, y, pageWidth - 40, 40, 3, 3, 'F') - - y += 15 - const phase1Steps = SDK_STEPS.filter(s => s.phase === 1) - const phase2Steps = SDK_STEPS.filter(s => s.phase === 2) - const phase1Completed = phase1Steps.filter(s => state.completedSteps.includes(s.id)).length - const phase2Completed = phase2Steps.filter(s => state.completedSteps.includes(s.id)).length - - doc.setFontSize(10) - doc.setTextColor(60) - doc.text(`${LABELS_DE.phase1}: ${phase1Completed}/${phase1Steps.length} ${LABELS_DE.completed}`, 30, y) - y += 8 - doc.text(`${LABELS_DE.phase2}: ${phase2Completed}/${phase2Steps.length} ${LABELS_DE.completed}`, 30, y) - - y += 25 - - // Key metrics - y = addSubsectionTitle(doc, 'Kennzahlen', y) - - const metrics = [ - { label: 'Use Cases', value: state.useCases.length }, - { label: 'Risiken identifiziert', value: state.risks.length }, - { label: 'Controls definiert', value: state.controls.length }, - { label: 'Anforderungen', value: state.requirements.length }, - { label: 'Nachweise', value: state.evidence.length }, - ] - - metrics.forEach(metric => { - doc.text(`${metric.label}: ${metric.value}`, 30, y) - y += 7 - }) - - // ========================================================================== - // Use Cases - // ========================================================================== - - y += 10 - y = checkPageBreak(doc, y) - y = addSectionTitle(doc, LABELS_DE.useCases, y) - - if (state.useCases.length === 0) { - y = addText(doc, LABELS_DE.noData, 25, y) - } else { - state.useCases.forEach((uc, idx) => { - y = checkPageBreak(doc, y, 50) - - doc.setFillColor(249, 250, 251) - doc.roundedRect(20, y - 5, pageWidth - 40, 35, 2, 2, 'F') - - doc.setFontSize(11) - doc.setTextColor(40) - doc.setFont('helvetica', 'bold') - doc.text(`${idx + 1}. ${uc.name}`, 25, y + 5) - doc.setFont('helvetica', 'normal') - - doc.setFontSize(9) - doc.setTextColor(100) - const ucStatus = uc.stepsCompleted === uc.steps.length ? LABELS_DE.completed : `${uc.stepsCompleted}/${uc.steps.length} Schritte` - doc.text(`ID: ${uc.id} | ${LABELS_DE.status}: ${ucStatus}`, 25, y + 13) - - if (uc.description) { - y = addText(doc, uc.description, 25, y + 21, 160) - } - - y += 40 - }) - } - - // ========================================================================== - // Risks - // ========================================================================== - - doc.addPage() - y = 30 - y = addSectionTitle(doc, LABELS_DE.risks, y) - - if (state.risks.length === 0) { - y = addText(doc, LABELS_DE.noData, 25, y) - } else { - // Sort by severity - const sortedRisks = [...state.risks].sort((a, b) => { - const order = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3 } - return (order[a.severity] || 4) - (order[b.severity] || 4) - }) - - sortedRisks.forEach((risk, idx) => { - y = checkPageBreak(doc, y, 45) - - // Severity color - const severityColors: Record = { - CRITICAL: [220, 38, 38], - HIGH: [234, 88, 12], - MEDIUM: [234, 179, 8], - LOW: [34, 197, 94], - } - const color = severityColors[risk.severity] || [100, 100, 100] - - doc.setFillColor(color[0], color[1], color[2]) - doc.rect(20, y - 3, 3, 30, 'F') - - doc.setFontSize(11) - doc.setTextColor(40) - doc.setFont('helvetica', 'bold') - doc.text(`${idx + 1}. ${risk.title}`, 28, y + 5) - doc.setFont('helvetica', 'normal') - - doc.setFontSize(9) - doc.setTextColor(100) - doc.text(`${LABELS_DE.severity}: ${risk.severity} | ${LABELS_DE.category}: ${risk.category}`, 28, y + 13) - - if (risk.description) { - y = addText(doc, risk.description, 28, y + 21, 155) - } - - if (risk.mitigation && risk.mitigation.length > 0) { - y += 5 - doc.setFontSize(9) - doc.setTextColor(34, 197, 94) - doc.text(`${LABELS_DE.mitigation}: ${risk.mitigation.join(', ')}`, 28, y) - } - - y += 15 - }) - } - - // ========================================================================== - // Controls - // ========================================================================== - - doc.addPage() - y = 30 - y = addSectionTitle(doc, LABELS_DE.controls, y) - - if (state.controls.length === 0) { - y = addText(doc, LABELS_DE.noData, 25, y) - } else { - state.controls.forEach((ctrl, idx) => { - y = checkPageBreak(doc, y, 35) - - doc.setFillColor(249, 250, 251) - doc.roundedRect(20, y - 5, pageWidth - 40, 28, 2, 2, 'F') - - doc.setFontSize(10) - doc.setTextColor(40) - doc.setFont('helvetica', 'bold') - doc.text(`${idx + 1}. ${ctrl.name}`, 25, y + 5) - doc.setFont('helvetica', 'normal') - - doc.setFontSize(9) - doc.setTextColor(100) - doc.text(`${LABELS_DE.category}: ${ctrl.category} | ${LABELS_DE.implementation}: ${ctrl.implementationStatus || 'Nicht definiert'}`, 25, y + 13) - - if (ctrl.description) { - y = addText(doc, ctrl.description.substring(0, 150) + (ctrl.description.length > 150 ? '...' : ''), 25, y + 20, 160) - } - - y += 35 - }) - } - - // ========================================================================== - // Checkpoints - // ========================================================================== - - doc.addPage() - y = 30 - y = addSectionTitle(doc, LABELS_DE.checkpoints, y) - - const checkpointIds = Object.keys(state.checkpoints) - - if (checkpointIds.length === 0) { - y = addText(doc, LABELS_DE.noData, 25, y) - } else { - checkpointIds.forEach((cpId) => { - const cp = state.checkpoints[cpId] - y = checkPageBreak(doc, y, 25) - - const statusColor = cp.passed ? [34, 197, 94] : [220, 38, 38] - doc.setFillColor(statusColor[0], statusColor[1], statusColor[2]) - doc.circle(25, y + 2, 3, 'F') - - doc.setFontSize(10) - doc.setTextColor(40) - doc.text(cpId, 35, y + 5) - - doc.setFontSize(9) - doc.setTextColor(100) - doc.text(`${LABELS_DE.status}: ${cp.passed ? LABELS_DE.completed : LABELS_DE.pending}`, 35, y + 12) - - if (cp.errors && cp.errors.length > 0) { - doc.setTextColor(220, 38, 38) - doc.text(`Fehler: ${cp.errors.map(e => e.message).join(', ')}`, 35, y + 19) - y += 7 - } - - y += 20 - }) - } - - // ========================================================================== - // Add page numbers - // ========================================================================== - - const pageCount = doc.getNumberOfPages() - for (let i = 1; i <= pageCount; i++) { - doc.setPage(i) - if (i > 1) { - addHeader(doc, LABELS_DE.title, i, pageCount) - } - addFooter(doc, state) - } - - return doc.output('blob') -} - -// ============================================================================= -// ZIP EXPORT -// ============================================================================= - -export async function exportToZIP(state: SDKState, options: ExportOptions = {}): Promise { - const opts = { ...DEFAULT_OPTIONS, ...options } - const zip = new JSZip() - - // Create folder structure - const rootFolder = zip.folder('ai-compliance-sdk-export') - if (!rootFolder) throw new Error('Failed to create ZIP folder') - - const phase1Folder = rootFolder.folder('phase1-assessment') - const phase2Folder = rootFolder.folder('phase2-documents') - const dataFolder = rootFolder.folder('data') - - // ========================================================================== - // Main State JSON - // ========================================================================== - - if (opts.includeRawData && dataFolder) { - dataFolder.file('state.json', JSON.stringify(state, null, 2)) - } - - // ========================================================================== - // README - // ========================================================================== - - const readmeContent = `# AI Compliance SDK Export - -Generated: ${formatDate(new Date())} -Tenant: ${state.tenantId} -Version: ${state.version} - -## Folder Structure - -- **phase1-assessment/**: Compliance Assessment Ergebnisse - - use-cases.json: Alle Use Cases - - risks.json: Identifizierte Risiken - - controls.json: Definierte Controls - - requirements.json: Compliance-Anforderungen - -- **phase2-documents/**: Generierte Dokumente - - dsfa.json: Datenschutz-Folgenabschaetzung - - toms.json: Technische und organisatorische Massnahmen - - vvt.json: Verarbeitungsverzeichnis - - documents.json: Rechtliche Dokumente - -- **data/**: Rohdaten - - state.json: Kompletter SDK State - -## Progress - -Phase 1: ${SDK_STEPS.filter(s => s.phase === 1 && state.completedSteps.includes(s.id)).length}/${SDK_STEPS.filter(s => s.phase === 1).length} completed -Phase 2: ${SDK_STEPS.filter(s => s.phase === 2 && state.completedSteps.includes(s.id)).length}/${SDK_STEPS.filter(s => s.phase === 2).length} completed - -## Key Metrics - -- Use Cases: ${state.useCases.length} -- Risks: ${state.risks.length} -- Controls: ${state.controls.length} -- Requirements: ${state.requirements.length} -- Evidence: ${state.evidence.length} -` - - rootFolder.file('README.md', readmeContent) - - // ========================================================================== - // Phase 1 Files - // ========================================================================== - - if (phase1Folder) { - // Use Cases - phase1Folder.file('use-cases.json', JSON.stringify({ - exportedAt: new Date().toISOString(), - count: state.useCases.length, - useCases: state.useCases, - }, null, 2)) - - // Risks - phase1Folder.file('risks.json', JSON.stringify({ - exportedAt: new Date().toISOString(), - count: state.risks.length, - risks: state.risks, - summary: { - critical: state.risks.filter(r => r.severity === 'CRITICAL').length, - high: state.risks.filter(r => r.severity === 'HIGH').length, - medium: state.risks.filter(r => r.severity === 'MEDIUM').length, - low: state.risks.filter(r => r.severity === 'LOW').length, - }, - }, null, 2)) - - // Controls - phase1Folder.file('controls.json', JSON.stringify({ - exportedAt: new Date().toISOString(), - count: state.controls.length, - controls: state.controls, - }, null, 2)) - - // Requirements - phase1Folder.file('requirements.json', JSON.stringify({ - exportedAt: new Date().toISOString(), - count: state.requirements.length, - requirements: state.requirements, - }, null, 2)) - - // Modules - phase1Folder.file('modules.json', JSON.stringify({ - exportedAt: new Date().toISOString(), - count: state.modules.length, - modules: state.modules, - }, null, 2)) - - // Evidence - if (opts.includeEvidence) { - phase1Folder.file('evidence.json', JSON.stringify({ - exportedAt: new Date().toISOString(), - count: state.evidence.length, - evidence: state.evidence, - }, null, 2)) - } - - // Checkpoints - phase1Folder.file('checkpoints.json', JSON.stringify({ - exportedAt: new Date().toISOString(), - checkpoints: state.checkpoints, - }, null, 2)) - - // Screening - if (state.screening) { - phase1Folder.file('screening.json', JSON.stringify({ - exportedAt: new Date().toISOString(), - screening: state.screening, - }, null, 2)) - } - } - - // ========================================================================== - // Phase 2 Files - // ========================================================================== - - if (phase2Folder) { - // DSFA - if (state.dsfa) { - phase2Folder.file('dsfa.json', JSON.stringify({ - exportedAt: new Date().toISOString(), - dsfa: state.dsfa, - }, null, 2)) - } - - // TOMs - phase2Folder.file('toms.json', JSON.stringify({ - exportedAt: new Date().toISOString(), - count: state.toms.length, - toms: state.toms, - }, null, 2)) - - // VVT (Processing Activities) - phase2Folder.file('vvt.json', JSON.stringify({ - exportedAt: new Date().toISOString(), - count: state.vvt.length, - processingActivities: state.vvt, - }, null, 2)) - - // Legal Documents - if (opts.includeDocuments) { - phase2Folder.file('documents.json', JSON.stringify({ - exportedAt: new Date().toISOString(), - count: state.documents.length, - documents: state.documents, - }, null, 2)) - } - - // Cookie Banner Config - if (state.cookieBanner) { - phase2Folder.file('cookie-banner.json', JSON.stringify({ - exportedAt: new Date().toISOString(), - config: state.cookieBanner, - }, null, 2)) - } - - // Retention Policies - phase2Folder.file('retention-policies.json', JSON.stringify({ - exportedAt: new Date().toISOString(), - count: state.retentionPolicies.length, - policies: state.retentionPolicies, - }, null, 2)) - - // AI Act Classification - if (state.aiActClassification) { - phase2Folder.file('ai-act-classification.json', JSON.stringify({ - exportedAt: new Date().toISOString(), - classification: state.aiActClassification, - }, null, 2)) - } - - // Obligations - phase2Folder.file('obligations.json', JSON.stringify({ - exportedAt: new Date().toISOString(), - count: state.obligations.length, - obligations: state.obligations, - }, null, 2)) - - // Consent Records - phase2Folder.file('consents.json', JSON.stringify({ - exportedAt: new Date().toISOString(), - count: state.consents.length, - consents: state.consents, - }, null, 2)) - - // DSR Config - if (state.dsrConfig) { - phase2Folder.file('dsr-config.json', JSON.stringify({ - exportedAt: new Date().toISOString(), - config: state.dsrConfig, - }, null, 2)) - } - - // Escalation Workflows - phase2Folder.file('escalation-workflows.json', JSON.stringify({ - exportedAt: new Date().toISOString(), - count: state.escalationWorkflows.length, - workflows: state.escalationWorkflows, - }, null, 2)) - } - - // ========================================================================== - // Security Data - // ========================================================================== - - if (dataFolder) { - if (state.sbom) { - dataFolder.file('sbom.json', JSON.stringify({ - exportedAt: new Date().toISOString(), - sbom: state.sbom, - }, null, 2)) - } - - if (state.securityIssues.length > 0) { - dataFolder.file('security-issues.json', JSON.stringify({ - exportedAt: new Date().toISOString(), - count: state.securityIssues.length, - issues: state.securityIssues, - }, null, 2)) - } - - if (state.securityBacklog.length > 0) { - dataFolder.file('security-backlog.json', JSON.stringify({ - exportedAt: new Date().toISOString(), - count: state.securityBacklog.length, - backlog: state.securityBacklog, - }, null, 2)) - } - } - - // ========================================================================== - // Generate PDF and include in ZIP - // ========================================================================== - - try { - const pdfBlob = await exportToPDF(state, options) - const pdfArrayBuffer = await pdfBlob.arrayBuffer() - rootFolder.file('compliance-report.pdf', pdfArrayBuffer) - } catch (error) { - console.error('Failed to generate PDF for ZIP:', error) - // Continue without PDF - } - - // Generate ZIP - return zip.generateAsync({ type: 'blob', compression: 'DEFLATE' }) -} +import { SDKState } from './types' +export { exportToPDF } from './export-pdf' +export type { ExportOptions } from './export-pdf' +export { exportToZIP } from './export-zip' // ============================================================================= // EXPORT HELPER @@ -714,7 +15,7 @@ Phase 2: ${SDK_STEPS.filter(s => s.phase === 2 && state.completedSteps.includes( export async function downloadExport( state: SDKState, format: 'json' | 'pdf' | 'zip', - options: ExportOptions = {} + options: import('./export-pdf').ExportOptions = {} ): Promise { let blob: Blob let filename: string @@ -727,15 +28,19 @@ export async function downloadExport( filename = `ai-compliance-sdk-${timestamp}.json` break - case 'pdf': + case 'pdf': { + const { exportToPDF } = await import('./export-pdf') blob = await exportToPDF(state, options) filename = `ai-compliance-sdk-${timestamp}.pdf` break + } - case 'zip': + case 'zip': { + const { exportToZIP } = await import('./export-zip') blob = await exportToZIP(state, options) filename = `ai-compliance-sdk-${timestamp}.zip` break + } default: throw new Error(`Unknown export format: ${format}`) diff --git a/admin-compliance/lib/sdk/incidents/api-helpers.ts b/admin-compliance/lib/sdk/incidents/api-helpers.ts new file mode 100644 index 0000000..2dc8500 --- /dev/null +++ b/admin-compliance/lib/sdk/incidents/api-helpers.ts @@ -0,0 +1,83 @@ +/** + * Incident API - Shared configuration and helper functions + */ + +// ============================================================================= +// CONFIGURATION +// ============================================================================= + +export const INCIDENTS_API_BASE = process.env.NEXT_PUBLIC_SDK_API_URL || 'http://localhost:8093' +export const API_TIMEOUT = 30000 // 30 seconds + +// ============================================================================= +// HELPER FUNCTIONS +// ============================================================================= + +export function getTenantId(): string { + if (typeof window !== 'undefined') { + return localStorage.getItem('bp_tenant_id') || 'default-tenant' + } + return 'default-tenant' +} + +export function getAuthHeaders(): HeadersInit { + const headers: HeadersInit = { + 'Content-Type': 'application/json', + 'X-Tenant-ID': getTenantId() + } + + if (typeof window !== 'undefined') { + const token = localStorage.getItem('authToken') + if (token) { + headers['Authorization'] = `Bearer ${token}` + } + const userId = localStorage.getItem('bp_user_id') + if (userId) { + headers['X-User-ID'] = userId + } + } + + return headers +} + +export async function fetchWithTimeout( + url: string, + options: RequestInit = {}, + timeout: number = API_TIMEOUT +): Promise { + const controller = new AbortController() + const timeoutId = setTimeout(() => controller.abort(), timeout) + + try { + const response = await fetch(url, { + ...options, + signal: controller.signal, + headers: { + ...getAuthHeaders(), + ...options.headers + } + }) + + if (!response.ok) { + const errorBody = await response.text() + let errorMessage = `HTTP ${response.status}: ${response.statusText}` + try { + const errorJson = JSON.parse(errorBody) + errorMessage = errorJson.error || errorJson.message || errorMessage + } catch { + // Keep the HTTP status message + } + throw new Error(errorMessage) + } + + // Handle empty responses + const contentType = response.headers.get('content-type') + if (contentType && contentType.includes('application/json')) { + return response.json() + } + + return {} as T + } finally { + clearTimeout(timeoutId) + } +} diff --git a/admin-compliance/lib/sdk/incidents/api-incidents.ts b/admin-compliance/lib/sdk/incidents/api-incidents.ts new file mode 100644 index 0000000..1c5a88c --- /dev/null +++ b/admin-compliance/lib/sdk/incidents/api-incidents.ts @@ -0,0 +1,372 @@ +/** + * Incident CRUD, Risk Assessment, Notifications, Measures, Timeline, Statistics + */ + +import { + Incident, + IncidentListResponse, + IncidentFilters, + IncidentCreateRequest, + IncidentUpdateRequest, + IncidentStatistics, + IncidentMeasure, + TimelineEntry, + RiskAssessmentRequest, + AuthorityNotification, + DataSubjectNotification, + IncidentSeverity, + IncidentStatus, + IncidentCategory, +} from './types' + +import { INCIDENTS_API_BASE, fetchWithTimeout, getAuthHeaders } from './api-helpers' + +// ============================================================================= +// INCIDENT LIST & CRUD +// ============================================================================= + +/** + * Alle Vorfaelle abrufen mit optionalen Filtern + */ +export async function fetchIncidents(filters?: IncidentFilters): Promise { + const params = new URLSearchParams() + + if (filters) { + if (filters.status) { + const statuses = Array.isArray(filters.status) ? filters.status : [filters.status] + statuses.forEach(s => params.append('status', s)) + } + if (filters.severity) { + const severities = Array.isArray(filters.severity) ? filters.severity : [filters.severity] + severities.forEach(s => params.append('severity', s)) + } + if (filters.category) { + const categories = Array.isArray(filters.category) ? filters.category : [filters.category] + categories.forEach(c => params.append('category', c)) + } + if (filters.assignedTo) params.set('assignedTo', filters.assignedTo) + if (filters.overdue !== undefined) params.set('overdue', String(filters.overdue)) + if (filters.search) params.set('search', filters.search) + if (filters.dateFrom) params.set('dateFrom', filters.dateFrom) + if (filters.dateTo) params.set('dateTo', filters.dateTo) + } + + const queryString = params.toString() + const url = `${INCIDENTS_API_BASE}/api/v1/incidents${queryString ? `?${queryString}` : ''}` + + return fetchWithTimeout(url) +} + +/** + * Einzelnen Vorfall per ID abrufen + */ +export async function fetchIncident(id: string): Promise { + return fetchWithTimeout(`${INCIDENTS_API_BASE}/api/v1/incidents/${id}`) +} + +/** + * Neuen Vorfall erstellen + */ +export async function createIncident(request: IncidentCreateRequest): Promise { + return fetchWithTimeout(`${INCIDENTS_API_BASE}/api/v1/incidents`, { + method: 'POST', + body: JSON.stringify(request) + }) +} + +/** + * Vorfall aktualisieren + */ +export async function updateIncident(id: string, update: IncidentUpdateRequest): Promise { + return fetchWithTimeout(`${INCIDENTS_API_BASE}/api/v1/incidents/${id}`, { + method: 'PUT', + body: JSON.stringify(update) + }) +} + +/** + * Vorfall loeschen (Soft Delete) + */ +export async function deleteIncident(id: string): Promise { + await fetchWithTimeout(`${INCIDENTS_API_BASE}/api/v1/incidents/${id}`, { + method: 'DELETE' + }) +} + +// ============================================================================= +// RISK ASSESSMENT +// ============================================================================= + +/** + * Risikobewertung fuer einen Vorfall durchfuehren (Art. 33 DSGVO) + */ +export async function submitRiskAssessment( + incidentId: string, + assessment: RiskAssessmentRequest +): Promise { + return fetchWithTimeout( + `${INCIDENTS_API_BASE}/api/v1/incidents/${incidentId}/risk-assessment`, + { + method: 'POST', + body: JSON.stringify(assessment) + } + ) +} + +// ============================================================================= +// AUTHORITY NOTIFICATION (Art. 33 DSGVO) +// ============================================================================= + +/** + * Meldeformular fuer die Aufsichtsbehoerde generieren + */ +export async function generateAuthorityForm(incidentId: string): Promise { + const response = await fetch( + `${INCIDENTS_API_BASE}/api/v1/incidents/${incidentId}/authority-form/pdf`, + { + headers: getAuthHeaders() + } + ) + + if (!response.ok) { + throw new Error(`PDF-Generierung fehlgeschlagen: ${response.statusText}`) + } + + return response.blob() +} + +/** + * Meldung an die Aufsichtsbehoerde einreichen (Art. 33 DSGVO) + */ +export async function submitAuthorityNotification( + incidentId: string, + data: Partial +): Promise { + return fetchWithTimeout( + `${INCIDENTS_API_BASE}/api/v1/incidents/${incidentId}/authority-notification`, + { + method: 'POST', + body: JSON.stringify(data) + } + ) +} + +// ============================================================================= +// DATA SUBJECT NOTIFICATION (Art. 34 DSGVO) +// ============================================================================= + +/** + * Betroffene Personen benachrichtigen (Art. 34 DSGVO) + */ +export async function sendDataSubjectNotification( + incidentId: string, + data: Partial +): Promise { + return fetchWithTimeout( + `${INCIDENTS_API_BASE}/api/v1/incidents/${incidentId}/data-subject-notification`, + { + method: 'POST', + body: JSON.stringify(data) + } + ) +} + +// ============================================================================= +// MEASURES (Massnahmen) +// ============================================================================= + +/** + * Massnahme hinzufuegen (Sofort-, Korrektur- oder Praeventionsmassnahme) + */ +export async function addMeasure( + incidentId: string, + measure: Omit +): Promise { + return fetchWithTimeout( + `${INCIDENTS_API_BASE}/api/v1/incidents/${incidentId}/measures`, + { + method: 'POST', + body: JSON.stringify(measure) + } + ) +} + +/** + * Massnahme aktualisieren + */ +export async function updateMeasure( + measureId: string, + update: Partial +): Promise { + return fetchWithTimeout( + `${INCIDENTS_API_BASE}/api/v1/measures/${measureId}`, + { + method: 'PUT', + body: JSON.stringify(update) + } + ) +} + +/** + * Massnahme als abgeschlossen markieren + */ +export async function completeMeasure(measureId: string): Promise { + return fetchWithTimeout( + `${INCIDENTS_API_BASE}/api/v1/measures/${measureId}/complete`, + { + method: 'POST' + } + ) +} + +// ============================================================================= +// TIMELINE +// ============================================================================= + +/** + * Zeitleisteneintrag hinzufuegen + */ +export async function addTimelineEntry( + incidentId: string, + entry: Omit +): Promise { + return fetchWithTimeout( + `${INCIDENTS_API_BASE}/api/v1/incidents/${incidentId}/timeline`, + { + method: 'POST', + body: JSON.stringify(entry) + } + ) +} + +// ============================================================================= +// CLOSE INCIDENT +// ============================================================================= + +/** + * Vorfall abschliessen mit Lessons Learned + */ +export async function closeIncident( + incidentId: string, + lessonsLearned: string +): Promise { + return fetchWithTimeout( + `${INCIDENTS_API_BASE}/api/v1/incidents/${incidentId}/close`, + { + method: 'POST', + body: JSON.stringify({ lessonsLearned }) + } + ) +} + +// ============================================================================= +// STATISTICS +// ============================================================================= + +/** + * Vorfall-Statistiken abrufen + */ +export async function fetchIncidentStatistics(): Promise { + return fetchWithTimeout( + `${INCIDENTS_API_BASE}/api/v1/incidents/statistics` + ) +} + +// ============================================================================= +// SDK PROXY FUNCTION (mit Fallback auf Mock-Daten) +// ============================================================================= + +/** + * Fetch Incident-Liste via SDK-Proxy mit Fallback auf Mock-Daten + */ +export async function fetchSDKIncidentList(): Promise<{ incidents: Incident[]; statistics: IncidentStatistics }> { + try { + const res = await fetch('/api/sdk/v1/incidents', { + headers: getAuthHeaders() + }) + if (!res.ok) { + throw new Error(`HTTP ${res.status}`) + } + const data = await res.json() + const incidents: Incident[] = data.incidents || [] + + const statistics = computeStatistics(incidents) + return { incidents, statistics } + } catch (error) { + console.warn('SDK-Backend nicht erreichbar, verwende Mock-Daten:', error) + // Import mock data lazily to keep this file lean + const { createMockIncidents, createMockStatistics } = await import('./api-mock') + const incidents = createMockIncidents() + const statistics = createMockStatistics() + return { incidents, statistics } + } +} + +/** + * Statistiken lokal aus Incident-Liste berechnen + */ +function computeStatistics(incidents: Incident[]): IncidentStatistics { + const countBy = (items: { [key: string]: unknown }[], field: string): Record => { + const result: Record = {} + items.forEach(item => { + const key = String(item[field]) + result[key] = (result[key] || 0) + 1 + }) + return result as Record + } + + const statusCounts = countBy(incidents as unknown as { [key: string]: unknown }[], 'status') + const severityCounts = countBy(incidents as unknown as { [key: string]: unknown }[], 'severity') + const categoryCounts = countBy(incidents as unknown as { [key: string]: unknown }[], 'category') + + const openIncidents = incidents.filter(i => i.status !== 'closed').length + const notificationsPending = incidents.filter(i => + i.authorityNotification !== null && + i.authorityNotification.status === 'pending' && + i.status !== 'closed' + ).length + + let totalResponseHours = 0 + let respondedCount = 0 + incidents.forEach(i => { + if (i.riskAssessment && i.riskAssessment.assessedAt) { + const detected = new Date(i.detectedAt).getTime() + const assessed = new Date(i.riskAssessment.assessedAt).getTime() + totalResponseHours += (assessed - detected) / (1000 * 60 * 60) + respondedCount++ + } + }) + + return { + totalIncidents: incidents.length, + openIncidents, + notificationsPending, + averageResponseTimeHours: respondedCount > 0 ? Math.round(totalResponseHours / respondedCount * 10) / 10 : 0, + bySeverity: { + low: severityCounts['low'] || 0, + medium: severityCounts['medium'] || 0, + high: severityCounts['high'] || 0, + critical: severityCounts['critical'] || 0 + }, + byCategory: { + data_breach: categoryCounts['data_breach'] || 0, + unauthorized_access: categoryCounts['unauthorized_access'] || 0, + data_loss: categoryCounts['data_loss'] || 0, + system_compromise: categoryCounts['system_compromise'] || 0, + phishing: categoryCounts['phishing'] || 0, + ransomware: categoryCounts['ransomware'] || 0, + insider_threat: categoryCounts['insider_threat'] || 0, + physical_breach: categoryCounts['physical_breach'] || 0, + other: categoryCounts['other'] || 0 + }, + byStatus: { + detected: statusCounts['detected'] || 0, + assessment: statusCounts['assessment'] || 0, + containment: statusCounts['containment'] || 0, + notification_required: statusCounts['notification_required'] || 0, + notification_sent: statusCounts['notification_sent'] || 0, + remediation: statusCounts['remediation'] || 0, + closed: statusCounts['closed'] || 0 + } + } +} diff --git a/admin-compliance/lib/sdk/incidents/api-mock.ts b/admin-compliance/lib/sdk/incidents/api-mock.ts new file mode 100644 index 0000000..686433a --- /dev/null +++ b/admin-compliance/lib/sdk/incidents/api-mock.ts @@ -0,0 +1,392 @@ +/** + * Incident Mock Data (Demo-Daten fuer Entwicklung und Tests) + */ + +import { + Incident, + IncidentStatistics, +} from './types' + +/** + * Erstellt Demo-Vorfaelle fuer die Entwicklung + */ +export function createMockIncidents(): Incident[] { + const now = new Date() + + return [ + // 1. Gerade erkannt - noch nicht bewertet (detected/new) + { + id: 'inc-001', + referenceNumber: 'INC-2026-000001', + title: 'Unbefugter Zugriff auf Schuelerdatenbank', + description: 'Ein ehemaliger Mitarbeiter hat sich mit noch aktiven Zugangsdaten in die Schuelerdatenbank eingeloggt. Der Zugriff wurde durch die Log-Analyse entdeckt.', + category: 'unauthorized_access', + severity: 'high', + status: 'detected', + detectedAt: new Date(now.getTime() - 3 * 60 * 60 * 1000).toISOString(), + detectedBy: 'Log-Analyse (automatisiert)', + affectedSystems: ['Schuelerdatenbank', 'Schulverwaltungssystem'], + affectedDataCategories: ['Personenbezogene Daten', 'Daten von Kindern', 'Gesundheitsdaten'], + estimatedAffectedPersons: 800, + riskAssessment: null, + authorityNotification: null, + dataSubjectNotification: null, + measures: [], + timeline: [ + { + id: 'tl-001', + incidentId: 'inc-001', + timestamp: new Date(now.getTime() - 3 * 60 * 60 * 1000).toISOString(), + action: 'Vorfall erkannt', + description: 'Automatische Log-Analyse meldet verdaechtigen Login eines deaktivierten Kontos', + performedBy: 'SIEM-System' + } + ], + assignedTo: undefined + }, + + // 2. In Bewertung (assessment) - Risikobewertung laeuft + { + id: 'inc-002', + referenceNumber: 'INC-2026-000002', + title: 'E-Mail mit Kundendaten an falschen Empfaenger', + description: 'Ein Mitarbeiter hat eine Excel-Datei mit Kundendaten (Name, Adresse, Vertragsnummer) an einen falschen E-Mail-Empfaenger gesendet. Der Empfaenger wurde kontaktiert und hat die Loeschung bestaetigt.', + category: 'data_breach', + severity: 'medium', + status: 'assessment', + detectedAt: new Date(now.getTime() - 18 * 60 * 60 * 1000).toISOString(), + detectedBy: 'Vertriebsabteilung', + affectedSystems: ['E-Mail-System (Exchange)'], + affectedDataCategories: ['Personenbezogene Daten', 'Kundendaten'], + estimatedAffectedPersons: 150, + riskAssessment: { + id: 'ra-002', + assessedBy: 'DSB Mueller', + assessedAt: new Date(now.getTime() - 12 * 60 * 60 * 1000).toISOString(), + likelihoodScore: 3, + impactScore: 2, + overallRisk: 'medium', + notificationRequired: false, + reasoning: 'Empfaenger hat Loeschung bestaetigt. Datenkategorie: allgemeine Kontaktdaten und Vertragsnummern. Geringes Risiko fuer betroffene Personen.' + }, + authorityNotification: { + id: 'an-002', + authority: 'LfD Niedersachsen', + deadline72h: new Date(new Date(now.getTime() - 18 * 60 * 60 * 1000).getTime() + 72 * 60 * 60 * 1000).toISOString(), + status: 'pending', + formData: {} + }, + dataSubjectNotification: null, + measures: [ + { + id: 'meas-001', + incidentId: 'inc-002', + title: 'Empfaenger kontaktiert', + description: 'Falscher Empfaenger kontaktiert mit Bitte um Loeschung', + type: 'immediate', + status: 'completed', + responsible: 'Vertriebsleitung', + dueDate: new Date(now.getTime() - 16 * 60 * 60 * 1000).toISOString(), + completedAt: new Date(now.getTime() - 15 * 60 * 60 * 1000).toISOString() + } + ], + timeline: [ + { + id: 'tl-002', + incidentId: 'inc-002', + timestamp: new Date(now.getTime() - 18 * 60 * 60 * 1000).toISOString(), + action: 'Vorfall gemeldet', + description: 'Mitarbeiter meldet versehentlichen E-Mail-Versand', + performedBy: 'M. Schmidt (Vertrieb)' + }, + { + id: 'tl-003', + incidentId: 'inc-002', + timestamp: new Date(now.getTime() - 15 * 60 * 60 * 1000).toISOString(), + action: 'Sofortmassnahme', + description: 'Empfaenger kontaktiert und Loeschung bestaetigt', + performedBy: 'Vertriebsleitung' + }, + { + id: 'tl-004', + incidentId: 'inc-002', + timestamp: new Date(now.getTime() - 12 * 60 * 60 * 1000).toISOString(), + action: 'Risikobewertung', + description: 'Bewertung durchgefuehrt - mittleres Risiko, keine Meldepflicht', + performedBy: 'DSB Mueller' + } + ], + assignedTo: 'DSB Mueller' + }, + + // 3. Gemeldet (notification_sent) - Ransomware-Angriff + { + id: 'inc-003', + referenceNumber: 'INC-2026-000003', + title: 'Ransomware-Angriff auf Dateiserver', + description: 'Am Montagmorgen wurde ein Ransomware-Angriff auf den zentralen Dateiserver erkannt. Mehrere verschluesselte Dateien wurden identifiziert. Der Angriffsvektor war eine Phishing-E-Mail an einen Mitarbeiter.', + category: 'ransomware', + severity: 'critical', + status: 'notification_sent', + detectedAt: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(), + detectedBy: 'IT-Sicherheitsteam', + affectedSystems: ['Dateiserver (FS-01)', 'E-Mail-System', 'Backup-Server'], + affectedDataCategories: ['Personenbezogene Daten', 'Beschaeftigtendaten', 'Kundendaten', 'Finanzdaten'], + estimatedAffectedPersons: 2500, + riskAssessment: { + id: 'ra-003', + assessedBy: 'DSB Mueller', + assessedAt: new Date(now.getTime() - 4.5 * 24 * 60 * 60 * 1000).toISOString(), + likelihoodScore: 5, + impactScore: 5, + overallRisk: 'critical', + notificationRequired: true, + reasoning: 'Hohes Risiko fuer Rechte und Freiheiten der betroffenen Personen durch potentiellen Zugriff auf personenbezogene Daten und Finanzdaten. Verschluesselung betrifft Verfuegbarkeit, Exfiltration nicht auszuschliessen.' + }, + authorityNotification: { + id: 'an-003', + authority: 'LfD Niedersachsen', + deadline72h: new Date(new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).getTime() + 72 * 60 * 60 * 1000).toISOString(), + submittedAt: new Date(now.getTime() - 4 * 24 * 60 * 60 * 1000).toISOString(), + status: 'submitted', + formData: { + referenceNumber: 'LfD-NI-2026-04821', + incidentType: 'Ransomware', + affectedPersons: 2500 + }, + pdfUrl: '/api/sdk/v1/incidents/inc-003/authority-form.pdf' + }, + dataSubjectNotification: { + id: 'dsn-003', + notificationRequired: true, + templateText: 'Sehr geehrte Damen und Herren, wir informieren Sie ueber einen Sicherheitsvorfall, bei dem moeglicherweise Ihre personenbezogenen Daten betroffen sind...', + sentAt: new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000).toISOString(), + recipientCount: 2500, + method: 'email' + }, + measures: [ + { + id: 'meas-002', + incidentId: 'inc-003', + title: 'Netzwerksegmentierung', + description: 'Betroffene Systeme vom Netzwerk isoliert', + type: 'immediate', + status: 'completed', + responsible: 'IT-Sicherheitsteam', + dueDate: new Date(now.getTime() - 4.8 * 24 * 60 * 60 * 1000).toISOString(), + completedAt: new Date(now.getTime() - 4.9 * 24 * 60 * 60 * 1000).toISOString() + }, + { + id: 'meas-003', + incidentId: 'inc-003', + title: 'Passwoerter zuruecksetzen', + description: 'Alle Benutzerpasswoerter zurueckgesetzt', + type: 'immediate', + status: 'completed', + responsible: 'IT-Administration', + dueDate: new Date(now.getTime() - 4.5 * 24 * 60 * 60 * 1000).toISOString(), + completedAt: new Date(now.getTime() - 4.5 * 24 * 60 * 60 * 1000).toISOString() + }, + { + id: 'meas-004', + incidentId: 'inc-003', + title: 'E-Mail-Security Gateway implementieren', + description: 'Implementierung eines fortgeschrittenen E-Mail-Sicherheitsgateways mit Sandboxing', + type: 'preventive', + status: 'in_progress', + responsible: 'IT-Sicherheitsteam', + dueDate: new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000).toISOString() + }, + { + id: 'meas-005', + incidentId: 'inc-003', + title: 'Mitarbeiterschulung Phishing', + description: 'Verpflichtende Schulung fuer alle Mitarbeiter zum Thema Phishing-Erkennung', + type: 'preventive', + status: 'planned', + responsible: 'Personalwesen', + dueDate: new Date(now.getTime() + 60 * 24 * 60 * 60 * 1000).toISOString() + } + ], + timeline: [ + { + id: 'tl-005', + incidentId: 'inc-003', + timestamp: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(), + action: 'Vorfall erkannt', + description: 'IT-Sicherheitsteam erkennt ungewoehnliche Verschluesselungsaktivitaet', + performedBy: 'IT-Sicherheitsteam' + }, + { + id: 'tl-006', + incidentId: 'inc-003', + timestamp: new Date(now.getTime() - 4.9 * 24 * 60 * 60 * 1000).toISOString(), + action: 'Eindaemmung gestartet', + description: 'Netzwerksegmentierung und Isolation betroffener Systeme', + performedBy: 'IT-Sicherheitsteam' + }, + { + id: 'tl-007', + incidentId: 'inc-003', + timestamp: new Date(now.getTime() - 4.5 * 24 * 60 * 60 * 1000).toISOString(), + action: 'Risikobewertung abgeschlossen', + description: 'Kritisches Risiko festgestellt - Meldepflicht ausgeloest', + performedBy: 'DSB Mueller' + }, + { + id: 'tl-008', + incidentId: 'inc-003', + timestamp: new Date(now.getTime() - 4 * 24 * 60 * 60 * 1000).toISOString(), + action: 'Behoerdenbenachrichtigung', + description: 'Meldung an LfD Niedersachsen eingereicht', + performedBy: 'DSB Mueller' + }, + { + id: 'tl-009', + incidentId: 'inc-003', + timestamp: new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000).toISOString(), + action: 'Betroffene benachrichtigt', + description: '2.500 betroffene Personen per E-Mail informiert', + performedBy: 'Kommunikationsabteilung' + } + ], + assignedTo: 'DSB Mueller' + }, + + // 4. Abgeschlossener Vorfall (closed) - Phishing + { + id: 'inc-004', + referenceNumber: 'INC-2026-000004', + title: 'Phishing-Angriff auf Personalabteilung', + description: 'Gezielter Phishing-Angriff auf die Personalabteilung. Ein Mitarbeiter hat Zugangsdaten auf einer gefaelschten Login-Seite eingegeben. Das Konto wurde sofort gesperrt. Keine Datenexfiltration festgestellt.', + category: 'phishing', + severity: 'high', + status: 'closed', + detectedAt: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString(), + detectedBy: 'IT-Sicherheitsteam (SIEM-Alert)', + affectedSystems: ['Active Directory', 'HR-Portal'], + affectedDataCategories: ['Beschaeftigtendaten', 'Personenbezogene Daten'], + estimatedAffectedPersons: 0, + riskAssessment: { + id: 'ra-004', + assessedBy: 'DSB Mueller', + assessedAt: new Date(now.getTime() - 29 * 24 * 60 * 60 * 1000).toISOString(), + likelihoodScore: 4, + impactScore: 3, + overallRisk: 'high', + notificationRequired: true, + reasoning: 'Zugangsdaten kompromittiert, potentieller Zugriff auf Personaldaten. Keine Exfiltration festgestellt, dennoch Meldung wegen Kompromittierung der Zugangsdaten.' + }, + authorityNotification: { + id: 'an-004', + authority: 'LfD Niedersachsen', + deadline72h: new Date(new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).getTime() + 72 * 60 * 60 * 1000).toISOString(), + submittedAt: new Date(now.getTime() - 29 * 24 * 60 * 60 * 1000).toISOString(), + status: 'acknowledged', + formData: { + referenceNumber: 'LfD-NI-2026-03912', + incidentType: 'Phishing', + affectedPersons: 0 + } + }, + dataSubjectNotification: { + id: 'dsn-004', + notificationRequired: false, + templateText: '', + recipientCount: 0, + method: 'email' + }, + measures: [ + { + id: 'meas-006', + incidentId: 'inc-004', + title: 'Konto gesperrt', + description: 'Kompromittiertes Benutzerkonto sofort gesperrt', + type: 'immediate', + status: 'completed', + responsible: 'IT-Administration', + dueDate: new Date(now.getTime() - 29.8 * 24 * 60 * 60 * 1000).toISOString(), + completedAt: new Date(now.getTime() - 29.9 * 24 * 60 * 60 * 1000).toISOString() + }, + { + id: 'meas-007', + incidentId: 'inc-004', + title: 'MFA fuer alle Mitarbeiter', + description: 'Einfuehrung von Multi-Faktor-Authentifizierung fuer alle Konten', + type: 'preventive', + status: 'completed', + responsible: 'IT-Sicherheitsteam', + dueDate: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString(), + completedAt: new Date(now.getTime() - 12 * 24 * 60 * 60 * 1000).toISOString() + } + ], + timeline: [ + { + id: 'tl-010', + incidentId: 'inc-004', + timestamp: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString(), + action: 'SIEM-Alert', + description: 'Verdaechtiger Login-Versuch aus unbekannter Region erkannt', + performedBy: 'IT-Sicherheitsteam' + }, + { + id: 'tl-011', + incidentId: 'inc-004', + timestamp: new Date(now.getTime() - 29 * 24 * 60 * 60 * 1000).toISOString(), + action: 'Behoerdenbenachrichtigung', + description: 'Meldung an LfD Niedersachsen', + performedBy: 'DSB Mueller' + }, + { + id: 'tl-012', + incidentId: 'inc-004', + timestamp: new Date(now.getTime() - 15 * 24 * 60 * 60 * 1000).toISOString(), + action: 'Vorfall abgeschlossen', + description: 'Alle Massnahmen umgesetzt, keine Datenexfiltration festgestellt', + performedBy: 'DSB Mueller' + } + ], + assignedTo: 'DSB Mueller', + closedAt: new Date(now.getTime() - 15 * 24 * 60 * 60 * 1000).toISOString(), + lessonsLearned: '1. MFA haette den Zugriff verhindert (jetzt implementiert). 2. E-Mail-Security-Gateway muss verbesserte Phishing-Erkennung erhalten. 3. Regelmaessige Phishing-Simulationen fuer alle Mitarbeiter einfuehren.' + } + ] +} + +/** + * Erstellt Mock-Statistiken fuer die Entwicklung + */ +export function createMockStatistics(): IncidentStatistics { + return { + totalIncidents: 4, + openIncidents: 3, + notificationsPending: 1, + averageResponseTimeHours: 8.5, + bySeverity: { + low: 0, + medium: 1, + high: 2, + critical: 1 + }, + byCategory: { + data_breach: 1, + unauthorized_access: 1, + data_loss: 0, + system_compromise: 0, + phishing: 1, + ransomware: 1, + insider_threat: 0, + physical_breach: 0, + other: 0 + }, + byStatus: { + detected: 1, + assessment: 1, + containment: 0, + notification_required: 0, + notification_sent: 1, + remediation: 0, + closed: 1 + } + } +} diff --git a/admin-compliance/lib/sdk/incidents/api.ts b/admin-compliance/lib/sdk/incidents/api.ts index e089267..17d1cdc 100644 --- a/admin-compliance/lib/sdk/incidents/api.ts +++ b/admin-compliance/lib/sdk/incidents/api.ts @@ -3,843 +3,30 @@ * * API client for DSGVO Art. 33/34 Incident & Data Breach Management * Connects via Next.js proxy to the ai-compliance-sdk backend + * + * Barrel re-export from split modules. */ -import { - Incident, - IncidentListResponse, - IncidentFilters, - IncidentCreateRequest, - IncidentUpdateRequest, - IncidentStatistics, - IncidentMeasure, - TimelineEntry, - RiskAssessmentRequest, - RiskAssessment, - AuthorityNotification, - DataSubjectNotification, - IncidentSeverity, - IncidentStatus, - IncidentCategory, - calculateRiskLevel, - isNotificationRequired, - get72hDeadline -} from './types' +export { + fetchIncidents, + fetchIncident, + createIncident, + updateIncident, + deleteIncident, + submitRiskAssessment, + generateAuthorityForm, + submitAuthorityNotification, + sendDataSubjectNotification, + addMeasure, + updateMeasure, + completeMeasure, + addTimelineEntry, + closeIncident, + fetchIncidentStatistics, + fetchSDKIncidentList, +} from './api-incidents' -// ============================================================================= -// CONFIGURATION -// ============================================================================= - -const INCIDENTS_API_BASE = process.env.NEXT_PUBLIC_SDK_API_URL || 'http://localhost:8093' -const API_TIMEOUT = 30000 // 30 seconds - -// ============================================================================= -// HELPER FUNCTIONS -// ============================================================================= - -function getTenantId(): string { - if (typeof window !== 'undefined') { - return localStorage.getItem('bp_tenant_id') || 'default-tenant' - } - return 'default-tenant' -} - -function getAuthHeaders(): HeadersInit { - const headers: HeadersInit = { - 'Content-Type': 'application/json', - 'X-Tenant-ID': getTenantId() - } - - if (typeof window !== 'undefined') { - const token = localStorage.getItem('authToken') - if (token) { - headers['Authorization'] = `Bearer ${token}` - } - const userId = localStorage.getItem('bp_user_id') - if (userId) { - headers['X-User-ID'] = userId - } - } - - return headers -} - -async function fetchWithTimeout( - url: string, - options: RequestInit = {}, - timeout: number = API_TIMEOUT -): Promise { - const controller = new AbortController() - const timeoutId = setTimeout(() => controller.abort(), timeout) - - try { - const response = await fetch(url, { - ...options, - signal: controller.signal, - headers: { - ...getAuthHeaders(), - ...options.headers - } - }) - - if (!response.ok) { - const errorBody = await response.text() - let errorMessage = `HTTP ${response.status}: ${response.statusText}` - try { - const errorJson = JSON.parse(errorBody) - errorMessage = errorJson.error || errorJson.message || errorMessage - } catch { - // Keep the HTTP status message - } - throw new Error(errorMessage) - } - - // Handle empty responses - const contentType = response.headers.get('content-type') - if (contentType && contentType.includes('application/json')) { - return response.json() - } - - return {} as T - } finally { - clearTimeout(timeoutId) - } -} - -// ============================================================================= -// INCIDENT LIST & CRUD -// ============================================================================= - -/** - * Alle Vorfaelle abrufen mit optionalen Filtern - */ -export async function fetchIncidents(filters?: IncidentFilters): Promise { - const params = new URLSearchParams() - - if (filters) { - if (filters.status) { - const statuses = Array.isArray(filters.status) ? filters.status : [filters.status] - statuses.forEach(s => params.append('status', s)) - } - if (filters.severity) { - const severities = Array.isArray(filters.severity) ? filters.severity : [filters.severity] - severities.forEach(s => params.append('severity', s)) - } - if (filters.category) { - const categories = Array.isArray(filters.category) ? filters.category : [filters.category] - categories.forEach(c => params.append('category', c)) - } - if (filters.assignedTo) params.set('assignedTo', filters.assignedTo) - if (filters.overdue !== undefined) params.set('overdue', String(filters.overdue)) - if (filters.search) params.set('search', filters.search) - if (filters.dateFrom) params.set('dateFrom', filters.dateFrom) - if (filters.dateTo) params.set('dateTo', filters.dateTo) - } - - const queryString = params.toString() - const url = `${INCIDENTS_API_BASE}/api/v1/incidents${queryString ? `?${queryString}` : ''}` - - return fetchWithTimeout(url) -} - -/** - * Einzelnen Vorfall per ID abrufen - */ -export async function fetchIncident(id: string): Promise { - return fetchWithTimeout(`${INCIDENTS_API_BASE}/api/v1/incidents/${id}`) -} - -/** - * Neuen Vorfall erstellen - */ -export async function createIncident(request: IncidentCreateRequest): Promise { - return fetchWithTimeout(`${INCIDENTS_API_BASE}/api/v1/incidents`, { - method: 'POST', - body: JSON.stringify(request) - }) -} - -/** - * Vorfall aktualisieren - */ -export async function updateIncident(id: string, update: IncidentUpdateRequest): Promise { - return fetchWithTimeout(`${INCIDENTS_API_BASE}/api/v1/incidents/${id}`, { - method: 'PUT', - body: JSON.stringify(update) - }) -} - -/** - * Vorfall loeschen (Soft Delete) - */ -export async function deleteIncident(id: string): Promise { - await fetchWithTimeout(`${INCIDENTS_API_BASE}/api/v1/incidents/${id}`, { - method: 'DELETE' - }) -} - -// ============================================================================= -// RISK ASSESSMENT -// ============================================================================= - -/** - * Risikobewertung fuer einen Vorfall durchfuehren (Art. 33 DSGVO) - */ -export async function submitRiskAssessment( - incidentId: string, - assessment: RiskAssessmentRequest -): Promise { - return fetchWithTimeout( - `${INCIDENTS_API_BASE}/api/v1/incidents/${incidentId}/risk-assessment`, - { - method: 'POST', - body: JSON.stringify(assessment) - } - ) -} - -// ============================================================================= -// AUTHORITY NOTIFICATION (Art. 33 DSGVO) -// ============================================================================= - -/** - * Meldeformular fuer die Aufsichtsbehoerde generieren - */ -export async function generateAuthorityForm(incidentId: string): Promise { - const response = await fetch( - `${INCIDENTS_API_BASE}/api/v1/incidents/${incidentId}/authority-form/pdf`, - { - headers: getAuthHeaders() - } - ) - - if (!response.ok) { - throw new Error(`PDF-Generierung fehlgeschlagen: ${response.statusText}`) - } - - return response.blob() -} - -/** - * Meldung an die Aufsichtsbehoerde einreichen (Art. 33 DSGVO) - */ -export async function submitAuthorityNotification( - incidentId: string, - data: Partial -): Promise { - return fetchWithTimeout( - `${INCIDENTS_API_BASE}/api/v1/incidents/${incidentId}/authority-notification`, - { - method: 'POST', - body: JSON.stringify(data) - } - ) -} - -// ============================================================================= -// DATA SUBJECT NOTIFICATION (Art. 34 DSGVO) -// ============================================================================= - -/** - * Betroffene Personen benachrichtigen (Art. 34 DSGVO) - */ -export async function sendDataSubjectNotification( - incidentId: string, - data: Partial -): Promise { - return fetchWithTimeout( - `${INCIDENTS_API_BASE}/api/v1/incidents/${incidentId}/data-subject-notification`, - { - method: 'POST', - body: JSON.stringify(data) - } - ) -} - -// ============================================================================= -// MEASURES (Massnahmen) -// ============================================================================= - -/** - * Massnahme hinzufuegen (Sofort-, Korrektur- oder Praeventionsmassnahme) - */ -export async function addMeasure( - incidentId: string, - measure: Omit -): Promise { - return fetchWithTimeout( - `${INCIDENTS_API_BASE}/api/v1/incidents/${incidentId}/measures`, - { - method: 'POST', - body: JSON.stringify(measure) - } - ) -} - -/** - * Massnahme aktualisieren - */ -export async function updateMeasure( - measureId: string, - update: Partial -): Promise { - return fetchWithTimeout( - `${INCIDENTS_API_BASE}/api/v1/measures/${measureId}`, - { - method: 'PUT', - body: JSON.stringify(update) - } - ) -} - -/** - * Massnahme als abgeschlossen markieren - */ -export async function completeMeasure(measureId: string): Promise { - return fetchWithTimeout( - `${INCIDENTS_API_BASE}/api/v1/measures/${measureId}/complete`, - { - method: 'POST' - } - ) -} - -// ============================================================================= -// TIMELINE -// ============================================================================= - -/** - * Zeitleisteneintrag hinzufuegen - */ -export async function addTimelineEntry( - incidentId: string, - entry: Omit -): Promise { - return fetchWithTimeout( - `${INCIDENTS_API_BASE}/api/v1/incidents/${incidentId}/timeline`, - { - method: 'POST', - body: JSON.stringify(entry) - } - ) -} - -// ============================================================================= -// CLOSE INCIDENT -// ============================================================================= - -/** - * Vorfall abschliessen mit Lessons Learned - */ -export async function closeIncident( - incidentId: string, - lessonsLearned: string -): Promise { - return fetchWithTimeout( - `${INCIDENTS_API_BASE}/api/v1/incidents/${incidentId}/close`, - { - method: 'POST', - body: JSON.stringify({ lessonsLearned }) - } - ) -} - -// ============================================================================= -// STATISTICS -// ============================================================================= - -/** - * Vorfall-Statistiken abrufen - */ -export async function fetchIncidentStatistics(): Promise { - return fetchWithTimeout( - `${INCIDENTS_API_BASE}/api/v1/incidents/statistics` - ) -} - -// ============================================================================= -// SDK PROXY FUNCTION (mit Fallback auf Mock-Daten) -// ============================================================================= - -/** - * Fetch Incident-Liste via SDK-Proxy mit Fallback auf Mock-Daten - */ -export async function fetchSDKIncidentList(): Promise<{ incidents: Incident[]; statistics: IncidentStatistics }> { - try { - const res = await fetch('/api/sdk/v1/incidents', { - headers: getAuthHeaders() - }) - if (!res.ok) { - throw new Error(`HTTP ${res.status}`) - } - const data = await res.json() - const incidents: Incident[] = data.incidents || [] - - // Statistiken lokal berechnen - const statistics = computeStatistics(incidents) - return { incidents, statistics } - } catch (error) { - console.warn('SDK-Backend nicht erreichbar, verwende Mock-Daten:', error) - const incidents = createMockIncidents() - const statistics = createMockStatistics() - return { incidents, statistics } - } -} - -/** - * Statistiken lokal aus Incident-Liste berechnen - */ -function computeStatistics(incidents: Incident[]): IncidentStatistics { - const countBy = (items: { [key: string]: unknown }[], field: string): Record => { - const result: Record = {} - items.forEach(item => { - const key = String(item[field]) - result[key] = (result[key] || 0) + 1 - }) - return result as Record - } - - const statusCounts = countBy(incidents as unknown as { [key: string]: unknown }[], 'status') - const severityCounts = countBy(incidents as unknown as { [key: string]: unknown }[], 'severity') - const categoryCounts = countBy(incidents as unknown as { [key: string]: unknown }[], 'category') - - const openIncidents = incidents.filter(i => i.status !== 'closed').length - const notificationsPending = incidents.filter(i => - i.authorityNotification !== null && - i.authorityNotification.status === 'pending' && - i.status !== 'closed' - ).length - - // Durchschnittliche Reaktionszeit berechnen - let totalResponseHours = 0 - let respondedCount = 0 - incidents.forEach(i => { - if (i.riskAssessment && i.riskAssessment.assessedAt) { - const detected = new Date(i.detectedAt).getTime() - const assessed = new Date(i.riskAssessment.assessedAt).getTime() - totalResponseHours += (assessed - detected) / (1000 * 60 * 60) - respondedCount++ - } - }) - - return { - totalIncidents: incidents.length, - openIncidents, - notificationsPending, - averageResponseTimeHours: respondedCount > 0 ? Math.round(totalResponseHours / respondedCount * 10) / 10 : 0, - bySeverity: { - low: severityCounts['low'] || 0, - medium: severityCounts['medium'] || 0, - high: severityCounts['high'] || 0, - critical: severityCounts['critical'] || 0 - }, - byCategory: { - data_breach: categoryCounts['data_breach'] || 0, - unauthorized_access: categoryCounts['unauthorized_access'] || 0, - data_loss: categoryCounts['data_loss'] || 0, - system_compromise: categoryCounts['system_compromise'] || 0, - phishing: categoryCounts['phishing'] || 0, - ransomware: categoryCounts['ransomware'] || 0, - insider_threat: categoryCounts['insider_threat'] || 0, - physical_breach: categoryCounts['physical_breach'] || 0, - other: categoryCounts['other'] || 0 - }, - byStatus: { - detected: statusCounts['detected'] || 0, - assessment: statusCounts['assessment'] || 0, - containment: statusCounts['containment'] || 0, - notification_required: statusCounts['notification_required'] || 0, - notification_sent: statusCounts['notification_sent'] || 0, - remediation: statusCounts['remediation'] || 0, - closed: statusCounts['closed'] || 0 - } - } -} - -// ============================================================================= -// MOCK DATA (Demo-Daten fuer Entwicklung und Tests) -// ============================================================================= - -/** - * Erstellt Demo-Vorfaelle fuer die Entwicklung - */ -export function createMockIncidents(): Incident[] { - const now = new Date() - - return [ - // 1. Gerade erkannt - noch nicht bewertet (detected/new) - { - id: 'inc-001', - referenceNumber: 'INC-2026-000001', - title: 'Unbefugter Zugriff auf Schuelerdatenbank', - description: 'Ein ehemaliger Mitarbeiter hat sich mit noch aktiven Zugangsdaten in die Schuelerdatenbank eingeloggt. Der Zugriff wurde durch die Log-Analyse entdeckt.', - category: 'unauthorized_access', - severity: 'high', - status: 'detected', - detectedAt: new Date(now.getTime() - 3 * 60 * 60 * 1000).toISOString(), // 3 Stunden her - detectedBy: 'Log-Analyse (automatisiert)', - affectedSystems: ['Schuelerdatenbank', 'Schulverwaltungssystem'], - affectedDataCategories: ['Personenbezogene Daten', 'Daten von Kindern', 'Gesundheitsdaten'], - estimatedAffectedPersons: 800, - riskAssessment: null, - authorityNotification: null, - dataSubjectNotification: null, - measures: [], - timeline: [ - { - id: 'tl-001', - incidentId: 'inc-001', - timestamp: new Date(now.getTime() - 3 * 60 * 60 * 1000).toISOString(), - action: 'Vorfall erkannt', - description: 'Automatische Log-Analyse meldet verdaechtigen Login eines deaktivierten Kontos', - performedBy: 'SIEM-System' - } - ], - assignedTo: undefined - }, - - // 2. In Bewertung (assessment) - Risikobewertung laeuft - { - id: 'inc-002', - referenceNumber: 'INC-2026-000002', - title: 'E-Mail mit Kundendaten an falschen Empfaenger', - description: 'Ein Mitarbeiter hat eine Excel-Datei mit Kundendaten (Name, Adresse, Vertragsnummer) an einen falschen E-Mail-Empfaenger gesendet. Der Empfaenger wurde kontaktiert und hat die Loeschung bestaetigt.', - category: 'data_breach', - severity: 'medium', - status: 'assessment', - detectedAt: new Date(now.getTime() - 18 * 60 * 60 * 1000).toISOString(), // 18 Stunden her - detectedBy: 'Vertriebsabteilung', - affectedSystems: ['E-Mail-System (Exchange)'], - affectedDataCategories: ['Personenbezogene Daten', 'Kundendaten'], - estimatedAffectedPersons: 150, - riskAssessment: { - id: 'ra-002', - assessedBy: 'DSB Mueller', - assessedAt: new Date(now.getTime() - 12 * 60 * 60 * 1000).toISOString(), - likelihoodScore: 3, - impactScore: 2, - overallRisk: 'medium', - notificationRequired: false, - reasoning: 'Empfaenger hat Loeschung bestaetigt. Datenkategorie: allgemeine Kontaktdaten und Vertragsnummern. Geringes Risiko fuer betroffene Personen.' - }, - authorityNotification: { - id: 'an-002', - authority: 'LfD Niedersachsen', - deadline72h: new Date(new Date(now.getTime() - 18 * 60 * 60 * 1000).getTime() + 72 * 60 * 60 * 1000).toISOString(), - status: 'pending', - formData: {} - }, - dataSubjectNotification: null, - measures: [ - { - id: 'meas-001', - incidentId: 'inc-002', - title: 'Empfaenger kontaktiert', - description: 'Falscher Empfaenger kontaktiert mit Bitte um Loeschung', - type: 'immediate', - status: 'completed', - responsible: 'Vertriebsleitung', - dueDate: new Date(now.getTime() - 16 * 60 * 60 * 1000).toISOString(), - completedAt: new Date(now.getTime() - 15 * 60 * 60 * 1000).toISOString() - } - ], - timeline: [ - { - id: 'tl-002', - incidentId: 'inc-002', - timestamp: new Date(now.getTime() - 18 * 60 * 60 * 1000).toISOString(), - action: 'Vorfall gemeldet', - description: 'Mitarbeiter meldet versehentlichen E-Mail-Versand', - performedBy: 'M. Schmidt (Vertrieb)' - }, - { - id: 'tl-003', - incidentId: 'inc-002', - timestamp: new Date(now.getTime() - 15 * 60 * 60 * 1000).toISOString(), - action: 'Sofortmassnahme', - description: 'Empfaenger kontaktiert und Loeschung bestaetigt', - performedBy: 'Vertriebsleitung' - }, - { - id: 'tl-004', - incidentId: 'inc-002', - timestamp: new Date(now.getTime() - 12 * 60 * 60 * 1000).toISOString(), - action: 'Risikobewertung', - description: 'Bewertung durchgefuehrt - mittleres Risiko, keine Meldepflicht', - performedBy: 'DSB Mueller' - } - ], - assignedTo: 'DSB Mueller' - }, - - // 3. Gemeldet (notification_sent) - Ransomware-Angriff - { - id: 'inc-003', - referenceNumber: 'INC-2026-000003', - title: 'Ransomware-Angriff auf Dateiserver', - description: 'Am Montagmorgen wurde ein Ransomware-Angriff auf den zentralen Dateiserver erkannt. Mehrere verschluesselte Dateien wurden identifiziert. Der Angriffsvektor war eine Phishing-E-Mail an einen Mitarbeiter.', - category: 'ransomware', - severity: 'critical', - status: 'notification_sent', - detectedAt: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(), - detectedBy: 'IT-Sicherheitsteam', - affectedSystems: ['Dateiserver (FS-01)', 'E-Mail-System', 'Backup-Server'], - affectedDataCategories: ['Personenbezogene Daten', 'Beschaeftigtendaten', 'Kundendaten', 'Finanzdaten'], - estimatedAffectedPersons: 2500, - riskAssessment: { - id: 'ra-003', - assessedBy: 'DSB Mueller', - assessedAt: new Date(now.getTime() - 4.5 * 24 * 60 * 60 * 1000).toISOString(), - likelihoodScore: 5, - impactScore: 5, - overallRisk: 'critical', - notificationRequired: true, - reasoning: 'Hohes Risiko fuer Rechte und Freiheiten der betroffenen Personen durch potentiellen Zugriff auf personenbezogene Daten und Finanzdaten. Verschluesselung betrifft Verfuegbarkeit, Exfiltration nicht auszuschliessen.' - }, - authorityNotification: { - id: 'an-003', - authority: 'LfD Niedersachsen', - deadline72h: new Date(new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).getTime() + 72 * 60 * 60 * 1000).toISOString(), - submittedAt: new Date(now.getTime() - 4 * 24 * 60 * 60 * 1000).toISOString(), - status: 'submitted', - formData: { - referenceNumber: 'LfD-NI-2026-04821', - incidentType: 'Ransomware', - affectedPersons: 2500 - }, - pdfUrl: '/api/sdk/v1/incidents/inc-003/authority-form.pdf' - }, - dataSubjectNotification: { - id: 'dsn-003', - notificationRequired: true, - templateText: 'Sehr geehrte Damen und Herren, wir informieren Sie ueber einen Sicherheitsvorfall, bei dem moeglicherweise Ihre personenbezogenen Daten betroffen sind...', - sentAt: new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000).toISOString(), - recipientCount: 2500, - method: 'email' - }, - measures: [ - { - id: 'meas-002', - incidentId: 'inc-003', - title: 'Netzwerksegmentierung', - description: 'Betroffene Systeme vom Netzwerk isoliert', - type: 'immediate', - status: 'completed', - responsible: 'IT-Sicherheitsteam', - dueDate: new Date(now.getTime() - 4.8 * 24 * 60 * 60 * 1000).toISOString(), - completedAt: new Date(now.getTime() - 4.9 * 24 * 60 * 60 * 1000).toISOString() - }, - { - id: 'meas-003', - incidentId: 'inc-003', - title: 'Passwoerter zuruecksetzen', - description: 'Alle Benutzerpasswoerter zurueckgesetzt', - type: 'immediate', - status: 'completed', - responsible: 'IT-Administration', - dueDate: new Date(now.getTime() - 4.5 * 24 * 60 * 60 * 1000).toISOString(), - completedAt: new Date(now.getTime() - 4.5 * 24 * 60 * 60 * 1000).toISOString() - }, - { - id: 'meas-004', - incidentId: 'inc-003', - title: 'E-Mail-Security Gateway implementieren', - description: 'Implementierung eines fortgeschrittenen E-Mail-Sicherheitsgateways mit Sandboxing', - type: 'preventive', - status: 'in_progress', - responsible: 'IT-Sicherheitsteam', - dueDate: new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000).toISOString() - }, - { - id: 'meas-005', - incidentId: 'inc-003', - title: 'Mitarbeiterschulung Phishing', - description: 'Verpflichtende Schulung fuer alle Mitarbeiter zum Thema Phishing-Erkennung', - type: 'preventive', - status: 'planned', - responsible: 'Personalwesen', - dueDate: new Date(now.getTime() + 60 * 24 * 60 * 60 * 1000).toISOString() - } - ], - timeline: [ - { - id: 'tl-005', - incidentId: 'inc-003', - timestamp: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(), - action: 'Vorfall erkannt', - description: 'IT-Sicherheitsteam erkennt ungewoehnliche Verschluesselungsaktivitaet', - performedBy: 'IT-Sicherheitsteam' - }, - { - id: 'tl-006', - incidentId: 'inc-003', - timestamp: new Date(now.getTime() - 4.9 * 24 * 60 * 60 * 1000).toISOString(), - action: 'Eindaemmung gestartet', - description: 'Netzwerksegmentierung und Isolation betroffener Systeme', - performedBy: 'IT-Sicherheitsteam' - }, - { - id: 'tl-007', - incidentId: 'inc-003', - timestamp: new Date(now.getTime() - 4.5 * 24 * 60 * 60 * 1000).toISOString(), - action: 'Risikobewertung abgeschlossen', - description: 'Kritisches Risiko festgestellt - Meldepflicht ausgeloest', - performedBy: 'DSB Mueller' - }, - { - id: 'tl-008', - incidentId: 'inc-003', - timestamp: new Date(now.getTime() - 4 * 24 * 60 * 60 * 1000).toISOString(), - action: 'Behoerdenbenachrichtigung', - description: 'Meldung an LfD Niedersachsen eingereicht', - performedBy: 'DSB Mueller' - }, - { - id: 'tl-009', - incidentId: 'inc-003', - timestamp: new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000).toISOString(), - action: 'Betroffene benachrichtigt', - description: '2.500 betroffene Personen per E-Mail informiert', - performedBy: 'Kommunikationsabteilung' - } - ], - assignedTo: 'DSB Mueller' - }, - - // 4. Abgeschlossener Vorfall (closed) - Phishing - { - id: 'inc-004', - referenceNumber: 'INC-2026-000004', - title: 'Phishing-Angriff auf Personalabteilung', - description: 'Gezielter Phishing-Angriff auf die Personalabteilung. Ein Mitarbeiter hat Zugangsdaten auf einer gefaelschten Login-Seite eingegeben. Das Konto wurde sofort gesperrt. Keine Datenexfiltration festgestellt.', - category: 'phishing', - severity: 'high', - status: 'closed', - detectedAt: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString(), - detectedBy: 'IT-Sicherheitsteam (SIEM-Alert)', - affectedSystems: ['Active Directory', 'HR-Portal'], - affectedDataCategories: ['Beschaeftigtendaten', 'Personenbezogene Daten'], - estimatedAffectedPersons: 0, - riskAssessment: { - id: 'ra-004', - assessedBy: 'DSB Mueller', - assessedAt: new Date(now.getTime() - 29 * 24 * 60 * 60 * 1000).toISOString(), - likelihoodScore: 4, - impactScore: 3, - overallRisk: 'high', - notificationRequired: true, - reasoning: 'Zugangsdaten kompromittiert, potentieller Zugriff auf Personaldaten. Keine Exfiltration festgestellt, dennoch Meldung wegen Kompromittierung der Zugangsdaten.' - }, - authorityNotification: { - id: 'an-004', - authority: 'LfD Niedersachsen', - deadline72h: new Date(new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).getTime() + 72 * 60 * 60 * 1000).toISOString(), - submittedAt: new Date(now.getTime() - 29 * 24 * 60 * 60 * 1000).toISOString(), - status: 'acknowledged', - formData: { - referenceNumber: 'LfD-NI-2026-03912', - incidentType: 'Phishing', - affectedPersons: 0 - } - }, - dataSubjectNotification: { - id: 'dsn-004', - notificationRequired: false, - templateText: '', - recipientCount: 0, - method: 'email' - }, - measures: [ - { - id: 'meas-006', - incidentId: 'inc-004', - title: 'Konto gesperrt', - description: 'Kompromittiertes Benutzerkonto sofort gesperrt', - type: 'immediate', - status: 'completed', - responsible: 'IT-Administration', - dueDate: new Date(now.getTime() - 29.8 * 24 * 60 * 60 * 1000).toISOString(), - completedAt: new Date(now.getTime() - 29.9 * 24 * 60 * 60 * 1000).toISOString() - }, - { - id: 'meas-007', - incidentId: 'inc-004', - title: 'MFA fuer alle Mitarbeiter', - description: 'Einfuehrung von Multi-Faktor-Authentifizierung fuer alle Konten', - type: 'preventive', - status: 'completed', - responsible: 'IT-Sicherheitsteam', - dueDate: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString(), - completedAt: new Date(now.getTime() - 12 * 24 * 60 * 60 * 1000).toISOString() - } - ], - timeline: [ - { - id: 'tl-010', - incidentId: 'inc-004', - timestamp: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString(), - action: 'SIEM-Alert', - description: 'Verdaechtiger Login-Versuch aus unbekannter Region erkannt', - performedBy: 'IT-Sicherheitsteam' - }, - { - id: 'tl-011', - incidentId: 'inc-004', - timestamp: new Date(now.getTime() - 29 * 24 * 60 * 60 * 1000).toISOString(), - action: 'Behoerdenbenachrichtigung', - description: 'Meldung an LfD Niedersachsen', - performedBy: 'DSB Mueller' - }, - { - id: 'tl-012', - incidentId: 'inc-004', - timestamp: new Date(now.getTime() - 15 * 24 * 60 * 60 * 1000).toISOString(), - action: 'Vorfall abgeschlossen', - description: 'Alle Massnahmen umgesetzt, keine Datenexfiltration festgestellt', - performedBy: 'DSB Mueller' - } - ], - assignedTo: 'DSB Mueller', - closedAt: new Date(now.getTime() - 15 * 24 * 60 * 60 * 1000).toISOString(), - lessonsLearned: '1. MFA haette den Zugriff verhindert (jetzt implementiert). 2. E-Mail-Security-Gateway muss verbesserte Phishing-Erkennung erhalten. 3. Regelmaessige Phishing-Simulationen fuer alle Mitarbeiter einfuehren.' - } - ] -} - -/** - * Erstellt Mock-Statistiken fuer die Entwicklung - */ -export function createMockStatistics(): IncidentStatistics { - return { - totalIncidents: 4, - openIncidents: 3, - notificationsPending: 1, - averageResponseTimeHours: 8.5, - bySeverity: { - low: 0, - medium: 1, - high: 2, - critical: 1 - }, - byCategory: { - data_breach: 1, - unauthorized_access: 1, - data_loss: 0, - system_compromise: 0, - phishing: 1, - ransomware: 1, - insider_threat: 0, - physical_breach: 0, - other: 0 - }, - byStatus: { - detected: 1, - assessment: 1, - containment: 0, - notification_required: 0, - notification_sent: 1, - remediation: 0, - closed: 1 - } - } -} +export { + createMockIncidents, + createMockStatistics, +} from './api-mock' diff --git a/admin-compliance/lib/sdk/tom-generator/context.tsx b/admin-compliance/lib/sdk/tom-generator/context.tsx index e411c39..267e1a9 100644 --- a/admin-compliance/lib/sdk/tom-generator/context.tsx +++ b/admin-compliance/lib/sdk/tom-generator/context.tsx @@ -1,720 +1,13 @@ 'use client' // ============================================================================= -// TOM Generator Context -// State management for the TOM Generator Wizard +// TOM Generator Context — Barrel re-exports +// Preserves the original public API so existing imports work unchanged. // ============================================================================= -import React, { - createContext, - useContext, - useReducer, - useCallback, - useEffect, - useRef, - ReactNode, -} from 'react' -import { - TOMGeneratorState, - TOMGeneratorStepId, - CompanyProfile, - DataProfile, - ArchitectureProfile, - SecurityProfile, - RiskProfile, - EvidenceDocument, - DerivedTOM, - GapAnalysisResult, - ExportRecord, - WizardStep, - createInitialTOMGeneratorState, - TOM_GENERATOR_STEPS, - getStepIndex, - calculateProtectionLevel, - isDSFARequired, - hasSpecialCategories, -} from './types' -import { TOMRulesEngine } from './rules-engine' - -// ============================================================================= -// ACTION TYPES -// ============================================================================= - -type TOMGeneratorAction = - | { type: 'INITIALIZE'; payload: { tenantId: string; state?: TOMGeneratorState } } - | { type: 'RESET'; payload: { tenantId: string } } - | { type: 'SET_CURRENT_STEP'; payload: TOMGeneratorStepId } - | { type: 'SET_COMPANY_PROFILE'; payload: CompanyProfile } - | { type: 'UPDATE_COMPANY_PROFILE'; payload: Partial } - | { type: 'SET_DATA_PROFILE'; payload: DataProfile } - | { type: 'UPDATE_DATA_PROFILE'; payload: Partial } - | { type: 'SET_ARCHITECTURE_PROFILE'; payload: ArchitectureProfile } - | { type: 'UPDATE_ARCHITECTURE_PROFILE'; payload: Partial } - | { type: 'SET_SECURITY_PROFILE'; payload: SecurityProfile } - | { type: 'UPDATE_SECURITY_PROFILE'; payload: Partial } - | { type: 'SET_RISK_PROFILE'; payload: RiskProfile } - | { type: 'UPDATE_RISK_PROFILE'; payload: Partial } - | { type: 'COMPLETE_STEP'; payload: { stepId: TOMGeneratorStepId; data: unknown } } - | { type: 'UNCOMPLETE_STEP'; payload: TOMGeneratorStepId } - | { type: 'ADD_EVIDENCE'; payload: EvidenceDocument } - | { type: 'UPDATE_EVIDENCE'; payload: { id: string; data: Partial } } - | { type: 'DELETE_EVIDENCE'; payload: string } - | { type: 'SET_DERIVED_TOMS'; payload: DerivedTOM[] } - | { type: 'UPDATE_DERIVED_TOM'; payload: { id: string; data: Partial } } - | { type: 'SET_GAP_ANALYSIS'; payload: GapAnalysisResult } - | { type: 'ADD_EXPORT'; payload: ExportRecord } - | { type: 'BULK_UPDATE_TOMS'; payload: { updates: Array<{ id: string; data: Partial }> } } - | { type: 'LOAD_STATE'; payload: TOMGeneratorState } - -// ============================================================================= -// REDUCER -// ============================================================================= - -function tomGeneratorReducer( - state: TOMGeneratorState, - action: TOMGeneratorAction -): TOMGeneratorState { - const updateState = (updates: Partial): TOMGeneratorState => ({ - ...state, - ...updates, - updatedAt: new Date(), - }) - - switch (action.type) { - case 'INITIALIZE': { - if (action.payload.state) { - return action.payload.state - } - return createInitialTOMGeneratorState(action.payload.tenantId) - } - - case 'RESET': { - return createInitialTOMGeneratorState(action.payload.tenantId) - } - - case 'SET_CURRENT_STEP': { - return updateState({ currentStep: action.payload }) - } - - case 'SET_COMPANY_PROFILE': { - return updateState({ companyProfile: action.payload }) - } - - case 'UPDATE_COMPANY_PROFILE': { - if (!state.companyProfile) return state - return updateState({ - companyProfile: { ...state.companyProfile, ...action.payload }, - }) - } - - case 'SET_DATA_PROFILE': { - // Automatically set hasSpecialCategories based on categories - const profile: DataProfile = { - ...action.payload, - hasSpecialCategories: hasSpecialCategories(action.payload.categories), - } - return updateState({ dataProfile: profile }) - } - - case 'UPDATE_DATA_PROFILE': { - if (!state.dataProfile) return state - const updatedProfile = { ...state.dataProfile, ...action.payload } - // Recalculate hasSpecialCategories if categories changed - if (action.payload.categories) { - updatedProfile.hasSpecialCategories = hasSpecialCategories( - action.payload.categories - ) - } - return updateState({ dataProfile: updatedProfile }) - } - - case 'SET_ARCHITECTURE_PROFILE': { - return updateState({ architectureProfile: action.payload }) - } - - case 'UPDATE_ARCHITECTURE_PROFILE': { - if (!state.architectureProfile) return state - return updateState({ - architectureProfile: { ...state.architectureProfile, ...action.payload }, - }) - } - - case 'SET_SECURITY_PROFILE': { - return updateState({ securityProfile: action.payload }) - } - - case 'UPDATE_SECURITY_PROFILE': { - if (!state.securityProfile) return state - return updateState({ - securityProfile: { ...state.securityProfile, ...action.payload }, - }) - } - - case 'SET_RISK_PROFILE': { - // Automatically calculate protection level and DSFA requirement - const profile: RiskProfile = { - ...action.payload, - protectionLevel: calculateProtectionLevel(action.payload.ciaAssessment), - dsfaRequired: isDSFARequired(state.dataProfile, action.payload), - } - return updateState({ riskProfile: profile }) - } - - case 'UPDATE_RISK_PROFILE': { - if (!state.riskProfile) return state - const updatedProfile = { ...state.riskProfile, ...action.payload } - // Recalculate protection level if CIA assessment changed - if (action.payload.ciaAssessment) { - updatedProfile.protectionLevel = calculateProtectionLevel( - action.payload.ciaAssessment - ) - } - // Recalculate DSFA requirement - updatedProfile.dsfaRequired = isDSFARequired(state.dataProfile, updatedProfile) - return updateState({ riskProfile: updatedProfile }) - } - - case 'COMPLETE_STEP': { - const updatedSteps = state.steps.map((step) => - step.id === action.payload.stepId - ? { - ...step, - completed: true, - data: action.payload.data, - validatedAt: new Date(), - } - : step - ) - return updateState({ steps: updatedSteps }) - } - - case 'UNCOMPLETE_STEP': { - const updatedSteps = state.steps.map((step) => - step.id === action.payload - ? { ...step, completed: false, validatedAt: null } - : step - ) - return updateState({ steps: updatedSteps }) - } - - case 'ADD_EVIDENCE': { - return updateState({ - documents: [...state.documents, action.payload], - }) - } - - case 'UPDATE_EVIDENCE': { - const updatedDocuments = state.documents.map((doc) => - doc.id === action.payload.id ? { ...doc, ...action.payload.data } : doc - ) - return updateState({ documents: updatedDocuments }) - } - - case 'DELETE_EVIDENCE': { - return updateState({ - documents: state.documents.filter((doc) => doc.id !== action.payload), - }) - } - - case 'SET_DERIVED_TOMS': { - return updateState({ derivedTOMs: action.payload }) - } - - case 'UPDATE_DERIVED_TOM': { - const updatedTOMs = state.derivedTOMs.map((tom) => - tom.id === action.payload.id ? { ...tom, ...action.payload.data } : tom - ) - return updateState({ derivedTOMs: updatedTOMs }) - } - - case 'SET_GAP_ANALYSIS': { - return updateState({ gapAnalysis: action.payload }) - } - - case 'ADD_EXPORT': { - return updateState({ - exports: [...state.exports, action.payload], - }) - } - - case 'BULK_UPDATE_TOMS': { - let updatedTOMs = [...state.derivedTOMs] - for (const update of action.payload.updates) { - updatedTOMs = updatedTOMs.map((tom) => - tom.id === update.id ? { ...tom, ...update.data } : tom - ) - } - return updateState({ derivedTOMs: updatedTOMs }) - } - - case 'LOAD_STATE': { - return action.payload - } - - default: - return state - } -} - -// ============================================================================= -// CONTEXT VALUE INTERFACE -// ============================================================================= - -interface TOMGeneratorContextValue { - state: TOMGeneratorState - dispatch: React.Dispatch - - // Navigation - currentStepIndex: number - totalSteps: number - canGoNext: boolean - canGoPrevious: boolean - goToStep: (stepId: TOMGeneratorStepId) => void - goToNextStep: () => void - goToPreviousStep: () => void - completeCurrentStep: (data: unknown) => void - - // Profile setters - setCompanyProfile: (profile: CompanyProfile) => void - updateCompanyProfile: (data: Partial) => void - setDataProfile: (profile: DataProfile) => void - updateDataProfile: (data: Partial) => void - setArchitectureProfile: (profile: ArchitectureProfile) => void - updateArchitectureProfile: (data: Partial) => void - setSecurityProfile: (profile: SecurityProfile) => void - updateSecurityProfile: (data: Partial) => void - setRiskProfile: (profile: RiskProfile) => void - updateRiskProfile: (data: Partial) => void - - // Evidence management - addEvidence: (document: EvidenceDocument) => void - updateEvidence: (id: string, data: Partial) => void - deleteEvidence: (id: string) => void - - // TOM derivation - deriveTOMs: () => void - updateDerivedTOM: (id: string, data: Partial) => void - bulkUpdateTOMs: (updates: Array<{ id: string; data: Partial }>) => void - - // Gap analysis - runGapAnalysis: () => void - - // Export - addExport: (record: ExportRecord) => void - - // Persistence - saveState: () => Promise - loadState: () => Promise - resetState: () => void - - // Status - isStepCompleted: (stepId: TOMGeneratorStepId) => boolean - getCompletionPercentage: () => number - isLoading: boolean - error: string | null -} - -// ============================================================================= -// CONTEXT -// ============================================================================= - -const TOMGeneratorContext = createContext(null) - -// ============================================================================= -// STORAGE KEYS -// ============================================================================= - -const STORAGE_KEY_PREFIX = 'tom-generator-state-' - -function getStorageKey(tenantId: string): string { - return `${STORAGE_KEY_PREFIX}${tenantId}` -} - -// ============================================================================= -// PROVIDER COMPONENT -// ============================================================================= - -interface TOMGeneratorProviderProps { - children: ReactNode - tenantId: string - initialState?: TOMGeneratorState - enablePersistence?: boolean -} - -export function TOMGeneratorProvider({ - children, - tenantId, - initialState, - enablePersistence = true, -}: TOMGeneratorProviderProps) { - const [state, dispatch] = useReducer( - tomGeneratorReducer, - initialState ?? createInitialTOMGeneratorState(tenantId) - ) - - const [isLoading, setIsLoading] = React.useState(false) - const [error, setError] = React.useState(null) - - const rulesEngineRef = useRef(null) - - // Initialize rules engine - useEffect(() => { - if (!rulesEngineRef.current) { - rulesEngineRef.current = new TOMRulesEngine() - } - }, []) - - // Load state from localStorage on mount - useEffect(() => { - if (enablePersistence && typeof window !== 'undefined') { - try { - const stored = localStorage.getItem(getStorageKey(tenantId)) - if (stored) { - const parsed = JSON.parse(stored) - // Convert date strings back to Date objects - if (parsed.createdAt) parsed.createdAt = new Date(parsed.createdAt) - if (parsed.updatedAt) parsed.updatedAt = new Date(parsed.updatedAt) - if (parsed.steps) { - parsed.steps = parsed.steps.map((step: WizardStep) => ({ - ...step, - validatedAt: step.validatedAt ? new Date(step.validatedAt) : null, - })) - } - if (parsed.documents) { - parsed.documents = parsed.documents.map((doc: EvidenceDocument) => ({ - ...doc, - uploadedAt: new Date(doc.uploadedAt), - validFrom: doc.validFrom ? new Date(doc.validFrom) : null, - validUntil: doc.validUntil ? new Date(doc.validUntil) : null, - aiAnalysis: doc.aiAnalysis - ? { - ...doc.aiAnalysis, - analyzedAt: new Date(doc.aiAnalysis.analyzedAt), - } - : null, - })) - } - if (parsed.derivedTOMs) { - parsed.derivedTOMs = parsed.derivedTOMs.map((tom: DerivedTOM) => ({ - ...tom, - implementationDate: tom.implementationDate - ? new Date(tom.implementationDate) - : null, - reviewDate: tom.reviewDate ? new Date(tom.reviewDate) : null, - })) - } - if (parsed.gapAnalysis?.generatedAt) { - parsed.gapAnalysis.generatedAt = new Date(parsed.gapAnalysis.generatedAt) - } - if (parsed.exports) { - parsed.exports = parsed.exports.map((exp: ExportRecord) => ({ - ...exp, - generatedAt: new Date(exp.generatedAt), - })) - } - dispatch({ type: 'LOAD_STATE', payload: parsed }) - } - } catch (e) { - console.error('Failed to load TOM Generator state from localStorage:', e) - } - } - }, [tenantId, enablePersistence]) - - // Save state to localStorage on changes - useEffect(() => { - if (enablePersistence && typeof window !== 'undefined') { - try { - localStorage.setItem(getStorageKey(tenantId), JSON.stringify(state)) - } catch (e) { - console.error('Failed to save TOM Generator state to localStorage:', e) - } - } - }, [state, tenantId, enablePersistence]) - - // Navigation helpers - const currentStepIndex = getStepIndex(state.currentStep) - const totalSteps = TOM_GENERATOR_STEPS.length - - const canGoNext = currentStepIndex < totalSteps - 1 - const canGoPrevious = currentStepIndex > 0 - - const goToStep = useCallback((stepId: TOMGeneratorStepId) => { - dispatch({ type: 'SET_CURRENT_STEP', payload: stepId }) - }, []) - - const goToNextStep = useCallback(() => { - if (canGoNext) { - const nextStep = TOM_GENERATOR_STEPS[currentStepIndex + 1] - dispatch({ type: 'SET_CURRENT_STEP', payload: nextStep.id }) - } - }, [canGoNext, currentStepIndex]) - - const goToPreviousStep = useCallback(() => { - if (canGoPrevious) { - const prevStep = TOM_GENERATOR_STEPS[currentStepIndex - 1] - dispatch({ type: 'SET_CURRENT_STEP', payload: prevStep.id }) - } - }, [canGoPrevious, currentStepIndex]) - - const completeCurrentStep = useCallback( - (data: unknown) => { - dispatch({ - type: 'COMPLETE_STEP', - payload: { stepId: state.currentStep, data }, - }) - }, - [state.currentStep] - ) - - // Profile setters - const setCompanyProfile = useCallback((profile: CompanyProfile) => { - dispatch({ type: 'SET_COMPANY_PROFILE', payload: profile }) - }, []) - - const updateCompanyProfile = useCallback((data: Partial) => { - dispatch({ type: 'UPDATE_COMPANY_PROFILE', payload: data }) - }, []) - - const setDataProfile = useCallback((profile: DataProfile) => { - dispatch({ type: 'SET_DATA_PROFILE', payload: profile }) - }, []) - - const updateDataProfile = useCallback((data: Partial) => { - dispatch({ type: 'UPDATE_DATA_PROFILE', payload: data }) - }, []) - - const setArchitectureProfile = useCallback((profile: ArchitectureProfile) => { - dispatch({ type: 'SET_ARCHITECTURE_PROFILE', payload: profile }) - }, []) - - const updateArchitectureProfile = useCallback( - (data: Partial) => { - dispatch({ type: 'UPDATE_ARCHITECTURE_PROFILE', payload: data }) - }, - [] - ) - - const setSecurityProfile = useCallback((profile: SecurityProfile) => { - dispatch({ type: 'SET_SECURITY_PROFILE', payload: profile }) - }, []) - - const updateSecurityProfile = useCallback((data: Partial) => { - dispatch({ type: 'UPDATE_SECURITY_PROFILE', payload: data }) - }, []) - - const setRiskProfile = useCallback((profile: RiskProfile) => { - dispatch({ type: 'SET_RISK_PROFILE', payload: profile }) - }, []) - - const updateRiskProfile = useCallback((data: Partial) => { - dispatch({ type: 'UPDATE_RISK_PROFILE', payload: data }) - }, []) - - // Evidence management - const addEvidence = useCallback((document: EvidenceDocument) => { - dispatch({ type: 'ADD_EVIDENCE', payload: document }) - }, []) - - const updateEvidence = useCallback( - (id: string, data: Partial) => { - dispatch({ type: 'UPDATE_EVIDENCE', payload: { id, data } }) - }, - [] - ) - - const deleteEvidence = useCallback((id: string) => { - dispatch({ type: 'DELETE_EVIDENCE', payload: id }) - }, []) - - // TOM derivation - const deriveTOMs = useCallback(() => { - if (!rulesEngineRef.current) return - - const derivedTOMs = rulesEngineRef.current.deriveAllTOMs({ - companyProfile: state.companyProfile, - dataProfile: state.dataProfile, - architectureProfile: state.architectureProfile, - securityProfile: state.securityProfile, - riskProfile: state.riskProfile, - }) - - dispatch({ type: 'SET_DERIVED_TOMS', payload: derivedTOMs }) - }, [ - state.companyProfile, - state.dataProfile, - state.architectureProfile, - state.securityProfile, - state.riskProfile, - ]) - - const updateDerivedTOM = useCallback( - (id: string, data: Partial) => { - dispatch({ type: 'UPDATE_DERIVED_TOM', payload: { id, data } }) - }, - [] - ) - - const bulkUpdateTOMs = useCallback( - (updates: Array<{ id: string; data: Partial }>) => { - for (const { id, data } of updates) { - dispatch({ type: 'UPDATE_DERIVED_TOM', payload: { id, data } }) - } - }, - [] - ) - - // Gap analysis - const runGapAnalysis = useCallback(() => { - if (!rulesEngineRef.current) return - - const result = rulesEngineRef.current.performGapAnalysis( - state.derivedTOMs, - state.documents - ) - - dispatch({ type: 'SET_GAP_ANALYSIS', payload: result }) - }, [state.derivedTOMs, state.documents]) - - // Export - const addExport = useCallback((record: ExportRecord) => { - dispatch({ type: 'ADD_EXPORT', payload: record }) - }, []) - - // Persistence - const saveState = useCallback(async () => { - setIsLoading(true) - setError(null) - try { - // API call to save state - const response = await fetch('/api/sdk/v1/tom-generator/state', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ tenantId, state }), - }) - - if (!response.ok) { - throw new Error('Failed to save state') - } - } catch (e) { - setError(e instanceof Error ? e.message : 'Unknown error') - throw e - } finally { - setIsLoading(false) - } - }, [tenantId, state]) - - const loadState = useCallback(async () => { - setIsLoading(true) - setError(null) - try { - const response = await fetch( - `/api/sdk/v1/tom-generator/state?tenantId=${tenantId}` - ) - - if (!response.ok) { - throw new Error('Failed to load state') - } - - const data = await response.json() - if (data.state) { - dispatch({ type: 'LOAD_STATE', payload: data.state }) - } - } catch (e) { - setError(e instanceof Error ? e.message : 'Unknown error') - throw e - } finally { - setIsLoading(false) - } - }, [tenantId]) - - const resetState = useCallback(() => { - dispatch({ type: 'RESET', payload: { tenantId } }) - }, [tenantId]) - - // Status helpers - const isStepCompleted = useCallback( - (stepId: TOMGeneratorStepId) => { - const step = state.steps.find((s) => s.id === stepId) - return step?.completed ?? false - }, - [state.steps] - ) - - const getCompletionPercentage = useCallback(() => { - const completedSteps = state.steps.filter((s) => s.completed).length - return Math.round((completedSteps / totalSteps) * 100) - }, [state.steps, totalSteps]) - - const contextValue: TOMGeneratorContextValue = { - state, - dispatch, - - currentStepIndex, - totalSteps, - canGoNext, - canGoPrevious, - goToStep, - goToNextStep, - goToPreviousStep, - completeCurrentStep, - - setCompanyProfile, - updateCompanyProfile, - setDataProfile, - updateDataProfile, - setArchitectureProfile, - updateArchitectureProfile, - setSecurityProfile, - updateSecurityProfile, - setRiskProfile, - updateRiskProfile, - - addEvidence, - updateEvidence, - deleteEvidence, - - deriveTOMs, - updateDerivedTOM, - bulkUpdateTOMs, - - runGapAnalysis, - - addExport, - - saveState, - loadState, - resetState, - - isStepCompleted, - getCompletionPercentage, - isLoading, - error, - } - - return ( - - {children} - - ) -} - -// ============================================================================= -// HOOK -// ============================================================================= - -export function useTOMGenerator(): TOMGeneratorContextValue { - const context = useContext(TOMGeneratorContext) - if (!context) { - throw new Error( - 'useTOMGenerator must be used within a TOMGeneratorProvider' - ) - } - return context -} - -// ============================================================================= -// EXPORTS -// ============================================================================= - -export { TOMGeneratorContext } -export type { TOMGeneratorAction, TOMGeneratorContextValue } +export { TOMGeneratorProvider } from './provider' +export { TOMGeneratorContext } from './provider' +export type { TOMGeneratorContextValue } from './provider' +export { useTOMGenerator } from './hooks' +export { tomGeneratorReducer } from './reducer' +export type { TOMGeneratorAction } from './reducer' diff --git a/admin-compliance/lib/sdk/tom-generator/hooks.tsx b/admin-compliance/lib/sdk/tom-generator/hooks.tsx new file mode 100644 index 0000000..6fe486f --- /dev/null +++ b/admin-compliance/lib/sdk/tom-generator/hooks.tsx @@ -0,0 +1,20 @@ +'use client' + +// ============================================================================= +// TOM Generator Hook +// Custom hook for consuming the TOM Generator context +// ============================================================================= + +import { useContext } from 'react' +import { TOMGeneratorContext } from './provider' +import type { TOMGeneratorContextValue } from './provider' + +export function useTOMGenerator(): TOMGeneratorContextValue { + const context = useContext(TOMGeneratorContext) + if (!context) { + throw new Error( + 'useTOMGenerator must be used within a TOMGeneratorProvider' + ) + } + return context +} diff --git a/admin-compliance/lib/sdk/tom-generator/provider.tsx b/admin-compliance/lib/sdk/tom-generator/provider.tsx new file mode 100644 index 0000000..b106583 --- /dev/null +++ b/admin-compliance/lib/sdk/tom-generator/provider.tsx @@ -0,0 +1,473 @@ +'use client' + +// ============================================================================= +// TOM Generator Provider +// Context provider component for the TOM Generator Wizard +// ============================================================================= + +import React, { + createContext, + useReducer, + useCallback, + useEffect, + useRef, + ReactNode, +} from 'react' +import { + TOMGeneratorState, + TOMGeneratorStepId, + CompanyProfile, + DataProfile, + ArchitectureProfile, + SecurityProfile, + RiskProfile, + EvidenceDocument, + DerivedTOM, + ExportRecord, + WizardStep, + createInitialTOMGeneratorState, + TOM_GENERATOR_STEPS, + getStepIndex, +} from './types' +import { TOMRulesEngine } from './rules-engine' +import { tomGeneratorReducer, TOMGeneratorAction } from './reducer' + +// ============================================================================= +// CONTEXT VALUE INTERFACE +// ============================================================================= + +export interface TOMGeneratorContextValue { + state: TOMGeneratorState + dispatch: React.Dispatch + + // Navigation + currentStepIndex: number + totalSteps: number + canGoNext: boolean + canGoPrevious: boolean + goToStep: (stepId: TOMGeneratorStepId) => void + goToNextStep: () => void + goToPreviousStep: () => void + completeCurrentStep: (data: unknown) => void + + // Profile setters + setCompanyProfile: (profile: CompanyProfile) => void + updateCompanyProfile: (data: Partial) => void + setDataProfile: (profile: DataProfile) => void + updateDataProfile: (data: Partial) => void + setArchitectureProfile: (profile: ArchitectureProfile) => void + updateArchitectureProfile: (data: Partial) => void + setSecurityProfile: (profile: SecurityProfile) => void + updateSecurityProfile: (data: Partial) => void + setRiskProfile: (profile: RiskProfile) => void + updateRiskProfile: (data: Partial) => void + + // Evidence management + addEvidence: (document: EvidenceDocument) => void + updateEvidence: (id: string, data: Partial) => void + deleteEvidence: (id: string) => void + + // TOM derivation + deriveTOMs: () => void + updateDerivedTOM: (id: string, data: Partial) => void + bulkUpdateTOMs: (updates: Array<{ id: string; data: Partial }>) => void + + // Gap analysis + runGapAnalysis: () => void + + // Export + addExport: (record: ExportRecord) => void + + // Persistence + saveState: () => Promise + loadState: () => Promise + resetState: () => void + + // Status + isStepCompleted: (stepId: TOMGeneratorStepId) => boolean + getCompletionPercentage: () => number + isLoading: boolean + error: string | null +} + +// ============================================================================= +// CONTEXT +// ============================================================================= + +export const TOMGeneratorContext = createContext(null) + +// ============================================================================= +// STORAGE KEYS +// ============================================================================= + +const STORAGE_KEY_PREFIX = 'tom-generator-state-' + +function getStorageKey(tenantId: string): string { + return `${STORAGE_KEY_PREFIX}${tenantId}` +} + +// ============================================================================= +// PROVIDER COMPONENT +// ============================================================================= + +interface TOMGeneratorProviderProps { + children: ReactNode + tenantId: string + initialState?: TOMGeneratorState + enablePersistence?: boolean +} + +export function TOMGeneratorProvider({ + children, + tenantId, + initialState, + enablePersistence = true, +}: TOMGeneratorProviderProps) { + const [state, dispatch] = useReducer( + tomGeneratorReducer, + initialState ?? createInitialTOMGeneratorState(tenantId) + ) + + const [isLoading, setIsLoading] = React.useState(false) + const [error, setError] = React.useState(null) + + const rulesEngineRef = useRef(null) + + // Initialize rules engine + useEffect(() => { + if (!rulesEngineRef.current) { + rulesEngineRef.current = new TOMRulesEngine() + } + }, []) + + // Load state from localStorage on mount + useEffect(() => { + if (enablePersistence && typeof window !== 'undefined') { + try { + const stored = localStorage.getItem(getStorageKey(tenantId)) + if (stored) { + const parsed = JSON.parse(stored) + if (parsed.createdAt) parsed.createdAt = new Date(parsed.createdAt) + if (parsed.updatedAt) parsed.updatedAt = new Date(parsed.updatedAt) + if (parsed.steps) { + parsed.steps = parsed.steps.map((step: WizardStep) => ({ + ...step, + validatedAt: step.validatedAt ? new Date(step.validatedAt) : null, + })) + } + if (parsed.documents) { + parsed.documents = parsed.documents.map((doc: EvidenceDocument) => ({ + ...doc, + uploadedAt: new Date(doc.uploadedAt), + validFrom: doc.validFrom ? new Date(doc.validFrom) : null, + validUntil: doc.validUntil ? new Date(doc.validUntil) : null, + aiAnalysis: doc.aiAnalysis + ? { + ...doc.aiAnalysis, + analyzedAt: new Date(doc.aiAnalysis.analyzedAt), + } + : null, + })) + } + if (parsed.derivedTOMs) { + parsed.derivedTOMs = parsed.derivedTOMs.map((tom: DerivedTOM) => ({ + ...tom, + implementationDate: tom.implementationDate + ? new Date(tom.implementationDate) + : null, + reviewDate: tom.reviewDate ? new Date(tom.reviewDate) : null, + })) + } + if (parsed.gapAnalysis?.generatedAt) { + parsed.gapAnalysis.generatedAt = new Date(parsed.gapAnalysis.generatedAt) + } + if (parsed.exports) { + parsed.exports = parsed.exports.map((exp: ExportRecord) => ({ + ...exp, + generatedAt: new Date(exp.generatedAt), + })) + } + dispatch({ type: 'LOAD_STATE', payload: parsed }) + } + } catch (e) { + console.error('Failed to load TOM Generator state from localStorage:', e) + } + } + }, [tenantId, enablePersistence]) + + // Save state to localStorage on changes + useEffect(() => { + if (enablePersistence && typeof window !== 'undefined') { + try { + localStorage.setItem(getStorageKey(tenantId), JSON.stringify(state)) + } catch (e) { + console.error('Failed to save TOM Generator state to localStorage:', e) + } + } + }, [state, tenantId, enablePersistence]) + + // Navigation helpers + const currentStepIndex = getStepIndex(state.currentStep) + const totalSteps = TOM_GENERATOR_STEPS.length + + const canGoNext = currentStepIndex < totalSteps - 1 + const canGoPrevious = currentStepIndex > 0 + + const goToStep = useCallback((stepId: TOMGeneratorStepId) => { + dispatch({ type: 'SET_CURRENT_STEP', payload: stepId }) + }, []) + + const goToNextStep = useCallback(() => { + if (canGoNext) { + const nextStep = TOM_GENERATOR_STEPS[currentStepIndex + 1] + dispatch({ type: 'SET_CURRENT_STEP', payload: nextStep.id }) + } + }, [canGoNext, currentStepIndex]) + + const goToPreviousStep = useCallback(() => { + if (canGoPrevious) { + const prevStep = TOM_GENERATOR_STEPS[currentStepIndex - 1] + dispatch({ type: 'SET_CURRENT_STEP', payload: prevStep.id }) + } + }, [canGoPrevious, currentStepIndex]) + + const completeCurrentStep = useCallback( + (data: unknown) => { + dispatch({ + type: 'COMPLETE_STEP', + payload: { stepId: state.currentStep, data }, + }) + }, + [state.currentStep] + ) + + // Profile setters + const setCompanyProfile = useCallback((profile: CompanyProfile) => { + dispatch({ type: 'SET_COMPANY_PROFILE', payload: profile }) + }, []) + + const updateCompanyProfile = useCallback((data: Partial) => { + dispatch({ type: 'UPDATE_COMPANY_PROFILE', payload: data }) + }, []) + + const setDataProfile = useCallback((profile: DataProfile) => { + dispatch({ type: 'SET_DATA_PROFILE', payload: profile }) + }, []) + + const updateDataProfile = useCallback((data: Partial) => { + dispatch({ type: 'UPDATE_DATA_PROFILE', payload: data }) + }, []) + + const setArchitectureProfile = useCallback((profile: ArchitectureProfile) => { + dispatch({ type: 'SET_ARCHITECTURE_PROFILE', payload: profile }) + }, []) + + const updateArchitectureProfile = useCallback( + (data: Partial) => { + dispatch({ type: 'UPDATE_ARCHITECTURE_PROFILE', payload: data }) + }, + [] + ) + + const setSecurityProfile = useCallback((profile: SecurityProfile) => { + dispatch({ type: 'SET_SECURITY_PROFILE', payload: profile }) + }, []) + + const updateSecurityProfile = useCallback((data: Partial) => { + dispatch({ type: 'UPDATE_SECURITY_PROFILE', payload: data }) + }, []) + + const setRiskProfile = useCallback((profile: RiskProfile) => { + dispatch({ type: 'SET_RISK_PROFILE', payload: profile }) + }, []) + + const updateRiskProfile = useCallback((data: Partial) => { + dispatch({ type: 'UPDATE_RISK_PROFILE', payload: data }) + }, []) + + // Evidence management + const addEvidence = useCallback((document: EvidenceDocument) => { + dispatch({ type: 'ADD_EVIDENCE', payload: document }) + }, []) + + const updateEvidence = useCallback( + (id: string, data: Partial) => { + dispatch({ type: 'UPDATE_EVIDENCE', payload: { id, data } }) + }, + [] + ) + + const deleteEvidence = useCallback((id: string) => { + dispatch({ type: 'DELETE_EVIDENCE', payload: id }) + }, []) + + // TOM derivation + const deriveTOMs = useCallback(() => { + if (!rulesEngineRef.current) return + + const derivedTOMs = rulesEngineRef.current.deriveAllTOMs({ + companyProfile: state.companyProfile, + dataProfile: state.dataProfile, + architectureProfile: state.architectureProfile, + securityProfile: state.securityProfile, + riskProfile: state.riskProfile, + }) + + dispatch({ type: 'SET_DERIVED_TOMS', payload: derivedTOMs }) + }, [ + state.companyProfile, + state.dataProfile, + state.architectureProfile, + state.securityProfile, + state.riskProfile, + ]) + + const updateDerivedTOM = useCallback( + (id: string, data: Partial) => { + dispatch({ type: 'UPDATE_DERIVED_TOM', payload: { id, data } }) + }, + [] + ) + + const bulkUpdateTOMs = useCallback( + (updates: Array<{ id: string; data: Partial }>) => { + for (const { id, data } of updates) { + dispatch({ type: 'UPDATE_DERIVED_TOM', payload: { id, data } }) + } + }, + [] + ) + + // Gap analysis + const runGapAnalysis = useCallback(() => { + if (!rulesEngineRef.current) return + + const result = rulesEngineRef.current.performGapAnalysis( + state.derivedTOMs, + state.documents + ) + + dispatch({ type: 'SET_GAP_ANALYSIS', payload: result }) + }, [state.derivedTOMs, state.documents]) + + // Export + const addExport = useCallback((record: ExportRecord) => { + dispatch({ type: 'ADD_EXPORT', payload: record }) + }, []) + + // Persistence + const saveState = useCallback(async () => { + setIsLoading(true) + setError(null) + try { + const response = await fetch('/api/sdk/v1/tom-generator/state', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ tenantId, state }), + }) + + if (!response.ok) { + throw new Error('Failed to save state') + } + } catch (e) { + setError(e instanceof Error ? e.message : 'Unknown error') + throw e + } finally { + setIsLoading(false) + } + }, [tenantId, state]) + + const loadState = useCallback(async () => { + setIsLoading(true) + setError(null) + try { + const response = await fetch( + `/api/sdk/v1/tom-generator/state?tenantId=${tenantId}` + ) + + if (!response.ok) { + throw new Error('Failed to load state') + } + + const data = await response.json() + if (data.state) { + dispatch({ type: 'LOAD_STATE', payload: data.state }) + } + } catch (e) { + setError(e instanceof Error ? e.message : 'Unknown error') + throw e + } finally { + setIsLoading(false) + } + }, [tenantId]) + + const resetState = useCallback(() => { + dispatch({ type: 'RESET', payload: { tenantId } }) + }, [tenantId]) + + // Status helpers + const isStepCompleted = useCallback( + (stepId: TOMGeneratorStepId) => { + const step = state.steps.find((s) => s.id === stepId) + return step?.completed ?? false + }, + [state.steps] + ) + + const getCompletionPercentage = useCallback(() => { + const completedSteps = state.steps.filter((s) => s.completed).length + return Math.round((completedSteps / totalSteps) * 100) + }, [state.steps, totalSteps]) + + const contextValue: TOMGeneratorContextValue = { + state, + dispatch, + + currentStepIndex, + totalSteps, + canGoNext, + canGoPrevious, + goToStep, + goToNextStep, + goToPreviousStep, + completeCurrentStep, + + setCompanyProfile, + updateCompanyProfile, + setDataProfile, + updateDataProfile, + setArchitectureProfile, + updateArchitectureProfile, + setSecurityProfile, + updateSecurityProfile, + setRiskProfile, + updateRiskProfile, + + addEvidence, + updateEvidence, + deleteEvidence, + + deriveTOMs, + updateDerivedTOM, + bulkUpdateTOMs, + + runGapAnalysis, + + addExport, + + saveState, + loadState, + resetState, + + isStepCompleted, + getCompletionPercentage, + isLoading, + error, + } + + return ( + + {children} + + ) +} diff --git a/admin-compliance/lib/sdk/tom-generator/reducer.ts b/admin-compliance/lib/sdk/tom-generator/reducer.ts new file mode 100644 index 0000000..dbfcbdd --- /dev/null +++ b/admin-compliance/lib/sdk/tom-generator/reducer.ts @@ -0,0 +1,238 @@ +// ============================================================================= +// TOM Generator Reducer +// Action types and state reducer for the TOM Generator Wizard +// ============================================================================= + +import { + TOMGeneratorState, + TOMGeneratorStepId, + CompanyProfile, + DataProfile, + ArchitectureProfile, + SecurityProfile, + RiskProfile, + EvidenceDocument, + DerivedTOM, + GapAnalysisResult, + ExportRecord, + WizardStep, + createInitialTOMGeneratorState, + calculateProtectionLevel, + isDSFARequired, + hasSpecialCategories, +} from './types' + +// ============================================================================= +// ACTION TYPES +// ============================================================================= + +export type TOMGeneratorAction = + | { type: 'INITIALIZE'; payload: { tenantId: string; state?: TOMGeneratorState } } + | { type: 'RESET'; payload: { tenantId: string } } + | { type: 'SET_CURRENT_STEP'; payload: TOMGeneratorStepId } + | { type: 'SET_COMPANY_PROFILE'; payload: CompanyProfile } + | { type: 'UPDATE_COMPANY_PROFILE'; payload: Partial } + | { type: 'SET_DATA_PROFILE'; payload: DataProfile } + | { type: 'UPDATE_DATA_PROFILE'; payload: Partial } + | { type: 'SET_ARCHITECTURE_PROFILE'; payload: ArchitectureProfile } + | { type: 'UPDATE_ARCHITECTURE_PROFILE'; payload: Partial } + | { type: 'SET_SECURITY_PROFILE'; payload: SecurityProfile } + | { type: 'UPDATE_SECURITY_PROFILE'; payload: Partial } + | { type: 'SET_RISK_PROFILE'; payload: RiskProfile } + | { type: 'UPDATE_RISK_PROFILE'; payload: Partial } + | { type: 'COMPLETE_STEP'; payload: { stepId: TOMGeneratorStepId; data: unknown } } + | { type: 'UNCOMPLETE_STEP'; payload: TOMGeneratorStepId } + | { type: 'ADD_EVIDENCE'; payload: EvidenceDocument } + | { type: 'UPDATE_EVIDENCE'; payload: { id: string; data: Partial } } + | { type: 'DELETE_EVIDENCE'; payload: string } + | { type: 'SET_DERIVED_TOMS'; payload: DerivedTOM[] } + | { type: 'UPDATE_DERIVED_TOM'; payload: { id: string; data: Partial } } + | { type: 'SET_GAP_ANALYSIS'; payload: GapAnalysisResult } + | { type: 'ADD_EXPORT'; payload: ExportRecord } + | { type: 'BULK_UPDATE_TOMS'; payload: { updates: Array<{ id: string; data: Partial }> } } + | { type: 'LOAD_STATE'; payload: TOMGeneratorState } + +// ============================================================================= +// REDUCER +// ============================================================================= + +export function tomGeneratorReducer( + state: TOMGeneratorState, + action: TOMGeneratorAction +): TOMGeneratorState { + const updateState = (updates: Partial): TOMGeneratorState => ({ + ...state, + ...updates, + updatedAt: new Date(), + }) + + switch (action.type) { + case 'INITIALIZE': { + if (action.payload.state) { + return action.payload.state + } + return createInitialTOMGeneratorState(action.payload.tenantId) + } + + case 'RESET': { + return createInitialTOMGeneratorState(action.payload.tenantId) + } + + case 'SET_CURRENT_STEP': { + return updateState({ currentStep: action.payload }) + } + + case 'SET_COMPANY_PROFILE': { + return updateState({ companyProfile: action.payload }) + } + + case 'UPDATE_COMPANY_PROFILE': { + if (!state.companyProfile) return state + return updateState({ + companyProfile: { ...state.companyProfile, ...action.payload }, + }) + } + + case 'SET_DATA_PROFILE': { + const profile: DataProfile = { + ...action.payload, + hasSpecialCategories: hasSpecialCategories(action.payload.categories), + } + return updateState({ dataProfile: profile }) + } + + case 'UPDATE_DATA_PROFILE': { + if (!state.dataProfile) return state + const updatedProfile = { ...state.dataProfile, ...action.payload } + if (action.payload.categories) { + updatedProfile.hasSpecialCategories = hasSpecialCategories( + action.payload.categories + ) + } + return updateState({ dataProfile: updatedProfile }) + } + + case 'SET_ARCHITECTURE_PROFILE': { + return updateState({ architectureProfile: action.payload }) + } + + case 'UPDATE_ARCHITECTURE_PROFILE': { + if (!state.architectureProfile) return state + return updateState({ + architectureProfile: { ...state.architectureProfile, ...action.payload }, + }) + } + + case 'SET_SECURITY_PROFILE': { + return updateState({ securityProfile: action.payload }) + } + + case 'UPDATE_SECURITY_PROFILE': { + if (!state.securityProfile) return state + return updateState({ + securityProfile: { ...state.securityProfile, ...action.payload }, + }) + } + + case 'SET_RISK_PROFILE': { + const profile: RiskProfile = { + ...action.payload, + protectionLevel: calculateProtectionLevel(action.payload.ciaAssessment), + dsfaRequired: isDSFARequired(state.dataProfile, action.payload), + } + return updateState({ riskProfile: profile }) + } + + case 'UPDATE_RISK_PROFILE': { + if (!state.riskProfile) return state + const updatedProfile = { ...state.riskProfile, ...action.payload } + if (action.payload.ciaAssessment) { + updatedProfile.protectionLevel = calculateProtectionLevel( + action.payload.ciaAssessment + ) + } + updatedProfile.dsfaRequired = isDSFARequired(state.dataProfile, updatedProfile) + return updateState({ riskProfile: updatedProfile }) + } + + case 'COMPLETE_STEP': { + const updatedSteps = state.steps.map((step) => + step.id === action.payload.stepId + ? { + ...step, + completed: true, + data: action.payload.data, + validatedAt: new Date(), + } + : step + ) + return updateState({ steps: updatedSteps }) + } + + case 'UNCOMPLETE_STEP': { + const updatedSteps = state.steps.map((step) => + step.id === action.payload + ? { ...step, completed: false, validatedAt: null } + : step + ) + return updateState({ steps: updatedSteps }) + } + + case 'ADD_EVIDENCE': { + return updateState({ + documents: [...state.documents, action.payload], + }) + } + + case 'UPDATE_EVIDENCE': { + const updatedDocuments = state.documents.map((doc) => + doc.id === action.payload.id ? { ...doc, ...action.payload.data } : doc + ) + return updateState({ documents: updatedDocuments }) + } + + case 'DELETE_EVIDENCE': { + return updateState({ + documents: state.documents.filter((doc) => doc.id !== action.payload), + }) + } + + case 'SET_DERIVED_TOMS': { + return updateState({ derivedTOMs: action.payload }) + } + + case 'UPDATE_DERIVED_TOM': { + const updatedTOMs = state.derivedTOMs.map((tom) => + tom.id === action.payload.id ? { ...tom, ...action.payload.data } : tom + ) + return updateState({ derivedTOMs: updatedTOMs }) + } + + case 'SET_GAP_ANALYSIS': { + return updateState({ gapAnalysis: action.payload }) + } + + case 'ADD_EXPORT': { + return updateState({ + exports: [...state.exports, action.payload], + }) + } + + case 'BULK_UPDATE_TOMS': { + let updatedTOMs = [...state.derivedTOMs] + for (const update of action.payload.updates) { + updatedTOMs = updatedTOMs.map((tom) => + tom.id === update.id ? { ...tom, ...update.data } : tom + ) + } + return updateState({ derivedTOMs: updatedTOMs }) + } + + case 'LOAD_STATE': { + return action.payload + } + + default: + return state + } +} diff --git a/admin-compliance/lib/sdk/vendor-compliance/context.tsx b/admin-compliance/lib/sdk/vendor-compliance/context.tsx index a271ea4..f91d261 100644 --- a/admin-compliance/lib/sdk/vendor-compliance/context.tsx +++ b/admin-compliance/lib/sdk/vendor-compliance/context.tsx @@ -1,30 +1,17 @@ 'use client' import React, { - createContext, - useContext, useReducer, - useCallback, useMemo, useEffect, useState, } from 'react' import { - VendorComplianceState, - VendorComplianceAction, VendorComplianceContextValue, - ProcessingActivity, - Vendor, - ContractDocument, - Finding, - Control, - ControlInstance, - RiskAssessment, VendorStatistics, ComplianceStatistics, RiskOverview, - ExportFormat, VendorStatus, VendorRole, RiskLevel, @@ -33,185 +20,20 @@ import { getRiskLevelFromScore, } from './types' -// ========================================== -// INITIAL STATE -// ========================================== +import { initialState, vendorComplianceReducer } from './reducer' +import { VendorComplianceContext } from './hooks' +import { useVendorComplianceActions } from './use-actions' -const initialState: VendorComplianceState = { - processingActivities: [], - vendors: [], - contracts: [], - findings: [], - controls: [], - controlInstances: [], - riskAssessments: [], - isLoading: false, - error: null, - selectedVendorId: null, - selectedActivityId: null, - activeTab: 'overview', - lastModified: null, -} - -// ========================================== -// REDUCER -// ========================================== - -function vendorComplianceReducer( - state: VendorComplianceState, - action: VendorComplianceAction -): VendorComplianceState { - const updateState = (updates: Partial): VendorComplianceState => ({ - ...state, - ...updates, - lastModified: new Date(), - }) - - switch (action.type) { - // Processing Activities - case 'SET_PROCESSING_ACTIVITIES': - return updateState({ processingActivities: action.payload }) - - case 'ADD_PROCESSING_ACTIVITY': - return updateState({ - processingActivities: [...state.processingActivities, action.payload], - }) - - case 'UPDATE_PROCESSING_ACTIVITY': - return updateState({ - processingActivities: state.processingActivities.map((activity) => - activity.id === action.payload.id - ? { ...activity, ...action.payload.data, updatedAt: new Date() } - : activity - ), - }) - - case 'DELETE_PROCESSING_ACTIVITY': - return updateState({ - processingActivities: state.processingActivities.filter( - (activity) => activity.id !== action.payload - ), - }) - - // Vendors - case 'SET_VENDORS': - return updateState({ vendors: action.payload }) - - case 'ADD_VENDOR': - return updateState({ - vendors: [...state.vendors, action.payload], - }) - - case 'UPDATE_VENDOR': - return updateState({ - vendors: state.vendors.map((vendor) => - vendor.id === action.payload.id - ? { ...vendor, ...action.payload.data, updatedAt: new Date() } - : vendor - ), - }) - - case 'DELETE_VENDOR': - return updateState({ - vendors: state.vendors.filter((vendor) => vendor.id !== action.payload), - }) - - // Contracts - case 'SET_CONTRACTS': - return updateState({ contracts: action.payload }) - - case 'ADD_CONTRACT': - return updateState({ - contracts: [...state.contracts, action.payload], - }) - - case 'UPDATE_CONTRACT': - return updateState({ - contracts: state.contracts.map((contract) => - contract.id === action.payload.id - ? { ...contract, ...action.payload.data, updatedAt: new Date() } - : contract - ), - }) - - case 'DELETE_CONTRACT': - return updateState({ - contracts: state.contracts.filter((contract) => contract.id !== action.payload), - }) - - // Findings - case 'SET_FINDINGS': - return updateState({ findings: action.payload }) - - case 'ADD_FINDINGS': - return updateState({ - findings: [...state.findings, ...action.payload], - }) - - case 'UPDATE_FINDING': - return updateState({ - findings: state.findings.map((finding) => - finding.id === action.payload.id - ? { ...finding, ...action.payload.data, updatedAt: new Date() } - : finding - ), - }) - - // Controls - case 'SET_CONTROLS': - return updateState({ controls: action.payload }) - - case 'SET_CONTROL_INSTANCES': - return updateState({ controlInstances: action.payload }) - - case 'UPDATE_CONTROL_INSTANCE': - return updateState({ - controlInstances: state.controlInstances.map((instance) => - instance.id === action.payload.id - ? { ...instance, ...action.payload.data } - : instance - ), - }) - - // Risk Assessments - case 'SET_RISK_ASSESSMENTS': - return updateState({ riskAssessments: action.payload }) - - case 'UPDATE_RISK_ASSESSMENT': - return updateState({ - riskAssessments: state.riskAssessments.map((assessment) => - assessment.id === action.payload.id - ? { ...assessment, ...action.payload.data } - : assessment - ), - }) - - // UI State - case 'SET_LOADING': - return { ...state, isLoading: action.payload } - - case 'SET_ERROR': - return { ...state, error: action.payload } - - case 'SET_SELECTED_VENDOR': - return { ...state, selectedVendorId: action.payload } - - case 'SET_SELECTED_ACTIVITY': - return { ...state, selectedActivityId: action.payload } - - case 'SET_ACTIVE_TAB': - return { ...state, activeTab: action.payload } - - default: - return state - } -} - -// ========================================== -// CONTEXT -// ========================================== - -const VendorComplianceContext = createContext(null) +// Re-export hooks and selectors for barrel +export { + useVendorCompliance, + useVendor, + useProcessingActivity, + useVendorContracts, + useVendorFindings, + useContractFindings, + useControlInstancesForEntity, +} from './hooks' // ========================================== // PROVIDER @@ -229,6 +51,8 @@ export function VendorComplianceProvider({ const [state, dispatch] = useReducer(vendorComplianceReducer, initialState) const [isInitialized, setIsInitialized] = useState(false) + const actions = useVendorComplianceActions(state, dispatch) + // ========================================== // COMPUTED VALUES // ========================================== @@ -254,7 +78,7 @@ export function VendorComplianceProvider({ const byRiskLevel = vendors.reduce( (acc, v) => { - const level = getRiskLevelFromScore(v.residualRiskScore / 4) // Normalize to 1-25 + const level = getRiskLevelFromScore(v.residualRiskScore / 4) acc[level] = (acc[level] || 0) + 1 return acc }, @@ -375,496 +199,16 @@ export function VendorComplianceProvider({ } }, [state.vendors, state.findings]) - // ========================================== - // API CALLS - // ========================================== - - const apiBase = '/api/sdk/v1/vendor-compliance' - - const loadData = useCallback(async () => { - dispatch({ type: 'SET_LOADING', payload: true }) - dispatch({ type: 'SET_ERROR', payload: null }) - - try { - const [ - activitiesRes, - vendorsRes, - contractsRes, - findingsRes, - controlsRes, - controlInstancesRes, - ] = await Promise.all([ - fetch(`${apiBase}/processing-activities`), - fetch(`${apiBase}/vendors`), - fetch(`${apiBase}/contracts`), - fetch(`${apiBase}/findings`), - fetch(`${apiBase}/controls`), - fetch(`${apiBase}/control-instances`), - ]) - - if (activitiesRes.ok) { - const data = await activitiesRes.json() - dispatch({ type: 'SET_PROCESSING_ACTIVITIES', payload: data.data || [] }) - } - - if (vendorsRes.ok) { - const data = await vendorsRes.json() - dispatch({ type: 'SET_VENDORS', payload: data.data || [] }) - } - - if (contractsRes.ok) { - const data = await contractsRes.json() - dispatch({ type: 'SET_CONTRACTS', payload: data.data || [] }) - } - - if (findingsRes.ok) { - const data = await findingsRes.json() - dispatch({ type: 'SET_FINDINGS', payload: data.data || [] }) - } - - if (controlsRes.ok) { - const data = await controlsRes.json() - dispatch({ type: 'SET_CONTROLS', payload: data.data || [] }) - } - - if (controlInstancesRes.ok) { - const data = await controlInstancesRes.json() - dispatch({ type: 'SET_CONTROL_INSTANCES', payload: data.data || [] }) - } - } catch (error) { - console.error('Failed to load vendor compliance data:', error) - dispatch({ - type: 'SET_ERROR', - payload: 'Fehler beim Laden der Daten', - }) - } finally { - dispatch({ type: 'SET_LOADING', payload: false }) - } - }, [apiBase]) - - const refresh = useCallback(async () => { - await loadData() - }, [loadData]) - - // ========================================== - // PROCESSING ACTIVITIES ACTIONS - // ========================================== - - const createProcessingActivity = useCallback( - async ( - data: Omit - ): Promise => { - const response = await fetch(`${apiBase}/processing-activities`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data), - }) - - if (!response.ok) { - const error = await response.json() - throw new Error(error.error || 'Fehler beim Erstellen der Verarbeitungstätigkeit') - } - - const result = await response.json() - const activity = result.data - - dispatch({ type: 'ADD_PROCESSING_ACTIVITY', payload: activity }) - - return activity - }, - [apiBase] - ) - - const updateProcessingActivity = useCallback( - async (id: string, data: Partial): Promise => { - const response = await fetch(`${apiBase}/processing-activities/${id}`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data), - }) - - if (!response.ok) { - const error = await response.json() - throw new Error(error.error || 'Fehler beim Aktualisieren der Verarbeitungstätigkeit') - } - - dispatch({ type: 'UPDATE_PROCESSING_ACTIVITY', payload: { id, data } }) - }, - [apiBase] - ) - - const deleteProcessingActivity = useCallback( - async (id: string): Promise => { - const response = await fetch(`${apiBase}/processing-activities/${id}`, { - method: 'DELETE', - }) - - if (!response.ok) { - const error = await response.json() - throw new Error(error.error || 'Fehler beim Löschen der Verarbeitungstätigkeit') - } - - dispatch({ type: 'DELETE_PROCESSING_ACTIVITY', payload: id }) - }, - [apiBase] - ) - - const duplicateProcessingActivity = useCallback( - async (id: string): Promise => { - const original = state.processingActivities.find((a) => a.id === id) - if (!original) { - throw new Error('Verarbeitungstätigkeit nicht gefunden') - } - - const { id: _id, vvtId: _vvtId, createdAt: _createdAt, updatedAt: _updatedAt, tenantId: _tenantId, ...rest } = original - - const newActivity = await createProcessingActivity({ - ...rest, - vvtId: '', // Will be generated by backend - name: { - de: `${original.name.de} (Kopie)`, - en: `${original.name.en} (Copy)`, - }, - status: 'DRAFT', - }) - - return newActivity - }, - [state.processingActivities, createProcessingActivity] - ) - - // ========================================== - // VENDOR ACTIONS - // ========================================== - - const createVendor = useCallback( - async ( - data: Omit - ): Promise => { - const response = await fetch(`${apiBase}/vendors`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data), - }) - - if (!response.ok) { - const error = await response.json() - throw new Error(error.error || 'Fehler beim Erstellen des Vendors') - } - - const result = await response.json() - const vendor = result.data - - dispatch({ type: 'ADD_VENDOR', payload: vendor }) - - return vendor - }, - [apiBase] - ) - - const updateVendor = useCallback( - async (id: string, data: Partial): Promise => { - const response = await fetch(`${apiBase}/vendors/${id}`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data), - }) - - if (!response.ok) { - const error = await response.json() - throw new Error(error.error || 'Fehler beim Aktualisieren des Vendors') - } - - dispatch({ type: 'UPDATE_VENDOR', payload: { id, data } }) - }, - [apiBase] - ) - - const deleteVendor = useCallback( - async (id: string): Promise => { - const response = await fetch(`${apiBase}/vendors/${id}`, { - method: 'DELETE', - }) - - if (!response.ok) { - const error = await response.json() - throw new Error(error.error || 'Fehler beim Löschen des Vendors') - } - - dispatch({ type: 'DELETE_VENDOR', payload: id }) - }, - [apiBase] - ) - - // ========================================== - // CONTRACT ACTIONS - // ========================================== - - const uploadContract = useCallback( - async ( - vendorId: string, - file: File, - metadata: Partial - ): Promise => { - const formData = new FormData() - formData.append('file', file) - formData.append('vendorId', vendorId) - formData.append('metadata', JSON.stringify(metadata)) - - const response = await fetch(`${apiBase}/contracts`, { - method: 'POST', - body: formData, - }) - - if (!response.ok) { - const error = await response.json() - throw new Error(error.error || 'Fehler beim Hochladen des Vertrags') - } - - const result = await response.json() - const contract = result.data - - dispatch({ type: 'ADD_CONTRACT', payload: contract }) - - // Update vendor's contracts list - const vendor = state.vendors.find((v) => v.id === vendorId) - if (vendor) { - dispatch({ - type: 'UPDATE_VENDOR', - payload: { - id: vendorId, - data: { contracts: [...vendor.contracts, contract.id] }, - }, - }) - } - - return contract - }, - [apiBase, state.vendors] - ) - - const updateContract = useCallback( - async (id: string, data: Partial): Promise => { - const response = await fetch(`${apiBase}/contracts/${id}`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data), - }) - - if (!response.ok) { - const error = await response.json() - throw new Error(error.error || 'Fehler beim Aktualisieren des Vertrags') - } - - dispatch({ type: 'UPDATE_CONTRACT', payload: { id, data } }) - }, - [apiBase] - ) - - const deleteContract = useCallback( - async (id: string): Promise => { - const contract = state.contracts.find((c) => c.id === id) - - const response = await fetch(`${apiBase}/contracts/${id}`, { - method: 'DELETE', - }) - - if (!response.ok) { - const error = await response.json() - throw new Error(error.error || 'Fehler beim Löschen des Vertrags') - } - - dispatch({ type: 'DELETE_CONTRACT', payload: id }) - - // Update vendor's contracts list - if (contract) { - const vendor = state.vendors.find((v) => v.id === contract.vendorId) - if (vendor) { - dispatch({ - type: 'UPDATE_VENDOR', - payload: { - id: vendor.id, - data: { contracts: vendor.contracts.filter((cId) => cId !== id) }, - }, - }) - } - } - }, - [apiBase, state.contracts, state.vendors] - ) - - const startContractReview = useCallback( - async (contractId: string): Promise => { - dispatch({ - type: 'UPDATE_CONTRACT', - payload: { id: contractId, data: { reviewStatus: 'IN_PROGRESS' } }, - }) - - const response = await fetch(`${apiBase}/contracts/${contractId}/review`, { - method: 'POST', - }) - - if (!response.ok) { - dispatch({ - type: 'UPDATE_CONTRACT', - payload: { id: contractId, data: { reviewStatus: 'FAILED' } }, - }) - const error = await response.json() - throw new Error(error.error || 'Fehler beim Starten der Vertragsprüfung') - } - - const result = await response.json() - - // Update contract with review results - dispatch({ - type: 'UPDATE_CONTRACT', - payload: { - id: contractId, - data: { - reviewStatus: 'COMPLETED', - reviewCompletedAt: new Date(), - complianceScore: result.data.complianceScore, - }, - }, - }) - - // Add findings - if (result.data.findings && result.data.findings.length > 0) { - dispatch({ type: 'ADD_FINDINGS', payload: result.data.findings }) - } - }, - [apiBase] - ) - - // ========================================== - // FINDINGS ACTIONS - // ========================================== - - const updateFinding = useCallback( - async (id: string, data: Partial): Promise => { - const response = await fetch(`${apiBase}/findings/${id}`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data), - }) - - if (!response.ok) { - const error = await response.json() - throw new Error(error.error || 'Fehler beim Aktualisieren des Findings') - } - - dispatch({ type: 'UPDATE_FINDING', payload: { id, data } }) - }, - [apiBase] - ) - - const resolveFinding = useCallback( - async (id: string, resolution: string): Promise => { - await updateFinding(id, { - status: 'RESOLVED', - resolution, - resolvedAt: new Date(), - }) - }, - [updateFinding] - ) - - // ========================================== - // CONTROL ACTIONS - // ========================================== - - const updateControlInstance = useCallback( - async (id: string, data: Partial): Promise => { - const response = await fetch(`${apiBase}/control-instances/${id}`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data), - }) - - if (!response.ok) { - const error = await response.json() - throw new Error(error.error || 'Fehler beim Aktualisieren des Control-Status') - } - - dispatch({ type: 'UPDATE_CONTROL_INSTANCE', payload: { id, data } }) - }, - [apiBase] - ) - - // ========================================== - // EXPORT ACTIONS - // ========================================== - - const exportVVT = useCallback( - async (format: ExportFormat, activityIds?: string[]): Promise => { - const params = new URLSearchParams({ format }) - if (activityIds && activityIds.length > 0) { - params.append('activityIds', activityIds.join(',')) - } - - const response = await fetch(`${apiBase}/export/vvt?${params}`) - - if (!response.ok) { - const error = await response.json() - throw new Error(error.error || 'Fehler beim Exportieren des VVT') - } - - const blob = await response.blob() - const url = URL.createObjectURL(blob) - - return url - }, - [apiBase] - ) - - const exportVendorAuditPack = useCallback( - async (vendorId: string, format: ExportFormat): Promise => { - const params = new URLSearchParams({ format, vendorId }) - - const response = await fetch(`${apiBase}/export/vendor-audit?${params}`) - - if (!response.ok) { - const error = await response.json() - throw new Error(error.error || 'Fehler beim Exportieren des Vendor Audit Packs') - } - - const blob = await response.blob() - const url = URL.createObjectURL(blob) - - return url - }, - [apiBase] - ) - - const exportRoPA = useCallback( - async (format: ExportFormat): Promise => { - const params = new URLSearchParams({ format }) - - const response = await fetch(`${apiBase}/export/ropa?${params}`) - - if (!response.ok) { - const error = await response.json() - throw new Error(error.error || 'Fehler beim Exportieren des RoPA') - } - - const blob = await response.blob() - const url = URL.createObjectURL(blob) - - return url - }, - [apiBase] - ) - // ========================================== // INITIALIZATION // ========================================== useEffect(() => { if (!isInitialized) { - loadData() + actions.loadData() setIsInitialized(true) } - }, [isInitialized, loadData]) + }, [isInitialized, actions]) // ========================================== // CONTEXT VALUE @@ -877,51 +221,9 @@ export function VendorComplianceProvider({ vendorStats, complianceStats, riskOverview, - createProcessingActivity, - updateProcessingActivity, - deleteProcessingActivity, - duplicateProcessingActivity, - createVendor, - updateVendor, - deleteVendor, - uploadContract, - updateContract, - deleteContract, - startContractReview, - updateFinding, - resolveFinding, - updateControlInstance, - exportVVT, - exportVendorAuditPack, - exportRoPA, - loadData, - refresh, + ...actions, }), - [ - state, - vendorStats, - complianceStats, - riskOverview, - createProcessingActivity, - updateProcessingActivity, - deleteProcessingActivity, - duplicateProcessingActivity, - createVendor, - updateVendor, - deleteVendor, - uploadContract, - updateContract, - deleteContract, - startContractReview, - updateFinding, - resolveFinding, - updateControlInstance, - exportVVT, - exportVendorAuditPack, - exportRoPA, - loadData, - refresh, - ] + [state, vendorStats, complianceStats, riskOverview, actions] ) return ( @@ -930,81 +232,3 @@ export function VendorComplianceProvider({ ) } - -// ========================================== -// HOOK -// ========================================== - -export function useVendorCompliance(): VendorComplianceContextValue { - const context = useContext(VendorComplianceContext) - - if (!context) { - throw new Error( - 'useVendorCompliance must be used within a VendorComplianceProvider' - ) - } - - return context -} - -// ========================================== -// SELECTORS -// ========================================== - -export function useVendor(vendorId: string | null) { - const { vendors } = useVendorCompliance() - return useMemo( - () => vendors.find((v) => v.id === vendorId) ?? null, - [vendors, vendorId] - ) -} - -export function useProcessingActivity(activityId: string | null) { - const { processingActivities } = useVendorCompliance() - return useMemo( - () => processingActivities.find((a) => a.id === activityId) ?? null, - [processingActivities, activityId] - ) -} - -export function useVendorContracts(vendorId: string | null) { - const { contracts } = useVendorCompliance() - return useMemo( - () => contracts.filter((c) => c.vendorId === vendorId), - [contracts, vendorId] - ) -} - -export function useVendorFindings(vendorId: string | null) { - const { findings } = useVendorCompliance() - return useMemo( - () => findings.filter((f) => f.vendorId === vendorId), - [findings, vendorId] - ) -} - -export function useContractFindings(contractId: string | null) { - const { findings } = useVendorCompliance() - return useMemo( - () => findings.filter((f) => f.contractId === contractId), - [findings, contractId] - ) -} - -export function useControlInstancesForEntity( - entityType: 'VENDOR' | 'PROCESSING_ACTIVITY', - entityId: string | null -) { - const { controlInstances, controls } = useVendorCompliance() - - return useMemo(() => { - if (!entityId) return [] - - return controlInstances - .filter((ci) => ci.entityType === entityType && ci.entityId === entityId) - .map((ci) => ({ - ...ci, - control: controls.find((c) => c.id === ci.controlId), - })) - }, [controlInstances, controls, entityType, entityId]) -} diff --git a/admin-compliance/lib/sdk/vendor-compliance/hooks.ts b/admin-compliance/lib/sdk/vendor-compliance/hooks.ts new file mode 100644 index 0000000..2094060 --- /dev/null +++ b/admin-compliance/lib/sdk/vendor-compliance/hooks.ts @@ -0,0 +1,88 @@ +'use client' + +import { useContext, useMemo, createContext } from 'react' +import { VendorComplianceContextValue } from './types' + +// ========================================== +// CONTEXT +// ========================================== + +export const VendorComplianceContext = createContext(null) + +// ========================================== +// HOOK +// ========================================== + +export function useVendorCompliance(): VendorComplianceContextValue { + const context = useContext(VendorComplianceContext) + + if (!context) { + throw new Error( + 'useVendorCompliance must be used within a VendorComplianceProvider' + ) + } + + return context +} + +// ========================================== +// SELECTORS +// ========================================== + +export function useVendor(vendorId: string | null) { + const { vendors } = useVendorCompliance() + return useMemo( + () => vendors.find((v) => v.id === vendorId) ?? null, + [vendors, vendorId] + ) +} + +export function useProcessingActivity(activityId: string | null) { + const { processingActivities } = useVendorCompliance() + return useMemo( + () => processingActivities.find((a) => a.id === activityId) ?? null, + [processingActivities, activityId] + ) +} + +export function useVendorContracts(vendorId: string | null) { + const { contracts } = useVendorCompliance() + return useMemo( + () => contracts.filter((c) => c.vendorId === vendorId), + [contracts, vendorId] + ) +} + +export function useVendorFindings(vendorId: string | null) { + const { findings } = useVendorCompliance() + return useMemo( + () => findings.filter((f) => f.vendorId === vendorId), + [findings, vendorId] + ) +} + +export function useContractFindings(contractId: string | null) { + const { findings } = useVendorCompliance() + return useMemo( + () => findings.filter((f) => f.contractId === contractId), + [findings, contractId] + ) +} + +export function useControlInstancesForEntity( + entityType: 'VENDOR' | 'PROCESSING_ACTIVITY', + entityId: string | null +) { + const { controlInstances, controls } = useVendorCompliance() + + return useMemo(() => { + if (!entityId) return [] + + return controlInstances + .filter((ci) => ci.entityType === entityType && ci.entityId === entityId) + .map((ci) => ({ + ...ci, + control: controls.find((c) => c.id === ci.controlId), + })) + }, [controlInstances, controls, entityType, entityId]) +} diff --git a/admin-compliance/lib/sdk/vendor-compliance/reducer.ts b/admin-compliance/lib/sdk/vendor-compliance/reducer.ts new file mode 100644 index 0000000..f4a5886 --- /dev/null +++ b/admin-compliance/lib/sdk/vendor-compliance/reducer.ts @@ -0,0 +1,178 @@ +import { + VendorComplianceState, + VendorComplianceAction, +} from './types' + +// ========================================== +// INITIAL STATE +// ========================================== + +export const initialState: VendorComplianceState = { + processingActivities: [], + vendors: [], + contracts: [], + findings: [], + controls: [], + controlInstances: [], + riskAssessments: [], + isLoading: false, + error: null, + selectedVendorId: null, + selectedActivityId: null, + activeTab: 'overview', + lastModified: null, +} + +// ========================================== +// REDUCER +// ========================================== + +export function vendorComplianceReducer( + state: VendorComplianceState, + action: VendorComplianceAction +): VendorComplianceState { + const updateState = (updates: Partial): VendorComplianceState => ({ + ...state, + ...updates, + lastModified: new Date(), + }) + + switch (action.type) { + // Processing Activities + case 'SET_PROCESSING_ACTIVITIES': + return updateState({ processingActivities: action.payload }) + + case 'ADD_PROCESSING_ACTIVITY': + return updateState({ + processingActivities: [...state.processingActivities, action.payload], + }) + + case 'UPDATE_PROCESSING_ACTIVITY': + return updateState({ + processingActivities: state.processingActivities.map((activity) => + activity.id === action.payload.id + ? { ...activity, ...action.payload.data, updatedAt: new Date() } + : activity + ), + }) + + case 'DELETE_PROCESSING_ACTIVITY': + return updateState({ + processingActivities: state.processingActivities.filter( + (activity) => activity.id !== action.payload + ), + }) + + // Vendors + case 'SET_VENDORS': + return updateState({ vendors: action.payload }) + + case 'ADD_VENDOR': + return updateState({ + vendors: [...state.vendors, action.payload], + }) + + case 'UPDATE_VENDOR': + return updateState({ + vendors: state.vendors.map((vendor) => + vendor.id === action.payload.id + ? { ...vendor, ...action.payload.data, updatedAt: new Date() } + : vendor + ), + }) + + case 'DELETE_VENDOR': + return updateState({ + vendors: state.vendors.filter((vendor) => vendor.id !== action.payload), + }) + + // Contracts + case 'SET_CONTRACTS': + return updateState({ contracts: action.payload }) + + case 'ADD_CONTRACT': + return updateState({ + contracts: [...state.contracts, action.payload], + }) + + case 'UPDATE_CONTRACT': + return updateState({ + contracts: state.contracts.map((contract) => + contract.id === action.payload.id + ? { ...contract, ...action.payload.data, updatedAt: new Date() } + : contract + ), + }) + + case 'DELETE_CONTRACT': + return updateState({ + contracts: state.contracts.filter((contract) => contract.id !== action.payload), + }) + + // Findings + case 'SET_FINDINGS': + return updateState({ findings: action.payload }) + + case 'ADD_FINDINGS': + return updateState({ + findings: [...state.findings, ...action.payload], + }) + + case 'UPDATE_FINDING': + return updateState({ + findings: state.findings.map((finding) => + finding.id === action.payload.id + ? { ...finding, ...action.payload.data, updatedAt: new Date() } + : finding + ), + }) + + // Controls + case 'SET_CONTROLS': + return updateState({ controls: action.payload }) + + case 'SET_CONTROL_INSTANCES': + return updateState({ controlInstances: action.payload }) + + case 'UPDATE_CONTROL_INSTANCE': + return updateState({ + controlInstances: state.controlInstances.map((instance) => + instance.id === action.payload.id + ? { ...instance, ...action.payload.data } + : instance + ), + }) + + // Risk Assessments + case 'SET_RISK_ASSESSMENTS': + return updateState({ riskAssessments: action.payload }) + + case 'UPDATE_RISK_ASSESSMENT': + return updateState({ + riskAssessments: state.riskAssessments.map((assessment) => + assessment.id === action.payload.id + ? { ...assessment, ...action.payload.data } + : assessment + ), + }) + + // UI State + case 'SET_LOADING': + return { ...state, isLoading: action.payload } + + case 'SET_ERROR': + return { ...state, error: action.payload } + + case 'SET_SELECTED_VENDOR': + return { ...state, selectedVendorId: action.payload } + + case 'SET_SELECTED_ACTIVITY': + return { ...state, selectedActivityId: action.payload } + + case 'SET_ACTIVE_TAB': + return { ...state, activeTab: action.payload } + + default: + return state + } +} diff --git a/admin-compliance/lib/sdk/vendor-compliance/use-actions.ts b/admin-compliance/lib/sdk/vendor-compliance/use-actions.ts new file mode 100644 index 0000000..e4c1ea9 --- /dev/null +++ b/admin-compliance/lib/sdk/vendor-compliance/use-actions.ts @@ -0,0 +1,448 @@ +'use client' + +import { useCallback } from 'react' + +import { + VendorComplianceState, + VendorComplianceAction, + ProcessingActivity, + Vendor, + ContractDocument, + Finding, + ControlInstance, + ExportFormat, +} from './types' + +const API_BASE = '/api/sdk/v1/vendor-compliance' + +/** + * Encapsulates all vendor-compliance API action callbacks. + * Called from the provider so that dispatch/state stay internal. + */ +export function useVendorComplianceActions( + state: VendorComplianceState, + dispatch: React.Dispatch +) { + // ========================================== + // DATA LOADING + // ========================================== + + const loadData = useCallback(async () => { + dispatch({ type: 'SET_LOADING', payload: true }) + dispatch({ type: 'SET_ERROR', payload: null }) + + try { + const [ + activitiesRes, vendorsRes, contractsRes, + findingsRes, controlsRes, controlInstancesRes, + ] = await Promise.all([ + fetch(`${API_BASE}/processing-activities`), + fetch(`${API_BASE}/vendors`), + fetch(`${API_BASE}/contracts`), + fetch(`${API_BASE}/findings`), + fetch(`${API_BASE}/controls`), + fetch(`${API_BASE}/control-instances`), + ]) + + if (activitiesRes.ok) { + const data = await activitiesRes.json() + dispatch({ type: 'SET_PROCESSING_ACTIVITIES', payload: data.data || [] }) + } + if (vendorsRes.ok) { + const data = await vendorsRes.json() + dispatch({ type: 'SET_VENDORS', payload: data.data || [] }) + } + if (contractsRes.ok) { + const data = await contractsRes.json() + dispatch({ type: 'SET_CONTRACTS', payload: data.data || [] }) + } + if (findingsRes.ok) { + const data = await findingsRes.json() + dispatch({ type: 'SET_FINDINGS', payload: data.data || [] }) + } + if (controlsRes.ok) { + const data = await controlsRes.json() + dispatch({ type: 'SET_CONTROLS', payload: data.data || [] }) + } + if (controlInstancesRes.ok) { + const data = await controlInstancesRes.json() + dispatch({ type: 'SET_CONTROL_INSTANCES', payload: data.data || [] }) + } + } catch (error) { + console.error('Failed to load vendor compliance data:', error) + dispatch({ type: 'SET_ERROR', payload: 'Fehler beim Laden der Daten' }) + } finally { + dispatch({ type: 'SET_LOADING', payload: false }) + } + }, [dispatch]) + + const refresh = useCallback(async () => { + await loadData() + }, [loadData]) + + // ========================================== + // PROCESSING ACTIVITIES + // ========================================== + + const createProcessingActivity = useCallback( + async ( + data: Omit + ): Promise => { + const response = await fetch(`${API_BASE}/processing-activities`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }) + if (!response.ok) { + const error = await response.json() + throw new Error(error.error || 'Fehler beim Erstellen der Verarbeitungstätigkeit') + } + const result = await response.json() + dispatch({ type: 'ADD_PROCESSING_ACTIVITY', payload: result.data }) + return result.data + }, + [dispatch] + ) + + const updateProcessingActivity = useCallback( + async (id: string, data: Partial): Promise => { + const response = await fetch(`${API_BASE}/processing-activities/${id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }) + if (!response.ok) { + const error = await response.json() + throw new Error(error.error || 'Fehler beim Aktualisieren der Verarbeitungstätigkeit') + } + dispatch({ type: 'UPDATE_PROCESSING_ACTIVITY', payload: { id, data } }) + }, + [dispatch] + ) + + const deleteProcessingActivity = useCallback( + async (id: string): Promise => { + const response = await fetch(`${API_BASE}/processing-activities/${id}`, { + method: 'DELETE', + }) + if (!response.ok) { + const error = await response.json() + throw new Error(error.error || 'Fehler beim Löschen der Verarbeitungstätigkeit') + } + dispatch({ type: 'DELETE_PROCESSING_ACTIVITY', payload: id }) + }, + [dispatch] + ) + + const duplicateProcessingActivity = useCallback( + async (id: string): Promise => { + const original = state.processingActivities.find((a) => a.id === id) + if (!original) { + throw new Error('Verarbeitungstätigkeit nicht gefunden') + } + const { id: _id, vvtId: _vvtId, createdAt: _createdAt, updatedAt: _updatedAt, tenantId: _tenantId, ...rest } = original + return createProcessingActivity({ + ...rest, + vvtId: '', + name: { + de: `${original.name.de} (Kopie)`, + en: `${original.name.en} (Copy)`, + }, + status: 'DRAFT', + }) + }, + [state.processingActivities, createProcessingActivity] + ) + + // ========================================== + // VENDORS + // ========================================== + + const createVendor = useCallback( + async ( + data: Omit + ): Promise => { + const response = await fetch(`${API_BASE}/vendors`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }) + if (!response.ok) { + const error = await response.json() + throw new Error(error.error || 'Fehler beim Erstellen des Vendors') + } + const result = await response.json() + dispatch({ type: 'ADD_VENDOR', payload: result.data }) + return result.data + }, + [dispatch] + ) + + const updateVendor = useCallback( + async (id: string, data: Partial): Promise => { + const response = await fetch(`${API_BASE}/vendors/${id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }) + if (!response.ok) { + const error = await response.json() + throw new Error(error.error || 'Fehler beim Aktualisieren des Vendors') + } + dispatch({ type: 'UPDATE_VENDOR', payload: { id, data } }) + }, + [dispatch] + ) + + const deleteVendor = useCallback( + async (id: string): Promise => { + const response = await fetch(`${API_BASE}/vendors/${id}`, { + method: 'DELETE', + }) + if (!response.ok) { + const error = await response.json() + throw new Error(error.error || 'Fehler beim Löschen des Vendors') + } + dispatch({ type: 'DELETE_VENDOR', payload: id }) + }, + [dispatch] + ) + + // ========================================== + // CONTRACTS + // ========================================== + + const uploadContract = useCallback( + async ( + vendorId: string, + file: File, + metadata: Partial + ): Promise => { + const formData = new FormData() + formData.append('file', file) + formData.append('vendorId', vendorId) + formData.append('metadata', JSON.stringify(metadata)) + + const response = await fetch(`${API_BASE}/contracts`, { + method: 'POST', + body: formData, + }) + if (!response.ok) { + const error = await response.json() + throw new Error(error.error || 'Fehler beim Hochladen des Vertrags') + } + const result = await response.json() + const contract = result.data + dispatch({ type: 'ADD_CONTRACT', payload: contract }) + + const vendor = state.vendors.find((v) => v.id === vendorId) + if (vendor) { + dispatch({ + type: 'UPDATE_VENDOR', + payload: { id: vendorId, data: { contracts: [...vendor.contracts, contract.id] } }, + }) + } + return contract + }, + [dispatch, state.vendors] + ) + + const updateContract = useCallback( + async (id: string, data: Partial): Promise => { + const response = await fetch(`${API_BASE}/contracts/${id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }) + if (!response.ok) { + const error = await response.json() + throw new Error(error.error || 'Fehler beim Aktualisieren des Vertrags') + } + dispatch({ type: 'UPDATE_CONTRACT', payload: { id, data } }) + }, + [dispatch] + ) + + const deleteContract = useCallback( + async (id: string): Promise => { + const contract = state.contracts.find((c) => c.id === id) + const response = await fetch(`${API_BASE}/contracts/${id}`, { + method: 'DELETE', + }) + if (!response.ok) { + const error = await response.json() + throw new Error(error.error || 'Fehler beim Löschen des Vertrags') + } + dispatch({ type: 'DELETE_CONTRACT', payload: id }) + + if (contract) { + const vendor = state.vendors.find((v) => v.id === contract.vendorId) + if (vendor) { + dispatch({ + type: 'UPDATE_VENDOR', + payload: { id: vendor.id, data: { contracts: vendor.contracts.filter((cId) => cId !== id) } }, + }) + } + } + }, + [dispatch, state.contracts, state.vendors] + ) + + const startContractReview = useCallback( + async (contractId: string): Promise => { + dispatch({ + type: 'UPDATE_CONTRACT', + payload: { id: contractId, data: { reviewStatus: 'IN_PROGRESS' } }, + }) + const response = await fetch(`${API_BASE}/contracts/${contractId}/review`, { + method: 'POST', + }) + if (!response.ok) { + dispatch({ + type: 'UPDATE_CONTRACT', + payload: { id: contractId, data: { reviewStatus: 'FAILED' } }, + }) + const error = await response.json() + throw new Error(error.error || 'Fehler beim Starten der Vertragsprüfung') + } + const result = await response.json() + dispatch({ + type: 'UPDATE_CONTRACT', + payload: { + id: contractId, + data: { + reviewStatus: 'COMPLETED', + reviewCompletedAt: new Date(), + complianceScore: result.data.complianceScore, + }, + }, + }) + if (result.data.findings && result.data.findings.length > 0) { + dispatch({ type: 'ADD_FINDINGS', payload: result.data.findings }) + } + }, + [dispatch] + ) + + // ========================================== + // FINDINGS + // ========================================== + + const updateFinding = useCallback( + async (id: string, data: Partial): Promise => { + const response = await fetch(`${API_BASE}/findings/${id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }) + if (!response.ok) { + const error = await response.json() + throw new Error(error.error || 'Fehler beim Aktualisieren des Findings') + } + dispatch({ type: 'UPDATE_FINDING', payload: { id, data } }) + }, + [dispatch] + ) + + const resolveFinding = useCallback( + async (id: string, resolution: string): Promise => { + await updateFinding(id, { + status: 'RESOLVED', + resolution, + resolvedAt: new Date(), + }) + }, + [updateFinding] + ) + + // ========================================== + // CONTROLS + // ========================================== + + const updateControlInstance = useCallback( + async (id: string, data: Partial): Promise => { + const response = await fetch(`${API_BASE}/control-instances/${id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }) + if (!response.ok) { + const error = await response.json() + throw new Error(error.error || 'Fehler beim Aktualisieren des Control-Status') + } + dispatch({ type: 'UPDATE_CONTROL_INSTANCE', payload: { id, data } }) + }, + [dispatch] + ) + + // ========================================== + // EXPORTS + // ========================================== + + const exportVVT = useCallback( + async (format: ExportFormat, activityIds?: string[]): Promise => { + const params = new URLSearchParams({ format }) + if (activityIds && activityIds.length > 0) { + params.append('activityIds', activityIds.join(',')) + } + const response = await fetch(`${API_BASE}/export/vvt?${params}`) + if (!response.ok) { + const error = await response.json() + throw new Error(error.error || 'Fehler beim Exportieren des VVT') + } + const blob = await response.blob() + return URL.createObjectURL(blob) + }, + [] + ) + + const exportVendorAuditPack = useCallback( + async (vendorId: string, format: ExportFormat): Promise => { + const params = new URLSearchParams({ format, vendorId }) + const response = await fetch(`${API_BASE}/export/vendor-audit?${params}`) + if (!response.ok) { + const error = await response.json() + throw new Error(error.error || 'Fehler beim Exportieren des Vendor Audit Packs') + } + const blob = await response.blob() + return URL.createObjectURL(blob) + }, + [] + ) + + const exportRoPA = useCallback( + async (format: ExportFormat): Promise => { + const params = new URLSearchParams({ format }) + const response = await fetch(`${API_BASE}/export/ropa?${params}`) + if (!response.ok) { + const error = await response.json() + throw new Error(error.error || 'Fehler beim Exportieren des RoPA') + } + const blob = await response.blob() + return URL.createObjectURL(blob) + }, + [] + ) + + return { + loadData, + refresh, + createProcessingActivity, + updateProcessingActivity, + deleteProcessingActivity, + duplicateProcessingActivity, + createVendor, + updateVendor, + deleteVendor, + uploadContract, + updateContract, + deleteContract, + startContractReview, + updateFinding, + resolveFinding, + updateControlInstance, + exportVVT, + exportVendorAuditPack, + exportRoPA, + } +}