Files
Lageplan/src/app/api/legal/accept/route.ts
Pepe Ziberi 8cf1aa4dbe 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>
2026-07-23 12:02:20 +02:00

52 lines
2.3 KiB
TypeScript

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 })
}
}