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>
66 lines
2.9 KiB
TypeScript
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 })
|
|
}
|
|
}
|