fix(admin-v2): Restore complete admin-v2 application

The admin-v2 application was incomplete in the repository. This commit
restores all missing components:

- Admin pages (76 pages): dashboard, ai, compliance, dsgvo, education,
  infrastructure, communication, development, onboarding, rbac
- SDK pages (45 pages): tom, dsfa, vvt, loeschfristen, einwilligungen,
  vendor-compliance, tom-generator, dsr, and more
- Developer portal (25 pages): API docs, SDK guides, frameworks
- All components, lib files, hooks, and types
- Updated package.json with all dependencies

The issue was caused by incomplete initial repository state - the full
admin-v2 codebase existed in backend/admin-v2 and docs-src/admin-v2
but was never fully synced to the main admin-v2 directory.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
BreakPilot Dev
2026-02-08 23:40:15 -08:00
parent f28244753f
commit 660295e218
385 changed files with 138126 additions and 3079 deletions

View File

@@ -0,0 +1,235 @@
import { NextRequest, NextResponse } from 'next/server'
/**
* SDK Checkpoints API
*
* GET /api/sdk/v1/checkpoints - Get all checkpoint statuses
* POST /api/sdk/v1/checkpoints - Validate a checkpoint
*/
// Checkpoint definitions
const CHECKPOINTS = {
'CP-PROF': {
id: 'CP-PROF',
step: 'company-profile',
name: 'Unternehmensprofil Checkpoint',
type: 'REQUIRED',
blocksProgress: true,
requiresReview: 'NONE',
},
'CP-UC': {
id: 'CP-UC',
step: 'use-case-assessment',
name: 'Anwendungsfall Checkpoint',
type: 'REQUIRED',
blocksProgress: true,
requiresReview: 'NONE',
},
'CP-SCAN': {
id: 'CP-SCAN',
step: 'screening',
name: 'Screening Checkpoint',
type: 'REQUIRED',
blocksProgress: true,
requiresReview: 'NONE',
},
'CP-MOD': {
id: 'CP-MOD',
step: 'modules',
name: 'Modules Checkpoint',
type: 'REQUIRED',
blocksProgress: true,
requiresReview: 'NONE',
},
'CP-REQ': {
id: 'CP-REQ',
step: 'requirements',
name: 'Requirements Checkpoint',
type: 'REQUIRED',
blocksProgress: true,
requiresReview: 'NONE',
},
'CP-CTRL': {
id: 'CP-CTRL',
step: 'controls',
name: 'Controls Checkpoint',
type: 'REQUIRED',
blocksProgress: true,
requiresReview: 'DSB',
},
'CP-EVI': {
id: 'CP-EVI',
step: 'evidence',
name: 'Evidence Checkpoint',
type: 'RECOMMENDED',
blocksProgress: false,
requiresReview: 'NONE',
},
'CP-CHK': {
id: 'CP-CHK',
step: 'audit-checklist',
name: 'Checklist Checkpoint',
type: 'RECOMMENDED',
blocksProgress: false,
requiresReview: 'NONE',
},
'CP-RISK': {
id: 'CP-RISK',
step: 'risks',
name: 'Risk Matrix Checkpoint',
type: 'REQUIRED',
blocksProgress: true,
requiresReview: 'DSB',
},
'CP-AI': {
id: 'CP-AI',
step: 'ai-act',
name: 'AI Act Checkpoint',
type: 'REQUIRED',
blocksProgress: true,
requiresReview: 'LEGAL',
},
'CP-DSFA': {
id: 'CP-DSFA',
step: 'dsfa',
name: 'DSFA Checkpoint',
type: 'REQUIRED',
blocksProgress: true,
requiresReview: 'DSB',
},
'CP-TOM': {
id: 'CP-TOM',
step: 'tom',
name: 'TOMs Checkpoint',
type: 'REQUIRED',
blocksProgress: true,
requiresReview: 'NONE',
},
'CP-VVT': {
id: 'CP-VVT',
step: 'vvt',
name: 'VVT Checkpoint',
type: 'REQUIRED',
blocksProgress: true,
requiresReview: 'DSB',
},
}
export async function GET() {
try {
return NextResponse.json({
success: true,
checkpoints: CHECKPOINTS,
count: Object.keys(CHECKPOINTS).length,
})
} catch (error) {
console.error('Failed to get checkpoints:', error)
return NextResponse.json(
{ error: 'Failed to get checkpoints' },
{ status: 500 }
)
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { checkpointId, state, context } = body
if (!checkpointId) {
return NextResponse.json(
{ error: 'checkpointId is required' },
{ status: 400 }
)
}
const checkpoint = CHECKPOINTS[checkpointId as keyof typeof CHECKPOINTS]
if (!checkpoint) {
return NextResponse.json(
{ error: 'Checkpoint not found', checkpointId },
{ status: 404 }
)
}
// Perform validation based on checkpoint
const errors: Array<{ ruleId: string; field: string; message: string; severity: string }> = []
const warnings: Array<{ ruleId: string; field: string; message: string; severity: string }> = []
// Basic validation rules
switch (checkpointId) {
case 'CP-UC':
if (!state?.useCases || state.useCases.length === 0) {
errors.push({
ruleId: 'uc-min-count',
field: 'useCases',
message: 'Mindestens ein Use Case muss erstellt werden',
severity: 'ERROR',
})
}
break
case 'CP-SCAN':
if (!state?.screening || state.screening.status !== 'COMPLETED') {
errors.push({
ruleId: 'scan-complete',
field: 'screening',
message: 'Security Scan muss abgeschlossen sein',
severity: 'ERROR',
})
}
break
case 'CP-MOD':
if (!state?.modules || state.modules.length === 0) {
errors.push({
ruleId: 'mod-min-count',
field: 'modules',
message: 'Mindestens ein Modul muss zugewiesen werden',
severity: 'ERROR',
})
}
break
case 'CP-RISK':
if (state?.risks) {
const criticalRisks = state.risks.filter(
(r: { severity: string; mitigation: unknown[] }) =>
(r.severity === 'CRITICAL' || r.severity === 'HIGH') && r.mitigation.length === 0
)
if (criticalRisks.length > 0) {
errors.push({
ruleId: 'critical-risks-mitigated',
field: 'risks',
message: `${criticalRisks.length} kritische Risiken ohne Mitigationsmaßnahmen`,
severity: 'ERROR',
})
}
}
break
}
const passed = errors.length === 0
const result = {
checkpointId,
passed,
validatedAt: new Date().toISOString(),
validatedBy: context?.userId || 'SYSTEM',
errors,
warnings,
checkpoint,
}
return NextResponse.json({
success: true,
...result,
})
} catch (error) {
console.error('Failed to validate checkpoint:', error)
return NextResponse.json(
{ error: 'Failed to validate checkpoint' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,52 @@
/**
* Demo Data Clear API Endpoint
*
* Clears demo data from the storage (same mechanism as real customer data).
*/
import { NextRequest, NextResponse } from 'next/server'
// Shared store reference (same as seed endpoint)
declare global {
// eslint-disable-next-line no-var
var demoStateStore: Map<string, { state: unknown; version: number; updatedAt: Date }> | undefined
}
if (!global.demoStateStore) {
global.demoStateStore = new Map()
}
const stateStore = global.demoStateStore
export async function DELETE(request: NextRequest) {
try {
const body = await request.json()
const { tenantId = 'demo-tenant' } = body
const existed = stateStore.has(tenantId)
stateStore.delete(tenantId)
return NextResponse.json({
success: true,
message: existed
? `Demo data cleared for tenant ${tenantId}`
: `No data found for tenant ${tenantId}`,
tenantId,
existed,
})
} catch (error) {
console.error('Failed to clear demo data:', error)
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 }
)
}
}
export async function POST(request: NextRequest) {
// Also support POST for clearing (for clients that don't support DELETE)
return DELETE(request)
}

View File

@@ -0,0 +1,77 @@
/**
* Demo Data Seed API Endpoint
*
* This endpoint seeds demo data via the same storage mechanism as real customer data.
* Demo data is NOT hardcoded - it goes through the normal API/database path.
*/
import { NextRequest, NextResponse } from 'next/server'
import { generateDemoState } from '@/lib/sdk/demo-data'
// In-memory store (same as state endpoint - will be replaced with PostgreSQL)
declare global {
// eslint-disable-next-line no-var
var demoStateStore: Map<string, { state: unknown; version: number; updatedAt: Date }> | undefined
}
if (!global.demoStateStore) {
global.demoStateStore = new Map()
}
const stateStore = global.demoStateStore
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { tenantId = 'demo-tenant', userId = 'demo-user' } = body
// Generate demo state using the seed data templates
const demoState = generateDemoState(tenantId, userId)
// Store via the same mechanism as real data
const storedState = {
state: demoState,
version: 1,
updatedAt: new Date(),
}
stateStore.set(tenantId, storedState)
return NextResponse.json({
success: true,
message: `Demo data seeded for tenant ${tenantId}`,
tenantId,
version: 1,
})
} catch (error) {
console.error('Failed to seed demo data:', error)
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 }
)
}
}
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const tenantId = searchParams.get('tenantId') || 'demo-tenant'
const stored = stateStore.get(tenantId)
if (!stored) {
return NextResponse.json({
hasData: false,
tenantId,
})
}
return NextResponse.json({
hasData: true,
tenantId,
version: stored.version,
updatedAt: stored.updatedAt,
})
}

View File

@@ -0,0 +1,214 @@
import { NextRequest, NextResponse } from 'next/server'
// Types
interface ExtractedSection {
title: string
content: string
type?: string
}
interface ExtractedContent {
title?: string
version?: string
lastModified?: string
sections?: ExtractedSection[]
metadata?: Record<string, string>
}
interface UploadResponse {
success: boolean
documentId: string
filename: string
documentType: string
extractedVersion?: string
extractedContent?: ExtractedContent
suggestedNextVersion?: string
}
// Helper: Detect version from filename
function detectVersionFromFilename(filename: string): string | undefined {
const patterns = [
/[vV](\d+(?:\.\d+)*)/,
/version[_-]?(\d+(?:\.\d+)*)/i,
/[_-]v?(\d+\.\d+(?:\.\d+)?)[_-]/,
]
for (const pattern of patterns) {
const match = filename.match(pattern)
if (match) {
return match[1]
}
}
return undefined
}
// Helper: Suggest next version
function suggestNextVersion(currentVersion?: string): string {
if (!currentVersion) return '1.0'
const parts = currentVersion.split('.').map(Number)
if (parts.length >= 2) {
parts[parts.length - 1] += 1
} else {
parts.push(1)
}
return parts.join('.')
}
// Helper: Extract content from PDF/DOCX (simplified - would need proper libraries in production)
async function extractDocumentContent(
_file: File,
documentType: string
): Promise<ExtractedContent> {
// In production, this would use libraries like:
// - pdf-parse for PDFs
// - mammoth for DOCX
// For now, return mock extracted content based on document type
const mockContentByType: Record<string, ExtractedContent> = {
tom: {
title: 'Technische und Organisatorische Maßnahmen',
sections: [
{ title: 'Vertraulichkeit', content: 'Zugangskontrollen, Zugriffsbeschränkungen...', type: 'category' },
{ title: 'Integrität', content: 'Eingabekontrollen, Änderungsprotokolle...', type: 'category' },
{ title: 'Verfügbarkeit', content: 'Backup-Strategien, Disaster Recovery...', type: 'category' },
{ title: 'Belastbarkeit', content: 'Redundanz, Lasttests...', type: 'category' },
],
metadata: {
lastReview: new Date().toISOString(),
responsible: 'Datenschutzbeauftragter',
},
},
dsfa: {
title: 'Datenschutz-Folgenabschätzung',
sections: [
{ title: 'Beschreibung der Verarbeitung', content: 'Systematische Beschreibung...', type: 'section' },
{ title: 'Erforderlichkeit und Verhältnismäßigkeit', content: 'Bewertung...', type: 'section' },
{ title: 'Risiken für Betroffene', content: 'Risikoanalyse...', type: 'section' },
{ title: 'Abhilfemaßnahmen', content: 'Geplante Maßnahmen...', type: 'section' },
],
},
vvt: {
title: 'Verzeichnis von Verarbeitungstätigkeiten',
sections: [
{ title: 'Verantwortlicher', content: 'Name und Kontaktdaten...', type: 'field' },
{ title: 'Verarbeitungszwecke', content: 'Liste der Zwecke...', type: 'list' },
{ title: 'Datenkategorien', content: 'Personenbezogene Daten...', type: 'list' },
{ title: 'Empfängerkategorien', content: 'Interne und externe Empfänger...', type: 'list' },
],
},
loeschfristen: {
title: 'Löschkonzept und Aufbewahrungsfristen',
sections: [
{ title: 'Personalakten', content: '10 Jahre nach Ausscheiden', type: 'retention' },
{ title: 'Kundendaten', content: '3 Jahre nach letzter Aktivität', type: 'retention' },
{ title: 'Buchhaltungsbelege', content: '10 Jahre (HGB)', type: 'retention' },
{ title: 'Bewerbungsunterlagen', content: '6 Monate nach Absage', type: 'retention' },
],
},
consent: {
title: 'Einwilligungserklärungen',
sections: [
{ title: 'Newsletter-Einwilligung', content: 'Vorlage für Newsletter...', type: 'template' },
{ title: 'Marketing-Einwilligung', content: 'Vorlage für Marketing...', type: 'template' },
],
},
policy: {
title: 'Datenschutzrichtlinie',
sections: [
{ title: 'Geltungsbereich', content: 'Diese Richtlinie gilt für...', type: 'section' },
{ title: 'Verantwortlichkeiten', content: 'Rollen und Pflichten...', type: 'section' },
],
},
}
return mockContentByType[documentType] || {
title: 'Unbekanntes Dokument',
sections: [],
}
}
export async function POST(request: NextRequest) {
try {
const formData = await request.formData()
const file = formData.get('file') as File | null
const documentType = formData.get('documentType') as string || 'custom'
const sessionId = formData.get('sessionId') as string || 'default'
if (!file) {
return NextResponse.json(
{ error: 'Keine Datei hochgeladen' },
{ status: 400 }
)
}
// Validate file type
const allowedTypes = [
'application/pdf',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/msword',
]
if (!allowedTypes.includes(file.type)) {
return NextResponse.json(
{ error: 'Ungültiger Dateityp. Erlaubt: PDF, DOCX, DOC' },
{ status: 400 }
)
}
// Generate document ID
const documentId = `doc-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
// Extract version from filename
const extractedVersion = detectVersionFromFilename(file.name)
// Extract content (in production, this would parse the actual file)
const extractedContent = await extractDocumentContent(file, documentType)
// Add version to extracted content if found
if (extractedVersion) {
extractedContent.version = extractedVersion
}
// Store file (in production, save to MinIO/S3)
// For now, we just process and return metadata
console.log(`[SDK Documents] Uploaded: ${file.name} (${file.size} bytes) for session ${sessionId}`)
const response: UploadResponse = {
success: true,
documentId,
filename: file.name,
documentType,
extractedVersion,
extractedContent,
suggestedNextVersion: suggestNextVersion(extractedVersion),
}
return NextResponse.json(response)
} catch (error) {
console.error('[SDK Documents] Upload error:', error)
return NextResponse.json(
{ error: 'Upload fehlgeschlagen' },
{ status: 500 }
)
}
}
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const sessionId = searchParams.get('sessionId')
if (!sessionId) {
return NextResponse.json(
{ error: 'Session ID erforderlich' },
{ status: 400 }
)
}
// In production, fetch uploaded documents from storage
// For now, return empty list
return NextResponse.json({
uploads: [],
sessionId,
})
}

View File

@@ -0,0 +1,255 @@
/**
* API Route: Datenpunktkatalog
*
* GET - Katalog abrufen (inkl. kundenspezifischer Datenpunkte)
* POST - Katalog speichern/aktualisieren
*/
import { NextRequest, NextResponse } from 'next/server'
import {
DataPointCatalog,
CompanyInfo,
CookieBannerConfig,
DataPoint,
} from '@/lib/sdk/einwilligungen/types'
import { createDefaultCatalog, PREDEFINED_DATA_POINTS } from '@/lib/sdk/einwilligungen/catalog/loader'
// In-Memory Storage (in Produktion: Datenbank)
const catalogStorage = new Map<string, {
catalog: DataPointCatalog
companyInfo: CompanyInfo | null
cookieBannerConfig: CookieBannerConfig | null
}>()
/**
* GET /api/sdk/v1/einwilligungen/catalog
*
* Laedt den Datenpunktkatalog fuer einen Tenant
*/
export async function GET(request: NextRequest) {
try {
const tenantId = request.headers.get('X-Tenant-ID')
if (!tenantId) {
return NextResponse.json(
{ error: 'Tenant ID required' },
{ status: 400 }
)
}
// Hole gespeicherte Daten oder erstelle Default
let stored = catalogStorage.get(tenantId)
if (!stored) {
// Erstelle Default-Katalog
const defaultCatalog = createDefaultCatalog(tenantId)
stored = {
catalog: defaultCatalog,
companyInfo: null,
cookieBannerConfig: null,
}
catalogStorage.set(tenantId, stored)
}
return NextResponse.json({
catalog: stored.catalog,
companyInfo: stored.companyInfo,
cookieBannerConfig: stored.cookieBannerConfig,
})
} catch (error) {
console.error('Error loading catalog:', error)
return NextResponse.json(
{ error: 'Failed to load catalog' },
{ status: 500 }
)
}
}
/**
* POST /api/sdk/v1/einwilligungen/catalog
*
* Speichert den Datenpunktkatalog fuer einen Tenant
*/
export async function POST(request: NextRequest) {
try {
const tenantId = request.headers.get('X-Tenant-ID')
if (!tenantId) {
return NextResponse.json(
{ error: 'Tenant ID required' },
{ status: 400 }
)
}
const body = await request.json()
const { catalog, companyInfo, cookieBannerConfig } = body
if (!catalog) {
return NextResponse.json(
{ error: 'Catalog data required' },
{ status: 400 }
)
}
// Validiere den Katalog
if (!catalog.tenantId || catalog.tenantId !== tenantId) {
return NextResponse.json(
{ error: 'Tenant ID mismatch' },
{ status: 400 }
)
}
// Aktualisiere den Katalog
const updatedCatalog: DataPointCatalog = {
...catalog,
updatedAt: new Date(),
}
// Speichere
catalogStorage.set(tenantId, {
catalog: updatedCatalog,
companyInfo: companyInfo || null,
cookieBannerConfig: cookieBannerConfig || null,
})
return NextResponse.json({
success: true,
catalog: updatedCatalog,
})
} catch (error) {
console.error('Error saving catalog:', error)
return NextResponse.json(
{ error: 'Failed to save catalog' },
{ status: 500 }
)
}
}
/**
* POST /api/sdk/v1/einwilligungen/catalog/customize
*
* Fuegt einen kundenspezifischen Datenpunkt hinzu
*/
export async function PUT(request: NextRequest) {
try {
const tenantId = request.headers.get('X-Tenant-ID')
if (!tenantId) {
return NextResponse.json(
{ error: 'Tenant ID required' },
{ status: 400 }
)
}
const body = await request.json()
const { action, dataPoint, dataPointId } = body
let stored = catalogStorage.get(tenantId)
if (!stored) {
const defaultCatalog = createDefaultCatalog(tenantId)
stored = {
catalog: defaultCatalog,
companyInfo: null,
cookieBannerConfig: null,
}
}
switch (action) {
case 'add': {
if (!dataPoint) {
return NextResponse.json(
{ error: 'Data point required' },
{ status: 400 }
)
}
// Generiere eindeutige ID
const newDataPoint: DataPoint = {
...dataPoint,
id: `custom-${tenantId}-${Date.now()}`,
isCustom: true,
}
stored.catalog.customDataPoints.push(newDataPoint)
stored.catalog.updatedAt = new Date()
catalogStorage.set(tenantId, stored)
return NextResponse.json({
success: true,
dataPoint: newDataPoint,
})
}
case 'update': {
if (!dataPointId || !dataPoint) {
return NextResponse.json(
{ error: 'Data point ID and data required' },
{ status: 400 }
)
}
// Pruefe ob es ein kundenspezifischer Datenpunkt ist
const customIndex = stored.catalog.customDataPoints.findIndex(
(dp) => dp.id === dataPointId
)
if (customIndex !== -1) {
stored.catalog.customDataPoints[customIndex] = {
...stored.catalog.customDataPoints[customIndex],
...dataPoint,
}
} else {
// Vordefinierter Datenpunkt - nur isActive aendern
const predefinedIndex = stored.catalog.dataPoints.findIndex(
(dp) => dp.id === dataPointId
)
if (predefinedIndex !== -1 && 'isActive' in dataPoint) {
stored.catalog.dataPoints[predefinedIndex] = {
...stored.catalog.dataPoints[predefinedIndex],
isActive: dataPoint.isActive,
}
}
}
stored.catalog.updatedAt = new Date()
catalogStorage.set(tenantId, stored)
return NextResponse.json({
success: true,
})
}
case 'delete': {
if (!dataPointId) {
return NextResponse.json(
{ error: 'Data point ID required' },
{ status: 400 }
)
}
stored.catalog.customDataPoints = stored.catalog.customDataPoints.filter(
(dp) => dp.id !== dataPointId
)
stored.catalog.updatedAt = new Date()
catalogStorage.set(tenantId, stored)
return NextResponse.json({
success: true,
})
}
default:
return NextResponse.json(
{ error: 'Invalid action' },
{ status: 400 }
)
}
} catch (error) {
console.error('Error customizing catalog:', error)
return NextResponse.json(
{ error: 'Failed to customize catalog' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,369 @@
/**
* API Route: Consent Management
*
* POST - Consent erfassen
* GET - Consent-Status abrufen
*/
import { NextRequest, NextResponse } from 'next/server'
import {
ConsentEntry,
ConsentStatistics,
DataPointCategory,
LegalBasis,
} from '@/lib/sdk/einwilligungen/types'
import { PREDEFINED_DATA_POINTS } from '@/lib/sdk/einwilligungen/catalog/loader'
// In-Memory Storage fuer Consents
const consentStorage = new Map<string, ConsentEntry[]>() // tenantId -> consents
// Hilfsfunktion: Generiere eindeutige ID
function generateId(): string {
return `consent-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
}
/**
* POST /api/sdk/v1/einwilligungen/consent
*
* Erfasst eine neue Einwilligung
*
* Body:
* - userId: string - Benutzer-ID
* - dataPointId: string - ID des Datenpunkts
* - granted: boolean - Einwilligung erteilt?
* - consentVersion?: string - Version der Einwilligung
*/
export async function POST(request: NextRequest) {
try {
const tenantId = request.headers.get('X-Tenant-ID')
if (!tenantId) {
return NextResponse.json(
{ error: 'Tenant ID required' },
{ status: 400 }
)
}
const body = await request.json()
const { userId, dataPointId, granted, consentVersion = '1.0.0' } = body
if (!userId || !dataPointId || typeof granted !== 'boolean') {
return NextResponse.json(
{ error: 'userId, dataPointId, and granted required' },
{ status: 400 }
)
}
// Hole IP und User-Agent
const ipAddress = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || null
const userAgent = request.headers.get('user-agent') || null
// Erstelle Consent-Eintrag
const consent: ConsentEntry = {
id: generateId(),
userId,
dataPointId,
granted,
grantedAt: new Date(),
revokedAt: undefined,
ipAddress: ipAddress || undefined,
userAgent: userAgent || undefined,
consentVersion,
}
// Hole bestehende Consents
const tenantConsents = consentStorage.get(tenantId) || []
// Pruefe auf bestehende Einwilligung fuer diesen Datenpunkt
const existingIndex = tenantConsents.findIndex(
(c) => c.userId === userId && c.dataPointId === dataPointId && !c.revokedAt
)
if (existingIndex !== -1) {
if (!granted) {
// Widerruf: Setze revokedAt
tenantConsents[existingIndex].revokedAt = new Date()
}
// Bei granted=true: Keine Aenderung noetig, Consent existiert bereits
} else if (granted) {
// Neuer Consent
tenantConsents.push(consent)
}
consentStorage.set(tenantId, tenantConsents)
return NextResponse.json({
success: true,
consent: {
id: consent.id,
dataPointId: consent.dataPointId,
granted: consent.granted,
grantedAt: consent.grantedAt,
},
})
} catch (error) {
console.error('Error recording consent:', error)
return NextResponse.json(
{ error: 'Failed to record consent' },
{ status: 500 }
)
}
}
/**
* GET /api/sdk/v1/einwilligungen/consent
*
* Ruft Consent-Status und Statistiken ab
*
* Query Parameters:
* - userId?: string - Fuer spezifischen Benutzer
* - stats?: boolean - Statistiken inkludieren
*/
export async function GET(request: NextRequest) {
try {
const tenantId = request.headers.get('X-Tenant-ID')
if (!tenantId) {
return NextResponse.json(
{ error: 'Tenant ID required' },
{ status: 400 }
)
}
const { searchParams } = new URL(request.url)
const userId = searchParams.get('userId')
const includeStats = searchParams.get('stats') === 'true'
const tenantConsents = consentStorage.get(tenantId) || []
if (userId) {
// Spezifischer Benutzer
const userConsents = tenantConsents.filter((c) => c.userId === userId)
// Gruppiere nach Datenpunkt
const consentMap: Record<string, { granted: boolean; grantedAt: Date; revokedAt?: Date }> = {}
for (const consent of userConsents) {
consentMap[consent.dataPointId] = {
granted: consent.granted && !consent.revokedAt,
grantedAt: consent.grantedAt,
revokedAt: consent.revokedAt,
}
}
return NextResponse.json({
userId,
consents: consentMap,
totalConsents: Object.keys(consentMap).length,
activeConsents: Object.values(consentMap).filter((c) => c.granted).length,
})
}
// Statistiken fuer alle Consents
if (includeStats) {
const stats = calculateStatistics(tenantConsents)
return NextResponse.json({
statistics: stats,
recentConsents: tenantConsents
.sort((a, b) => new Date(b.grantedAt).getTime() - new Date(a.grantedAt).getTime())
.slice(0, 10)
.map((c) => ({
id: c.id,
userId: c.userId.substring(0, 8) + '...', // Anonymisiert
dataPointId: c.dataPointId,
granted: c.granted,
grantedAt: c.grantedAt,
})),
})
}
// Standard: Alle Consents (anonymisiert)
return NextResponse.json({
totalConsents: tenantConsents.length,
activeConsents: tenantConsents.filter((c) => c.granted && !c.revokedAt).length,
revokedConsents: tenantConsents.filter((c) => c.revokedAt).length,
})
} catch (error) {
console.error('Error fetching consents:', error)
return NextResponse.json(
{ error: 'Failed to fetch consents' },
{ status: 500 }
)
}
}
/**
* PUT /api/sdk/v1/einwilligungen/consent
*
* Batch-Update von Consents (z.B. Cookie-Banner)
*/
export async function PUT(request: NextRequest) {
try {
const tenantId = request.headers.get('X-Tenant-ID')
if (!tenantId) {
return NextResponse.json(
{ error: 'Tenant ID required' },
{ status: 400 }
)
}
const body = await request.json()
const { userId, consents, consentVersion = '1.0.0' } = body
if (!userId || !consents || typeof consents !== 'object') {
return NextResponse.json(
{ error: 'userId and consents object required' },
{ status: 400 }
)
}
const ipAddress = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || null
const userAgent = request.headers.get('user-agent') || null
const tenantConsents = consentStorage.get(tenantId) || []
const now = new Date()
// Verarbeite jeden Consent
for (const [dataPointId, granted] of Object.entries(consents)) {
if (typeof granted !== 'boolean') continue
const existingIndex = tenantConsents.findIndex(
(c) => c.userId === userId && c.dataPointId === dataPointId && !c.revokedAt
)
if (existingIndex !== -1) {
const existing = tenantConsents[existingIndex]
if (existing.granted !== granted) {
if (!granted) {
// Widerruf
tenantConsents[existingIndex].revokedAt = now
} else {
// Neuer Consent nach Widerruf
tenantConsents.push({
id: generateId(),
userId,
dataPointId,
granted: true,
grantedAt: now,
ipAddress: ipAddress || undefined,
userAgent: userAgent || undefined,
consentVersion,
})
}
}
} else if (granted) {
// Neuer Consent
tenantConsents.push({
id: generateId(),
userId,
dataPointId,
granted: true,
grantedAt: now,
ipAddress: ipAddress || undefined,
userAgent: userAgent || undefined,
consentVersion,
})
}
}
consentStorage.set(tenantId, tenantConsents)
// Zaehle aktive Consents fuer diesen User
const activeConsents = tenantConsents.filter(
(c) => c.userId === userId && c.granted && !c.revokedAt
).length
return NextResponse.json({
success: true,
userId,
activeConsents,
updatedAt: now,
})
} catch (error) {
console.error('Error updating consents:', error)
return NextResponse.json(
{ error: 'Failed to update consents' },
{ status: 500 }
)
}
}
/**
* Berechnet Consent-Statistiken
*/
function calculateStatistics(consents: ConsentEntry[]): ConsentStatistics {
const activeConsents = consents.filter((c) => c.granted && !c.revokedAt)
const revokedConsents = consents.filter((c) => c.revokedAt)
// Gruppiere nach Kategorie (18 Kategorien A-R)
const byCategory: Record<DataPointCategory, { total: number; active: number; revoked: number }> = {
MASTER_DATA: { total: 0, active: 0, revoked: 0 },
CONTACT_DATA: { total: 0, active: 0, revoked: 0 },
AUTHENTICATION: { total: 0, active: 0, revoked: 0 },
CONSENT: { total: 0, active: 0, revoked: 0 },
COMMUNICATION: { total: 0, active: 0, revoked: 0 },
PAYMENT: { total: 0, active: 0, revoked: 0 },
USAGE_DATA: { total: 0, active: 0, revoked: 0 },
LOCATION: { total: 0, active: 0, revoked: 0 },
DEVICE_DATA: { total: 0, active: 0, revoked: 0 },
MARKETING: { total: 0, active: 0, revoked: 0 },
ANALYTICS: { total: 0, active: 0, revoked: 0 },
SOCIAL_MEDIA: { total: 0, active: 0, revoked: 0 },
HEALTH_DATA: { total: 0, active: 0, revoked: 0 },
EMPLOYEE_DATA: { total: 0, active: 0, revoked: 0 },
CONTRACT_DATA: { total: 0, active: 0, revoked: 0 },
LOG_DATA: { total: 0, active: 0, revoked: 0 },
AI_DATA: { total: 0, active: 0, revoked: 0 },
SECURITY: { total: 0, active: 0, revoked: 0 },
}
for (const consent of consents) {
const dataPoint = PREDEFINED_DATA_POINTS.find((dp) => dp.id === consent.dataPointId)
if (dataPoint) {
byCategory[dataPoint.category].total++
if (consent.granted && !consent.revokedAt) {
byCategory[dataPoint.category].active++
}
if (consent.revokedAt) {
byCategory[dataPoint.category].revoked++
}
}
}
// Gruppiere nach Rechtsgrundlage (7 Rechtsgrundlagen)
const byLegalBasis: Record<LegalBasis, { total: number; active: number }> = {
CONTRACT: { total: 0, active: 0 },
CONSENT: { total: 0, active: 0 },
EXPLICIT_CONSENT: { total: 0, active: 0 },
LEGITIMATE_INTEREST: { total: 0, active: 0 },
LEGAL_OBLIGATION: { total: 0, active: 0 },
VITAL_INTERESTS: { total: 0, active: 0 },
PUBLIC_INTEREST: { total: 0, active: 0 },
}
for (const consent of consents) {
const dataPoint = PREDEFINED_DATA_POINTS.find((dp) => dp.id === consent.dataPointId)
if (dataPoint) {
byLegalBasis[dataPoint.legalBasis].total++
if (consent.granted && !consent.revokedAt) {
byLegalBasis[dataPoint.legalBasis].active++
}
}
}
// Berechne Conversion Rate (Unique Users mit mindestens einem Consent)
const uniqueUsers = new Set(consents.map((c) => c.userId))
const usersWithActiveConsent = new Set(activeConsents.map((c) => c.userId))
const conversionRate = uniqueUsers.size > 0
? (usersWithActiveConsent.size / uniqueUsers.size) * 100
: 0
return {
totalConsents: consents.length,
activeConsents: activeConsents.length,
revokedConsents: revokedConsents.length,
byCategory,
byLegalBasis,
conversionRate: Math.round(conversionRate * 10) / 10,
}
}

View File

@@ -0,0 +1,215 @@
/**
* API Route: Cookie Banner Configuration
*
* GET - Cookie Banner Konfiguration abrufen
* POST - Cookie Banner Konfiguration speichern
*/
import { NextRequest, NextResponse } from 'next/server'
import {
CookieBannerConfig,
CookieBannerStyling,
CookieBannerTexts,
DataPoint,
} from '@/lib/sdk/einwilligungen/types'
import {
generateCookieBannerConfig,
DEFAULT_COOKIE_BANNER_STYLING,
DEFAULT_COOKIE_BANNER_TEXTS,
} from '@/lib/sdk/einwilligungen/generator/cookie-banner'
import { PREDEFINED_DATA_POINTS } from '@/lib/sdk/einwilligungen/catalog/loader'
// In-Memory Storage fuer Cookie Banner Configs
const configStorage = new Map<string, CookieBannerConfig>()
/**
* GET /api/sdk/v1/einwilligungen/cookie-banner/config
*
* Laedt die Cookie Banner Konfiguration fuer einen Tenant
*/
export async function GET(request: NextRequest) {
try {
const tenantId = request.headers.get('X-Tenant-ID')
if (!tenantId) {
return NextResponse.json(
{ error: 'Tenant ID required' },
{ status: 400 }
)
}
let config = configStorage.get(tenantId)
if (!config) {
// Generiere Default-Konfiguration
config = generateCookieBannerConfig(tenantId, PREDEFINED_DATA_POINTS)
configStorage.set(tenantId, config)
}
return NextResponse.json(config)
} catch (error) {
console.error('Error loading cookie banner config:', error)
return NextResponse.json(
{ error: 'Failed to load cookie banner config' },
{ status: 500 }
)
}
}
/**
* POST /api/sdk/v1/einwilligungen/cookie-banner/config
*
* Speichert oder aktualisiert die Cookie Banner Konfiguration
*
* Body:
* - dataPointIds?: string[] - IDs der Datenpunkte (fuer Neuberechnung)
* - styling?: Partial<CookieBannerStyling> - Styling-Optionen
* - texts?: Partial<CookieBannerTexts> - Text-Optionen
* - customDataPoints?: DataPoint[] - Kundenspezifische Datenpunkte
*/
export async function POST(request: NextRequest) {
try {
const tenantId = request.headers.get('X-Tenant-ID')
if (!tenantId) {
return NextResponse.json(
{ error: 'Tenant ID required' },
{ status: 400 }
)
}
const body = await request.json()
const {
dataPointIds,
styling,
texts,
customDataPoints = [],
} = body
// Hole bestehende Konfiguration oder erstelle neue
let config = configStorage.get(tenantId)
if (dataPointIds && Array.isArray(dataPointIds)) {
// Neu berechnen basierend auf Datenpunkten
const allDataPoints: DataPoint[] = [
...PREDEFINED_DATA_POINTS,
...customDataPoints,
]
const selectedDataPoints = dataPointIds
.map((id: string) => allDataPoints.find((dp) => dp.id === id))
.filter((dp): dp is DataPoint => dp !== undefined)
config = generateCookieBannerConfig(
tenantId,
selectedDataPoints,
texts,
styling
)
} else if (config) {
// Nur Styling/Texts aktualisieren
if (styling) {
config.styling = {
...config.styling,
...styling,
}
}
if (texts) {
config.texts = {
...config.texts,
...texts,
}
}
config.updatedAt = new Date()
} else {
// Erstelle Default
config = generateCookieBannerConfig(
tenantId,
PREDEFINED_DATA_POINTS,
texts,
styling
)
}
configStorage.set(tenantId, config)
return NextResponse.json({
success: true,
config,
})
} catch (error) {
console.error('Error saving cookie banner config:', error)
return NextResponse.json(
{ error: 'Failed to save cookie banner config' },
{ status: 500 }
)
}
}
/**
* PUT /api/sdk/v1/einwilligungen/cookie-banner/config
*
* Aktualisiert einzelne Kategorien
*/
export async function PUT(request: NextRequest) {
try {
const tenantId = request.headers.get('X-Tenant-ID')
if (!tenantId) {
return NextResponse.json(
{ error: 'Tenant ID required' },
{ status: 400 }
)
}
const body = await request.json()
const { categoryId, enabled } = body
if (!categoryId || typeof enabled !== 'boolean') {
return NextResponse.json(
{ error: 'categoryId and enabled required' },
{ status: 400 }
)
}
let config = configStorage.get(tenantId)
if (!config) {
config = generateCookieBannerConfig(tenantId, PREDEFINED_DATA_POINTS)
}
// Finde und aktualisiere die Kategorie
const categoryIndex = config.categories.findIndex((c) => c.id === categoryId)
if (categoryIndex === -1) {
return NextResponse.json(
{ error: 'Category not found' },
{ status: 404 }
)
}
// Essenzielle Cookies koennen nicht deaktiviert werden
if (config.categories[categoryIndex].isRequired && !enabled) {
return NextResponse.json(
{ error: 'Essential cookies cannot be disabled' },
{ status: 400 }
)
}
config.categories[categoryIndex].defaultEnabled = enabled
config.updatedAt = new Date()
configStorage.set(tenantId, config)
return NextResponse.json({
success: true,
category: config.categories[categoryIndex],
})
} catch (error) {
console.error('Error updating cookie category:', error)
return NextResponse.json(
{ error: 'Failed to update cookie category' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,256 @@
/**
* API Route: Cookie Banner Embed Code
*
* GET - Generiert den Embed-Code fuer den Cookie Banner
*/
import { NextRequest, NextResponse } from 'next/server'
import { CookieBannerConfig, CookieBannerEmbedCode } from '@/lib/sdk/einwilligungen/types'
import {
generateCookieBannerConfig,
generateEmbedCode,
} from '@/lib/sdk/einwilligungen/generator/cookie-banner'
import { PREDEFINED_DATA_POINTS } from '@/lib/sdk/einwilligungen/catalog/loader'
// In-Memory Storage (in Produktion mit configStorage aus config/route.ts teilen)
const configStorage = new Map<string, CookieBannerConfig>()
/**
* GET /api/sdk/v1/einwilligungen/cookie-banner/embed-code
*
* Generiert den Embed-Code fuer den Cookie Banner
*
* Query Parameters:
* - privacyPolicyUrl: string - URL zur Datenschutzerklaerung (default: /datenschutz)
* - format: 'combined' | 'separate' - Ausgabeformat (default: combined)
*/
export async function GET(request: NextRequest) {
try {
const tenantId = request.headers.get('X-Tenant-ID')
if (!tenantId) {
return NextResponse.json(
{ error: 'Tenant ID required' },
{ status: 400 }
)
}
const { searchParams } = new URL(request.url)
const privacyPolicyUrl = searchParams.get('privacyPolicyUrl') || '/datenschutz'
const format = searchParams.get('format') || 'combined'
// Hole oder erstelle Konfiguration
let config = configStorage.get(tenantId)
if (!config) {
config = generateCookieBannerConfig(tenantId, PREDEFINED_DATA_POINTS)
configStorage.set(tenantId, config)
}
// Generiere Embed-Code
const embedCode = generateEmbedCode(config, privacyPolicyUrl)
if (format === 'separate') {
// Separate Dateien zurueckgeben
return NextResponse.json({
html: embedCode.html,
css: embedCode.css,
js: embedCode.js,
scriptTag: embedCode.scriptTag,
instructions: {
de: `
Fuegen Sie den folgenden Code in Ihre Website ein:
1. CSS in den <head>-Bereich:
<style>${embedCode.css}</style>
2. HTML vor dem schliessenden </body>-Tag:
${embedCode.html}
3. JavaScript vor dem schliessenden </body>-Tag:
<script>${embedCode.js}</script>
Alternativ koennen Sie die Dateien separat einbinden:
- /cookie-banner.css
- /cookie-banner.js
`,
en: `
Add the following code to your website:
1. CSS in the <head> section:
<style>${embedCode.css}</style>
2. HTML before the closing </body> tag:
${embedCode.html}
3. JavaScript before the closing </body> tag:
<script>${embedCode.js}</script>
Alternatively, you can include the files separately:
- /cookie-banner.css
- /cookie-banner.js
`,
},
})
}
// Combined: Alles in einem HTML-Block
const combinedCode = `
<!-- Cookie Banner - Start -->
<style>
${embedCode.css}
</style>
${embedCode.html}
<script>
${embedCode.js}
</script>
<!-- Cookie Banner - End -->
`.trim()
return NextResponse.json({
embedCode: combinedCode,
scriptTag: embedCode.scriptTag,
config: {
tenantId: config.tenantId,
categories: config.categories.map((c) => ({
id: c.id,
name: c.name,
isRequired: c.isRequired,
defaultEnabled: c.defaultEnabled,
})),
styling: config.styling,
},
instructions: {
de: `Fuegen Sie den folgenden Code vor dem schliessenden </body>-Tag Ihrer Website ein.`,
en: `Add the following code before the closing </body> tag of your website.`,
},
})
} catch (error) {
console.error('Error generating embed code:', error)
return NextResponse.json(
{ error: 'Failed to generate embed code' },
{ status: 500 }
)
}
}
/**
* POST /api/sdk/v1/einwilligungen/cookie-banner/embed-code
*
* Generiert Embed-Code mit benutzerdefinierten Optionen
*/
export async function POST(request: NextRequest) {
try {
const tenantId = request.headers.get('X-Tenant-ID')
if (!tenantId) {
return NextResponse.json(
{ error: 'Tenant ID required' },
{ status: 400 }
)
}
const body = await request.json()
const {
privacyPolicyUrl = '/datenschutz',
styling,
texts,
language = 'de',
} = body
// Hole oder erstelle Konfiguration
let config = configStorage.get(tenantId)
if (!config) {
config = generateCookieBannerConfig(tenantId, PREDEFINED_DATA_POINTS, texts, styling)
} else {
// Wende temporaere Anpassungen an
if (styling) {
config = {
...config,
styling: { ...config.styling, ...styling },
}
}
if (texts) {
config = {
...config,
texts: { ...config.texts, ...texts },
}
}
}
const embedCode = generateEmbedCode(config, privacyPolicyUrl)
// Generiere Preview HTML
const previewHtml = `
<!DOCTYPE html>
<html lang="${language}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cookie Banner Preview</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f1f5f9;
min-height: 100vh;
margin: 0;
padding: 20px;
}
.preview-content {
max-width: 800px;
margin: 0 auto;
padding: 40px;
background: white;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
h1 { color: #1e293b; }
p { color: #64748b; line-height: 1.6; }
${embedCode.css}
</style>
</head>
<body>
<div class="preview-content">
<h1>Cookie Banner Preview</h1>
<p>Dies ist eine Vorschau des Cookie Banners. In der produktiven Umgebung wird der Banner auf Ihrer Website angezeigt.</p>
</div>
${embedCode.html}
<script>
${embedCode.js}
// Force show banner for preview
setTimeout(() => {
document.getElementById('cookieBanner')?.classList.add('active');
document.getElementById('cookieBannerOverlay')?.classList.add('active');
}, 100);
</script>
</body>
</html>
`.trim()
return NextResponse.json({
embedCode: {
html: embedCode.html,
css: embedCode.css,
js: embedCode.js,
scriptTag: embedCode.scriptTag,
},
previewHtml,
config: {
tenantId: config.tenantId,
categories: config.categories.length,
styling: config.styling,
},
})
} catch (error) {
console.error('Error generating custom embed code:', error)
return NextResponse.json(
{ error: 'Failed to generate embed code' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,186 @@
/**
* API Route: Privacy Policy Generator
*
* POST - Generiert eine Datenschutzerklaerung aus dem Datenpunktkatalog
*/
import { NextRequest, NextResponse } from 'next/server'
import {
CompanyInfo,
DataPoint,
SupportedLanguage,
ExportFormat,
GeneratedPrivacyPolicy,
} from '@/lib/sdk/einwilligungen/types'
import {
generatePrivacyPolicy,
generatePrivacyPolicySections,
} from '@/lib/sdk/einwilligungen/generator/privacy-policy'
import {
PREDEFINED_DATA_POINTS,
getDataPointById,
} from '@/lib/sdk/einwilligungen/catalog/loader'
// In-Memory Storage fuer generierte Policies
const policyStorage = new Map<string, GeneratedPrivacyPolicy>()
/**
* POST /api/sdk/v1/einwilligungen/privacy-policy/generate
*
* Generiert eine Datenschutzerklaerung
*
* Body:
* - dataPointIds: string[] - IDs der zu inkludierenden Datenpunkte
* - companyInfo: CompanyInfo - Firmeninformationen
* - language: 'de' | 'en' - Sprache
* - format: 'HTML' | 'MARKDOWN' | 'PDF' | 'DOCX' - Ausgabeformat
* - customDataPoints?: DataPoint[] - Kundenspezifische Datenpunkte
*/
export async function POST(request: NextRequest) {
try {
const tenantId = request.headers.get('X-Tenant-ID')
if (!tenantId) {
return NextResponse.json(
{ error: 'Tenant ID required' },
{ status: 400 }
)
}
const body = await request.json()
const {
dataPointIds,
companyInfo,
language = 'de',
format = 'HTML',
customDataPoints = [],
} = body
// Validierung
if (!companyInfo || !companyInfo.name || !companyInfo.address || !companyInfo.email) {
return NextResponse.json(
{ error: 'Company info (name, address, email) required' },
{ status: 400 }
)
}
if (!dataPointIds || !Array.isArray(dataPointIds) || dataPointIds.length === 0) {
return NextResponse.json(
{ error: 'At least one data point ID required' },
{ status: 400 }
)
}
// Validiere Sprache
const validLanguages: SupportedLanguage[] = ['de', 'en']
if (!validLanguages.includes(language)) {
return NextResponse.json(
{ error: 'Invalid language. Must be "de" or "en"' },
{ status: 400 }
)
}
// Validiere Format
const validFormats: ExportFormat[] = ['HTML', 'MARKDOWN', 'PDF', 'DOCX']
if (!validFormats.includes(format)) {
return NextResponse.json(
{ error: 'Invalid format. Must be HTML, MARKDOWN, PDF, or DOCX' },
{ status: 400 }
)
}
// Sammle alle Datenpunkte
const allDataPoints: DataPoint[] = [
...PREDEFINED_DATA_POINTS,
...customDataPoints,
]
// Filtere nach ausgewaehlten IDs
const selectedDataPoints = dataPointIds
.map((id: string) => allDataPoints.find((dp) => dp.id === id))
.filter((dp): dp is DataPoint => dp !== undefined)
if (selectedDataPoints.length === 0) {
return NextResponse.json(
{ error: 'No valid data points found for the provided IDs' },
{ status: 400 }
)
}
// Generiere die Privacy Policy
const policy = generatePrivacyPolicy(
tenantId,
selectedDataPoints,
companyInfo as CompanyInfo,
language as SupportedLanguage,
format as ExportFormat
)
// Speichere fuer spaeteres Abrufen
policyStorage.set(policy.id, policy)
// Fuer PDF/DOCX: Nur Metadaten zurueckgeben, Download separat
if (format === 'PDF' || format === 'DOCX') {
return NextResponse.json({
id: policy.id,
tenantId: policy.tenantId,
language: policy.language,
format: policy.format,
generatedAt: policy.generatedAt,
version: policy.version,
sections: policy.sections.map((s) => ({
id: s.id,
title: s.title,
order: s.order,
})),
downloadUrl: `/api/sdk/v1/einwilligungen/privacy-policy/${policy.id}/download`,
})
}
// Fuer HTML/Markdown: Vollstaendige Policy zurueckgeben
return NextResponse.json(policy)
} catch (error) {
console.error('Error generating privacy policy:', error)
return NextResponse.json(
{ error: 'Failed to generate privacy policy' },
{ status: 500 }
)
}
}
/**
* GET /api/sdk/v1/einwilligungen/privacy-policy/generate
*
* Liefert eine Vorschau der Abschnitte ohne vollstaendige Generierung
*/
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const language = (searchParams.get('language') as SupportedLanguage) || 'de'
// Liefere die Standard-Abschnittsstruktur
const sections = [
{ id: 'controller', order: 1, title: { de: '1. Verantwortlicher', en: '1. Data Controller' } },
{ id: 'data-collection', order: 2, title: { de: '2. Erhobene personenbezogene Daten', en: '2. Personal Data We Collect' } },
{ id: 'purposes', order: 3, title: { de: '3. Zwecke der Datenverarbeitung', en: '3. Purposes of Data Processing' } },
{ id: 'legal-basis', order: 4, title: { de: '4. Rechtsgrundlagen der Verarbeitung', en: '4. Legal Basis for Processing' } },
{ id: 'recipients', order: 5, title: { de: '5. Empfaenger und Datenweitergabe', en: '5. Recipients and Data Sharing' } },
{ id: 'retention', order: 6, title: { de: '6. Speicherdauer', en: '6. Data Retention' } },
{ id: 'rights', order: 7, title: { de: '7. Ihre Rechte als betroffene Person', en: '7. Your Rights as a Data Subject' } },
{ id: 'cookies', order: 8, title: { de: '8. Cookies und aehnliche Technologien', en: '8. Cookies and Similar Technologies' } },
{ id: 'changes', order: 9, title: { de: '9. Aenderungen dieser Datenschutzerklaerung', en: '9. Changes to this Privacy Policy' } },
]
return NextResponse.json({
sections,
availableLanguages: ['de', 'en'],
availableFormats: ['HTML', 'MARKDOWN', 'PDF', 'DOCX'],
})
} catch (error) {
console.error('Error fetching sections:', error)
return NextResponse.json(
{ error: 'Failed to fetch sections' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,88 @@
import { NextRequest, NextResponse } from 'next/server'
/**
* SDK Export API
*
* GET /api/sdk/v1/export?format=json|pdf|zip - Export SDK data
*/
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const format = searchParams.get('format') || 'json'
const tenantId = searchParams.get('tenantId') || 'default'
switch (format) {
case 'json':
return exportJSON(tenantId)
case 'pdf':
return exportPDF(tenantId)
case 'zip':
return exportZIP(tenantId)
default:
return NextResponse.json(
{ error: `Unknown export format: ${format}` },
{ status: 400 }
)
}
} catch (error) {
console.error('Failed to export:', error)
return NextResponse.json(
{ error: 'Failed to export' },
{ status: 500 }
)
}
}
function exportJSON(tenantId: string) {
// In production, this would fetch the actual state from the database
const exportData = {
version: '1.0.0',
exportedAt: new Date().toISOString(),
tenantId,
data: {
useCases: [],
screening: null,
modules: [],
requirements: [],
controls: [],
evidence: [],
risks: [],
dsfa: null,
toms: [],
vvt: [],
documents: [],
},
}
return new NextResponse(JSON.stringify(exportData, null, 2), {
status: 200,
headers: {
'Content-Type': 'application/json',
'Content-Disposition': `attachment; filename="compliance-export-${tenantId}-${new Date().toISOString().split('T')[0]}.json"`,
},
})
}
function exportPDF(tenantId: string) {
// In production, this would generate a proper PDF using a library like pdfkit or puppeteer
// For now, return a placeholder response
return NextResponse.json({
success: false,
error: 'PDF export not yet implemented',
message: 'PDF generation requires server-side rendering. Use JSON export for now.',
}, { status: 501 })
}
function exportZIP(tenantId: string) {
// In production, this would create a ZIP file with multiple documents
// For now, return a placeholder response
return NextResponse.json({
success: false,
error: 'ZIP export not yet implemented',
message: 'ZIP generation requires additional server-side processing. Use JSON export for now.',
}, { status: 501 })
}

View File

@@ -0,0 +1,150 @@
import { NextRequest, NextResponse } from 'next/server'
/**
* SDK Flow API
*
* GET /api/sdk/v1/flow - Get current flow state and suggestions
* POST /api/sdk/v1/flow/next - Navigate to next step
* POST /api/sdk/v1/flow/previous - Navigate to previous step
*/
const SDK_STEPS = [
// Phase 1
{ id: 'company-profile', phase: 1, order: 1, name: 'Unternehmensprofil', url: '/sdk/company-profile' },
{ id: 'use-case-assessment', phase: 1, order: 2, name: 'Anwendungsfall-Erfassung', url: '/sdk/advisory-board' },
{ id: 'screening', phase: 1, order: 3, name: 'System Screening', url: '/sdk/screening' },
{ id: 'modules', phase: 1, order: 4, name: 'Compliance Modules', url: '/sdk/modules' },
{ id: 'requirements', phase: 1, order: 5, name: 'Requirements', url: '/sdk/requirements' },
{ id: 'controls', phase: 1, order: 6, name: 'Controls', url: '/sdk/controls' },
{ id: 'evidence', phase: 1, order: 7, name: 'Evidence', url: '/sdk/evidence' },
{ id: 'audit-checklist', phase: 1, order: 8, name: 'Audit Checklist', url: '/sdk/audit-checklist' },
{ id: 'risks', phase: 1, order: 9, name: 'Risk Matrix', url: '/sdk/risks' },
// Phase 2
{ id: 'ai-act', phase: 2, order: 1, name: 'AI Act Klassifizierung', url: '/sdk/ai-act' },
{ id: 'obligations', phase: 2, order: 2, name: 'Pflichtenübersicht', url: '/sdk/obligations' },
{ id: 'dsfa', phase: 2, order: 3, name: 'DSFA', url: '/sdk/dsfa' },
{ id: 'tom', phase: 2, order: 4, name: 'TOMs', url: '/sdk/tom' },
{ id: 'loeschfristen', phase: 2, order: 5, name: 'Löschfristen', url: '/sdk/loeschfristen' },
{ id: 'vvt', phase: 2, order: 6, name: 'Verarbeitungsverzeichnis', url: '/sdk/vvt' },
{ id: 'consent', phase: 2, order: 7, name: 'Rechtliche Vorlagen', url: '/sdk/consent' },
{ id: 'cookie-banner', phase: 2, order: 8, name: 'Cookie Banner', url: '/sdk/cookie-banner' },
{ id: 'einwilligungen', phase: 2, order: 9, name: 'Einwilligungen', url: '/sdk/einwilligungen' },
{ id: 'dsr', phase: 2, order: 10, name: 'DSR Portal', url: '/sdk/dsr' },
{ id: 'escalations', phase: 2, order: 11, name: 'Escalations', url: '/sdk/escalations' },
]
function getStepIndex(stepId: string): number {
return SDK_STEPS.findIndex(s => s.id === stepId)
}
function getNextStep(currentStepId: string) {
const currentIndex = getStepIndex(currentStepId)
if (currentIndex === -1 || currentIndex >= SDK_STEPS.length - 1) {
return null
}
return SDK_STEPS[currentIndex + 1]
}
function getPreviousStep(currentStepId: string) {
const currentIndex = getStepIndex(currentStepId)
if (currentIndex <= 0) {
return null
}
return SDK_STEPS[currentIndex - 1]
}
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const currentStepId = searchParams.get('currentStep') || 'company-profile'
const currentStep = SDK_STEPS.find(s => s.id === currentStepId)
const nextStep = getNextStep(currentStepId)
const previousStep = getPreviousStep(currentStepId)
// Generate suggestions based on context
const suggestions = [
{
type: 'NAVIGATION',
label: nextStep ? `Weiter zu ${nextStep.name}` : 'Flow abgeschlossen',
action: nextStep ? `navigate:${nextStep.url}` : null,
},
{
type: 'ACTION',
label: 'Checkpoint validieren',
action: 'validate:current',
},
{
type: 'HELP',
label: 'Hilfe anzeigen',
action: 'help:show',
},
]
return NextResponse.json({
success: true,
currentStep,
nextStep,
previousStep,
totalSteps: SDK_STEPS.length,
currentIndex: getStepIndex(currentStepId) + 1,
suggestions,
steps: SDK_STEPS,
})
} catch (error) {
console.error('Failed to get flow:', error)
return NextResponse.json(
{ error: 'Failed to get flow' },
{ status: 500 }
)
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { action, currentStepId } = body
if (!action || !currentStepId) {
return NextResponse.json(
{ error: 'action and currentStepId are required' },
{ status: 400 }
)
}
let targetStep = null
switch (action) {
case 'next':
targetStep = getNextStep(currentStepId)
break
case 'previous':
targetStep = getPreviousStep(currentStepId)
break
default:
return NextResponse.json(
{ error: 'Invalid action' },
{ status: 400 }
)
}
if (!targetStep) {
return NextResponse.json(
{ error: 'No target step available' },
{ status: 400 }
)
}
return NextResponse.json({
success: true,
targetStep,
redirectUrl: targetStep.url,
})
} catch (error) {
console.error('Failed to navigate flow:', error)
return NextResponse.json(
{ error: 'Failed to navigate flow' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,309 @@
import { NextRequest, NextResponse } from 'next/server'
/**
* SDK Document Generation API
*
* POST /api/sdk/v1/generate - Generate compliance documents
*
* Supported document types:
* - dsfa: Data Protection Impact Assessment
* - tom: Technical and Organizational Measures
* - vvt: Processing Register (Art. 30 GDPR)
* - cookie-banner: Cookie consent banner code
* - audit-report: Audit report
*/
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { documentType, context, options } = body
if (!documentType) {
return NextResponse.json(
{ error: 'documentType is required' },
{ status: 400 }
)
}
// Generate document based on type
let document: unknown = null
let generationTime = Date.now()
switch (documentType) {
case 'dsfa':
document = generateDSFA(context, options)
break
case 'tom':
document = generateTOMs(context, options)
break
case 'vvt':
document = generateVVT(context, options)
break
case 'cookie-banner':
document = generateCookieBanner(context, options)
break
case 'audit-report':
document = generateAuditReport(context, options)
break
default:
return NextResponse.json(
{ error: `Unknown document type: ${documentType}` },
{ status: 400 }
)
}
generationTime = Date.now() - generationTime
return NextResponse.json({
success: true,
documentType,
document,
generatedAt: new Date().toISOString(),
generationTimeMs: generationTime,
})
} catch (error) {
console.error('Failed to generate document:', error)
return NextResponse.json(
{ error: 'Failed to generate document' },
{ status: 500 }
)
}
}
// =============================================================================
// DOCUMENT GENERATORS
// =============================================================================
function generateDSFA(context: unknown, options: unknown) {
return {
id: `dsfa-${Date.now()}`,
status: 'DRAFT',
version: 1,
sections: [
{
id: 'section-1',
title: '1. Systematische Beschreibung der Verarbeitungsvorgänge',
content: 'Die geplante Verarbeitung umfasst...',
status: 'DRAFT',
order: 1,
},
{
id: 'section-2',
title: '2. Bewertung der Notwendigkeit und Verhältnismäßigkeit',
content: 'Die Verarbeitung ist notwendig für...',
status: 'DRAFT',
order: 2,
},
{
id: 'section-3',
title: '3. Bewertung der Risiken für die Rechte und Freiheiten',
content: 'Identifizierte Risiken:\n- Risiko 1\n- Risiko 2',
status: 'DRAFT',
order: 3,
},
{
id: 'section-4',
title: '4. Abhilfemaßnahmen',
content: 'Folgende Maßnahmen werden ergriffen...',
status: 'DRAFT',
order: 4,
},
],
approvals: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
}
}
function generateTOMs(context: unknown, options: unknown) {
return {
toms: [
{
id: 'tom-1',
category: 'Zutrittskontrolle',
name: 'Physische Zugangskontrollen',
description: 'Maßnahmen zur Verhinderung unbefugten Zutritts zu Datenverarbeitungsanlagen',
type: 'TECHNICAL',
implementationStatus: 'NOT_IMPLEMENTED',
priority: 'HIGH',
},
{
id: 'tom-2',
category: 'Zugangskontrolle',
name: 'Authentifizierung',
description: 'Multi-Faktor-Authentifizierung für alle Systeme',
type: 'TECHNICAL',
implementationStatus: 'NOT_IMPLEMENTED',
priority: 'HIGH',
},
{
id: 'tom-3',
category: 'Zugriffskontrolle',
name: 'Rollenbasierte Zugriffskontrolle',
description: 'RBAC-System für granulare Berechtigungsvergabe',
type: 'ORGANIZATIONAL',
implementationStatus: 'NOT_IMPLEMENTED',
priority: 'HIGH',
},
{
id: 'tom-4',
category: 'Weitergabekontrolle',
name: 'Verschlüsselung',
description: 'Ende-zu-Ende-Verschlüsselung für Datenübertragung',
type: 'TECHNICAL',
implementationStatus: 'NOT_IMPLEMENTED',
priority: 'HIGH',
},
{
id: 'tom-5',
category: 'Eingabekontrolle',
name: 'Audit Logging',
description: 'Protokollierung aller Dateneingaben und -änderungen',
type: 'TECHNICAL',
implementationStatus: 'NOT_IMPLEMENTED',
priority: 'MEDIUM',
},
],
generatedAt: new Date().toISOString(),
}
}
function generateVVT(context: unknown, options: unknown) {
return {
processingActivities: [
{
id: 'pa-1',
name: 'Kundenmanagement',
purpose: 'Verwaltung von Kundenbeziehungen und Aufträgen',
legalBasis: 'Art. 6 Abs. 1 lit. b DSGVO (Vertrag)',
dataCategories: ['Name', 'Kontaktdaten', 'Bestellhistorie'],
dataSubjects: ['Kunden'],
recipients: ['Interne Mitarbeiter', 'Zahlungsdienstleister'],
thirdCountryTransfers: false,
retentionPeriod: '10 Jahre (handelsrechtliche Aufbewahrungspflicht)',
technicalMeasures: ['Verschlüsselung', 'Zugriffskontrolle'],
organizationalMeasures: ['Schulungen', 'Vertraulichkeitsverpflichtung'],
},
],
generatedAt: new Date().toISOString(),
version: '1.0',
}
}
function generateCookieBanner(context: unknown, options: unknown) {
return {
id: `cookie-${Date.now()}`,
style: 'BANNER',
position: 'BOTTOM',
theme: 'LIGHT',
texts: {
title: 'Cookie-Einstellungen',
description: 'Wir verwenden Cookies, um Ihnen die beste Nutzererfahrung zu bieten.',
acceptAll: 'Alle akzeptieren',
rejectAll: 'Alle ablehnen',
settings: 'Einstellungen',
save: 'Speichern',
},
categories: [
{
id: 'necessary',
name: 'Notwendig',
description: 'Diese Cookies sind für die Grundfunktionen erforderlich.',
required: true,
cookies: [],
},
{
id: 'analytics',
name: 'Analyse',
description: 'Diese Cookies helfen uns, die Nutzung zu verstehen.',
required: false,
cookies: [],
},
{
id: 'marketing',
name: 'Marketing',
description: 'Diese Cookies werden für Werbezwecke verwendet.',
required: false,
cookies: [],
},
],
generatedCode: {
html: `<!-- Cookie Banner HTML -->
<div id="cookie-banner" class="cookie-banner">
<div class="cookie-content">
<h3>Cookie-Einstellungen</h3>
<p>Wir verwenden Cookies, um Ihnen die beste Nutzererfahrung zu bieten.</p>
<div class="cookie-actions">
<button onclick="acceptAll()">Alle akzeptieren</button>
<button onclick="rejectAll()">Alle ablehnen</button>
<button onclick="showSettings()">Einstellungen</button>
</div>
</div>
</div>`,
css: `.cookie-banner {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: white;
box-shadow: 0 -2px 10px rgba(0,0,0,0.1);
padding: 20px;
z-index: 9999;
}
.cookie-content { max-width: 1200px; margin: 0 auto; }
.cookie-actions { margin-top: 15px; display: flex; gap: 10px; }
.cookie-actions button { padding: 10px 20px; border-radius: 5px; cursor: pointer; }`,
js: `function acceptAll() {
setCookie('consent', 'all', 365);
document.getElementById('cookie-banner').style.display = 'none';
}
function rejectAll() {
setCookie('consent', 'necessary', 365);
document.getElementById('cookie-banner').style.display = 'none';
}
function setCookie(name, value, days) {
const expires = new Date(Date.now() + days * 864e5).toUTCString();
document.cookie = name + '=' + value + '; expires=' + expires + '; path=/; SameSite=Lax';
}`,
},
generatedAt: new Date().toISOString(),
}
}
function generateAuditReport(context: unknown, options: unknown) {
return {
id: `audit-${Date.now()}`,
title: 'Compliance Audit Report',
generatedAt: new Date().toISOString(),
summary: {
totalChecks: 50,
passed: 35,
failed: 10,
warnings: 5,
complianceScore: 70,
},
sections: [
{
title: 'Executive Summary',
content: 'Dieser Bericht fasst den aktuellen Compliance-Status zusammen...',
},
{
title: 'Methodik',
content: 'Die Prüfung wurde gemäß ISO 27001 und DSGVO durchgeführt...',
},
{
title: 'Ergebnisse',
content: 'Hauptabweichungen: 3\nNebenabweichungen: 7\nEmpfehlungen: 5',
},
{
title: 'Empfehlungen',
content: '1. Implementierung von MFA\n2. Verbesserung der Dokumentation\n3. Regelmäßige Schulungen',
},
],
}
}

View File

@@ -0,0 +1,345 @@
import { NextRequest, NextResponse } from 'next/server'
/**
* SDK State Management API
*
* GET /api/sdk/v1/state?tenantId=xxx - Load state for a tenant
* POST /api/sdk/v1/state - Save state for a tenant
* DELETE /api/sdk/v1/state?tenantId=xxx - Clear state for a tenant
*
* Features:
* - Versioning for optimistic locking
* - Last-Modified headers
* - ETag support for caching
* - Prepared for PostgreSQL migration
*/
// =============================================================================
// TYPES
// =============================================================================
interface StoredState {
state: unknown
version: number
userId?: string
createdAt: string
updatedAt: string
}
// =============================================================================
// STORAGE LAYER (Abstract - Easy to swap to PostgreSQL)
// =============================================================================
/**
* In-memory storage for development
* TODO: Replace with PostgreSQL implementation
*
* PostgreSQL Schema:
* CREATE TABLE sdk_states (
* id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
* tenant_id VARCHAR(255) NOT NULL UNIQUE,
* user_id VARCHAR(255),
* state JSONB NOT NULL,
* version INTEGER DEFAULT 1,
* created_at TIMESTAMP DEFAULT NOW(),
* updated_at TIMESTAMP DEFAULT NOW()
* );
*
* CREATE INDEX idx_sdk_states_tenant ON sdk_states(tenant_id);
*/
interface StateStore {
get(tenantId: string): Promise<StoredState | null>
save(tenantId: string, state: unknown, userId?: string, expectedVersion?: number): Promise<StoredState>
delete(tenantId: string): Promise<boolean>
}
class InMemoryStateStore implements StateStore {
private store: Map<string, StoredState> = new Map()
async get(tenantId: string): Promise<StoredState | null> {
return this.store.get(tenantId) || null
}
async save(
tenantId: string,
state: unknown,
userId?: string,
expectedVersion?: number
): Promise<StoredState> {
const existing = this.store.get(tenantId)
// Optimistic locking check
if (expectedVersion !== undefined && existing && existing.version !== expectedVersion) {
const error = new Error('Version conflict') as Error & { status: number }
error.status = 409
throw error
}
const now = new Date().toISOString()
const newVersion = existing ? existing.version + 1 : 1
const stored: StoredState = {
state: {
...(state as object),
lastModified: now,
},
version: newVersion,
userId,
createdAt: existing?.createdAt || now,
updatedAt: now,
}
this.store.set(tenantId, stored)
return stored
}
async delete(tenantId: string): Promise<boolean> {
return this.store.delete(tenantId)
}
}
// Future PostgreSQL implementation would look like:
// class PostgreSQLStateStore implements StateStore {
// private db: Pool
//
// constructor(connectionString: string) {
// this.db = new Pool({ connectionString })
// }
//
// async get(tenantId: string): Promise<StoredState | null> {
// const result = await this.db.query(
// 'SELECT state, version, user_id, created_at, updated_at FROM sdk_states WHERE tenant_id = $1',
// [tenantId]
// )
// if (result.rows.length === 0) return null
// const row = result.rows[0]
// return {
// state: row.state,
// version: row.version,
// userId: row.user_id,
// createdAt: row.created_at,
// updatedAt: row.updated_at,
// }
// }
//
// async save(tenantId: string, state: unknown, userId?: string, expectedVersion?: number): Promise<StoredState> {
// // Use UPSERT with version check
// const result = await this.db.query(`
// INSERT INTO sdk_states (tenant_id, user_id, state, version)
// VALUES ($1, $2, $3, 1)
// ON CONFLICT (tenant_id) DO UPDATE SET
// state = $3,
// user_id = COALESCE($2, sdk_states.user_id),
// version = sdk_states.version + 1,
// updated_at = NOW()
// WHERE ($4::int IS NULL OR sdk_states.version = $4)
// RETURNING version, created_at, updated_at
// `, [tenantId, userId, JSON.stringify(state), expectedVersion])
//
// if (result.rows.length === 0) {
// throw new Error('Version conflict')
// }
//
// return {
// state,
// version: result.rows[0].version,
// userId,
// createdAt: result.rows[0].created_at,
// updatedAt: result.rows[0].updated_at,
// }
// }
//
// async delete(tenantId: string): Promise<boolean> {
// const result = await this.db.query(
// 'DELETE FROM sdk_states WHERE tenant_id = $1',
// [tenantId]
// )
// return result.rowCount > 0
// }
// }
// Use in-memory store for now
const stateStore: StateStore = new InMemoryStateStore()
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
function generateETag(version: number, updatedAt: string): string {
return `"${version}-${Buffer.from(updatedAt).toString('base64').slice(0, 8)}"`
}
// =============================================================================
// HANDLERS
// =============================================================================
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const tenantId = searchParams.get('tenantId')
if (!tenantId) {
return NextResponse.json(
{ success: false, error: 'tenantId is required' },
{ status: 400 }
)
}
const stored = await stateStore.get(tenantId)
if (!stored) {
return NextResponse.json(
{ success: false, error: 'State not found', tenantId },
{ status: 404 }
)
}
const etag = generateETag(stored.version, stored.updatedAt)
// Check If-None-Match header for caching
const ifNoneMatch = request.headers.get('If-None-Match')
if (ifNoneMatch === etag) {
return new NextResponse(null, { status: 304 })
}
return NextResponse.json(
{
success: true,
data: {
tenantId,
state: stored.state,
version: stored.version,
lastModified: stored.updatedAt,
},
},
{
headers: {
'ETag': etag,
'Last-Modified': stored.updatedAt,
'Cache-Control': 'private, no-cache',
},
}
)
} catch (error) {
console.error('Failed to load SDK state:', error)
return NextResponse.json(
{ success: false, error: 'Failed to load state' },
{ status: 500 }
)
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { tenantId, state, version } = body
if (!tenantId) {
return NextResponse.json(
{ success: false, error: 'tenantId is required' },
{ status: 400 }
)
}
if (!state) {
return NextResponse.json(
{ success: false, error: 'state is required' },
{ status: 400 }
)
}
// Check If-Match header for optimistic locking
const ifMatch = request.headers.get('If-Match')
const expectedVersion = ifMatch ? parseInt(ifMatch, 10) : version
const stored = await stateStore.save(tenantId, state, body.userId, expectedVersion)
const etag = generateETag(stored.version, stored.updatedAt)
return NextResponse.json(
{
success: true,
data: {
tenantId,
state: stored.state,
version: stored.version,
lastModified: stored.updatedAt,
},
},
{
headers: {
'ETag': etag,
'Last-Modified': stored.updatedAt,
},
}
)
} catch (error) {
const err = error as Error & { status?: number }
// Handle version conflict
if (err.status === 409 || err.message === 'Version conflict') {
return NextResponse.json(
{
success: false,
error: 'Version conflict. State was modified by another request.',
code: 'VERSION_CONFLICT',
},
{ status: 409 }
)
}
console.error('Failed to save SDK state:', error)
return NextResponse.json(
{ success: false, error: 'Failed to save state' },
{ status: 500 }
)
}
}
export async function DELETE(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const tenantId = searchParams.get('tenantId')
if (!tenantId) {
return NextResponse.json(
{ success: false, error: 'tenantId is required' },
{ status: 400 }
)
}
const deleted = await stateStore.delete(tenantId)
if (!deleted) {
return NextResponse.json(
{ success: false, error: 'State not found', tenantId },
{ status: 404 }
)
}
return NextResponse.json({
success: true,
tenantId,
deletedAt: new Date().toISOString(),
})
} catch (error) {
console.error('Failed to delete SDK state:', error)
return NextResponse.json(
{ success: false, error: 'Failed to delete state' },
{ status: 500 }
)
}
}
// =============================================================================
// HEALTH CHECK
// =============================================================================
export async function OPTIONS() {
return NextResponse.json({ status: 'ok' }, {
headers: {
'Allow': 'GET, POST, DELETE, OPTIONS',
},
})
}

View File

@@ -0,0 +1,107 @@
import { NextRequest, NextResponse } from 'next/server'
import { TOMRulesEngine } from '@/lib/sdk/tom-generator/rules-engine'
import { TOMGeneratorState } from '@/lib/sdk/tom-generator/types'
/**
* TOM Generator Controls Evaluation API
*
* POST /api/sdk/v1/tom-generator/controls/evaluate - Evaluate controls for given state
*
* Request body:
* {
* state: TOMGeneratorState
* }
*
* Response:
* {
* evaluations: RulesEngineResult[]
* derivedTOMs: DerivedTOM[]
* summary: {
* total: number
* required: number
* recommended: number
* optional: number
* notApplicable: number
* }
* }
*/
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { state } = body
if (!state) {
return NextResponse.json(
{ success: false, error: 'state is required in request body' },
{ status: 400 }
)
}
// Parse dates in state
const parsedState: TOMGeneratorState = {
...state,
createdAt: new Date(state.createdAt),
updatedAt: new Date(state.updatedAt),
steps: state.steps?.map((step: { id: string; completed: boolean; data: unknown; validatedAt: string | null }) => ({
...step,
validatedAt: step.validatedAt ? new Date(step.validatedAt) : null,
})) || [],
documents: [],
derivedTOMs: [],
gapAnalysis: null,
exports: [],
}
// Initialize rules engine and evaluate
const engine = new TOMRulesEngine()
const evaluations = engine.evaluateControls(parsedState)
const derivedTOMs = engine.deriveAllTOMs(parsedState)
// Calculate summary
const summary = {
total: evaluations.length,
required: evaluations.filter((e) => e.applicability === 'REQUIRED').length,
recommended: evaluations.filter((e) => e.applicability === 'RECOMMENDED').length,
optional: evaluations.filter((e) => e.applicability === 'OPTIONAL').length,
notApplicable: evaluations.filter((e) => e.applicability === 'NOT_APPLICABLE').length,
}
// Group by category
const byCategory: Record<string, typeof evaluations> = {}
evaluations.forEach((e) => {
const category = e.controlId.split('-')[1] // Extract category from ID like TOM-AC-01
if (!byCategory[category]) {
byCategory[category] = []
}
byCategory[category].push(e)
})
return NextResponse.json({
success: true,
data: {
evaluations,
derivedTOMs,
summary,
byCategory,
},
})
} catch (error) {
console.error('Failed to evaluate controls:', error)
return NextResponse.json(
{ success: false, error: 'Failed to evaluate controls' },
{ status: 500 }
)
}
}
export async function OPTIONS() {
return NextResponse.json(
{ status: 'ok' },
{
headers: {
Allow: 'POST, OPTIONS',
},
}
)
}

View File

@@ -0,0 +1,128 @@
import { NextRequest, NextResponse } from 'next/server'
import {
getAllControls,
getControlById,
getControlsByCategory,
searchControls,
getCategories,
} from '@/lib/sdk/tom-generator/controls/loader'
import { ControlCategory } from '@/lib/sdk/tom-generator/types'
/**
* TOM Generator Controls API
*
* GET /api/sdk/v1/tom-generator/controls - List all controls
* GET /api/sdk/v1/tom-generator/controls?id=xxx - Get single control
* GET /api/sdk/v1/tom-generator/controls?category=xxx - Filter by category
* GET /api/sdk/v1/tom-generator/controls?search=xxx - Search controls
* GET /api/sdk/v1/tom-generator/controls?categories=true - Get categories list
*/
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const id = searchParams.get('id')
const category = searchParams.get('category')
const search = searchParams.get('search')
const categoriesOnly = searchParams.get('categories')
const language = (searchParams.get('language') || 'de') as 'de' | 'en'
// Get categories list
if (categoriesOnly === 'true') {
const categories = getCategories()
return NextResponse.json({
success: true,
data: categories,
})
}
// Get single control by ID
if (id) {
const control = getControlById(id)
if (!control) {
return NextResponse.json(
{ success: false, error: `Control not found: ${id}` },
{ status: 404 }
)
}
return NextResponse.json({
success: true,
data: {
...control,
// Return localized name and description
localizedName: control.name[language],
localizedDescription: control.description[language],
},
})
}
// Filter by category
if (category) {
const controls = getControlsByCategory(category as ControlCategory)
return NextResponse.json({
success: true,
data: controls.map((c) => ({
...c,
localizedName: c.name[language],
localizedDescription: c.description[language],
})),
meta: {
category,
count: controls.length,
},
})
}
// Search controls
if (search) {
const controls = searchControls(search, language)
return NextResponse.json({
success: true,
data: controls.map((c) => ({
...c,
localizedName: c.name[language],
localizedDescription: c.description[language],
})),
meta: {
query: search,
count: controls.length,
},
})
}
// Return all controls
const controls = getAllControls()
const categories = getCategories()
return NextResponse.json({
success: true,
data: controls.map((c) => ({
...c,
localizedName: c.name[language],
localizedDescription: c.description[language],
})),
meta: {
totalControls: controls.length,
categories: categories.length,
language,
},
})
} catch (error) {
console.error('Failed to fetch controls:', error)
return NextResponse.json(
{ success: false, error: 'Failed to fetch controls' },
{ status: 500 }
)
}
}
export async function OPTIONS() {
return NextResponse.json(
{ status: 'ok' },
{
headers: {
Allow: 'GET, OPTIONS',
},
}
)
}

View File

@@ -0,0 +1,121 @@
import { NextRequest, NextResponse } from 'next/server'
import { TOMDocumentAnalyzer } from '@/lib/sdk/tom-generator/ai/document-analyzer'
import { evidenceStore } from '@/lib/sdk/tom-generator/evidence-store'
/**
* TOM Generator Evidence Analysis API
*
* POST /api/sdk/v1/tom-generator/evidence/[id]/analyze - Analyze evidence document with AI
*
* Request body:
* {
* tenantId: string
* documentText?: string (if already extracted)
* }
*/
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
const body = await request.json()
const { tenantId, documentText } = body
if (!tenantId) {
return NextResponse.json(
{ success: false, error: 'tenantId is required' },
{ status: 400 }
)
}
// Get the document
const document = await evidenceStore.getById(tenantId, id)
if (!document) {
return NextResponse.json(
{ success: false, error: `Document not found: ${id}` },
{ status: 404 }
)
}
// Check if already analyzed
if (document.aiAnalysis && document.status === 'ANALYZED') {
return NextResponse.json({
success: true,
data: document.aiAnalysis,
meta: {
alreadyAnalyzed: true,
analyzedAt: document.aiAnalysis.analyzedAt,
},
})
}
// Get document text (in production, this would be extracted from the file)
const text = documentText || `[Document content from ${document.originalName}]`
// Initialize analyzer
const analyzer = new TOMDocumentAnalyzer()
// Analyze the document
const analysisResult = await analyzer.analyzeDocument(
document,
text,
'de'
)
// Check if analysis was successful
if (!analysisResult.success || !analysisResult.analysis) {
return NextResponse.json(
{ success: false, error: analysisResult.error || 'Analysis failed' },
{ status: 500 }
)
}
const analysis = analysisResult.analysis
// Update the document with analysis results
const updatedDocument = await evidenceStore.update(tenantId, id, {
aiAnalysis: analysis,
status: 'ANALYZED',
linkedControlIds: [
...new Set([
...document.linkedControlIds,
...analysis.applicableControls,
]),
],
})
return NextResponse.json({
success: true,
data: {
analysis,
document: updatedDocument,
},
meta: {
documentId: id,
analyzedAt: analysis.analyzedAt,
confidence: analysis.confidence,
applicableControlsCount: analysis.applicableControls.length,
gapsCount: analysis.gaps.length,
},
})
} catch (error) {
console.error('Failed to analyze evidence:', error)
return NextResponse.json(
{ success: false, error: 'Failed to analyze evidence' },
{ status: 500 }
)
}
}
export async function OPTIONS() {
return NextResponse.json(
{ status: 'ok' },
{
headers: {
Allow: 'POST, OPTIONS',
},
}
)
}

View File

@@ -0,0 +1,153 @@
import { NextRequest, NextResponse } from 'next/server'
import { DocumentType } from '@/lib/sdk/tom-generator/types'
import { evidenceStore } from '@/lib/sdk/tom-generator/evidence-store'
/**
* TOM Generator Evidence API
*
* GET /api/sdk/v1/tom-generator/evidence?tenantId=xxx - List all evidence documents
* DELETE /api/sdk/v1/tom-generator/evidence?tenantId=xxx&id=xxx - Delete evidence
*/
// =============================================================================
// HANDLERS
// =============================================================================
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const tenantId = searchParams.get('tenantId')
const documentType = searchParams.get('type') as DocumentType | null
const status = searchParams.get('status')
const id = searchParams.get('id')
if (!tenantId) {
return NextResponse.json(
{ success: false, error: 'tenantId is required' },
{ status: 400 }
)
}
// Get single document
if (id) {
const document = await evidenceStore.getById(tenantId, id)
if (!document) {
return NextResponse.json(
{ success: false, error: `Document not found: ${id}` },
{ status: 404 }
)
}
return NextResponse.json({
success: true,
data: document,
})
}
// Filter by type
if (documentType) {
const documents = await evidenceStore.getByType(tenantId, documentType)
return NextResponse.json({
success: true,
data: documents,
meta: {
count: documents.length,
filter: { type: documentType },
},
})
}
// Filter by status
if (status) {
const documents = await evidenceStore.getByStatus(tenantId, status)
return NextResponse.json({
success: true,
data: documents,
meta: {
count: documents.length,
filter: { status },
},
})
}
// Get all documents
const documents = await evidenceStore.getAll(tenantId)
// Group by type for summary
const byType: Record<string, number> = {}
const byStatus: Record<string, number> = {}
documents.forEach((doc) => {
byType[doc.documentType] = (byType[doc.documentType] || 0) + 1
byStatus[doc.status] = (byStatus[doc.status] || 0) + 1
})
return NextResponse.json({
success: true,
data: documents,
meta: {
count: documents.length,
byType,
byStatus,
},
})
} catch (error) {
console.error('Failed to fetch evidence:', error)
return NextResponse.json(
{ success: false, error: 'Failed to fetch evidence' },
{ status: 500 }
)
}
}
export async function DELETE(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const tenantId = searchParams.get('tenantId')
const id = searchParams.get('id')
if (!tenantId) {
return NextResponse.json(
{ success: false, error: 'tenantId is required' },
{ status: 400 }
)
}
if (!id) {
return NextResponse.json(
{ success: false, error: 'id is required' },
{ status: 400 }
)
}
const deleted = await evidenceStore.delete(tenantId, id)
if (!deleted) {
return NextResponse.json(
{ success: false, error: `Document not found: ${id}` },
{ status: 404 }
)
}
return NextResponse.json({
success: true,
id,
deletedAt: new Date().toISOString(),
})
} catch (error) {
console.error('Failed to delete evidence:', error)
return NextResponse.json(
{ success: false, error: 'Failed to delete evidence' },
{ status: 500 }
)
}
}
export async function OPTIONS() {
return NextResponse.json(
{ status: 'ok' },
{
headers: {
Allow: 'GET, DELETE, OPTIONS',
},
}
)
}

View File

@@ -0,0 +1,155 @@
import { NextRequest, NextResponse } from 'next/server'
import { EvidenceDocument, DocumentType } from '@/lib/sdk/tom-generator/types'
import { evidenceStore } from '@/lib/sdk/tom-generator/evidence-store'
import crypto from 'crypto'
/**
* TOM Generator Evidence Upload API
*
* POST /api/sdk/v1/tom-generator/evidence/upload - Upload evidence document
*
* Request: multipart/form-data
* - file: File
* - tenantId: string
* - documentType: DocumentType
* - validFrom?: string (ISO date)
* - validUntil?: string (ISO date)
* - linkedControlIds?: string (comma-separated)
*/
// Document type detection based on filename patterns
function detectDocumentType(filename: string, mimeType: string): DocumentType {
const lower = filename.toLowerCase()
if (lower.includes('avv') || lower.includes('auftragsverarbeitung')) {
return 'AVV'
}
if (lower.includes('dpa') || lower.includes('data processing')) {
return 'DPA'
}
if (lower.includes('sla') || lower.includes('service level')) {
return 'SLA'
}
if (lower.includes('nda') || lower.includes('vertraulichkeit') || lower.includes('geheimhaltung')) {
return 'NDA'
}
if (lower.includes('policy') || lower.includes('richtlinie')) {
return 'POLICY'
}
if (lower.includes('cert') || lower.includes('zertifikat') || lower.includes('iso')) {
return 'CERTIFICATE'
}
if (lower.includes('audit') || lower.includes('prüf') || lower.includes('bericht')) {
return 'AUDIT_REPORT'
}
return 'OTHER'
}
export async function POST(request: NextRequest) {
try {
const formData = await request.formData()
const file = formData.get('file') as File | null
const tenantId = formData.get('tenantId') as string | null
const documentType = formData.get('documentType') as DocumentType | null
const validFrom = formData.get('validFrom') as string | null
const validUntil = formData.get('validUntil') as string | null
const linkedControlIdsStr = formData.get('linkedControlIds') as string | null
const uploadedBy = formData.get('uploadedBy') as string | null
if (!file) {
return NextResponse.json(
{ success: false, error: 'file is required' },
{ status: 400 }
)
}
if (!tenantId) {
return NextResponse.json(
{ success: false, error: 'tenantId is required' },
{ status: 400 }
)
}
// Read file data
const arrayBuffer = await file.arrayBuffer()
const buffer = Buffer.from(arrayBuffer)
// Generate hash for deduplication
const hash = crypto.createHash('sha256').update(buffer).digest('hex')
// Generate unique filename
const id = crypto.randomUUID()
const ext = file.name.split('.').pop() || 'bin'
const filename = `${id}.${ext}`
// Detect document type if not provided
const detectedType = detectDocumentType(file.name, file.type)
const finalDocumentType = documentType || detectedType
// Parse linked control IDs
const linkedControlIds = linkedControlIdsStr
? linkedControlIdsStr.split(',').map((s) => s.trim()).filter(Boolean)
: []
// Create evidence document
const document: EvidenceDocument = {
id,
filename,
originalName: file.name,
mimeType: file.type,
size: file.size,
uploadedAt: new Date(),
uploadedBy: uploadedBy || 'unknown',
documentType: finalDocumentType,
detectedType,
hash,
validFrom: validFrom ? new Date(validFrom) : null,
validUntil: validUntil ? new Date(validUntil) : null,
linkedControlIds,
aiAnalysis: null,
status: 'PENDING',
}
// Store the document metadata
// Note: In production, the actual file would be stored in MinIO/S3
await evidenceStore.add(tenantId, document)
return NextResponse.json({
success: true,
data: {
id: document.id,
filename: document.filename,
originalName: document.originalName,
mimeType: document.mimeType,
size: document.size,
documentType: document.documentType,
detectedType: document.detectedType,
status: document.status,
uploadedAt: document.uploadedAt.toISOString(),
},
meta: {
hash,
needsAnalysis: true,
analyzeUrl: `/api/sdk/v1/tom-generator/evidence/${id}/analyze`,
},
})
} catch (error) {
console.error('Failed to upload evidence:', error)
return NextResponse.json(
{ success: false, error: 'Failed to upload evidence' },
{ status: 500 }
)
}
}
export async function OPTIONS() {
return NextResponse.json(
{ status: 'ok' },
{
headers: {
Allow: 'POST, OPTIONS',
},
}
)
}

View File

@@ -0,0 +1,245 @@
import { NextRequest, NextResponse } from 'next/server'
import { TOMGeneratorState } from '@/lib/sdk/tom-generator/types'
import { generateDOCXContent, generateDOCXFilename } from '@/lib/sdk/tom-generator/export/docx'
import { generatePDFContent, generatePDFFilename } from '@/lib/sdk/tom-generator/export/pdf'
import { generateZIPFiles, generateZIPFilename } from '@/lib/sdk/tom-generator/export/zip'
import crypto from 'crypto'
/**
* TOM Generator Export API
*
* POST /api/sdk/v1/tom-generator/export - Generate export
*
* Request body:
* {
* tenantId: string
* format: 'DOCX' | 'PDF' | 'JSON' | 'ZIP'
* language: 'de' | 'en'
* state: TOMGeneratorState
* options?: {
* includeEvidence?: boolean
* includeGapAnalysis?: boolean
* companyLogo?: string (base64)
* }
* }
*/
// In-memory export store for tracking exports
interface StoredExport {
id: string
tenantId: string
format: string
filename: string
content: string // Base64 encoded content
generatedAt: Date
size: number
}
const exportStore: Map<string, StoredExport> = new Map()
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { tenantId, format, language = 'de', state, options = {} } = body
if (!tenantId) {
return NextResponse.json(
{ success: false, error: 'tenantId is required' },
{ status: 400 }
)
}
if (!format) {
return NextResponse.json(
{ success: false, error: 'format is required (DOCX, PDF, JSON, ZIP)' },
{ status: 400 }
)
}
if (!state) {
return NextResponse.json(
{ success: false, error: 'state is required' },
{ status: 400 }
)
}
// Parse dates in state
const parsedState: TOMGeneratorState = {
...state,
createdAt: new Date(state.createdAt),
updatedAt: new Date(state.updatedAt),
steps: state.steps?.map((step: { id: string; completed: boolean; data: unknown; validatedAt: string | null }) => ({
...step,
validatedAt: step.validatedAt ? new Date(step.validatedAt) : null,
})) || [],
documents: state.documents?.map((doc: { uploadedAt: string; validFrom?: string; validUntil?: string; aiAnalysis?: { analyzedAt: string } }) => ({
...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,
})) || [],
derivedTOMs: state.derivedTOMs?.map((tom: { implementationDate?: string; reviewDate?: string }) => ({
...tom,
implementationDate: tom.implementationDate ? new Date(tom.implementationDate) : null,
reviewDate: tom.reviewDate ? new Date(tom.reviewDate) : null,
})) || [],
gapAnalysis: state.gapAnalysis ? {
...state.gapAnalysis,
generatedAt: new Date(state.gapAnalysis.generatedAt),
} : null,
exports: state.exports?.map((exp: { generatedAt: string }) => ({
...exp,
generatedAt: new Date(exp.generatedAt),
})) || [],
}
const exportId = crypto.randomUUID()
let content: string
let filename: string
let mimeType: string
switch (format.toUpperCase()) {
case 'DOCX': {
// Generate DOCX structure (actual binary conversion would require docx library)
const docxContent = generateDOCXContent(parsedState, { language: language as 'de' | 'en', ...options })
content = Buffer.from(JSON.stringify(docxContent, null, 2)).toString('base64')
filename = generateDOCXFilename(parsedState, language as 'de' | 'en')
mimeType = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
break
}
case 'PDF': {
// Generate PDF structure (actual binary conversion would require pdf library)
const pdfContent = generatePDFContent(parsedState, { language: language as 'de' | 'en', ...options })
content = Buffer.from(JSON.stringify(pdfContent, null, 2)).toString('base64')
filename = generatePDFFilename(parsedState, language as 'de' | 'en')
mimeType = 'application/pdf'
break
}
case 'JSON':
content = Buffer.from(JSON.stringify(parsedState, null, 2)).toString('base64')
filename = `tom-export-${tenantId}-${new Date().toISOString().split('T')[0]}.json`
mimeType = 'application/json'
break
case 'ZIP': {
const files = generateZIPFiles(parsedState, { language: language as 'de' | 'en', ...options })
// For now, return the files metadata (actual ZIP generation would require a library)
content = Buffer.from(JSON.stringify(files, null, 2)).toString('base64')
filename = generateZIPFilename(parsedState, language as 'de' | 'en')
mimeType = 'application/zip'
break
}
default:
return NextResponse.json(
{ success: false, error: `Unsupported format: ${format}` },
{ status: 400 }
)
}
// Store the export
const storedExport: StoredExport = {
id: exportId,
tenantId,
format: format.toUpperCase(),
filename,
content,
generatedAt: new Date(),
size: Buffer.from(content, 'base64').length,
}
exportStore.set(exportId, storedExport)
return NextResponse.json({
success: true,
data: {
exportId,
filename,
format: format.toUpperCase(),
mimeType,
size: storedExport.size,
generatedAt: storedExport.generatedAt.toISOString(),
downloadUrl: `/api/sdk/v1/tom-generator/export?exportId=${exportId}`,
},
})
} catch (error) {
console.error('Failed to generate export:', error)
return NextResponse.json(
{ success: false, error: 'Failed to generate export' },
{ status: 500 }
)
}
}
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const exportId = searchParams.get('exportId')
if (!exportId) {
return NextResponse.json(
{ success: false, error: 'exportId is required' },
{ status: 400 }
)
}
const storedExport = exportStore.get(exportId)
if (!storedExport) {
return NextResponse.json(
{ success: false, error: `Export not found: ${exportId}` },
{ status: 404 }
)
}
// Return the file as download
const buffer = Buffer.from(storedExport.content, 'base64')
let mimeType: string
switch (storedExport.format) {
case 'DOCX':
mimeType = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
break
case 'PDF':
mimeType = 'application/pdf'
break
case 'JSON':
mimeType = 'application/json'
break
case 'ZIP':
mimeType = 'application/zip'
break
default:
mimeType = 'application/octet-stream'
}
return new NextResponse(buffer, {
headers: {
'Content-Type': mimeType,
'Content-Disposition': `attachment; filename="${storedExport.filename}"`,
'Content-Length': buffer.length.toString(),
},
})
} catch (error) {
console.error('Failed to download export:', error)
return NextResponse.json(
{ success: false, error: 'Failed to download export' },
{ status: 500 }
)
}
}
export async function OPTIONS() {
return NextResponse.json(
{ status: 'ok' },
{
headers: {
Allow: 'GET, POST, OPTIONS',
},
}
)
}

View File

@@ -0,0 +1,205 @@
import { NextRequest, NextResponse } from 'next/server'
import { TOMRulesEngine } from '@/lib/sdk/tom-generator/rules-engine'
import { TOMGeneratorState, GapAnalysisResult } from '@/lib/sdk/tom-generator/types'
/**
* TOM Generator Gap Analysis API
*
* POST /api/sdk/v1/tom-generator/gap-analysis - Perform gap analysis
*
* Request body:
* {
* tenantId: string
* state: TOMGeneratorState
* }
*
* Response:
* {
* gapAnalysis: GapAnalysisResult
* }
*/
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { tenantId, state } = body
if (!tenantId) {
return NextResponse.json(
{ success: false, error: 'tenantId is required' },
{ status: 400 }
)
}
if (!state) {
return NextResponse.json(
{ success: false, error: 'state is required in request body' },
{ status: 400 }
)
}
// Parse dates in state
const parsedState: TOMGeneratorState = {
...state,
createdAt: new Date(state.createdAt),
updatedAt: new Date(state.updatedAt),
steps: state.steps?.map((step: { id: string; completed: boolean; data: unknown; validatedAt: string | null }) => ({
...step,
validatedAt: step.validatedAt ? new Date(step.validatedAt) : null,
})) || [],
documents: state.documents?.map((doc: { uploadedAt: string; validFrom?: string; validUntil?: string; aiAnalysis?: { analyzedAt: string } }) => ({
...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,
})) || [],
derivedTOMs: state.derivedTOMs?.map((tom: { implementationDate?: string; reviewDate?: string }) => ({
...tom,
implementationDate: tom.implementationDate ? new Date(tom.implementationDate) : null,
reviewDate: tom.reviewDate ? new Date(tom.reviewDate) : null,
})) || [],
gapAnalysis: state.gapAnalysis ? {
...state.gapAnalysis,
generatedAt: new Date(state.gapAnalysis.generatedAt),
} : null,
exports: state.exports?.map((exp: { generatedAt: string }) => ({
...exp,
generatedAt: new Date(exp.generatedAt),
})) || [],
}
// Initialize rules engine
const engine = new TOMRulesEngine()
// Perform gap analysis using derived TOMs and documents from state
const gapAnalysis = engine.performGapAnalysis(
parsedState.derivedTOMs,
parsedState.documents
)
// Calculate detailed metrics
const metrics = calculateGapMetrics(gapAnalysis)
return NextResponse.json({
success: true,
data: {
gapAnalysis,
metrics,
generatedAt: gapAnalysis.generatedAt.toISOString(),
},
})
} catch (error) {
console.error('Failed to perform gap analysis:', error)
return NextResponse.json(
{ success: false, error: 'Failed to perform gap analysis' },
{ status: 500 }
)
}
}
function calculateGapMetrics(gapAnalysis: GapAnalysisResult) {
const totalGaps = gapAnalysis.missingControls.length +
gapAnalysis.partialControls.length +
gapAnalysis.missingEvidence.length
const criticalGaps = gapAnalysis.missingControls.filter(
(c) => c.priority === 'CRITICAL' || c.priority === 'HIGH'
).length
const mediumGaps = gapAnalysis.missingControls.filter(
(c) => c.priority === 'MEDIUM'
).length
const lowGaps = gapAnalysis.missingControls.filter(
(c) => c.priority === 'LOW'
).length
// Group missing controls by category
const gapsByCategory: Record<string, number> = {}
gapAnalysis.missingControls.forEach((control) => {
const category = control.controlId.split('-')[1] || 'OTHER'
gapsByCategory[category] = (gapsByCategory[category] || 0) + 1
})
// Calculate compliance readiness
const maxScore = 100
const deductionPerCritical = 10
const deductionPerMedium = 5
const deductionPerLow = 2
const deductionPerPartial = 3
const deductionPerMissingEvidence = 1
const deductions =
criticalGaps * deductionPerCritical +
mediumGaps * deductionPerMedium +
lowGaps * deductionPerLow +
gapAnalysis.partialControls.length * deductionPerPartial +
gapAnalysis.missingEvidence.length * deductionPerMissingEvidence
const complianceReadiness = Math.max(0, Math.min(100, maxScore - deductions))
// Prioritized action items
const prioritizedActions = [
...gapAnalysis.missingControls
.filter((c) => c.priority === 'CRITICAL')
.map((c) => ({
type: 'MISSING_CONTROL',
priority: 'CRITICAL',
controlId: c.controlId,
reason: c.reason,
action: `Implement control ${c.controlId}`,
})),
...gapAnalysis.missingControls
.filter((c) => c.priority === 'HIGH')
.map((c) => ({
type: 'MISSING_CONTROL',
priority: 'HIGH',
controlId: c.controlId,
reason: c.reason,
action: `Implement control ${c.controlId}`,
})),
...gapAnalysis.partialControls.map((c) => ({
type: 'PARTIAL_CONTROL',
priority: 'MEDIUM',
controlId: c.controlId,
missingAspects: c.missingAspects,
action: `Complete implementation of ${c.controlId}`,
})),
...gapAnalysis.missingEvidence.map((e) => ({
type: 'MISSING_EVIDENCE',
priority: 'LOW',
controlId: e.controlId,
requiredEvidence: e.requiredEvidence,
action: `Upload evidence for ${e.controlId}`,
})),
]
return {
totalGaps,
criticalGaps,
mediumGaps,
lowGaps,
partialControls: gapAnalysis.partialControls.length,
missingEvidence: gapAnalysis.missingEvidence.length,
gapsByCategory,
complianceReadiness,
overallScore: gapAnalysis.overallScore,
prioritizedActionsCount: prioritizedActions.length,
prioritizedActions: prioritizedActions.slice(0, 10), // Top 10 actions
}
}
export async function OPTIONS() {
return NextResponse.json(
{ status: 'ok' },
{
headers: {
Allow: 'POST, OPTIONS',
},
}
)
}

View File

@@ -0,0 +1,250 @@
import { NextRequest, NextResponse } from 'next/server'
import {
TOMGeneratorState,
createEmptyTOMGeneratorState,
} from '@/lib/sdk/tom-generator/types'
/**
* TOM Generator State API
*
* GET /api/sdk/v1/tom-generator/state?tenantId=xxx - Load TOM generator state
* POST /api/sdk/v1/tom-generator/state - Save TOM generator state
* DELETE /api/sdk/v1/tom-generator/state?tenantId=xxx - Clear state
*/
// =============================================================================
// STORAGE (In-Memory for development)
// =============================================================================
interface StoredTOMState {
state: TOMGeneratorState
version: number
createdAt: string
updatedAt: string
}
class InMemoryTOMStateStore {
private store: Map<string, StoredTOMState> = new Map()
async get(tenantId: string): Promise<StoredTOMState | null> {
return this.store.get(tenantId) || null
}
async save(tenantId: string, state: TOMGeneratorState, expectedVersion?: number): Promise<StoredTOMState> {
const existing = this.store.get(tenantId)
if (expectedVersion !== undefined && existing && existing.version !== expectedVersion) {
const error = new Error('Version conflict') as Error & { status: number }
error.status = 409
throw error
}
const now = new Date().toISOString()
const newVersion = existing ? existing.version + 1 : 1
const stored: StoredTOMState = {
state: {
...state,
updatedAt: new Date(now),
},
version: newVersion,
createdAt: existing?.createdAt || now,
updatedAt: now,
}
this.store.set(tenantId, stored)
return stored
}
async delete(tenantId: string): Promise<boolean> {
return this.store.delete(tenantId)
}
async list(): Promise<{ tenantId: string; updatedAt: string }[]> {
const result: { tenantId: string; updatedAt: string }[] = []
this.store.forEach((value, key) => {
result.push({ tenantId: key, updatedAt: value.updatedAt })
})
return result
}
}
const stateStore = new InMemoryTOMStateStore()
// =============================================================================
// HANDLERS
// =============================================================================
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const tenantId = searchParams.get('tenantId')
// List all states if no tenantId provided
if (!tenantId) {
const states = await stateStore.list()
return NextResponse.json({
success: true,
data: states,
})
}
const stored = await stateStore.get(tenantId)
if (!stored) {
// Return empty state for new tenants
const emptyState = createEmptyTOMGeneratorState(tenantId)
return NextResponse.json({
success: true,
data: {
tenantId,
state: emptyState,
version: 0,
isNew: true,
},
})
}
return NextResponse.json({
success: true,
data: {
tenantId,
state: stored.state,
version: stored.version,
lastModified: stored.updatedAt,
},
})
} catch (error) {
console.error('Failed to load TOM generator state:', error)
return NextResponse.json(
{ success: false, error: 'Failed to load state' },
{ status: 500 }
)
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { tenantId, state, version } = body
if (!tenantId) {
return NextResponse.json(
{ success: false, error: 'tenantId is required' },
{ status: 400 }
)
}
if (!state) {
return NextResponse.json(
{ success: false, error: 'state is required' },
{ status: 400 }
)
}
// Deserialize dates
const parsedState: TOMGeneratorState = {
...state,
createdAt: new Date(state.createdAt),
updatedAt: new Date(state.updatedAt),
steps: state.steps.map((step: { id: string; completed: boolean; data: unknown; validatedAt: string | null }) => ({
...step,
validatedAt: step.validatedAt ? new Date(step.validatedAt) : null,
})),
documents: state.documents?.map((doc: { uploadedAt: string; validFrom?: string; validUntil?: string; aiAnalysis?: { analyzedAt: string } }) => ({
...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,
})) || [],
derivedTOMs: state.derivedTOMs?.map((tom: { implementationDate?: string; reviewDate?: string }) => ({
...tom,
implementationDate: tom.implementationDate ? new Date(tom.implementationDate) : null,
reviewDate: tom.reviewDate ? new Date(tom.reviewDate) : null,
})) || [],
gapAnalysis: state.gapAnalysis ? {
...state.gapAnalysis,
generatedAt: new Date(state.gapAnalysis.generatedAt),
} : null,
exports: state.exports?.map((exp: { generatedAt: string }) => ({
...exp,
generatedAt: new Date(exp.generatedAt),
})) || [],
}
const stored = await stateStore.save(tenantId, parsedState, version)
return NextResponse.json({
success: true,
data: {
tenantId,
state: stored.state,
version: stored.version,
lastModified: stored.updatedAt,
},
})
} catch (error) {
const err = error as Error & { status?: number }
if (err.status === 409 || err.message === 'Version conflict') {
return NextResponse.json(
{
success: false,
error: 'Version conflict. State was modified by another request.',
code: 'VERSION_CONFLICT',
},
{ status: 409 }
)
}
console.error('Failed to save TOM generator state:', error)
return NextResponse.json(
{ success: false, error: 'Failed to save state' },
{ status: 500 }
)
}
}
export async function DELETE(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const tenantId = searchParams.get('tenantId')
if (!tenantId) {
return NextResponse.json(
{ success: false, error: 'tenantId is required' },
{ status: 400 }
)
}
const deleted = await stateStore.delete(tenantId)
return NextResponse.json({
success: true,
tenantId,
deleted,
deletedAt: new Date().toISOString(),
})
} catch (error) {
console.error('Failed to delete TOM generator state:', error)
return NextResponse.json(
{ success: false, error: 'Failed to delete state' },
{ status: 500 }
)
}
}
export async function OPTIONS() {
return NextResponse.json(
{ status: 'ok' },
{
headers: {
Allow: 'GET, POST, DELETE, OPTIONS',
},
}
)
}

View File

@@ -0,0 +1,40 @@
import { NextRequest, NextResponse } from 'next/server'
const SDK_BASE_URL = process.env.SDK_BASE_URL || 'http://localhost:8085'
export async function POST(request: NextRequest) {
try {
const body = await request.json()
// Forward the request to the SDK backend
const response = await fetch(`${SDK_BASE_URL}/sdk/v1/ucca/obligations/assess`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// Forward tenant ID if present
...(request.headers.get('X-Tenant-ID') && {
'X-Tenant-ID': request.headers.get('X-Tenant-ID') as string,
}),
},
body: JSON.stringify(body),
})
if (!response.ok) {
const errorText = await response.text()
console.error('SDK backend error:', errorText)
return NextResponse.json(
{ error: 'SDK backend error', details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Failed to call SDK backend:', error)
return NextResponse.json(
{ error: 'Failed to connect to SDK backend' },
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,40 @@
import { NextRequest, NextResponse } from 'next/server'
const SDK_BASE_URL = process.env.SDK_BASE_URL || 'http://localhost:8085'
export async function POST(request: NextRequest) {
try {
const body = await request.json()
// Forward the request to the SDK backend
const response = await fetch(`${SDK_BASE_URL}/sdk/v1/ucca/obligations/export/direct`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// Forward tenant ID if present
...(request.headers.get('X-Tenant-ID') && {
'X-Tenant-ID': request.headers.get('X-Tenant-ID') as string,
}),
},
body: JSON.stringify(body),
})
if (!response.ok) {
const errorText = await response.text()
console.error('SDK backend error:', errorText)
return NextResponse.json(
{ error: 'SDK backend error', details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Failed to call SDK backend:', error)
return NextResponse.json(
{ error: 'Failed to connect to SDK backend' },
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,197 @@
import { NextRequest, NextResponse } from 'next/server'
import { v4 as uuidv4 } from 'uuid'
import {
Finding,
CONTRACT_REVIEW_SYSTEM_PROMPT,
} from '@/lib/sdk/vendor-compliance'
/**
* POST /api/sdk/v1/vendor-compliance/contracts/[id]/review
*
* Starts the LLM-based contract review process
*/
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id: contractId } = await params
// In production:
// 1. Fetch contract from database
// 2. Extract text from PDF/DOCX using embedding-service
// 3. Send to LLM for analysis
// 4. Store findings in database
// 5. Update contract with compliance score
// For demo, return mock analysis results
const mockFindings: Finding[] = [
{
id: uuidv4(),
tenantId: 'default',
contractId,
vendorId: 'mock-vendor',
type: 'OK',
category: 'AVV_CONTENT',
severity: 'LOW',
title: {
de: 'Weisungsgebundenheit vorhanden',
en: 'Instruction binding present',
},
description: {
de: 'Der Vertrag enthält eine angemessene Regelung zur Weisungsgebundenheit des Auftragsverarbeiters.',
en: 'The contract contains an appropriate provision for processor instruction binding.',
},
citations: [
{
documentId: contractId,
page: 2,
startChar: 150,
endChar: 350,
quotedText: 'Der Auftragnehmer verarbeitet personenbezogene Daten ausschließlich auf dokumentierte Weisung des Auftraggebers.',
quoteHash: 'abc123',
},
],
affectedRequirement: 'Art. 28 Abs. 3 lit. a DSGVO',
triggeredControls: ['VND-CON-01'],
status: 'OPEN',
createdAt: new Date(),
updatedAt: new Date(),
},
{
id: uuidv4(),
tenantId: 'default',
contractId,
vendorId: 'mock-vendor',
type: 'GAP',
category: 'INCIDENT',
severity: 'HIGH',
title: {
de: 'Meldefrist für Datenpannen zu lang',
en: 'Data breach notification deadline too long',
},
description: {
de: 'Die vereinbarte Meldefrist von 72 Stunden ist zu lang, um die eigene Meldepflicht gegenüber der Aufsichtsbehörde fristgerecht erfüllen zu können.',
en: 'The agreed notification deadline of 72 hours is too long to meet own notification obligations to the supervisory authority in time.',
},
recommendation: {
de: 'Verhandeln Sie eine kürzere Meldefrist von maximal 24-48 Stunden.',
en: 'Negotiate a shorter notification deadline of maximum 24-48 hours.',
},
citations: [
{
documentId: contractId,
page: 5,
startChar: 820,
endChar: 950,
quotedText: 'Der Auftragnehmer wird den Auftraggeber innerhalb von 72 Stunden über eine Verletzung des Schutzes personenbezogener Daten informieren.',
quoteHash: 'def456',
},
],
affectedRequirement: 'Art. 33 Abs. 2 DSGVO',
triggeredControls: ['VND-INC-01'],
status: 'OPEN',
createdAt: new Date(),
updatedAt: new Date(),
},
{
id: uuidv4(),
tenantId: 'default',
contractId,
vendorId: 'mock-vendor',
type: 'RISK',
category: 'TRANSFER',
severity: 'MEDIUM',
title: {
de: 'Drittlandtransfer USA ohne TIA',
en: 'Third country transfer to USA without TIA',
},
description: {
de: 'Der Vertrag erlaubt Datenverarbeitung in den USA. Es liegt jedoch kein Transfer Impact Assessment (TIA) vor.',
en: 'The contract allows data processing in the USA. However, no Transfer Impact Assessment (TIA) is available.',
},
recommendation: {
de: 'Führen Sie ein TIA durch und dokumentieren Sie zusätzliche Schutzmaßnahmen.',
en: 'Conduct a TIA and document supplementary measures.',
},
citations: [
{
documentId: contractId,
page: 8,
startChar: 1200,
endChar: 1350,
quotedText: 'Die Verarbeitung kann auch in Rechenzentren in den Vereinigten Staaten von Amerika erfolgen.',
quoteHash: 'ghi789',
},
],
affectedRequirement: 'Art. 44-49 DSGVO, Schrems II',
triggeredControls: ['VND-TRF-01', 'VND-TRF-03'],
status: 'OPEN',
createdAt: new Date(),
updatedAt: new Date(),
},
]
// Calculate compliance score based on findings
const okFindings = mockFindings.filter((f) => f.type === 'OK').length
const totalChecks = mockFindings.length + 5 // Assume 5 additional checks passed
const complianceScore = Math.round((okFindings / totalChecks) * 100 + 60) // Base score + passed checks
return NextResponse.json({
success: true,
data: {
contractId,
findings: mockFindings,
complianceScore: Math.min(100, complianceScore),
reviewCompletedAt: new Date().toISOString(),
topRisks: [
{ de: 'Meldefrist für Datenpannen zu lang', en: 'Data breach notification deadline too long' },
{ de: 'Fehlende TIA für USA-Transfer', en: 'Missing TIA for USA transfer' },
],
requiredActions: [
{ de: 'Meldefrist auf 24-48h verkürzen', en: 'Reduce notification deadline to 24-48h' },
{ de: 'TIA für USA-Transfer durchführen', en: 'Conduct TIA for USA transfer' },
],
},
timestamp: new Date().toISOString(),
})
} catch (error) {
console.error('Error reviewing contract:', error)
return NextResponse.json(
{ success: false, error: 'Failed to review contract' },
{ status: 500 }
)
}
}
/**
* GET /api/sdk/v1/vendor-compliance/contracts/[id]/review
*
* Get existing review results
*/
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id: contractId } = await params
// In production, fetch from database
return NextResponse.json({
success: true,
data: {
contractId,
findings: [],
complianceScore: null,
reviewStatus: 'PENDING',
},
timestamp: new Date().toISOString(),
})
} catch (error) {
console.error('Error fetching review:', error)
return NextResponse.json(
{ success: false, error: 'Failed to fetch review' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,88 @@
import { NextRequest, NextResponse } from 'next/server'
import { v4 as uuidv4 } from 'uuid'
import { ContractDocument } from '@/lib/sdk/vendor-compliance'
// In-memory storage for demo purposes
const contracts: Map<string, ContractDocument> = new Map()
export async function GET(request: NextRequest) {
try {
const contractList = Array.from(contracts.values())
return NextResponse.json({
success: true,
data: contractList,
timestamp: new Date().toISOString(),
})
} catch (error) {
console.error('Error fetching contracts:', error)
return NextResponse.json(
{ success: false, error: 'Failed to fetch contracts' },
{ status: 500 }
)
}
}
export async function POST(request: NextRequest) {
try {
// Handle multipart form data for file upload
const formData = await request.formData()
const file = formData.get('file') as File | null
const vendorId = formData.get('vendorId') as string
const metadataStr = formData.get('metadata') as string
if (!file || !vendorId) {
return NextResponse.json(
{ success: false, error: 'File and vendorId are required' },
{ status: 400 }
)
}
const metadata = metadataStr ? JSON.parse(metadataStr) : {}
const id = uuidv4()
// In production, upload file to storage (MinIO, S3, etc.)
const storagePath = `contracts/${id}/${file.name}`
const contract: ContractDocument = {
id,
tenantId: 'default',
vendorId,
fileName: `${id}-${file.name}`,
originalName: file.name,
mimeType: file.type,
fileSize: file.size,
storagePath,
documentType: metadata.documentType || 'OTHER',
version: metadata.version || '1.0',
previousVersionId: metadata.previousVersionId,
parties: metadata.parties,
effectiveDate: metadata.effectiveDate ? new Date(metadata.effectiveDate) : undefined,
expirationDate: metadata.expirationDate ? new Date(metadata.expirationDate) : undefined,
autoRenewal: metadata.autoRenewal,
renewalNoticePeriod: metadata.renewalNoticePeriod,
terminationNoticePeriod: metadata.terminationNoticePeriod,
reviewStatus: 'PENDING',
status: 'DRAFT',
createdAt: new Date(),
updatedAt: new Date(),
}
contracts.set(id, contract)
return NextResponse.json(
{
success: true,
data: contract,
timestamp: new Date().toISOString(),
},
{ status: 201 }
)
} catch (error) {
console.error('Error uploading contract:', error)
return NextResponse.json(
{ success: false, error: 'Failed to upload contract' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,28 @@
import { NextRequest, NextResponse } from 'next/server'
import { CONTROLS_LIBRARY } from '@/lib/sdk/vendor-compliance'
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams
const domain = searchParams.get('domain')
let controls = [...CONTROLS_LIBRARY]
// Filter by domain if provided
if (domain) {
controls = controls.filter((c) => c.domain === domain)
}
return NextResponse.json({
success: true,
data: controls,
timestamp: new Date().toISOString(),
})
} catch (error) {
console.error('Error fetching controls:', error)
return NextResponse.json(
{ success: false, error: 'Failed to fetch controls' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,75 @@
import { NextRequest, NextResponse } from 'next/server'
/**
* GET /api/sdk/v1/vendor-compliance/export/[reportId]/download
*
* Download a generated report file.
* In production, this would redirect to a signed MinIO/S3 URL or stream the file.
*/
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ reportId: string }> }
) {
const { reportId } = await params
// TODO: Implement actual file download
// This would typically:
// 1. Verify report exists and user has access
// 2. Generate signed URL for MinIO/S3
// 3. Redirect to signed URL or stream file
// For now, return a placeholder PDF
const placeholderContent = `
%PDF-1.4
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
2 0 obj
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
endobj
3 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >>
endobj
4 0 obj
<< /Length 200 >>
stream
BT
/F1 24 Tf
100 700 Td
(Vendor Compliance Report) Tj
/F1 12 Tf
100 650 Td
(Report ID: ${reportId}) Tj
100 620 Td
(Generated: ${new Date().toISOString()}) Tj
100 580 Td
(This is a placeholder. Implement actual report generation.) Tj
ET
endstream
endobj
5 0 obj
<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>
endobj
xref
0 6
0000000000 65535 f
0000000009 00000 n
0000000058 00000 n
0000000115 00000 n
0000000266 00000 n
0000000519 00000 n
trailer
<< /Size 6 /Root 1 0 R >>
startxref
598
%%EOF
`.trim()
// Return as PDF
return new NextResponse(placeholderContent, {
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': `attachment; filename="Report_${reportId.slice(0, 8)}.pdf"`,
},
})
}

View File

@@ -0,0 +1,44 @@
import { NextRequest, NextResponse } from 'next/server'
/**
* GET /api/sdk/v1/vendor-compliance/export/[reportId]
*
* Get report metadata by ID.
*/
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ reportId: string }> }
) {
const { reportId } = await params
// TODO: Fetch report metadata from database
// For now, return mock data
return NextResponse.json({
id: reportId,
status: 'completed',
filename: `Report_${reportId.slice(0, 8)}.pdf`,
generatedAt: new Date().toISOString(),
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), // 24h
})
}
/**
* DELETE /api/sdk/v1/vendor-compliance/export/[reportId]
*
* Delete a generated report.
*/
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ reportId: string }> }
) {
const { reportId } = await params
// TODO: Delete report from storage and database
console.log('Deleting report:', reportId)
return NextResponse.json({
success: true,
deletedId: reportId,
})
}

View File

@@ -0,0 +1,118 @@
import { NextRequest, NextResponse } from 'next/server'
import { v4 as uuidv4 } from 'uuid'
/**
* POST /api/sdk/v1/vendor-compliance/export
*
* Generate and export reports in various formats.
* Currently returns mock data - integrate with actual report generation service.
*/
interface ExportConfig {
reportType: 'VVT_EXPORT' | 'VENDOR_AUDIT' | 'ROPA' | 'MANAGEMENT_SUMMARY' | 'DPIA_INPUT'
format: 'PDF' | 'DOCX' | 'XLSX' | 'JSON'
scope: {
vendorIds: string[]
processingActivityIds: string[]
includeFindings: boolean
includeControls: boolean
includeRiskAssessment: boolean
dateRange?: {
from: string
to: string
}
}
}
const REPORT_TYPE_NAMES: Record<ExportConfig['reportType'], string> = {
VVT_EXPORT: 'Verarbeitungsverzeichnis',
VENDOR_AUDIT: 'Vendor-Audit-Pack',
ROPA: 'RoPA',
MANAGEMENT_SUMMARY: 'Management-Summary',
DPIA_INPUT: 'DSFA-Input',
}
const FORMAT_EXTENSIONS: Record<ExportConfig['format'], string> = {
PDF: 'pdf',
DOCX: 'docx',
XLSX: 'xlsx',
JSON: 'json',
}
export async function POST(request: NextRequest) {
try {
const config = (await request.json()) as ExportConfig
// Validate request
if (!config.reportType || !config.format) {
return NextResponse.json(
{ error: 'reportType and format are required' },
{ status: 400 }
)
}
// Generate report ID and filename
const reportId = uuidv4()
const timestamp = new Date().toISOString().slice(0, 10).replace(/-/g, '')
const filename = `${REPORT_TYPE_NAMES[config.reportType]}_${timestamp}.${FORMAT_EXTENSIONS[config.format]}`
// TODO: Implement actual report generation
// This would typically:
// 1. Fetch data from database based on scope
// 2. Generate report using template engine (e.g., docx-templates, pdfkit)
// 3. Store in MinIO/S3
// 4. Return download URL
// Mock implementation - simulate processing time
await new Promise((resolve) => setTimeout(resolve, 500))
// In production, this would be a signed URL to MinIO/S3
const downloadUrl = `/api/sdk/v1/vendor-compliance/export/${reportId}/download`
// Log export for audit trail
console.log('Export generated:', {
reportId,
reportType: config.reportType,
format: config.format,
scope: config.scope,
filename,
generatedAt: new Date().toISOString(),
})
return NextResponse.json({
id: reportId,
reportType: config.reportType,
format: config.format,
filename,
downloadUrl,
generatedAt: new Date().toISOString(),
scope: {
vendorCount: config.scope.vendorIds?.length || 0,
activityCount: config.scope.processingActivityIds?.length || 0,
includesFindings: config.scope.includeFindings,
includesControls: config.scope.includeControls,
includesRiskAssessment: config.scope.includeRiskAssessment,
},
})
} catch (error) {
console.error('Export error:', error)
return NextResponse.json(
{ error: 'Failed to generate export' },
{ status: 500 }
)
}
}
/**
* GET /api/sdk/v1/vendor-compliance/export
*
* List recent exports for the current tenant.
*/
export async function GET() {
// TODO: Implement fetching recent exports from database
// For now, return empty list
return NextResponse.json({
exports: [],
totalCount: 0,
})
}

View File

@@ -0,0 +1,43 @@
import { NextRequest, NextResponse } from 'next/server'
import { Finding } from '@/lib/sdk/vendor-compliance'
// In-memory storage for demo purposes
const findings: Map<string, Finding> = new Map()
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams
const vendorId = searchParams.get('vendorId')
const contractId = searchParams.get('contractId')
const status = searchParams.get('status')
let findingsList = Array.from(findings.values())
// Filter by vendor
if (vendorId) {
findingsList = findingsList.filter((f) => f.vendorId === vendorId)
}
// Filter by contract
if (contractId) {
findingsList = findingsList.filter((f) => f.contractId === contractId)
}
// Filter by status
if (status) {
findingsList = findingsList.filter((f) => f.status === status)
}
return NextResponse.json({
success: true,
data: findingsList,
timestamp: new Date().toISOString(),
})
} catch (error) {
console.error('Error fetching findings:', error)
return NextResponse.json(
{ success: false, error: 'Failed to fetch findings' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,70 @@
import { NextRequest, NextResponse } from 'next/server'
// This would reference the same storage as the main route
// In production, this would be database calls
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
// In production, fetch from database
return NextResponse.json({
success: true,
data: null, // Would return the activity
timestamp: new Date().toISOString(),
})
} catch (error) {
console.error('Error fetching processing activity:', error)
return NextResponse.json(
{ success: false, error: 'Failed to fetch processing activity' },
{ status: 500 }
)
}
}
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
const body = await request.json()
// In production, update in database
return NextResponse.json({
success: true,
data: { id, ...body, updatedAt: new Date() },
timestamp: new Date().toISOString(),
})
} catch (error) {
console.error('Error updating processing activity:', error)
return NextResponse.json(
{ success: false, error: 'Failed to update processing activity' },
{ status: 500 }
)
}
}
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
// In production, delete from database
return NextResponse.json({
success: true,
timestamp: new Date().toISOString(),
})
} catch (error) {
console.error('Error deleting processing activity:', error)
return NextResponse.json(
{ success: false, error: 'Failed to delete processing activity' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,84 @@
import { NextRequest, NextResponse } from 'next/server'
import { v4 as uuidv4 } from 'uuid'
import { ProcessingActivity, generateVVTId } from '@/lib/sdk/vendor-compliance'
// In-memory storage for demo purposes
// In production, this would be replaced with database calls
const processingActivities: Map<string, ProcessingActivity> = new Map()
export async function GET(request: NextRequest) {
try {
const activities = Array.from(processingActivities.values())
return NextResponse.json({
success: true,
data: activities,
timestamp: new Date().toISOString(),
})
} catch (error) {
console.error('Error fetching processing activities:', error)
return NextResponse.json(
{ success: false, error: 'Failed to fetch processing activities' },
{ status: 500 }
)
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json()
// Generate IDs
const id = uuidv4()
const existingIds = Array.from(processingActivities.values()).map((a) => a.vvtId)
const vvtId = body.vvtId || generateVVTId(existingIds)
const activity: ProcessingActivity = {
id,
tenantId: 'default', // Would come from auth context
vvtId,
name: body.name,
responsible: body.responsible,
dpoContact: body.dpoContact,
purposes: body.purposes || [],
dataSubjectCategories: body.dataSubjectCategories || [],
personalDataCategories: body.personalDataCategories || [],
recipientCategories: body.recipientCategories || [],
thirdCountryTransfers: body.thirdCountryTransfers || [],
retentionPeriod: body.retentionPeriod || { description: { de: '', en: '' } },
technicalMeasures: body.technicalMeasures || [],
legalBasis: body.legalBasis || [],
dataSources: body.dataSources || [],
systems: body.systems || [],
dataFlows: body.dataFlows || [],
protectionLevel: body.protectionLevel || 'MEDIUM',
dpiaRequired: body.dpiaRequired || false,
dpiaJustification: body.dpiaJustification,
subProcessors: body.subProcessors || [],
legalRetentionBasis: body.legalRetentionBasis,
status: body.status || 'DRAFT',
owner: body.owner || '',
lastReviewDate: body.lastReviewDate,
nextReviewDate: body.nextReviewDate,
createdAt: new Date(),
updatedAt: new Date(),
}
processingActivities.set(id, activity)
return NextResponse.json(
{
success: true,
data: activity,
timestamp: new Date().toISOString(),
},
{ status: 201 }
)
} catch (error) {
console.error('Error creating processing activity:', error)
return NextResponse.json(
{ success: false, error: 'Failed to create processing activity' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,82 @@
import { NextRequest, NextResponse } from 'next/server'
import { v4 as uuidv4 } from 'uuid'
import { Vendor } from '@/lib/sdk/vendor-compliance'
// In-memory storage for demo purposes
const vendors: Map<string, Vendor> = new Map()
export async function GET(request: NextRequest) {
try {
const vendorList = Array.from(vendors.values())
return NextResponse.json({
success: true,
data: vendorList,
timestamp: new Date().toISOString(),
})
} catch (error) {
console.error('Error fetching vendors:', error)
return NextResponse.json(
{ success: false, error: 'Failed to fetch vendors' },
{ status: 500 }
)
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const id = uuidv4()
const vendor: Vendor = {
id,
tenantId: 'default',
name: body.name,
legalForm: body.legalForm,
country: body.country,
address: body.address,
website: body.website,
role: body.role,
serviceDescription: body.serviceDescription,
serviceCategory: body.serviceCategory,
dataAccessLevel: body.dataAccessLevel || 'NONE',
processingLocations: body.processingLocations || [],
transferMechanisms: body.transferMechanisms || [],
certifications: body.certifications || [],
primaryContact: body.primaryContact,
dpoContact: body.dpoContact,
securityContact: body.securityContact,
contractTypes: body.contractTypes || [],
contracts: body.contracts || [],
inherentRiskScore: body.inherentRiskScore || 50,
residualRiskScore: body.residualRiskScore || 50,
manualRiskAdjustment: body.manualRiskAdjustment,
riskJustification: body.riskJustification,
reviewFrequency: body.reviewFrequency || 'ANNUAL',
lastReviewDate: body.lastReviewDate,
nextReviewDate: body.nextReviewDate,
status: body.status || 'ACTIVE',
processingActivityIds: body.processingActivityIds || [],
notes: body.notes,
createdAt: new Date(),
updatedAt: new Date(),
}
vendors.set(id, vendor)
return NextResponse.json(
{
success: true,
data: vendor,
timestamp: new Date().toISOString(),
},
{ status: 201 }
)
} catch (error) {
console.error('Error creating vendor:', error)
return NextResponse.json(
{ success: false, error: 'Failed to create vendor' },
{ status: 500 }
)
}
}