/** * 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() // 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 = {} 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 = { 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 = { 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, } }