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>
651 lines
28 KiB
TypeScript
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>
|
|
)
|
|
}
|