Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website, Klausur-Service, School-Service, Voice-Service, Geo-Service, BreakPilot Drive, Agent-Core 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>
|
|
)
|
|
}
|