This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/admin-v2/app/(admin)/infrastructure/middleware/page.tsx
BreakPilot Dev f09e24d52c refactor(admin-v2): Consolidate compliance/DSGVO pages into SDK pipeline
Remove duplicate compliance and DSGVO admin pages that have been superseded
by the unified SDK pipeline. Update navigation, sidebar, roles, and module
registry to reflect the new structure. Add DSFA corpus API proxy and
source-policy components.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 23:26:05 +01:00

651 lines
28 KiB
TypeScript

'use client'
/**
* Middleware Admin - Rate Limiting, IP Whitelist/Blacklist, Events
*
* Manage middleware configurations and monitor events
* Migrated from old admin (/admin/middleware)
*/
import { useEffect, useState, useCallback } from 'react'
import { PagePurpose } from '@/components/common/PagePurpose'
interface MiddlewareConfig {
id: string
middleware_name: string
enabled: boolean
config: Record<string, unknown>
updated_at: string | null
}
interface RateLimitIP {
id: string
ip_address: string
list_type: 'whitelist' | 'blacklist'
reason: string | null
expires_at: string | null
created_at: string
}
interface MiddlewareEvent {
id: string
middleware_name: string
event_type: string
ip_address: string | null
user_id: string | null
request_path: string | null
request_method: string | null
details: Record<string, unknown> | null
created_at: string
}
interface MiddlewareStats {
middleware_name: string
total_events: number
events_last_hour: number
events_last_24h: number
top_event_types: Array<{ event_type: string; count: number }>
top_ips: Array<{ ip_address: string; count: number }>
}
export default function MiddlewareAdminPage() {
const [configs, setConfigs] = useState<MiddlewareConfig[]>([])
const [ipList, setIpList] = useState<RateLimitIP[]>([])
const [events, setEvents] = useState<MiddlewareEvent[]>([])
const [stats, setStats] = useState<MiddlewareStats[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [activeTab, setActiveTab] = useState<'overview' | 'config' | 'ip-list' | 'events' | 'stats'>('overview')
const [actionLoading, setActionLoading] = useState<string | null>(null)
// IP Form
const [newIP, setNewIP] = useState('')
const [newIPType, setNewIPType] = useState<'whitelist' | 'blacklist'>('whitelist')
const [newIPReason, setNewIPReason] = useState('')
const fetchData = useCallback(async () => {
setLoading(true)
setError(null)
try {
const [configsRes, ipListRes, eventsRes, statsRes] = await Promise.all([
fetch('/api/admin/middleware'),
fetch('/api/admin/middleware/rate-limit/ip-list'),
fetch('/api/admin/middleware/events?limit=50'),
fetch('/api/admin/middleware/stats'),
])
if (configsRes.ok) {
setConfigs(await configsRes.json())
}
if (ipListRes.ok) {
setIpList(await ipListRes.json())
}
if (eventsRes.ok) {
setEvents(await eventsRes.json())
}
if (statsRes.ok) {
setStats(await statsRes.json())
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Verbindung zum Backend fehlgeschlagen')
} finally {
setLoading(false)
}
}, [])
useEffect(() => {
fetchData()
}, [fetchData])
useEffect(() => {
const interval = setInterval(fetchData, 30000)
return () => clearInterval(interval)
}, [fetchData])
const toggleMiddleware = async (name: string, enabled: boolean) => {
setActionLoading(name)
setError(null)
try {
const response = await fetch(`/api/admin/middleware/${name}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enabled }),
})
if (!response.ok) {
throw new Error(`Fehler beim Aktualisieren: ${response.statusText}`)
}
// Update local state
setConfigs(prev =>
prev.map(c => (c.middleware_name === name ? { ...c, enabled } : c))
)
} catch (err) {
setError(err instanceof Error ? err.message : 'Aktualisierung fehlgeschlagen')
} finally {
setActionLoading(null)
}
}
const addIP = async (e: React.FormEvent) => {
e.preventDefault()
if (!newIP.trim()) return
setActionLoading('add-ip')
setError(null)
try {
const response = await fetch('/api/admin/middleware/rate-limit/ip-list', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
ip_address: newIP.trim(),
list_type: newIPType,
reason: newIPReason.trim() || null,
}),
})
if (!response.ok) {
const data = await response.json()
throw new Error(data.detail || `Fehler: ${response.statusText}`)
}
const newEntry = await response.json()
setIpList(prev => [newEntry, ...prev])
setNewIP('')
setNewIPReason('')
} catch (err) {
setError(err instanceof Error ? err.message : 'IP konnte nicht hinzugefuegt werden')
} finally {
setActionLoading(null)
}
}
const removeIP = async (id: string) => {
setActionLoading(`remove-${id}`)
setError(null)
try {
const response = await fetch(`/api/admin/middleware/rate-limit/ip-list/${id}`, {
method: 'DELETE',
})
if (!response.ok) {
throw new Error(`Fehler beim Loeschen: ${response.statusText}`)
}
setIpList(prev => prev.filter(ip => ip.id !== id))
} catch (err) {
setError(err instanceof Error ? err.message : 'IP konnte nicht entfernt werden')
} finally {
setActionLoading(null)
}
}
const getMiddlewareDescription = (name: string): { icon: string; desc: string } => {
const descriptions: Record<string, { icon: string; desc: string }> = {
request_id: { icon: '🆔', desc: 'Generiert eindeutige Request-IDs fuer Tracing' },
security_headers: { icon: '🛡️', desc: 'Fuegt Security-Header hinzu (CSP, HSTS, etc.)' },
cors: { icon: '🌐', desc: 'Cross-Origin Resource Sharing Konfiguration' },
rate_limiter: { icon: '⏱️', desc: 'Rate Limiting zum Schutz vor Missbrauch' },
pii_redactor: { icon: '🔒', desc: 'Redaktiert personenbezogene Daten in Logs' },
input_gate: { icon: '🚪', desc: 'Validiert und sanitisiert Eingaben' },
}
return descriptions[name] || { icon: '⚙️', desc: 'Middleware-Komponente' }
}
const getEventTypeColor = (eventType: string) => {
if (eventType.includes('error') || eventType.includes('blocked') || eventType.includes('blacklist')) {
return 'bg-red-100 text-red-800'
}
if (eventType.includes('warning') || eventType.includes('rate_limit')) {
return 'bg-yellow-100 text-yellow-800'
}
if (eventType.includes('success') || eventType.includes('whitelist')) {
return 'bg-green-100 text-green-800'
}
return 'bg-slate-100 text-slate-800'
}
const whitelistCount = ipList.filter(ip => ip.list_type === 'whitelist').length
const blacklistCount = ipList.filter(ip => ip.list_type === 'blacklist').length
return (
<div>
<PagePurpose
title="Middleware Admin"
purpose="Verwalten Sie die Middleware-Konfiguration, Rate Limiting und IP-Listen. Ueberwachen Sie Middleware-Events und Statistiken in Echtzeit."
audience={['DevOps', 'Security', 'System-Admins']}
gdprArticles={['Art. 32 (Sicherheit der Verarbeitung)']}
architecture={{
services: ['FastAPI Middleware Stack', 'PostgreSQL'],
databases: ['middleware_config', 'rate_limit_ip_list', 'middleware_events'],
}}
relatedPages={[
{ name: 'Security', href: '/infrastructure/security', description: 'DevSecOps Dashboard' },
{ name: 'Mac Mini', href: '/infrastructure/mac-mini', description: 'Server-Monitoring' },
{ name: 'Controls', href: '/sdk/controls', description: 'Security Controls' },
]}
collapsible={true}
defaultCollapsed={true}
/>
{/* Stats Overview */}
<div className="bg-white rounded-xl border border-slate-200 p-6 mb-6">
<div className="flex justify-between items-center mb-6">
<h2 className="text-xl font-semibold text-slate-900">Middleware Status</h2>
<button
onClick={fetchData}
disabled={loading}
className="px-4 py-2 text-sm bg-orange-600 text-white rounded-lg hover:bg-orange-700 disabled:opacity-50 transition-colors"
>
{loading ? 'Laden...' : 'Aktualisieren'}
</button>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="bg-slate-50 rounded-lg p-4 border border-slate-200">
<div className="text-2xl font-bold text-slate-900">{configs.length}</div>
<div className="text-sm text-slate-600">Middleware</div>
</div>
<div className="bg-green-50 rounded-lg p-4 border border-green-200">
<div className="text-2xl font-bold text-green-600">{whitelistCount}</div>
<div className="text-sm text-slate-600">Whitelist IPs</div>
</div>
<div className="bg-red-50 rounded-lg p-4 border border-red-200">
<div className="text-2xl font-bold text-red-600">{blacklistCount}</div>
<div className="text-sm text-slate-600">Blacklist IPs</div>
</div>
<div className="bg-blue-50 rounded-lg p-4 border border-blue-200">
<div className="text-2xl font-bold text-blue-600">{events.length}</div>
<div className="text-sm text-slate-600">Recent Events</div>
</div>
</div>
</div>
{/* Tabs */}
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden mb-6">
<div className="flex border-b border-slate-200 overflow-x-auto">
{(['overview', 'config', 'ip-list', 'events', 'stats'] as const).map(tab => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
className={`px-6 py-3 text-sm font-medium whitespace-nowrap transition-colors ${
activeTab === tab
? 'bg-orange-50 text-orange-700 border-b-2 border-orange-600'
: 'text-slate-600 hover:bg-slate-50'
}`}
>
{tab === 'overview' && 'Uebersicht'}
{tab === 'config' && 'Konfiguration'}
{tab === 'ip-list' && `IP-Listen (${ipList.length})`}
{tab === 'events' && 'Events'}
{tab === 'stats' && 'Statistiken'}
</button>
))}
</div>
<div className="p-6">
{error && (
<div className="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg text-red-700">{error}</div>
)}
{loading ? (
<div className="flex justify-center py-12">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-orange-600" />
</div>
) : (
<>
{/* Overview Tab */}
{activeTab === 'overview' && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{configs.map(config => {
const info = getMiddlewareDescription(config.middleware_name)
return (
<div
key={config.id}
className={`rounded-lg p-4 border ${
config.enabled
? 'bg-green-50 border-green-200'
: 'bg-slate-50 border-slate-200'
}`}
>
<div className="flex justify-between items-start mb-2">
<div className="flex items-center gap-2">
<span className="text-xl">{info.icon}</span>
<span className="font-semibold text-slate-900 capitalize">
{config.middleware_name.replace('_', ' ')}
</span>
</div>
<button
onClick={() => toggleMiddleware(config.middleware_name, !config.enabled)}
disabled={actionLoading === config.middleware_name}
className={`px-3 py-1 rounded-full text-xs font-semibold transition-colors ${
config.enabled
? 'bg-green-200 text-green-800 hover:bg-green-300'
: 'bg-slate-200 text-slate-600 hover:bg-slate-300'
}`}
>
{actionLoading === config.middleware_name
? '...'
: config.enabled
? 'Aktiv'
: 'Inaktiv'}
</button>
</div>
<p className="text-sm text-slate-600">{info.desc}</p>
{config.updated_at && (
<div className="mt-2 text-xs text-slate-400">
Aktualisiert: {new Date(config.updated_at).toLocaleString('de-DE')}
</div>
)}
</div>
)
})}
</div>
)}
{/* Config Tab */}
{activeTab === 'config' && (
<div className="space-y-4">
{configs.map(config => {
const info = getMiddlewareDescription(config.middleware_name)
return (
<div key={config.id} className="bg-slate-50 rounded-lg p-4 border border-slate-200">
<div className="flex justify-between items-start mb-4">
<div>
<h3 className="font-semibold text-slate-900 flex items-center gap-2">
<span>{info.icon}</span>
<span className="capitalize">{config.middleware_name.replace('_', ' ')}</span>
</h3>
<p className="text-sm text-slate-600">{info.desc}</p>
</div>
<div className="flex items-center gap-3">
<span
className={`px-3 py-1 rounded-full text-xs font-semibold ${
config.enabled ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
}`}
>
{config.enabled ? 'Aktiviert' : 'Deaktiviert'}
</span>
<button
onClick={() => toggleMiddleware(config.middleware_name, !config.enabled)}
disabled={actionLoading === config.middleware_name}
className="px-4 py-1.5 text-sm border border-slate-300 rounded-lg hover:bg-slate-100 disabled:opacity-50 transition-colors"
>
{actionLoading === config.middleware_name
? '...'
: config.enabled
? 'Deaktivieren'
: 'Aktivieren'}
</button>
</div>
</div>
{Object.keys(config.config).length > 0 && (
<div className="mt-3 pt-3 border-t border-slate-200">
<div className="text-xs font-semibold text-slate-500 uppercase mb-2">
Konfiguration
</div>
<pre className="text-xs bg-white p-3 rounded border border-slate-200 overflow-x-auto">
{JSON.stringify(config.config, null, 2)}
</pre>
</div>
)}
</div>
)
})}
</div>
)}
{/* IP List Tab */}
{activeTab === 'ip-list' && (
<div>
{/* Add IP Form */}
<form onSubmit={addIP} className="mb-6 p-4 bg-slate-50 rounded-lg border border-slate-200">
<h3 className="font-semibold text-slate-900 mb-4">IP hinzufuegen</h3>
<div className="flex flex-wrap gap-3">
<input
type="text"
value={newIP}
onChange={e => setNewIP(e.target.value)}
placeholder="IP-Adresse (z.B. 192.168.1.1)"
className="flex-1 min-w-[200px] px-4 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-500"
/>
<select
value={newIPType}
onChange={e => setNewIPType(e.target.value as 'whitelist' | 'blacklist')}
className="px-4 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-500"
>
<option value="whitelist">Whitelist</option>
<option value="blacklist">Blacklist</option>
</select>
<input
type="text"
value={newIPReason}
onChange={e => setNewIPReason(e.target.value)}
placeholder="Grund (optional)"
className="flex-1 min-w-[150px] px-4 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-500"
/>
<button
type="submit"
disabled={!newIP.trim() || actionLoading === 'add-ip'}
className="px-6 py-2 bg-orange-600 text-white rounded-lg font-medium hover:bg-orange-700 disabled:opacity-50 transition-colors"
>
{actionLoading === 'add-ip' ? 'Hinzufuegen...' : 'Hinzufuegen'}
</button>
</div>
</form>
{/* IP List Table */}
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-slate-200">
<th className="text-left py-3 px-4 text-xs font-semibold text-slate-500 uppercase">
IP-Adresse
</th>
<th className="text-left py-3 px-4 text-xs font-semibold text-slate-500 uppercase">
Typ
</th>
<th className="text-left py-3 px-4 text-xs font-semibold text-slate-500 uppercase">
Grund
</th>
<th className="text-left py-3 px-4 text-xs font-semibold text-slate-500 uppercase">
Hinzugefuegt
</th>
<th className="text-right py-3 px-4 text-xs font-semibold text-slate-500 uppercase">
Aktion
</th>
</tr>
</thead>
<tbody>
{ipList.length === 0 ? (
<tr>
<td colSpan={5} className="text-center py-8 text-slate-500">
Keine IP-Eintraege vorhanden.
</td>
</tr>
) : (
ipList.map(ip => (
<tr key={ip.id} className="border-b border-slate-100 hover:bg-slate-50">
<td className="py-3 px-4 font-mono text-sm">{ip.ip_address}</td>
<td className="py-3 px-4">
<span
className={`px-2 py-1 rounded text-xs font-semibold ${
ip.list_type === 'whitelist'
? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800'
}`}
>
{ip.list_type === 'whitelist' ? 'Whitelist' : 'Blacklist'}
</span>
</td>
<td className="py-3 px-4 text-sm text-slate-600">{ip.reason || '-'}</td>
<td className="py-3 px-4 text-sm text-slate-500">
{new Date(ip.created_at).toLocaleString('de-DE')}
</td>
<td className="py-3 px-4 text-right">
<button
onClick={() => removeIP(ip.id)}
disabled={actionLoading === `remove-${ip.id}`}
className="px-3 py-1 text-sm text-red-600 hover:bg-red-50 rounded transition-colors disabled:opacity-50"
>
{actionLoading === `remove-${ip.id}` ? '...' : 'Entfernen'}
</button>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
)}
{/* Events Tab */}
{activeTab === 'events' && (
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-slate-200">
<th className="text-left py-3 px-4 text-xs font-semibold text-slate-500 uppercase">
Zeit
</th>
<th className="text-left py-3 px-4 text-xs font-semibold text-slate-500 uppercase">
Middleware
</th>
<th className="text-left py-3 px-4 text-xs font-semibold text-slate-500 uppercase">
Event
</th>
<th className="text-left py-3 px-4 text-xs font-semibold text-slate-500 uppercase">
IP
</th>
<th className="text-left py-3 px-4 text-xs font-semibold text-slate-500 uppercase">
Pfad
</th>
</tr>
</thead>
<tbody>
{events.length === 0 ? (
<tr>
<td colSpan={5} className="text-center py-8 text-slate-500">
Keine Events vorhanden.
</td>
</tr>
) : (
events.map(event => (
<tr key={event.id} className="border-b border-slate-100 hover:bg-slate-50">
<td className="py-3 px-4 text-sm text-slate-500">
{new Date(event.created_at).toLocaleString('de-DE')}
</td>
<td className="py-3 px-4 text-sm capitalize">
{event.middleware_name.replace('_', ' ')}
</td>
<td className="py-3 px-4">
<span
className={`px-2 py-1 rounded text-xs font-semibold ${getEventTypeColor(event.event_type)}`}
>
{event.event_type}
</span>
</td>
<td className="py-3 px-4 text-sm font-mono text-slate-600">
{event.ip_address || '-'}
</td>
<td className="py-3 px-4 text-sm text-slate-600 max-w-xs truncate">
{event.request_method && event.request_path
? `${event.request_method} ${event.request_path}`
: '-'}
</td>
</tr>
))
)}
</tbody>
</table>
</div>
)}
{/* Stats Tab */}
{activeTab === 'stats' && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{stats.map(stat => {
const info = getMiddlewareDescription(stat.middleware_name)
return (
<div key={stat.middleware_name} className="bg-slate-50 rounded-lg p-4 border border-slate-200">
<h3 className="font-semibold text-slate-900 flex items-center gap-2 mb-4">
<span>{info.icon}</span>
<span className="capitalize">{stat.middleware_name.replace('_', ' ')}</span>
</h3>
<div className="grid grid-cols-3 gap-4 mb-4">
<div>
<div className="text-2xl font-bold text-slate-900">{stat.total_events}</div>
<div className="text-xs text-slate-500">Gesamt</div>
</div>
<div>
<div className="text-2xl font-bold text-blue-600">{stat.events_last_hour}</div>
<div className="text-xs text-slate-500">Letzte Stunde</div>
</div>
<div>
<div className="text-2xl font-bold text-orange-600">{stat.events_last_24h}</div>
<div className="text-xs text-slate-500">24 Stunden</div>
</div>
</div>
{stat.top_event_types.length > 0 && (
<div className="mb-3">
<div className="text-xs font-semibold text-slate-500 uppercase mb-2">
Top Event-Typen
</div>
<div className="flex flex-wrap gap-2">
{stat.top_event_types.slice(0, 3).map(et => (
<span
key={et.event_type}
className={`px-2 py-1 rounded text-xs ${getEventTypeColor(et.event_type)}`}
>
{et.event_type} ({et.count})
</span>
))}
</div>
</div>
)}
{stat.top_ips.length > 0 && (
<div>
<div className="text-xs font-semibold text-slate-500 uppercase mb-2">Top IPs</div>
<div className="text-xs text-slate-600">
{stat.top_ips
.slice(0, 3)
.map(ip => `${ip.ip_address} (${ip.count})`)
.join(', ')}
</div>
</div>
)}
</div>
)
})}
</div>
)}
</>
)}
</div>
</div>
{/* Info Box */}
<div className="bg-orange-50 border border-orange-200 rounded-xl p-4">
<div className="flex gap-3">
<svg className="w-5 h-5 text-orange-600 flex-shrink-0 mt-0.5" 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>
<h4 className="font-semibold text-orange-900">Middleware Stack</h4>
<p className="text-sm text-orange-800 mt-1">
Der Middleware Stack verarbeitet alle API-Anfragen in der konfigurierten Reihenfolge.
Aenderungen an der Konfiguration werden sofort wirksam.
Verwenden Sie die Whitelist fuer vertrauenswuerdige IPs und die Blacklist fuer bekannte Angreifer.
</p>
</div>
</div>
</div>
</div>
)
}