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:
@@ -5,12 +5,16 @@ import { sendEmail } from '@/lib/email'
|
||||
import { randomBytes } from 'crypto'
|
||||
import { z } from 'zod'
|
||||
import { registerLimiter, getClientIp, rateLimitResponse } from '@/lib/rate-limit'
|
||||
import { recordAcceptances } from '@/lib/legal'
|
||||
|
||||
const registerSchema = z.object({
|
||||
organizationName: z.string().min(2, 'Organisationsname zu kurz').max(200),
|
||||
name: z.string().min(2, 'Name zu kurz').max(200),
|
||||
email: z.string().email('Ungültige E-Mail-Adresse'),
|
||||
password: z.string().min(8, 'Passwort muss mindestens 8 Zeichen haben'),
|
||||
// Aktive Zustimmung erforderlich (keine vorausgewählte Checkbox im UI).
|
||||
acceptTerms: z.literal(true, { errorMap: () => ({ message: 'Bitte die Nutzungsbedingungen akzeptieren.' }) }),
|
||||
acceptPrivacy: z.literal(true, { errorMap: () => ({ message: 'Bitte die Kenntnisnahme der Datenschutzerklärung bestätigen.' }) }),
|
||||
})
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
@@ -79,8 +83,8 @@ export async function POST(req: NextRequest) {
|
||||
maxUsers: 5,
|
||||
maxProjects: 10,
|
||||
contactEmail: data.email,
|
||||
privacyAccepted: body.privacyAccepted === true,
|
||||
privacyAcceptedAt: body.privacyAccepted ? new Date() : null,
|
||||
privacyAccepted: true,
|
||||
privacyAcceptedAt: new Date(),
|
||||
adminAccessAccepted: body.adminAccessAccepted === true,
|
||||
},
|
||||
})
|
||||
@@ -106,6 +110,20 @@ export async function POST(req: NextRequest) {
|
||||
},
|
||||
})
|
||||
|
||||
// Zustimmungen revisionssicher protokollieren (aktive Dokumentversion).
|
||||
// Die Organisationsbestätigung wird separat nach dem ersten Login abgefragt (Re-Consent).
|
||||
try {
|
||||
await recordAcceptances({
|
||||
userId: user.id,
|
||||
organizationId: tenant.id,
|
||||
context: 'REGISTRATION',
|
||||
types: ['TERMS', 'PRIVACY'],
|
||||
locale: 'de-CH',
|
||||
})
|
||||
} catch (e) {
|
||||
console.warn('[register] Zustimmungsprotokollierung fehlgeschlagen (nicht blockierend):', e)
|
||||
}
|
||||
|
||||
// Send verification email
|
||||
let baseUrl = process.env.NEXTAUTH_URL || req.headers.get('origin') || `${req.headers.get('x-forwarded-proto') || 'https'}://${req.headers.get('host')}` || 'http://localhost:3000'
|
||||
if (baseUrl && !baseUrl.startsWith('http://') && !baseUrl.startsWith('https://')) {
|
||||
|
||||
Reference in New Issue
Block a user