Files
Lageplan/src/app/api/admin/legal/acceptances/route.ts
Pepe Ziberi fa1e911ecd feat(legal): Zustimmungs-Nachweise mit IP, Gerät/Browser & Hash (v1.7.8)
Belastbarer Einwilligungsnachweis:
- Schema/Migration: legal_acceptances + ipAddress, userAgent (contentHash bestand bereits)
- Erfassung bei Registrierung und jeder Zustimmung (/api/legal/accept)
- Admin /admin/legal: neue Spalten IP + Gerät/Browser + Hash; CSV-Export um ipAddress,
  userAgent, contentHash erweitert
- Datenschutzerklärung + Dateninventar: IP/User-Agent als dokumentierter Beweiszweck ergänzt

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 17:55:57 +02:00

66 lines
2.9 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/db'
import { getSession } from '@/lib/auth'
// GET: Zustimmungshistorie suchen (nach Benutzer-E-Mail/Name oder Organisation).
// Query: ?q=<suchbegriff>&format=json|csv (nur SERVER_ADMIN)
export async function GET(req: NextRequest) {
try {
const user = await getSession()
if (!user || user.role !== 'SERVER_ADMIN') {
return NextResponse.json({ error: 'Keine Berechtigung' }, { status: 403 })
}
const q = (req.nextUrl.searchParams.get('q') || '').trim()
const format = req.nextUrl.searchParams.get('format') || 'json'
// Passende Benutzer ermitteln (falls Suchbegriff gesetzt)
let userFilter: any = {}
if (q) {
const users = await (prisma as any).user.findMany({
where: { OR: [{ email: { contains: q, mode: 'insensitive' } }, { name: { contains: q, mode: 'insensitive' } }] },
select: { id: true },
take: 500,
})
userFilter = { userId: { in: users.map((u: any) => u.id) } }
}
const acceptances = await (prisma as any).legalAcceptance.findMany({
where: userFilter,
orderBy: { acceptedAt: 'desc' },
take: 2000,
select: { id: true, userId: true, organizationId: true, documentType: true, documentVersion: true, context: true, acceptedAt: true, contentHash: true, ipAddress: true, userAgent: true },
})
// Nur die notwendigen Nachweisdaten anreichern (E-Mail/Name) — keine überflüssigen Personendaten.
const userIds = Array.from(new Set(acceptances.map((a: any) => a.userId)))
const users = await (prisma as any).user.findMany({ where: { id: { in: userIds } }, select: { id: true, email: true, name: true } })
const uMap = new Map<string, any>(users.map((u: any) => [u.id, u]))
const rows = acceptances.map((a: any) => ({
...a,
email: uMap.get(a.userId)?.email || '',
name: uMap.get(a.userId)?.name || '',
}))
if (format === 'csv') {
const header = ['acceptedAt', 'email', 'name', 'documentType', 'documentVersion', 'context', 'ipAddress', 'userAgent', 'contentHash', 'organizationId']
const esc = (v: any) => `"${String(v ?? '').replace(/"/g, '""')}"`
const lines = [header.join(',')]
for (const r of rows) {
lines.push([new Date(r.acceptedAt).toISOString(), r.email, r.name, r.documentType, r.documentVersion, r.context, r.ipAddress || '', r.userAgent || '', r.contentHash || '', r.organizationId || ''].map(esc).join(','))
}
return new NextResponse(lines.join('\r\n'), {
headers: {
'Content-Type': 'text/csv; charset=utf-8',
'Content-Disposition': `attachment; filename="zustimmungen-${new Date().toISOString().slice(0, 10)}.csv"`,
},
})
}
return NextResponse.json({ acceptances: rows })
} catch (error) {
console.error('Error searching acceptances:', error)
return NextResponse.json({ error: 'Serverfehler' }, { status: 500 })
}
}