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:
165
admin-compliance/lib/sdk/academy/api-helpers.ts
Normal file
165
admin-compliance/lib/sdk/academy/api-helpers.ts
Normal 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)
|
||||
}
|
||||
383
admin-compliance/lib/sdk/api-docs/endpoints-go.ts
Normal file
383
admin-compliance/lib/sdk/api-docs/endpoints-go.ts
Normal 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' },
|
||||
],
|
||||
},
|
||||
]
|
||||
191
admin-compliance/lib/sdk/api-docs/endpoints-python-core.ts
Normal file
191
admin-compliance/lib/sdk/api-docs/endpoints-python-core.ts
Normal 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' },
|
||||
],
|
||||
},
|
||||
]
|
||||
262
admin-compliance/lib/sdk/api-docs/endpoints-python-gdpr.ts
Normal file
262
admin-compliance/lib/sdk/api-docs/endpoints-python-gdpr.ts
Normal 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' },
|
||||
],
|
||||
},
|
||||
]
|
||||
449
admin-compliance/lib/sdk/api-docs/endpoints-python-ops.ts
Normal file
449
admin-compliance/lib/sdk/api-docs/endpoints-python-ops.ts
Normal 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' },
|
||||
],
|
||||
},
|
||||
]
|
||||
146
admin-compliance/lib/sdk/dsr/api-crud.ts
Normal file
146
admin-compliance/lib/sdk/dsr/api-crud.ts
Normal 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())
|
||||
}
|
||||
259
admin-compliance/lib/sdk/dsr/api-mock.ts
Normal file
259
admin-compliance/lib/sdk/dsr/api-mock.ts
Normal 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
|
||||
}
|
||||
}
|
||||
133
admin-compliance/lib/sdk/dsr/api-types.ts
Normal file
133
admin-compliance/lib/sdk/dsr/api-types.ts
Normal 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') || '',
|
||||
}
|
||||
}
|
||||
161
admin-compliance/lib/sdk/dsr/api-workflow.ts
Normal file
161
admin-compliance/lib/sdk/dsr/api-workflow.ts
Normal 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()
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
18
admin-compliance/lib/sdk/einwilligungen/hooks.tsx
Normal file
18
admin-compliance/lib/sdk/einwilligungen/hooks.tsx
Normal 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
|
||||
}
|
||||
384
admin-compliance/lib/sdk/einwilligungen/provider.tsx
Normal file
384
admin-compliance/lib/sdk/einwilligungen/provider.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
237
admin-compliance/lib/sdk/einwilligungen/reducer.ts
Normal file
237
admin-compliance/lib/sdk/einwilligungen/reducer.ts
Normal 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
|
||||
}
|
||||
}
|
||||
361
admin-compliance/lib/sdk/export-pdf.ts
Normal file
361
admin-compliance/lib/sdk/export-pdf.ts
Normal 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')
|
||||
}
|
||||
240
admin-compliance/lib/sdk/export-zip.ts
Normal file
240
admin-compliance/lib/sdk/export-zip.ts
Normal 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' })
|
||||
}
|
||||
@@ -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}`)
|
||||
|
||||
83
admin-compliance/lib/sdk/incidents/api-helpers.ts
Normal file
83
admin-compliance/lib/sdk/incidents/api-helpers.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
372
admin-compliance/lib/sdk/incidents/api-incidents.ts
Normal file
372
admin-compliance/lib/sdk/incidents/api-incidents.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
392
admin-compliance/lib/sdk/incidents/api-mock.ts
Normal file
392
admin-compliance/lib/sdk/incidents/api-mock.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
20
admin-compliance/lib/sdk/tom-generator/hooks.tsx
Normal file
20
admin-compliance/lib/sdk/tom-generator/hooks.tsx
Normal 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
|
||||
}
|
||||
473
admin-compliance/lib/sdk/tom-generator/provider.tsx
Normal file
473
admin-compliance/lib/sdk/tom-generator/provider.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
238
admin-compliance/lib/sdk/tom-generator/reducer.ts
Normal file
238
admin-compliance/lib/sdk/tom-generator/reducer.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
88
admin-compliance/lib/sdk/vendor-compliance/hooks.ts
Normal file
88
admin-compliance/lib/sdk/vendor-compliance/hooks.ts
Normal 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])
|
||||
}
|
||||
178
admin-compliance/lib/sdk/vendor-compliance/reducer.ts
Normal file
178
admin-compliance/lib/sdk/vendor-compliance/reducer.ts
Normal 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
|
||||
}
|
||||
}
|
||||
448
admin-compliance/lib/sdk/vendor-compliance/use-actions.ts
Normal file
448
admin-compliance/lib/sdk/vendor-compliance/use-actions.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user