Replaced mock data in the admin ban page with real API calls for fetching, banning, and unbanning users. Improved form validation, loading states, and search functionality. Updated banned users table to show live data and added confirmation dialogs for ban/unban actions. Enhanced user status display in the admin users page with tooltips for blocked reasons.
376 lines
14 KiB
TypeScript
376 lines
14 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useEffect } from "react";
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
|
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@/components/ui/alert-dialog";
|
|
import { UserX, Shield, Search, Ban, Unlock, Loader2 } from "lucide-react";
|
|
import { fetchClient } from "@/lib/api-client";
|
|
import { useToast } from "@/hooks/use-toast";
|
|
|
|
interface BlockedUser {
|
|
_id: string;
|
|
telegramUserId: number;
|
|
reason?: string;
|
|
blockedBy?: {
|
|
_id: string;
|
|
username: string;
|
|
};
|
|
blockedAt: string;
|
|
}
|
|
|
|
export default function AdminBanPage() {
|
|
const { toast } = useToast();
|
|
const [loading, setLoading] = useState(true);
|
|
const [banning, setBanning] = useState(false);
|
|
const [unbanning, setUnbanning] = useState<string | null>(null);
|
|
const [blockedUsers, setBlockedUsers] = useState<BlockedUser[]>([]);
|
|
const [searchQuery, setSearchQuery] = useState("");
|
|
const [banData, setBanData] = useState({
|
|
telegramUserId: "",
|
|
reason: "",
|
|
additionalDetails: "",
|
|
});
|
|
|
|
useEffect(() => {
|
|
fetchBlockedUsers();
|
|
}, []);
|
|
|
|
const fetchBlockedUsers = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const data = await fetchClient<BlockedUser[]>("/admin/blocked-users");
|
|
setBlockedUsers(data);
|
|
} catch (error) {
|
|
console.error("Failed to fetch blocked users:", error);
|
|
toast({
|
|
title: "Error",
|
|
description: "Failed to load blocked users",
|
|
variant: "destructive",
|
|
});
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleBanUser = async () => {
|
|
if (!banData.telegramUserId) {
|
|
toast({
|
|
title: "Error",
|
|
description: "Telegram User ID is required",
|
|
variant: "destructive",
|
|
});
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setBanning(true);
|
|
await fetchClient("/admin/ban", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
telegramUserId: parseInt(banData.telegramUserId),
|
|
reason: banData.additionalDetails || banData.reason || undefined,
|
|
}),
|
|
});
|
|
|
|
toast({
|
|
title: "Success",
|
|
description: "User has been banned",
|
|
});
|
|
|
|
setBanData({ telegramUserId: "", reason: "", additionalDetails: "" });
|
|
fetchBlockedUsers();
|
|
} catch (error: any) {
|
|
console.error("Failed to ban user:", error);
|
|
toast({
|
|
title: "Error",
|
|
description: error.message || "Failed to ban user",
|
|
variant: "destructive",
|
|
});
|
|
} finally {
|
|
setBanning(false);
|
|
}
|
|
};
|
|
|
|
const handleUnbanUser = async (telegramUserId: number) => {
|
|
try {
|
|
setUnbanning(telegramUserId.toString());
|
|
await fetchClient(`/admin/ban/${telegramUserId}`, {
|
|
method: "DELETE",
|
|
});
|
|
|
|
toast({
|
|
title: "Success",
|
|
description: "User has been unbanned",
|
|
});
|
|
|
|
fetchBlockedUsers();
|
|
} catch (error: any) {
|
|
console.error("Failed to unban user:", error);
|
|
toast({
|
|
title: "Error",
|
|
description: error.message || "Failed to unban user",
|
|
variant: "destructive",
|
|
});
|
|
} finally {
|
|
setUnbanning(null);
|
|
}
|
|
};
|
|
|
|
const filteredUsers = blockedUsers.filter((user) => {
|
|
if (!searchQuery) return true;
|
|
const query = searchQuery.toLowerCase();
|
|
return (
|
|
user.telegramUserId.toString().includes(query) ||
|
|
user.reason?.toLowerCase().includes(query) ||
|
|
user.blockedBy?.username?.toLowerCase().includes(query)
|
|
);
|
|
});
|
|
|
|
const activeBans = blockedUsers.length;
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div>
|
|
<h1 className="text-2xl font-semibold tracking-tight">Ban Users</h1>
|
|
<p className="text-sm text-muted-foreground mt-1">Manage user bans and suspensions</p>
|
|
</div>
|
|
|
|
{/* Stats Cards */}
|
|
<div className="grid gap-4 md:grid-cols-3">
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">Active Bans</CardTitle>
|
|
<Shield className="h-4 w-4 text-muted-foreground" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">{activeBans}</div>
|
|
<p className="text-xs text-muted-foreground">Currently banned</p>
|
|
</CardContent>
|
|
</Card>
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">Total Bans</CardTitle>
|
|
<UserX className="h-4 w-4 text-muted-foreground" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">{activeBans}</div>
|
|
<p className="text-xs text-muted-foreground">All time bans</p>
|
|
</CardContent>
|
|
</Card>
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">Recent Bans</CardTitle>
|
|
<Ban className="h-4 w-4 text-muted-foreground" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">
|
|
{blockedUsers.filter(
|
|
(u) => new Date(u.blockedAt) > new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
|
|
).length}
|
|
</div>
|
|
<p className="text-xs text-muted-foreground">Last 7 days</p>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Ban User Form */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center">
|
|
<UserX className="h-5 w-5 mr-2" />
|
|
Ban User
|
|
</CardTitle>
|
|
<CardDescription>
|
|
Enter Telegram User ID and reason for banning
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="grid gap-4 md:grid-cols-3">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="telegramUserId">Telegram User ID</Label>
|
|
<Input
|
|
id="telegramUserId"
|
|
type="number"
|
|
placeholder="Enter Telegram User ID"
|
|
value={banData.telegramUserId}
|
|
onChange={(e) => setBanData({...banData, telegramUserId: e.target.value})}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="reason">Reason</Label>
|
|
<Select value={banData.reason} onValueChange={(value) => setBanData({...banData, reason: value})}>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Select ban reason (optional)" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="spam">Spam</SelectItem>
|
|
<SelectItem value="fraud">Fraud</SelectItem>
|
|
<SelectItem value="harassment">Harassment</SelectItem>
|
|
<SelectItem value="policy_violation">Policy Violation</SelectItem>
|
|
<SelectItem value="suspicious_activity">Suspicious Activity</SelectItem>
|
|
<SelectItem value="other">Other</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="description">Additional Details (Optional)</Label>
|
|
<Input
|
|
id="description"
|
|
placeholder="Additional context..."
|
|
value={banData.additionalDetails}
|
|
onChange={(e) => setBanData({...banData, additionalDetails: e.target.value})}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="mt-4">
|
|
<AlertDialog>
|
|
<AlertDialogTrigger asChild>
|
|
<Button variant="destructive" disabled={banning}>
|
|
{banning ? (
|
|
<>
|
|
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
|
Banning...
|
|
</>
|
|
) : (
|
|
<>
|
|
<Ban className="h-4 w-4 mr-2" />
|
|
Ban User
|
|
</>
|
|
)}
|
|
</Button>
|
|
</AlertDialogTrigger>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>Confirm Ban</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
Are you sure you want to ban Telegram User ID "{banData.telegramUserId}"? This action can be reversed later.
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
<AlertDialogAction onClick={handleBanUser}>
|
|
Confirm Ban
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Banned Users Table */}
|
|
<Card>
|
|
<CardHeader>
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<CardTitle>Banned Users</CardTitle>
|
|
<CardDescription>View and manage current bans</CardDescription>
|
|
</div>
|
|
<div className="relative w-64">
|
|
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
<Input
|
|
placeholder="Search by User ID, reason, or admin..."
|
|
className="pl-8"
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{loading ? (
|
|
<div className="flex items-center justify-center py-8">
|
|
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
|
</div>
|
|
) : (
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Telegram User ID</TableHead>
|
|
<TableHead>Reason</TableHead>
|
|
<TableHead>Banned By</TableHead>
|
|
<TableHead>Date</TableHead>
|
|
<TableHead className="text-right">Actions</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{filteredUsers.length === 0 ? (
|
|
<TableRow>
|
|
<TableCell colSpan={5} className="text-center py-8 text-muted-foreground">
|
|
{searchQuery ? "No users found matching your search" : "No banned users"}
|
|
</TableCell>
|
|
</TableRow>
|
|
) : (
|
|
filteredUsers.map((user) => (
|
|
<TableRow key={user._id}>
|
|
<TableCell>
|
|
<div className="font-mono text-sm">{user.telegramUserId}</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
{user.reason ? (
|
|
<Badge variant="secondary">{user.reason}</Badge>
|
|
) : (
|
|
<span className="text-muted-foreground">No reason provided</span>
|
|
)}
|
|
</TableCell>
|
|
<TableCell>
|
|
{user.blockedBy?.username || "Unknown"}
|
|
</TableCell>
|
|
<TableCell>
|
|
{new Date(user.blockedAt).toLocaleDateString()}
|
|
</TableCell>
|
|
<TableCell className="text-right">
|
|
<AlertDialog>
|
|
<AlertDialogTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
disabled={unbanning === user.telegramUserId.toString()}
|
|
>
|
|
{unbanning === user.telegramUserId.toString() ? (
|
|
<Loader2 className="h-4 w-4 animate-spin" />
|
|
) : (
|
|
<Unlock className="h-4 w-4" />
|
|
)}
|
|
</Button>
|
|
</AlertDialogTrigger>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>Confirm Unban</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
Are you sure you want to unban Telegram User ID "{user.telegramUserId}"? They will be able to use the platform again.
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
<AlertDialogAction onClick={() => handleUnbanUser(user.telegramUserId)}>
|
|
Confirm Unban
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</TableCell>
|
|
</TableRow>
|
|
))
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|