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>
This commit is contained in:
Pepe Ziberi
2026-07-23 17:55:57 +02:00
parent 5cdf0e63c9
commit fa1e911ecd
10 changed files with 27 additions and 8 deletions

View File

@@ -8,7 +8,7 @@ import { useAuth } from '@/components/providers/auth-provider'
interface Doc { id: string; type: string; version: string; title: string; url: string | null; isActive: boolean; publishedAt: string; contentHash: string; acceptanceCount: number }
interface Miss { type: string; version: string; missing: number }
interface Acc { id: string; email: string; name: string; documentType: string; documentVersion: string; context: string; acceptedAt: string; organizationId: string | null }
interface Acc { id: string; email: string; name: string; documentType: string; documentVersion: string; context: string; acceptedAt: string; organizationId: string | null; ipAddress: string | null; userAgent: string | null; contentHash: string | null }
export default function AdminLegalPage() {
const { user, loading, isServerAdmin } = useAuth()
@@ -137,6 +137,8 @@ export default function AdminLegalPage() {
<th className="px-3 py-2">Benutzer</th>
<th className="px-3 py-2">Dokument</th>
<th className="px-3 py-2">Kontext</th>
<th className="px-3 py-2">IP</th>
<th className="px-3 py-2">Gerät / Browser</th>
</tr>
</thead>
<tbody>
@@ -144,8 +146,10 @@ export default function AdminLegalPage() {
<tr key={a.id} className="border-t border-border">
<td className="px-3 py-2 text-muted-foreground whitespace-nowrap">{new Date(a.acceptedAt).toLocaleString('de-CH', { dateStyle: 'short', timeStyle: 'short' })}</td>
<td className="px-3 py-2"><span className="text-foreground">{a.name}</span><br /><span className="text-xs text-muted-foreground">{a.email}</span></td>
<td className="px-3 py-2">{a.documentType} <span className="text-muted-foreground">v{a.documentVersion}</span></td>
<td className="px-3 py-2">{a.documentType} <span className="text-muted-foreground">v{a.documentVersion}</span>{a.contentHash && <><br /><span className="font-mono text-[10px] text-muted-foreground">{a.contentHash.slice(0, 12)}</span></>}</td>
<td className="px-3 py-2 text-xs text-muted-foreground">{a.context}</td>
<td className="px-3 py-2 text-xs text-muted-foreground font-mono whitespace-nowrap">{a.ipAddress || '—'}</td>
<td className="px-3 py-2 text-[10px] text-muted-foreground max-w-[200px] truncate" title={a.userAgent || ''}>{a.userAgent || '—'}</td>
</tr>
))}
</tbody>