Services: Admin-Compliance, Backend-Compliance, AI-Compliance-SDK, Consent-SDK, Developer-Portal, PCA-Platform, DSMS Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
370 lines
11 KiB
TypeScript
370 lines
11 KiB
TypeScript
/**
|
|
* 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,
|
|
}
|
|
}
|