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:
Pepe Ziberi
2026-07-23 12:02:20 +02:00
parent 872b8927cc
commit 8cf1aa4dbe
8 changed files with 343 additions and 11 deletions

View File

@@ -134,10 +134,56 @@ model User {
iconAssets IconAsset[]
upgradeRequests UpgradeRequest[]
rapports Rapport[]
legalAcceptances LegalAcceptance[]
@@map("users")
}
// ─── Rechtsdokumente & revisionssichere Zustimmungen ──────────
// Dokumenttypen (als String gehalten, damit die idempotente Raw-SQL-Migration einfach bleibt):
// TERMS | PRIVACY | ORGANIZATION_DECLARATION | RESPONSIBLE_USE | DATA_PROCESSING_AGREEMENT
model LegalDocument {
id String @id @default(uuid())
type String
version String
title String
publishedAt DateTime @default(now())
effectiveAt DateTime?
contentHash String
url String?
isActive Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([type, version])
@@index([type, isActive])
@@map("legal_documents")
}
model LegalAcceptance {
id String @id @default(uuid())
userId String
// NULL = persönliche Zustimmung (nicht organisationsbezogen)
organizationId String?
documentType String
documentVersion String
contentHash String
acceptedAt DateTime @default(now())
locale String?
// REGISTRATION | LOGIN_RECONSENT | ORGANIZATION_CREATION | ADMIN_ROLE_ACCEPTANCE | DONATION
context String
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
// Verhindert doppelte identische Zustimmungen; neue Version erzeugt neuen Datensatz.
@@unique([userId, organizationId, documentType, documentVersion, context])
@@index([userId])
@@index([documentType, documentVersion])
@@map("legal_acceptances")
}
model TenantMembership {
id String @id @default(uuid())
role Role @default(OPERATOR)