mono/packages/ui/src/components/admin/ViolationsMonitor.tsx

201 lines
8.4 KiB
TypeScript

import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { toast } from "sonner";
import { AlertTriangle, RefreshCw } from "lucide-react";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
interface ViolationRecord {
key: string;
count: number;
firstViolation: number;
lastViolation: number;
}
interface ViolationStats {
totalViolations: number;
violations: ViolationRecord[];
}
export const ViolationsMonitor = ({ session }: { session: any }) => {
const [stats, setStats] = useState<ViolationStats>({
totalViolations: 0,
violations: [],
});
const [loading, setLoading] = useState(false);
const fetchViolationStats = async () => {
try {
setLoading(true);
const res = await fetch(`${import.meta.env.VITE_SERVER_IMAGE_API_URL}/api/admin/bans/violations`, {
headers: {
'Authorization': `Bearer ${session?.access_token || ''}`
}
});
if (!res.ok) {
throw new Error('Failed to fetch violation stats');
}
const data = await res.json();
setStats(data);
} catch (err: any) {
toast.error("Failed to fetch violation stats", {
description: err.message
});
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchViolationStats();
// Auto-refresh every 5 seconds
const interval = setInterval(fetchViolationStats, 5000);
return () => clearInterval(interval);
}, []);
const formatTimestamp = (timestamp: number) => {
return new Date(timestamp).toLocaleString();
};
const getViolationType = (key: string) => {
const [type] = key.split(':', 2);
return type;
};
const getViolationValue = (key: string) => {
const [, value] = key.split(':', 2);
return value;
};
const getSeverityColor = (count: number) => {
if (count >= 4) return "destructive";
if (count >= 2) return "default";
return "secondary";
};
return (
<div>
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-2">
<AlertTriangle className="h-6 w-6" />
<h1 className="text-2xl font-bold">Violation Monitor</h1>
</div>
<Button onClick={fetchViolationStats} disabled={loading} variant="outline">
{loading && <RefreshCw className="mr-2 h-4 w-4 animate-spin" />}
Refresh
</Button>
</div>
<div className="grid gap-4 md:grid-cols-2 mb-6">
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium">Active Violations</CardTitle>
<CardDescription>Currently tracked violation records</CardDescription>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.totalViolations}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium">Auto-Refresh</CardTitle>
<CardDescription>Updates every 5 seconds</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-center gap-2">
<div className="h-2 w-2 rounded-full bg-green-500 animate-pulse" />
<span className="text-sm text-muted-foreground">Live monitoring</span>
</div>
</CardContent>
</Card>
</div>
{stats.totalViolations === 0 ? (
<Card>
<CardContent className="py-8 text-center text-muted-foreground">
No active violations
</CardContent>
</Card>
) : (
<Card>
<CardHeader>
<CardTitle>Violation Records</CardTitle>
<CardDescription>
Entities approaching the ban threshold (5 violations within the configured window)
</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Type</TableHead>
<TableHead>Identifier</TableHead>
<TableHead>Count</TableHead>
<TableHead>First Violation</TableHead>
<TableHead>Last Violation</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{stats.violations
.sort((a, b) => b.count - a.count)
.map((violation) => (
<TableRow key={violation.key}>
<TableCell>
<Badge variant="outline" className="capitalize">
{getViolationType(violation.key)}
</Badge>
</TableCell>
<TableCell className="font-mono text-sm">
{getViolationValue(violation.key).substring(0, 40)}
{getViolationValue(violation.key).length > 40 && '...'}
</TableCell>
<TableCell>
<Badge variant={getSeverityColor(violation.count)}>
{violation.count} / 5
</Badge>
</TableCell>
<TableCell className="text-sm text-muted-foreground">
{formatTimestamp(violation.firstViolation)}
</TableCell>
<TableCell className="text-sm text-muted-foreground">
{formatTimestamp(violation.lastViolation)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
)}
<Card className="mt-6">
<CardHeader>
<CardTitle>About Violations</CardTitle>
</CardHeader>
<CardContent className="text-sm text-muted-foreground space-y-2">
<p>
<strong>Violation Tracking:</strong> The system tracks rate limit violations for IPs and authenticated users.
</p>
<p>
<strong>Auto-Ban Threshold:</strong> When an entity reaches 5 violations within the configured time window,
they are automatically banned and moved to the ban list.
</p>
<p>
<strong>Cleanup:</strong> Violation records are automatically cleaned up after the time window expires.
</p>
</CardContent>
</Card>
</div>
);
};