Initial commit: breakpilot-compliance - Compliance SDK Platform
Services: Admin-Compliance, Backend-Compliance, AI-Compliance-SDK, Consent-SDK, Developer-Portal, PCA-Platform, DSMS Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
413
admin-compliance/components/sdk/source-policy/AuditTab.tsx
Normal file
413
admin-compliance/components/sdk/source-policy/AuditTab.tsx
Normal file
@@ -0,0 +1,413 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
interface AuditLogEntry {
|
||||
id: string
|
||||
action: string
|
||||
entity_type: string
|
||||
entity_id?: string
|
||||
old_value?: any
|
||||
new_value?: any
|
||||
user_email?: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
interface BlockedContentEntry {
|
||||
id: string
|
||||
url: string
|
||||
domain: string
|
||||
block_reason: string
|
||||
rule_id?: string
|
||||
details?: any
|
||||
created_at: string
|
||||
}
|
||||
|
||||
interface AuditTabProps {
|
||||
apiBase: string
|
||||
}
|
||||
|
||||
const ACTION_LABELS: Record<string, { label: string; color: string }> = {
|
||||
create: { label: 'Erstellt', color: 'bg-green-100 text-green-700' },
|
||||
update: { label: 'Aktualisiert', color: 'bg-blue-100 text-blue-700' },
|
||||
delete: { label: 'Geloescht', color: 'bg-red-100 text-red-700' },
|
||||
}
|
||||
|
||||
const ENTITY_LABELS: Record<string, string> = {
|
||||
source_policy: 'Policy',
|
||||
allowed_source: 'Quelle',
|
||||
operation_permission: 'Operation',
|
||||
pii_rule: 'PII-Regel',
|
||||
}
|
||||
|
||||
const BLOCK_REASON_LABELS: Record<string, { label: string; color: string }> = {
|
||||
not_whitelisted: { label: 'Nicht in Whitelist', color: 'bg-amber-100 text-amber-700' },
|
||||
pii_detected: { label: 'PII erkannt', color: 'bg-red-100 text-red-700' },
|
||||
license_violation: { label: 'Lizenzverletzung', color: 'bg-orange-100 text-orange-700' },
|
||||
training_forbidden: { label: 'Training verboten', color: 'bg-slate-800 text-white' },
|
||||
}
|
||||
|
||||
export function AuditTab({ apiBase }: AuditTabProps) {
|
||||
const [activeView, setActiveView] = useState<'changes' | 'blocked'>('changes')
|
||||
|
||||
// Audit logs
|
||||
const [auditLogs, setAuditLogs] = useState<AuditLogEntry[]>([])
|
||||
const [auditLoading, setAuditLoading] = useState(true)
|
||||
const [auditTotal, setAuditTotal] = useState(0)
|
||||
|
||||
// Blocked content
|
||||
const [blockedContent, setBlockedContent] = useState<BlockedContentEntry[]>([])
|
||||
const [blockedLoading, setBlockedLoading] = useState(true)
|
||||
const [blockedTotal, setBlockedTotal] = useState(0)
|
||||
|
||||
// Filters
|
||||
const [dateFrom, setDateFrom] = useState('')
|
||||
const [dateTo, setDateTo] = useState('')
|
||||
const [entityFilter, setEntityFilter] = useState('')
|
||||
|
||||
// Export
|
||||
const [exporting, setExporting] = useState(false)
|
||||
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (activeView === 'changes') {
|
||||
fetchAuditLogs()
|
||||
} else {
|
||||
fetchBlockedContent()
|
||||
}
|
||||
}, [activeView, dateFrom, dateTo, entityFilter])
|
||||
|
||||
const fetchAuditLogs = async () => {
|
||||
try {
|
||||
setAuditLoading(true)
|
||||
const params = new URLSearchParams()
|
||||
if (dateFrom) params.append('from', dateFrom)
|
||||
if (dateTo) params.append('to', dateTo)
|
||||
if (entityFilter) params.append('entity_type', entityFilter)
|
||||
params.append('limit', '100')
|
||||
|
||||
const res = await fetch(`${apiBase}/v1/admin/policy-audit?${params}`)
|
||||
if (!res.ok) throw new Error('Fehler beim Laden')
|
||||
|
||||
const data = await res.json()
|
||||
setAuditLogs(data.logs || [])
|
||||
setAuditTotal(data.total || 0)
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Unbekannter Fehler')
|
||||
} finally {
|
||||
setAuditLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const fetchBlockedContent = async () => {
|
||||
try {
|
||||
setBlockedLoading(true)
|
||||
const params = new URLSearchParams()
|
||||
if (dateFrom) params.append('from', dateFrom)
|
||||
if (dateTo) params.append('to', dateTo)
|
||||
params.append('limit', '100')
|
||||
|
||||
const res = await fetch(`${apiBase}/v1/admin/blocked-content?${params}`)
|
||||
if (!res.ok) throw new Error('Fehler beim Laden')
|
||||
|
||||
const data = await res.json()
|
||||
setBlockedContent(data.blocked || [])
|
||||
setBlockedTotal(data.total || 0)
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Unbekannter Fehler')
|
||||
} finally {
|
||||
setBlockedLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const exportReport = async () => {
|
||||
try {
|
||||
setExporting(true)
|
||||
const params = new URLSearchParams()
|
||||
if (dateFrom) params.append('from', dateFrom)
|
||||
if (dateTo) params.append('to', dateTo)
|
||||
params.append('format', 'download')
|
||||
|
||||
const res = await fetch(`${apiBase}/v1/admin/compliance-report?${params}`)
|
||||
if (!res.ok) throw new Error('Fehler beim Export')
|
||||
|
||||
const blob = await res.blob()
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `compliance-report-${new Date().toISOString().split('T')[0]}.json`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
window.URL.revokeObjectURL(url)
|
||||
document.body.removeChild(a)
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Unbekannter Fehler')
|
||||
} finally {
|
||||
setExporting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const formatDate = (dateStr: string) => {
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Error Display */}
|
||||
{error && (
|
||||
<div className="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg text-red-700 flex items-center justify-between">
|
||||
<span>{error}</span>
|
||||
<button onClick={() => setError(null)} className="text-red-500 hover:text-red-700">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* View Toggle & Filters */}
|
||||
<div className="flex flex-col md:flex-row gap-4 mb-6">
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setActiveView('changes')}
|
||||
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
|
||||
activeView === 'changes'
|
||||
? 'bg-purple-600 text-white'
|
||||
: 'bg-slate-100 text-slate-700 hover:bg-slate-200'
|
||||
}`}
|
||||
>
|
||||
Aenderungshistorie
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveView('blocked')}
|
||||
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
|
||||
activeView === 'blocked'
|
||||
? 'bg-purple-600 text-white'
|
||||
: 'bg-slate-100 text-slate-700 hover:bg-slate-200'
|
||||
}`}
|
||||
>
|
||||
Blockierte URLs
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 flex flex-wrap gap-4 items-center">
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-sm text-slate-600">Von:</label>
|
||||
<input
|
||||
type="date"
|
||||
value={dateFrom}
|
||||
onChange={(e) => setDateFrom(e.target.value)}
|
||||
className="px-3 py-2 border border-slate-200 rounded-lg text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-sm text-slate-600">Bis:</label>
|
||||
<input
|
||||
type="date"
|
||||
value={dateTo}
|
||||
onChange={(e) => setDateTo(e.target.value)}
|
||||
className="px-3 py-2 border border-slate-200 rounded-lg text-sm"
|
||||
/>
|
||||
</div>
|
||||
{activeView === 'changes' && (
|
||||
<select
|
||||
value={entityFilter}
|
||||
onChange={(e) => setEntityFilter(e.target.value)}
|
||||
className="px-3 py-2 border border-slate-200 rounded-lg text-sm"
|
||||
>
|
||||
<option value="">Alle Typen</option>
|
||||
<option value="source_policy">Policies</option>
|
||||
<option value="allowed_source">Quellen</option>
|
||||
<option value="operation_permission">Operations</option>
|
||||
<option value="pii_rule">PII-Regeln</option>
|
||||
</select>
|
||||
)}
|
||||
<button
|
||||
onClick={exportReport}
|
||||
disabled={exporting}
|
||||
className="px-4 py-2 bg-slate-100 text-slate-700 rounded-lg hover:bg-slate-200 flex items-center gap-2 ml-auto"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
{exporting ? 'Exportiere...' : 'JSON Export'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Changes View */}
|
||||
{activeView === 'changes' && (
|
||||
<>
|
||||
{auditLoading ? (
|
||||
<div className="text-center py-12 text-slate-500">Lade Audit-Log...</div>
|
||||
) : auditLogs.length === 0 ? (
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-8 text-center">
|
||||
<svg className="w-12 h-12 mx-auto text-slate-300 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<h3 className="text-lg font-medium text-slate-700 mb-2">Keine Eintraege vorhanden</h3>
|
||||
<p className="text-sm text-slate-500">
|
||||
Aenderungen werden hier protokolliert.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
|
||||
<div className="px-4 py-3 bg-slate-50 border-b border-slate-200 text-sm text-slate-600">
|
||||
{auditTotal} Eintraege gesamt
|
||||
</div>
|
||||
<div className="divide-y divide-slate-100">
|
||||
{auditLogs.map((log) => {
|
||||
const actionConfig = ACTION_LABELS[log.action] || { label: log.action, color: 'bg-slate-100 text-slate-700' }
|
||||
return (
|
||||
<div key={log.id} className="px-4 py-4 hover:bg-slate-50">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className={`px-2 py-1 rounded text-xs font-medium ${actionConfig.color}`}>
|
||||
{actionConfig.label}
|
||||
</span>
|
||||
<span className="text-sm text-slate-700">
|
||||
{ENTITY_LABELS[log.entity_type] || log.entity_type}
|
||||
</span>
|
||||
{log.entity_id && (
|
||||
<code className="text-xs bg-slate-100 px-2 py-1 rounded text-slate-500">
|
||||
{log.entity_id.substring(0, 8)}...
|
||||
</code>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-slate-500">
|
||||
{formatDate(log.created_at)}
|
||||
</div>
|
||||
</div>
|
||||
{log.user_email && (
|
||||
<div className="mt-2 text-xs text-slate-500">
|
||||
Benutzer: {log.user_email}
|
||||
</div>
|
||||
)}
|
||||
{(log.old_value || log.new_value) && (
|
||||
<div className="mt-2 flex gap-4 text-xs">
|
||||
{log.old_value && (
|
||||
<div className="flex-1 p-2 bg-red-50 rounded">
|
||||
<div className="text-red-600 font-medium mb-1">Vorher:</div>
|
||||
<pre className="text-red-700 overflow-x-auto">
|
||||
{typeof log.old_value === 'string' ? log.old_value : JSON.stringify(log.old_value, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
{log.new_value && (
|
||||
<div className="flex-1 p-2 bg-green-50 rounded">
|
||||
<div className="text-green-600 font-medium mb-1">Nachher:</div>
|
||||
<pre className="text-green-700 overflow-x-auto">
|
||||
{typeof log.new_value === 'string' ? log.new_value : JSON.stringify(log.new_value, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Blocked Content View */}
|
||||
{activeView === 'blocked' && (
|
||||
<>
|
||||
{blockedLoading ? (
|
||||
<div className="text-center py-12 text-slate-500">Lade blockierte URLs...</div>
|
||||
) : blockedContent.length === 0 ? (
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-8 text-center">
|
||||
<svg className="w-12 h-12 mx-auto text-green-300 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<h3 className="text-lg font-medium text-slate-700 mb-2">Keine blockierten URLs</h3>
|
||||
<p className="text-sm text-slate-500">
|
||||
Alle gecrawlten URLs waren in der Whitelist.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
|
||||
<div className="px-4 py-3 bg-slate-50 border-b border-slate-200 text-sm text-slate-600">
|
||||
{blockedTotal} blockierte URLs gesamt
|
||||
</div>
|
||||
<table className="w-full">
|
||||
<thead className="bg-slate-50 border-b border-slate-200">
|
||||
<tr>
|
||||
<th className="text-left px-4 py-3 text-xs font-medium text-slate-500 uppercase">URL</th>
|
||||
<th className="text-left px-4 py-3 text-xs font-medium text-slate-500 uppercase">Domain</th>
|
||||
<th className="text-left px-4 py-3 text-xs font-medium text-slate-500 uppercase">Grund</th>
|
||||
<th className="text-left px-4 py-3 text-xs font-medium text-slate-500 uppercase">Zeitpunkt</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-100">
|
||||
{blockedContent.map((entry) => {
|
||||
const reasonConfig = BLOCK_REASON_LABELS[entry.block_reason] || {
|
||||
label: entry.block_reason,
|
||||
color: 'bg-slate-100 text-slate-700',
|
||||
}
|
||||
return (
|
||||
<tr key={entry.id} className="hover:bg-slate-50">
|
||||
<td className="px-4 py-3">
|
||||
<div className="text-sm text-slate-700 truncate max-w-md" title={entry.url}>
|
||||
{entry.url}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<code className="text-xs bg-slate-100 px-2 py-1 rounded">{entry.domain}</code>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className={`px-2 py-1 rounded text-xs font-medium ${reasonConfig.color}`}>
|
||||
{reasonConfig.label}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-xs text-slate-500">
|
||||
{formatDate(entry.created_at)}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Auditor Info Box */}
|
||||
<div className="mt-6 bg-blue-50 border border-blue-200 rounded-xl p-6">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center flex-shrink-0">
|
||||
<svg className="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-blue-800">Fuer Auditoren</h3>
|
||||
<p className="text-sm text-blue-700 mt-1">
|
||||
Dieses Audit-Log ist unveraenderlich und protokolliert alle Aenderungen an der Quellen-Policy.
|
||||
Jeder Eintrag enthaelt:
|
||||
</p>
|
||||
<ul className="text-sm text-blue-700 mt-2 list-disc list-inside">
|
||||
<li>Zeitstempel der Aenderung</li>
|
||||
<li>Art der Aenderung (Erstellen/Aendern/Loeschen)</li>
|
||||
<li>Betroffene Entitaet und ID</li>
|
||||
<li>Vorheriger und neuer Wert</li>
|
||||
<li>E-Mail des Benutzers (falls angemeldet)</li>
|
||||
</ul>
|
||||
<p className="text-sm text-blue-600 mt-2 font-medium">
|
||||
Der JSON-Export ist fuer die externe Pruefung und Archivierung geeignet.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user