user reaearch and fix function delete

This commit is contained in:
nBiqoz
2025-10-20 17:17:03 +02:00
parent e0232b1fcb
commit 0efe96f4e2
2 changed files with 189 additions and 90 deletions

View File

@@ -1,12 +1,35 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from "next/server";
import { getDatabase } from '@/lib/db/mongodb'; import { getDatabase } from "@/lib/db/mongodb";
import { ObjectId } from "mongodb";
const ALLOWED_COLLECTIONS = [ const ALLOWED_COLLECTIONS = [
'accessroles', 'aclentries', 'actions', 'agentcategories', 'agents', "accessroles",
'assistants', 'balances', 'banners', 'conversations', 'conversationtags', "aclentries",
'files', 'groups', 'keys', 'memoryentries', 'messages', 'pluginauths', "actions",
'presets', 'projects', 'promptgroups', 'prompts', 'roles', 'sessions', "agentcategories",
'sharedlinks', 'tokens', 'toolcalls', 'transactions', 'users' "agents",
"assistants",
"balances",
"banners",
"conversations",
"conversationtags",
"files",
"groups",
"keys",
"memoryentries",
"messages",
"pluginauths",
"presets",
"projects",
"promptgroups",
"prompts",
"roles",
"sessions",
"sharedlinks",
"tokens",
"toolcalls",
"transactions",
"users",
]; ];
export async function GET( export async function GET(
@@ -14,32 +37,53 @@ export async function GET(
{ params }: { params: Promise<{ collection: string }> } { params }: { params: Promise<{ collection: string }> }
) { ) {
const { collection } = await params; const { collection } = await params;
try { try {
if (!ALLOWED_COLLECTIONS.includes(collection)) { if (!ALLOWED_COLLECTIONS.includes(collection)) {
return NextResponse.json( return NextResponse.json(
{ error: 'Collection non autorisée' }, { error: "Collection non autorisée" },
{ status: 400 } { status: 400 }
); );
} }
const { searchParams } = new URL(request.url); const { searchParams } = new URL(request.url);
const page = parseInt(searchParams.get('page') || '1'); const page = parseInt(searchParams.get("page") || "1");
const limit = parseInt(searchParams.get('limit') || '20'); const limit = parseInt(searchParams.get("limit") || "20");
const filter = JSON.parse(searchParams.get('filter') || '{}'); const filter = JSON.parse(searchParams.get("filter") || "{}");
// Gestion spéciale pour la collection users avec recherche par email ou id
if (collection === "users") {
const email = searchParams.get("email");
const id = searchParams.get("id");
if (email) {
filter.email = email.toLowerCase();
} else if (id) {
// Vérifier si l'ID est un ObjectId valide
if (ObjectId.isValid(id)) {
filter._id = new ObjectId(id);
} else {
// Si l'ID n'est pas valide, retourner une erreur
return NextResponse.json(
{ error: "ID utilisateur invalide" },
{ status: 400 }
);
}
}
}
const db = await getDatabase(); const db = await getDatabase();
const skip = (page - 1) * limit; const skip = (page - 1) * limit;
const [data, total] = await Promise.all([ const [data, total] = await Promise.all([
db.collection(collection) db
.collection(collection)
.find(filter) .find(filter)
.skip(skip) .skip(skip)
.limit(limit) .limit(limit)
.sort({ createdAt: -1 }) .sort({ createdAt: -1 })
.toArray(), .toArray(),
db.collection(collection).countDocuments(filter) db.collection(collection).countDocuments(filter),
]); ]);
return NextResponse.json({ return NextResponse.json({
@@ -47,13 +91,10 @@ export async function GET(
total, total,
page, page,
limit, limit,
totalPages: Math.ceil(total / limit) totalPages: Math.ceil(total / limit),
}); });
} catch (error) { } catch (error) {
console.error(`Erreur lors de la récupération de ${collection}:`, error); console.error(`Erreur lors de la récupération de ${collection}:`, error);
return NextResponse.json( return NextResponse.json({ error: "Erreur serveur" }, { status: 500 });
{ error: 'Erreur serveur' },
{ status: 500 }
);
} }
} }

View File

@@ -13,13 +13,14 @@ import {
} from "@/components/ui/table"; } from "@/components/ui/table";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { ChevronLeft, ChevronRight } from "lucide-react"; import { Input } from "@/components/ui/input";
import { ChevronLeft, ChevronRight, Search } from "lucide-react";
import { formatDate } from "@/lib/utils"; import { formatDate } from "@/lib/utils";
import { LibreChatUser, LibreChatBalance } from "@/lib/types"; import { LibreChatUser, LibreChatBalance } from "@/lib/types";
export function UsersTable() { export function UsersTable() {
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const [searchTerm, setSearchTerm] = useState("");
const limit = 20; const limit = 20;
// Charger les utilisateurs // Charger les utilisateurs
@@ -46,6 +47,20 @@ export function UsersTable() {
return map; return map;
}, [balances]); }, [balances]);
// Filtrer les utilisateurs selon le terme de recherche
const filteredUsers = useMemo(() => {
if (!searchTerm.trim()) {
return users;
}
const searchLower = searchTerm.toLowerCase();
return users.filter(
(user) =>
user.name?.toLowerCase().includes(searchLower) ||
user.email?.toLowerCase().includes(searchLower)
);
}, [users, searchTerm]);
const totalPages = Math.ceil(total / limit); const totalPages = Math.ceil(total / limit);
const handlePrevPage = () => { const handlePrevPage = () => {
@@ -76,7 +91,20 @@ export function UsersTable() {
return ( return (
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Liste des utilisateurs ({total})</CardTitle> <div className="flex flex-col space-y-4 sm:flex-row sm:items-center sm:justify-between sm:space-y-0">
<CardTitle>
Liste des utilisateurs ({searchTerm ? filteredUsers.length : total})
</CardTitle>
<div className="relative w-full sm:w-80">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
placeholder="Rechercher par nom ou email..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
</div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="rounded-md border"> <div className="rounded-md border">
@@ -93,78 +121,108 @@ export function UsersTable() {
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{users.map((user) => { {filteredUsers.length > 0 ? (
const userCredits = creditsMap.get(user._id) || 0; filteredUsers.map((user) => {
const isActive = new Date(user.updatedAt || user.createdAt) > const userCredits = creditsMap.get(user._id) || 0;
new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); // 30 jours en millisecondes const isActive =
new Date(user.updatedAt || user.createdAt) >
return ( new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); // 30 jours en millisecondes
<TableRow key={user._id}>
<TableCell> return (
<span className="font-mono text-xs"> <TableRow key={user._id}>
{user._id.slice(-8)} <TableCell>
</span> <span className="font-mono text-xs">
</TableCell> {user._id.slice(-8)}
<TableCell> </span>
<span className="font-medium">{user.name}</span> </TableCell>
</TableCell> <TableCell>
<TableCell> <span className="font-medium">{user.name}</span>
<span className="text-sm">{user.email}</span> </TableCell>
</TableCell> <TableCell>
<TableCell> <span className="text-sm">{user.email}</span>
<Badge variant={user.role === 'ADMIN' ? 'default' : 'secondary'}> </TableCell>
{user.role} <TableCell>
</Badge> <Badge
</TableCell> variant={
<TableCell> user.role === "ADMIN" ? "default" : "secondary"
<span className="font-semibold"> }
{userCredits.toLocaleString()} crédits >
</span> {user.role}
</TableCell> </Badge>
<TableCell> </TableCell>
<Badge variant={isActive ? 'default' : 'secondary'}> <TableCell>
{isActive ? 'Actif' : 'Inactif'} <span className="font-semibold">
</Badge> {userCredits.toLocaleString()} crédits
</TableCell> </span>
<TableCell> </TableCell>
<span className="text-sm text-muted-foreground"> <TableCell>
{formatDate(user.createdAt)} <span className="text-sm text-muted-foreground">
</span> <Badge variant={isActive ? "default" : "secondary"}>
</TableCell> {isActive ? "Actif" : "Inactif"}
</TableRow> </Badge>
); </span>
})} </TableCell>
<TableCell>
<span className="text-sm text-muted-foreground">
{formatDate(user.createdAt)}
</span>
</TableCell>
</TableRow>
);
})
) : (
<TableRow>
<TableCell
colSpan={7}
className="text-center py-8 text-muted-foreground"
>
{searchTerm
? `Aucun utilisateur trouvé pour "${searchTerm}"`
: "Aucun utilisateur trouvé"}
</TableCell>
</TableRow>
)}
</TableBody> </TableBody>
</Table> </Table>
</div> </div>
{/* Pagination */} {/* Pagination - masquée lors de la recherche */}
<div className="flex items-center justify-between space-x-2 py-4"> {!searchTerm && (
<div className="text-sm text-muted-foreground"> <div className="flex items-center justify-between space-x-2 py-4">
Page {page} sur {totalPages} ({total} éléments au total) <div className="text-sm text-muted-foreground">
Page {page} sur {totalPages} ({total} éléments au total)
</div>
<div className="flex space-x-2">
<Button
variant="outline"
size="sm"
onClick={handlePrevPage}
disabled={page <= 1}
>
<ChevronLeft className="h-4 w-4" />
Précédent
</Button>
<Button
variant="outline"
size="sm"
onClick={handleNextPage}
disabled={page >= totalPages}
>
Suivant
<ChevronRight className="h-4 w-4" />
</Button>
</div>
</div> </div>
<div className="flex space-x-2"> )}
<Button
variant="outline" {/* Info de recherche */}
size="sm" {searchTerm && (
onClick={handlePrevPage} <div className="py-4 text-sm text-muted-foreground">
disabled={page <= 1} {filteredUsers.length} résultat(s) trouvé(s) pour &quot;{searchTerm}
> &quot;
<ChevronLeft className="h-4 w-4" />
Précédent
</Button>
<Button
variant="outline"
size="sm"
onClick={handleNextPage}
disabled={page >= totalPages}
>
Suivant
<ChevronRight className="h-4 w-4" />
</Button>
</div> </div>
</div> )}
</CardContent> </CardContent>
</Card> </Card>
); );
} }