201 lines
8.4 KiB
TypeScript
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>
|
|
);
|
|
};
|