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

292 lines
12 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 { Shield, Trash2, RefreshCw } from "lucide-react";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
interface BanList {
bannedIPs: string[];
bannedUserIds: string[];
bannedTokens: string[];
}
export const BansManager = ({ session }: { session: any }) => {
const [banList, setBanList] = useState<BanList>({
bannedIPs: [],
bannedUserIds: [],
bannedTokens: [],
});
const [loading, setLoading] = useState(false);
const [unbanTarget, setUnbanTarget] = useState<{ type: 'ip' | 'user'; value: string } | null>(null);
const fetchBanList = async () => {
try {
setLoading(true);
const res = await fetch(`${import.meta.env.VITE_SERVER_IMAGE_API_URL}/api/admin/bans`, {
headers: {
'Authorization': `Bearer ${session?.access_token || ''}`
}
});
if (!res.ok) {
throw new Error('Failed to fetch ban list');
}
const data = await res.json();
setBanList(data);
} catch (err: any) {
toast.error("Failed to fetch ban list", {
description: err.message
});
} finally {
setLoading(false);
}
};
const handleUnban = async () => {
if (!unbanTarget) return;
try {
const endpoint = unbanTarget.type === 'ip'
? '/api/admin/bans/unban-ip'
: '/api/admin/bans/unban-user';
const body = unbanTarget.type === 'ip'
? { ip: unbanTarget.value }
: { userId: unbanTarget.value };
const res = await fetch(`${import.meta.env.VITE_SERVER_IMAGE_API_URL}${endpoint}`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${session?.access_token || ''}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
});
if (!res.ok) {
throw new Error('Failed to unban');
}
const data = await res.json();
if (data.success) {
toast.success("Unbanned successfully", {
description: data.message
});
fetchBanList();
} else {
toast.warning("Not found", {
description: data.message
});
}
} catch (err: any) {
toast.error("Failed to unban", {
description: err.message
});
} finally {
setUnbanTarget(null);
}
};
useEffect(() => {
fetchBanList();
}, []);
const totalBans = banList.bannedIPs.length + banList.bannedUserIds.length + banList.bannedTokens.length;
return (
<div>
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-2">
<Shield className="h-6 w-6" />
<h1 className="text-2xl font-bold">Ban Management</h1>
</div>
<Button onClick={fetchBanList} 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-3 mb-6">
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium">Banned IPs</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{banList.bannedIPs.length}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium">Banned Users</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{banList.bannedUserIds.length}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium">Banned Tokens</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{banList.bannedTokens.length}</div>
</CardContent>
</Card>
</div>
{totalBans === 0 ? (
<Card>
<CardContent className="py-8 text-center text-muted-foreground">
No active bans
</CardContent>
</Card>
) : (
<div className="space-y-6">
{banList.bannedIPs.length > 0 && (
<Card>
<CardHeader>
<CardTitle>Banned IP Addresses</CardTitle>
<CardDescription>
IP addresses that have been auto-banned for excessive requests
</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>IP Address</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{banList.bannedIPs.map((ip) => (
<TableRow key={ip}>
<TableCell className="font-mono">{ip}</TableCell>
<TableCell className="text-right">
<Button
variant="destructive"
size="sm"
onClick={() => setUnbanTarget({ type: 'ip', value: ip })}
>
<Trash2 className="h-4 w-4 mr-1" />
Unban
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
)}
{banList.bannedUserIds.length > 0 && (
<Card>
<CardHeader>
<CardTitle>Banned Users</CardTitle>
<CardDescription>
User accounts that have been auto-banned for excessive requests
</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>User ID</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{banList.bannedUserIds.map((userId) => (
<TableRow key={userId}>
<TableCell className="font-mono text-sm">{userId}</TableCell>
<TableCell className="text-right">
<Button
variant="destructive"
size="sm"
onClick={() => setUnbanTarget({ type: 'user', value: userId })}
>
<Trash2 className="h-4 w-4 mr-1" />
Unban
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
)}
{banList.bannedTokens.length > 0 && (
<Card>
<CardHeader>
<CardTitle>Banned Tokens</CardTitle>
<CardDescription>
Authentication tokens that have been auto-banned (cannot be unbanned via UI)
</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Token (truncated)</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{banList.bannedTokens.map((token, idx) => (
<TableRow key={idx}>
<TableCell className="font-mono text-sm">
{token.substring(0, 40)}...
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
)}
</div>
)}
<AlertDialog open={!!unbanTarget} onOpenChange={(open) => !open && setUnbanTarget(null)}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Confirm Unban</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to unban this {unbanTarget?.type}?
<div className="mt-2 p-2 bg-muted rounded font-mono text-sm">
{unbanTarget?.value}
</div>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={handleUnban}>Unban</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
};