refactor(admin): split 9 more oversized lib/ files into focused modules

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) <noreply@anthropic.com>
This commit is contained in:
Sharang Parnerkar
2026-04-10 19:12:09 +02:00
parent 786bb409e4
commit 58e95d5e8e
29 changed files with 5785 additions and 4383 deletions

View File

@@ -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<T>(
url: string,
options: RequestInit = {},
timeout: number = API_TIMEOUT
): Promise<T> {
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)
}

View File

@@ -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' },
],
},
]

View File

@@ -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' },
],
},
]

View File

@@ -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' },
],
},
]

View File

@@ -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' },
],
},
]

View File

@@ -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<void> {
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<DSRRequest | null> {
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<void> {
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<string, any>): Promise<DSRRequest> {
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())
}

View File

@@ -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
}
}

View File

@@ -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') || '',
}
}

View File

@@ -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<DSRRequest> {
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<DSRRequest> {
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<DSRRequest> {
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<DSRRequest> {
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<DSRRequest> {
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<any[]> {
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<any> {
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<any[]> {
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<any[]> {
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<any> {
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<any[]> {
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()
}

View File

@@ -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<void> {
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<DSRRequest | null> {
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<void> {
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<DSRRequest> {
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<DSRRequest> {
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<DSRRequest> {
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<DSRRequest> {
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<DSRRequest> {
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<any[]> {
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<any> {
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<any[]> {
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<any[]> {
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<any> {
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<any[]> {
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<string, any>): Promise<DSRRequest> {
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'

View File

@@ -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<EinwilligungenAction>
// Computed Values
allDataPoints: DataPoint[]
selectedDataPointsData: DataPoint[]
dataPointsByCategory: Record<DataPointCategory, DataPoint[]>
categoryStats: Record<DataPointCategory, number>
riskStats: Record<RiskLevel, number>
legalBasisStats: Record<LegalBasis, number>
// Actions
initializeCatalog: (tenantId: string) => void
loadCatalog: (tenantId: string) => Promise<void>
saveCatalog: () => Promise<void>
toggleDataPoint: (id: string) => void
addCustomDataPoint: (dataPoint: DataPoint) => void
updateDataPoint: (id: string, data: Partial<DataPoint>) => void
deleteCustomDataPoint: (id: string) => void
setActiveTab: (tab: EinwilligungenTab) => void
setPreviewLanguage: (language: SupportedLanguage) => void
setPreviewFormat: (format: ExportFormat) => void
setCompanyInfo: (info: CompanyInfo) => void
generatePrivacyPolicy: () => Promise<void>
generateCookieBannerConfig: () => void
}
const EinwilligungenContext = createContext<EinwilligungenContextValue | null>(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<Record<DataPointCategory, DataPoint[]>> = {}
// 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<DataPointCategory, DataPoint[]>
}, [selectedDataPointsData])
const categoryStats = useMemo(() => {
const counts: Partial<Record<DataPointCategory, number>> = {}
for (const dp of selectedDataPointsData) {
counts[dp.category] = (counts[dp.category] || 0) + 1
}
return counts as Record<DataPointCategory, number>
}, [selectedDataPointsData])
const riskStats = useMemo(() => {
const counts: Record<RiskLevel, number> = { 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<LegalBasis, number> = {
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<DataPoint>) => {
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 (
<EinwilligungenContext.Provider value={value}>{children}</EinwilligungenContext.Provider>
)
}
// =============================================================================
// 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'

View File

@@ -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
}

View File

@@ -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<EinwilligungenAction>
// Computed Values
allDataPoints: DataPoint[]
selectedDataPointsData: DataPoint[]
dataPointsByCategory: Record<DataPointCategory, DataPoint[]>
categoryStats: Record<DataPointCategory, number>
riskStats: Record<RiskLevel, number>
legalBasisStats: Record<LegalBasis, number>
// Actions
initializeCatalog: (tenantId: string) => void
loadCatalog: (tenantId: string) => Promise<void>
saveCatalog: () => Promise<void>
toggleDataPoint: (id: string) => void
addCustomDataPoint: (dataPoint: DataPoint) => void
updateDataPoint: (id: string, data: Partial<DataPoint>) => void
deleteCustomDataPoint: (id: string) => void
setActiveTab: (tab: EinwilligungenTab) => void
setPreviewLanguage: (language: SupportedLanguage) => void
setPreviewFormat: (format: ExportFormat) => void
setCompanyInfo: (info: CompanyInfo) => void
generatePrivacyPolicy: () => Promise<void>
generateCookieBannerConfig: () => void
}
export const EinwilligungenContext = createContext<EinwilligungenContextValue | null>(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<Record<DataPointCategory, DataPoint[]>> = {}
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<DataPointCategory, DataPoint[]>
}, [selectedDataPointsData])
const categoryStats = useMemo(() => {
const counts: Partial<Record<DataPointCategory, number>> = {}
for (const dp of selectedDataPointsData) {
counts[dp.category] = (counts[dp.category] || 0) + 1
}
return counts as Record<DataPointCategory, number>
}, [selectedDataPointsData])
const riskStats = useMemo(() => {
const counts: Record<RiskLevel, number> = { LOW: 0, MEDIUM: 0, HIGH: 0 }
for (const dp of selectedDataPointsData) {
counts[dp.riskLevel]++
}
return counts
}, [selectedDataPointsData])
const legalBasisStats = useMemo(() => {
const counts: Record<LegalBasis, number> = {
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<DataPoint>) => {
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 (
<EinwilligungenContext.Provider value={value}>{children}</EinwilligungenContext.Provider>
)
}

View File

@@ -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
}
}

View File

@@ -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<Blob> {
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<string, [number, number, number]> = {
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')
}

View File

@@ -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<Blob> {
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' })
}

View File

@@ -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<Blob> {
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<string, [number, number, number]> = {
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<Blob> {
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<void> {
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}`)

View File

@@ -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<T>(
url: string,
options: RequestInit = {},
timeout: number = API_TIMEOUT
): Promise<T> {
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)
}
}

View File

@@ -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<IncidentListResponse> {
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<IncidentListResponse>(url)
}
/**
* Einzelnen Vorfall per ID abrufen
*/
export async function fetchIncident(id: string): Promise<Incident> {
return fetchWithTimeout<Incident>(`${INCIDENTS_API_BASE}/api/v1/incidents/${id}`)
}
/**
* Neuen Vorfall erstellen
*/
export async function createIncident(request: IncidentCreateRequest): Promise<Incident> {
return fetchWithTimeout<Incident>(`${INCIDENTS_API_BASE}/api/v1/incidents`, {
method: 'POST',
body: JSON.stringify(request)
})
}
/**
* Vorfall aktualisieren
*/
export async function updateIncident(id: string, update: IncidentUpdateRequest): Promise<Incident> {
return fetchWithTimeout<Incident>(`${INCIDENTS_API_BASE}/api/v1/incidents/${id}`, {
method: 'PUT',
body: JSON.stringify(update)
})
}
/**
* Vorfall loeschen (Soft Delete)
*/
export async function deleteIncident(id: string): Promise<void> {
await fetchWithTimeout<void>(`${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<Incident> {
return fetchWithTimeout<Incident>(
`${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<Blob> {
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<AuthorityNotification>
): Promise<Incident> {
return fetchWithTimeout<Incident>(
`${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<DataSubjectNotification>
): Promise<Incident> {
return fetchWithTimeout<Incident>(
`${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<IncidentMeasure, 'id' | 'incidentId'>
): Promise<Incident> {
return fetchWithTimeout<Incident>(
`${INCIDENTS_API_BASE}/api/v1/incidents/${incidentId}/measures`,
{
method: 'POST',
body: JSON.stringify(measure)
}
)
}
/**
* Massnahme aktualisieren
*/
export async function updateMeasure(
measureId: string,
update: Partial<IncidentMeasure>
): Promise<IncidentMeasure> {
return fetchWithTimeout<IncidentMeasure>(
`${INCIDENTS_API_BASE}/api/v1/measures/${measureId}`,
{
method: 'PUT',
body: JSON.stringify(update)
}
)
}
/**
* Massnahme als abgeschlossen markieren
*/
export async function completeMeasure(measureId: string): Promise<IncidentMeasure> {
return fetchWithTimeout<IncidentMeasure>(
`${INCIDENTS_API_BASE}/api/v1/measures/${measureId}/complete`,
{
method: 'POST'
}
)
}
// =============================================================================
// TIMELINE
// =============================================================================
/**
* Zeitleisteneintrag hinzufuegen
*/
export async function addTimelineEntry(
incidentId: string,
entry: Omit<TimelineEntry, 'id' | 'incidentId'>
): Promise<Incident> {
return fetchWithTimeout<Incident>(
`${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<Incident> {
return fetchWithTimeout<Incident>(
`${INCIDENTS_API_BASE}/api/v1/incidents/${incidentId}/close`,
{
method: 'POST',
body: JSON.stringify({ lessonsLearned })
}
)
}
// =============================================================================
// STATISTICS
// =============================================================================
/**
* Vorfall-Statistiken abrufen
*/
export async function fetchIncidentStatistics(): Promise<IncidentStatistics> {
return fetchWithTimeout<IncidentStatistics>(
`${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 = <K extends string>(items: { [key: string]: unknown }[], field: string): Record<K, number> => {
const result: Record<string, number> = {}
items.forEach(item => {
const key = String(item[field])
result[key] = (result[key] || 0) + 1
})
return result as Record<K, number>
}
const statusCounts = countBy<IncidentStatus>(incidents as unknown as { [key: string]: unknown }[], 'status')
const severityCounts = countBy<IncidentSeverity>(incidents as unknown as { [key: string]: unknown }[], 'severity')
const categoryCounts = countBy<IncidentCategory>(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
}
}
}

View File

@@ -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
}
}
}

View File

@@ -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<T>(
url: string,
options: RequestInit = {},
timeout: number = API_TIMEOUT
): Promise<T> {
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<IncidentListResponse> {
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<IncidentListResponse>(url)
}
/**
* Einzelnen Vorfall per ID abrufen
*/
export async function fetchIncident(id: string): Promise<Incident> {
return fetchWithTimeout<Incident>(`${INCIDENTS_API_BASE}/api/v1/incidents/${id}`)
}
/**
* Neuen Vorfall erstellen
*/
export async function createIncident(request: IncidentCreateRequest): Promise<Incident> {
return fetchWithTimeout<Incident>(`${INCIDENTS_API_BASE}/api/v1/incidents`, {
method: 'POST',
body: JSON.stringify(request)
})
}
/**
* Vorfall aktualisieren
*/
export async function updateIncident(id: string, update: IncidentUpdateRequest): Promise<Incident> {
return fetchWithTimeout<Incident>(`${INCIDENTS_API_BASE}/api/v1/incidents/${id}`, {
method: 'PUT',
body: JSON.stringify(update)
})
}
/**
* Vorfall loeschen (Soft Delete)
*/
export async function deleteIncident(id: string): Promise<void> {
await fetchWithTimeout<void>(`${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<Incident> {
return fetchWithTimeout<Incident>(
`${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<Blob> {
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<AuthorityNotification>
): Promise<Incident> {
return fetchWithTimeout<Incident>(
`${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<DataSubjectNotification>
): Promise<Incident> {
return fetchWithTimeout<Incident>(
`${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<IncidentMeasure, 'id' | 'incidentId'>
): Promise<Incident> {
return fetchWithTimeout<Incident>(
`${INCIDENTS_API_BASE}/api/v1/incidents/${incidentId}/measures`,
{
method: 'POST',
body: JSON.stringify(measure)
}
)
}
/**
* Massnahme aktualisieren
*/
export async function updateMeasure(
measureId: string,
update: Partial<IncidentMeasure>
): Promise<IncidentMeasure> {
return fetchWithTimeout<IncidentMeasure>(
`${INCIDENTS_API_BASE}/api/v1/measures/${measureId}`,
{
method: 'PUT',
body: JSON.stringify(update)
}
)
}
/**
* Massnahme als abgeschlossen markieren
*/
export async function completeMeasure(measureId: string): Promise<IncidentMeasure> {
return fetchWithTimeout<IncidentMeasure>(
`${INCIDENTS_API_BASE}/api/v1/measures/${measureId}/complete`,
{
method: 'POST'
}
)
}
// =============================================================================
// TIMELINE
// =============================================================================
/**
* Zeitleisteneintrag hinzufuegen
*/
export async function addTimelineEntry(
incidentId: string,
entry: Omit<TimelineEntry, 'id' | 'incidentId'>
): Promise<Incident> {
return fetchWithTimeout<Incident>(
`${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<Incident> {
return fetchWithTimeout<Incident>(
`${INCIDENTS_API_BASE}/api/v1/incidents/${incidentId}/close`,
{
method: 'POST',
body: JSON.stringify({ lessonsLearned })
}
)
}
// =============================================================================
// STATISTICS
// =============================================================================
/**
* Vorfall-Statistiken abrufen
*/
export async function fetchIncidentStatistics(): Promise<IncidentStatistics> {
return fetchWithTimeout<IncidentStatistics>(
`${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 = <K extends string>(items: { [key: string]: unknown }[], field: string): Record<K, number> => {
const result: Record<string, number> = {}
items.forEach(item => {
const key = String(item[field])
result[key] = (result[key] || 0) + 1
})
return result as Record<K, number>
}
const statusCounts = countBy<IncidentStatus>(incidents as unknown as { [key: string]: unknown }[], 'status')
const severityCounts = countBy<IncidentSeverity>(incidents as unknown as { [key: string]: unknown }[], 'severity')
const categoryCounts = countBy<IncidentCategory>(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'

View File

@@ -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<CompanyProfile> }
| { type: 'SET_DATA_PROFILE'; payload: DataProfile }
| { type: 'UPDATE_DATA_PROFILE'; payload: Partial<DataProfile> }
| { type: 'SET_ARCHITECTURE_PROFILE'; payload: ArchitectureProfile }
| { type: 'UPDATE_ARCHITECTURE_PROFILE'; payload: Partial<ArchitectureProfile> }
| { type: 'SET_SECURITY_PROFILE'; payload: SecurityProfile }
| { type: 'UPDATE_SECURITY_PROFILE'; payload: Partial<SecurityProfile> }
| { type: 'SET_RISK_PROFILE'; payload: RiskProfile }
| { type: 'UPDATE_RISK_PROFILE'; payload: Partial<RiskProfile> }
| { 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<EvidenceDocument> } }
| { type: 'DELETE_EVIDENCE'; payload: string }
| { type: 'SET_DERIVED_TOMS'; payload: DerivedTOM[] }
| { type: 'UPDATE_DERIVED_TOM'; payload: { id: string; data: Partial<DerivedTOM> } }
| { type: 'SET_GAP_ANALYSIS'; payload: GapAnalysisResult }
| { type: 'ADD_EXPORT'; payload: ExportRecord }
| { type: 'BULK_UPDATE_TOMS'; payload: { updates: Array<{ id: string; data: Partial<DerivedTOM> }> } }
| { type: 'LOAD_STATE'; payload: TOMGeneratorState }
// =============================================================================
// REDUCER
// =============================================================================
function tomGeneratorReducer(
state: TOMGeneratorState,
action: TOMGeneratorAction
): TOMGeneratorState {
const updateState = (updates: Partial<TOMGeneratorState>): 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<TOMGeneratorAction>
// 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<CompanyProfile>) => void
setDataProfile: (profile: DataProfile) => void
updateDataProfile: (data: Partial<DataProfile>) => void
setArchitectureProfile: (profile: ArchitectureProfile) => void
updateArchitectureProfile: (data: Partial<ArchitectureProfile>) => void
setSecurityProfile: (profile: SecurityProfile) => void
updateSecurityProfile: (data: Partial<SecurityProfile>) => void
setRiskProfile: (profile: RiskProfile) => void
updateRiskProfile: (data: Partial<RiskProfile>) => void
// Evidence management
addEvidence: (document: EvidenceDocument) => void
updateEvidence: (id: string, data: Partial<EvidenceDocument>) => void
deleteEvidence: (id: string) => void
// TOM derivation
deriveTOMs: () => void
updateDerivedTOM: (id: string, data: Partial<DerivedTOM>) => void
bulkUpdateTOMs: (updates: Array<{ id: string; data: Partial<DerivedTOM> }>) => void
// Gap analysis
runGapAnalysis: () => void
// Export
addExport: (record: ExportRecord) => void
// Persistence
saveState: () => Promise<void>
loadState: () => Promise<void>
resetState: () => void
// Status
isStepCompleted: (stepId: TOMGeneratorStepId) => boolean
getCompletionPercentage: () => number
isLoading: boolean
error: string | null
}
// =============================================================================
// CONTEXT
// =============================================================================
const TOMGeneratorContext = createContext<TOMGeneratorContextValue | null>(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<string | null>(null)
const rulesEngineRef = useRef<TOMRulesEngine | null>(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<CompanyProfile>) => {
dispatch({ type: 'UPDATE_COMPANY_PROFILE', payload: data })
}, [])
const setDataProfile = useCallback((profile: DataProfile) => {
dispatch({ type: 'SET_DATA_PROFILE', payload: profile })
}, [])
const updateDataProfile = useCallback((data: Partial<DataProfile>) => {
dispatch({ type: 'UPDATE_DATA_PROFILE', payload: data })
}, [])
const setArchitectureProfile = useCallback((profile: ArchitectureProfile) => {
dispatch({ type: 'SET_ARCHITECTURE_PROFILE', payload: profile })
}, [])
const updateArchitectureProfile = useCallback(
(data: Partial<ArchitectureProfile>) => {
dispatch({ type: 'UPDATE_ARCHITECTURE_PROFILE', payload: data })
},
[]
)
const setSecurityProfile = useCallback((profile: SecurityProfile) => {
dispatch({ type: 'SET_SECURITY_PROFILE', payload: profile })
}, [])
const updateSecurityProfile = useCallback((data: Partial<SecurityProfile>) => {
dispatch({ type: 'UPDATE_SECURITY_PROFILE', payload: data })
}, [])
const setRiskProfile = useCallback((profile: RiskProfile) => {
dispatch({ type: 'SET_RISK_PROFILE', payload: profile })
}, [])
const updateRiskProfile = useCallback((data: Partial<RiskProfile>) => {
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<EvidenceDocument>) => {
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<DerivedTOM>) => {
dispatch({ type: 'UPDATE_DERIVED_TOM', payload: { id, data } })
},
[]
)
const bulkUpdateTOMs = useCallback(
(updates: Array<{ id: string; data: Partial<DerivedTOM> }>) => {
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 (
<TOMGeneratorContext.Provider value={contextValue}>
{children}
</TOMGeneratorContext.Provider>
)
}
// =============================================================================
// 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'

View File

@@ -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
}

View File

@@ -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<TOMGeneratorAction>
// 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<CompanyProfile>) => void
setDataProfile: (profile: DataProfile) => void
updateDataProfile: (data: Partial<DataProfile>) => void
setArchitectureProfile: (profile: ArchitectureProfile) => void
updateArchitectureProfile: (data: Partial<ArchitectureProfile>) => void
setSecurityProfile: (profile: SecurityProfile) => void
updateSecurityProfile: (data: Partial<SecurityProfile>) => void
setRiskProfile: (profile: RiskProfile) => void
updateRiskProfile: (data: Partial<RiskProfile>) => void
// Evidence management
addEvidence: (document: EvidenceDocument) => void
updateEvidence: (id: string, data: Partial<EvidenceDocument>) => void
deleteEvidence: (id: string) => void
// TOM derivation
deriveTOMs: () => void
updateDerivedTOM: (id: string, data: Partial<DerivedTOM>) => void
bulkUpdateTOMs: (updates: Array<{ id: string; data: Partial<DerivedTOM> }>) => void
// Gap analysis
runGapAnalysis: () => void
// Export
addExport: (record: ExportRecord) => void
// Persistence
saveState: () => Promise<void>
loadState: () => Promise<void>
resetState: () => void
// Status
isStepCompleted: (stepId: TOMGeneratorStepId) => boolean
getCompletionPercentage: () => number
isLoading: boolean
error: string | null
}
// =============================================================================
// CONTEXT
// =============================================================================
export const TOMGeneratorContext = createContext<TOMGeneratorContextValue | null>(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<string | null>(null)
const rulesEngineRef = useRef<TOMRulesEngine | null>(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<CompanyProfile>) => {
dispatch({ type: 'UPDATE_COMPANY_PROFILE', payload: data })
}, [])
const setDataProfile = useCallback((profile: DataProfile) => {
dispatch({ type: 'SET_DATA_PROFILE', payload: profile })
}, [])
const updateDataProfile = useCallback((data: Partial<DataProfile>) => {
dispatch({ type: 'UPDATE_DATA_PROFILE', payload: data })
}, [])
const setArchitectureProfile = useCallback((profile: ArchitectureProfile) => {
dispatch({ type: 'SET_ARCHITECTURE_PROFILE', payload: profile })
}, [])
const updateArchitectureProfile = useCallback(
(data: Partial<ArchitectureProfile>) => {
dispatch({ type: 'UPDATE_ARCHITECTURE_PROFILE', payload: data })
},
[]
)
const setSecurityProfile = useCallback((profile: SecurityProfile) => {
dispatch({ type: 'SET_SECURITY_PROFILE', payload: profile })
}, [])
const updateSecurityProfile = useCallback((data: Partial<SecurityProfile>) => {
dispatch({ type: 'UPDATE_SECURITY_PROFILE', payload: data })
}, [])
const setRiskProfile = useCallback((profile: RiskProfile) => {
dispatch({ type: 'SET_RISK_PROFILE', payload: profile })
}, [])
const updateRiskProfile = useCallback((data: Partial<RiskProfile>) => {
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<EvidenceDocument>) => {
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<DerivedTOM>) => {
dispatch({ type: 'UPDATE_DERIVED_TOM', payload: { id, data } })
},
[]
)
const bulkUpdateTOMs = useCallback(
(updates: Array<{ id: string; data: Partial<DerivedTOM> }>) => {
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 (
<TOMGeneratorContext.Provider value={contextValue}>
{children}
</TOMGeneratorContext.Provider>
)
}

View File

@@ -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<CompanyProfile> }
| { type: 'SET_DATA_PROFILE'; payload: DataProfile }
| { type: 'UPDATE_DATA_PROFILE'; payload: Partial<DataProfile> }
| { type: 'SET_ARCHITECTURE_PROFILE'; payload: ArchitectureProfile }
| { type: 'UPDATE_ARCHITECTURE_PROFILE'; payload: Partial<ArchitectureProfile> }
| { type: 'SET_SECURITY_PROFILE'; payload: SecurityProfile }
| { type: 'UPDATE_SECURITY_PROFILE'; payload: Partial<SecurityProfile> }
| { type: 'SET_RISK_PROFILE'; payload: RiskProfile }
| { type: 'UPDATE_RISK_PROFILE'; payload: Partial<RiskProfile> }
| { 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<EvidenceDocument> } }
| { type: 'DELETE_EVIDENCE'; payload: string }
| { type: 'SET_DERIVED_TOMS'; payload: DerivedTOM[] }
| { type: 'UPDATE_DERIVED_TOM'; payload: { id: string; data: Partial<DerivedTOM> } }
| { type: 'SET_GAP_ANALYSIS'; payload: GapAnalysisResult }
| { type: 'ADD_EXPORT'; payload: ExportRecord }
| { type: 'BULK_UPDATE_TOMS'; payload: { updates: Array<{ id: string; data: Partial<DerivedTOM> }> } }
| { type: 'LOAD_STATE'; payload: TOMGeneratorState }
// =============================================================================
// REDUCER
// =============================================================================
export function tomGeneratorReducer(
state: TOMGeneratorState,
action: TOMGeneratorAction
): TOMGeneratorState {
const updateState = (updates: Partial<TOMGeneratorState>): 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
}
}

View File

@@ -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>): 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<VendorComplianceContextValue | null>(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<ProcessingActivity, 'id' | 'tenantId' | 'createdAt' | 'updatedAt'>
): Promise<ProcessingActivity> => {
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<ProcessingActivity>): Promise<void> => {
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<void> => {
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<ProcessingActivity> => {
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<Vendor, 'id' | 'tenantId' | 'createdAt' | 'updatedAt'>
): Promise<Vendor> => {
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<Vendor>): Promise<void> => {
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<void> => {
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<ContractDocument>
): Promise<ContractDocument> => {
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<ContractDocument>): Promise<void> => {
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<void> => {
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<void> => {
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<Finding>): Promise<void> => {
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<void> => {
await updateFinding(id, {
status: 'RESOLVED',
resolution,
resolvedAt: new Date(),
})
},
[updateFinding]
)
// ==========================================
// CONTROL ACTIONS
// ==========================================
const updateControlInstance = useCallback(
async (id: string, data: Partial<ControlInstance>): Promise<void> => {
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<string> => {
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<string> => {
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<string> => {
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({
</VendorComplianceContext.Provider>
)
}
// ==========================================
// 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])
}

View File

@@ -0,0 +1,88 @@
'use client'
import { useContext, useMemo, createContext } from 'react'
import { VendorComplianceContextValue } from './types'
// ==========================================
// CONTEXT
// ==========================================
export const VendorComplianceContext = createContext<VendorComplianceContextValue | null>(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])
}

View File

@@ -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>): 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
}
}

View File

@@ -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<VendorComplianceAction>
) {
// ==========================================
// 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<ProcessingActivity, 'id' | 'tenantId' | 'createdAt' | 'updatedAt'>
): Promise<ProcessingActivity> => {
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<ProcessingActivity>): Promise<void> => {
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<void> => {
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<ProcessingActivity> => {
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<Vendor, 'id' | 'tenantId' | 'createdAt' | 'updatedAt'>
): Promise<Vendor> => {
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<Vendor>): Promise<void> => {
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<void> => {
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<ContractDocument>
): Promise<ContractDocument> => {
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<ContractDocument>): Promise<void> => {
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<void> => {
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<void> => {
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<Finding>): Promise<void> => {
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<void> => {
await updateFinding(id, {
status: 'RESOLVED',
resolution,
resolvedAt: new Date(),
})
},
[updateFinding]
)
// ==========================================
// CONTROLS
// ==========================================
const updateControlInstance = useCallback(
async (id: string, data: Partial<ControlInstance>): Promise<void> => {
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<string> => {
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<string> => {
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<string> => {
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,
}
}