feat(legal): Consent-Datenmodell, Migration & Registrierungs-Zustimmung (v1.7.1)
Teil 2 des Compliance-Updates — revisionssichere Zustimmungsprotokollierung: - Prisma: Modelle LegalDocument (versioniert, contentHash, isActive, unique[type,version]) und LegalAcceptance (userId, organizationId?, documentType/-Version, contentHash, context, unique gegen Duplikate, User-Relation) - migrate.js: idempotente Tabellen + Indizes + Seeding/Aktivierung der 5 Rechtsdokumente (eine aktive Version je Typ; contentHash aus bewusster Versionsangabe) - src/lib/legal.ts: getActiveDocuments, getPendingAcceptances, recordAcceptances (skipDuplicates, nie überschreiben), Pflichtsets (alle: TERMS+PRIVACY; Org-Admin zusätzlich ORGANIZATION_DECLARATION) - API: GET /api/legal/status (offene Zustimmungen), POST /api/legal/accept (erfassen, CSRF-Check) - Registrierung: zwei getrennte, nicht vorausgewählte Zustimmungen (Nutzungsbedingungen akzeptieren + Datenschutz-Kenntnisnahme), serverseitig via zod erzwungen, Version protokolliert Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
51
src/app/api/legal/accept/route.ts
Normal file
51
src/app/api/legal/accept/route.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { recordAcceptances, getPendingAcceptances, type LegalAcceptanceContext, type LegalDocumentType } from '@/lib/legal'
|
||||
|
||||
const VALID_TYPES: LegalDocumentType[] = ['TERMS', 'PRIVACY', 'ORGANIZATION_DECLARATION', 'RESPONSIBLE_USE', 'DATA_PROCESSING_AGREEMENT']
|
||||
const VALID_CONTEXTS: LegalAcceptanceContext[] = ['REGISTRATION', 'LOGIN_RECONSENT', 'ORGANIZATION_CREATION', 'ADMIN_ROLE_ACCEPTANCE', 'DONATION']
|
||||
|
||||
// POST: erfasst Zustimmungen des angemeldeten Benutzers revisionssicher.
|
||||
// body: { types: LegalDocumentType[], context: LegalAcceptanceContext }
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const user = await getSession()
|
||||
if (!user) return NextResponse.json({ error: 'Nicht autorisiert' }, { status: 401 })
|
||||
|
||||
// CSRF-Schutz: nur same-origin JSON-Requests (Cookie ist sameSite=lax).
|
||||
const contentType = req.headers.get('content-type') || ''
|
||||
if (!contentType.includes('application/json')) {
|
||||
return NextResponse.json({ error: 'Ungültiger Request' }, { status: 400 })
|
||||
}
|
||||
|
||||
const body = await req.json().catch(() => ({}))
|
||||
const types: LegalDocumentType[] = Array.isArray(body.types)
|
||||
? body.types.filter((t: any) => VALID_TYPES.includes(t))
|
||||
: []
|
||||
const context: LegalAcceptanceContext = VALID_CONTEXTS.includes(body.context) ? body.context : 'LOGIN_RECONSENT'
|
||||
|
||||
if (types.length === 0) {
|
||||
return NextResponse.json({ error: 'Keine gültigen Dokumenttypen' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Organisationsbezogene Zustimmung (Org-Bestätigung) an den Tenant binden.
|
||||
const orgBound = context === 'ORGANIZATION_CREATION' || context === 'ADMIN_ROLE_ACCEPTANCE' || types.includes('ORGANIZATION_DECLARATION')
|
||||
const organizationId = orgBound ? (user.tenantId ?? null) : null
|
||||
|
||||
const count = await recordAcceptances({
|
||||
userId: user.id,
|
||||
organizationId,
|
||||
context,
|
||||
types,
|
||||
locale: 'de-CH',
|
||||
})
|
||||
|
||||
const isOrgAdmin = user.role === 'TENANT_ADMIN'
|
||||
const pending = await getPendingAcceptances(user.id, isOrgAdmin)
|
||||
|
||||
return NextResponse.json({ success: true, recorded: count, needsAcceptance: pending.length > 0, pending })
|
||||
} catch (error) {
|
||||
console.error('Error recording legal acceptance:', error)
|
||||
return NextResponse.json({ error: 'Serverfehler' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user