diff --git a/app/api/add-credits/route.ts b/app/api/add-credits/route.ts index 483c8c9..bd206f3 100644 --- a/app/api/add-credits/route.ts +++ b/app/api/add-credits/route.ts @@ -4,9 +4,11 @@ import { getDatabase } from "@/lib/db/mongodb"; export async function POST() { try { const db = await getDatabase(); - const CREDITS_TO_ADD = 5000000; // 5 millions de tokens + const CREDITS_TO_ADD = 3000000; // 3 millions de tokens - console.log(`🚀 DÉBUT: Ajout de ${CREDITS_TO_ADD.toLocaleString()} crédits à tous les utilisateurs`); + console.log( + `🚀 DÉBUT: Ajout de ${CREDITS_TO_ADD.toLocaleString()} crédits à tous les utilisateurs` + ); // Récupérer tous les utilisateurs existants const users = await db.collection("users").find({}).toArray(); @@ -15,14 +17,14 @@ export async function POST() { if (users.length === 0) { return NextResponse.json({ success: false, - message: "Aucun utilisateur trouvé" + message: "Aucun utilisateur trouvé", }); } // Récupérer toutes les balances existantes const existingBalances = await db.collection("balances").find({}).toArray(); const existingBalanceUserIds = new Set( - existingBalances.map(balance => balance.user.toString()) + existingBalances.map((balance) => balance.user.toString()) ); console.log(`💰 Balances existantes: ${existingBalances.length}`); @@ -34,21 +36,23 @@ export async function POST() { // Pour chaque utilisateur for (const user of users) { const userId = user._id; - + if (existingBalanceUserIds.has(userId.toString())) { // Utilisateur a déjà une balance - ajouter les crédits const updateResult = await db.collection("balances").updateOne( { user: userId }, - { + { $inc: { tokenCredits: CREDITS_TO_ADD }, - $set: { lastRefill: new Date() } + $set: { lastRefill: new Date() }, } ); - + if (updateResult.modifiedCount > 0) { updatedCount++; totalCreditsAdded += CREDITS_TO_ADD; - console.log(`✅ Crédits ajoutés pour l'utilisateur: ${user.email || user.name}`); + console.log( + `✅ Crédits ajoutés pour l'utilisateur: ${user.email || user.name}` + ); } } else { // Utilisateur n'a pas de balance - créer une nouvelle balance @@ -60,20 +64,24 @@ export async function POST() { refillAmount: 0, refillIntervalUnit: "month", refillIntervalValue: 1, - __v: 0 + __v: 0, }; await db.collection("balances").insertOne(newBalance); createdCount++; totalCreditsAdded += CREDITS_TO_ADD; - console.log(`🆕 Nouvelle balance créée pour: ${user.email || user.name}`); + console.log( + `🆕 Nouvelle balance créée pour: ${user.email || user.name}` + ); } } console.log(`✅ TERMINÉ:`); console.log(`- Balances mises à jour: ${updatedCount}`); console.log(`- Nouvelles balances créées: ${createdCount}`); - console.log(`- Total crédits ajoutés: ${totalCreditsAdded.toLocaleString()}`); + console.log( + `- Total crédits ajoutés: ${totalCreditsAdded.toLocaleString()}` + ); return NextResponse.json({ success: true, @@ -82,17 +90,21 @@ export async function POST() { updatedBalances: updatedCount, createdBalances: createdCount, creditsPerUser: CREDITS_TO_ADD, - totalCreditsAdded + totalCreditsAdded, }, - message: `${CREDITS_TO_ADD.toLocaleString()} crédits ajoutés à ${users.length} utilisateurs (${updatedCount} mis à jour, ${createdCount} créés)` + message: `${CREDITS_TO_ADD.toLocaleString()} crédits ajoutés à ${ + users.length + } utilisateurs (${updatedCount} mis à jour, ${createdCount} créés)`, }); - } catch (error) { console.error("Erreur lors de l'ajout des crédits:", error); - return NextResponse.json({ - success: false, - error: "Erreur serveur lors de l'ajout des crédits" - }, { status: 500 }); + return NextResponse.json( + { + success: false, + error: "Erreur serveur lors de l'ajout des crédits", + }, + { status: 500 } + ); } } @@ -104,8 +116,12 @@ export async function GET() { const users = await db.collection("users").find({}).toArray(); const balances = await db.collection("balances").find({}).toArray(); - const totalCredits = balances.reduce((sum, balance) => sum + (balance.tokenCredits || 0), 0); - const averageCredits = balances.length > 0 ? totalCredits / balances.length : 0; + const totalCredits = balances.reduce( + (sum, balance) => sum + (balance.tokenCredits || 0), + 0 + ); + const averageCredits = + balances.length > 0 ? totalCredits / balances.length : 0; return NextResponse.json({ statistics: { @@ -113,12 +129,11 @@ export async function GET() { totalBalances: balances.length, totalCredits, averageCredits: Math.round(averageCredits), - usersWithoutBalance: users.length - balances.length - } + usersWithoutBalance: users.length - balances.length, + }, }); - } catch (error) { console.error("Erreur lors de la récupération des statistiques:", error); return NextResponse.json({ error: "Erreur serveur" }, { status: 500 }); } -} \ No newline at end of file +} diff --git a/app/api/create-user/route.ts b/app/api/create-user/route.ts new file mode 100644 index 0000000..f16b475 --- /dev/null +++ b/app/api/create-user/route.ts @@ -0,0 +1,126 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getDatabase } from "@/lib/db/mongodb"; +import bcrypt from "bcryptjs"; +import { ObjectId } from "mongodb"; + +export async function POST(request: NextRequest) { + try { + const { name, email, password, role = "USER" } = await request.json(); + + // Validation des données + if (!name || !email || !password) { + return NextResponse.json( + { error: "Nom, email et mot de passe sont requis" }, + { status: 400 } + ); + } + + // Validation de l'email + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(email)) { + return NextResponse.json( + { error: "Format d'email invalide" }, + { status: 400 } + ); + } + + // Validation du mot de passe (minimum 8 caractères) + if (password.length < 8) { + return NextResponse.json( + { error: "Le mot de passe doit contenir au moins 8 caractères" }, + { status: 400 } + ); + } + + // Validation du rôle + if (!["USER", "ADMIN"].includes(role)) { + return NextResponse.json( + { error: "Rôle invalide. Doit être USER ou ADMIN" }, + { status: 400 } + ); + } + + const db = await getDatabase(); + + // Vérifier si l'utilisateur existe déjà + const existingUser = await db.collection("users").findOne({ email }); + if (existingUser) { + return NextResponse.json( + { error: "Un utilisateur avec cet email existe déjà" }, + { status: 409 } + ); + } + + // Hasher le mot de passe + const saltRounds = 12; + const hashedPassword = await bcrypt.hash(password, saltRounds); + + // Créer le nouvel utilisateur + const newUser = { + name, + username: email.split("@")[0], // Utiliser la partie avant @ comme username + email, + emailVerified: false, + password: hashedPassword, + avatar: null, + provider: "local", + role, + plugins: [], + twoFactorEnabled: false, + termsAccepted: true, + personalization: { + memories: false, + _id: new ObjectId(), + }, + backupCodes: [], + refreshToken: [], + createdAt: new Date(), + updatedAt: new Date(), + __v: 0, + }; + + // Insérer l'utilisateur dans la base de données + const result = await db.collection("users").insertOne(newUser); + + if (!result.insertedId) { + return NextResponse.json( + { error: "Erreur lors de la création de l'utilisateur" }, + { status: 500 } + ); + } + + // Créer une balance initiale pour l'utilisateur + const initialBalance = { + user: result.insertedId, + tokenCredits: 3000000, // 3 millions de tokens par défaut + autoRefillEnabled: false, + lastRefill: new Date(), + refillAmount: 0, + refillIntervalUnit: "month", + refillIntervalValue: 1, + __v: 0, + }; + + await db.collection("balances").insertOne(initialBalance); + + console.log(`✅ Nouvel utilisateur créé: ${email} (${role})`); + + return NextResponse.json({ + success: true, + message: `Utilisateur ${name} créé avec succès`, + user: { + id: result.insertedId, + name, + email, + role, + createdAt: newUser.createdAt, + }, + }); + } catch (error) { + console.error("Erreur lors de la création de l'utilisateur:", error); + return NextResponse.json( + { error: "Erreur serveur lors de la création de l'utilisateur" }, + { status: 500 } + ); + } +} diff --git a/app/api/delete-user/route.ts b/app/api/delete-user/route.ts new file mode 100644 index 0000000..c0a0212 --- /dev/null +++ b/app/api/delete-user/route.ts @@ -0,0 +1,160 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getDatabase } from "@/lib/db/mongodb"; +import { ObjectId } from "mongodb"; + +interface QueryFilter { + _id?: ObjectId; + email?: string; +} + +export async function DELETE(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const userId = searchParams.get("id"); + const email = searchParams.get("email"); + + if (!userId && !email) { + return NextResponse.json( + { error: "ID utilisateur ou email requis" }, + { status: 400 } + ); + } + + const db = await getDatabase(); + const usersCollection = db.collection("users"); + const balancesCollection = db.collection("balances"); + + // Construire la requête de recherche + const query: QueryFilter = {}; + if (userId) { + // Vérifier si l'ID est un ObjectId valide + if (!ObjectId.isValid(userId)) { + return NextResponse.json( + { error: "ID utilisateur invalide" }, + { status: 400 } + ); + } + query._id = new ObjectId(userId); + } else if (email) { + query.email = email.toLowerCase(); + } + + // Vérifier si l'utilisateur existe + const existingUser = await usersCollection.findOne(query); + if (!existingUser) { + return NextResponse.json( + { error: "Utilisateur non trouvé" }, + { status: 404 } + ); + } + + // Supprimer l'utilisateur + const deleteUserResult = await usersCollection.deleteOne(query); + + // Supprimer le solde associé + const deleteBalanceResult = await balancesCollection.deleteOne({ + user: existingUser._id + }); + + if (deleteUserResult.deletedCount === 0) { + return NextResponse.json( + { error: "Erreur lors de la suppression de l'utilisateur" }, + { status: 500 } + ); + } + + return NextResponse.json({ + success: true, + message: "Utilisateur supprimé avec succès", + deletedUser: { + id: existingUser._id.toString(), + name: existingUser.name, + email: existingUser.email, + role: existingUser.role + }, + balanceDeleted: deleteBalanceResult.deletedCount > 0 + }); + + } catch (error) { + console.error("Erreur lors de la suppression de l'utilisateur:", error); + return NextResponse.json( + { error: "Erreur interne du serveur" }, + { status: 500 } + ); + } +} + +export async function POST(request: NextRequest) { + try { + const { userId, email } = await request.json(); + + if (!userId && !email) { + return NextResponse.json( + { error: "ID utilisateur ou email requis" }, + { status: 400 } + ); + } + + const db = await getDatabase(); + const usersCollection = db.collection("users"); + const balancesCollection = db.collection("balances"); + + // Construire la requête de recherche + const query: QueryFilter = {}; + if (userId) { + // Vérifier si l'ID est un ObjectId valide + if (!ObjectId.isValid(userId)) { + return NextResponse.json( + { error: "ID utilisateur invalide" }, + { status: 400 } + ); + } + query._id = new ObjectId(userId); + } else if (email) { + query.email = email.toLowerCase(); + } + + // Vérifier si l'utilisateur existe + const existingUser = await usersCollection.findOne(query); + if (!existingUser) { + return NextResponse.json( + { error: "Utilisateur non trouvé" }, + { status: 404 } + ); + } + + // Supprimer l'utilisateur + const deleteUserResult = await usersCollection.deleteOne(query); + + // Supprimer le solde associé + const deleteBalanceResult = await balancesCollection.deleteOne({ + user: existingUser._id + }); + + if (deleteUserResult.deletedCount === 0) { + return NextResponse.json( + { error: "Erreur lors de la suppression de l'utilisateur" }, + { status: 500 } + ); + } + + return NextResponse.json({ + success: true, + message: "Utilisateur supprimé avec succès", + deletedUser: { + id: existingUser._id.toString(), + name: existingUser.name, + email: existingUser.email, + role: existingUser.role + }, + balanceDeleted: deleteBalanceResult.deletedCount > 0 + }); + + } catch (error) { + console.error("Erreur lors de la suppression de l'utilisateur:", error); + return NextResponse.json( + { error: "Erreur interne du serveur" }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/globals.css b/app/globals.css index dc98be7..4255366 100644 --- a/app/globals.css +++ b/app/globals.css @@ -51,51 +51,53 @@ --card-foreground: oklch(0.145 0 0); --popover: oklch(1 0 0); --popover-foreground: oklch(0.145 0 0); - --primary: oklch(0.205 0 0); + --primary: oklch(0.145 0 0); --primary-foreground: oklch(0.985 0 0); - --secondary: oklch(0.97 0 0); - --secondary-foreground: oklch(0.205 0 0); - --muted: oklch(0.97 0 0); + --secondary: oklch(0.969 0 0); + --secondary-foreground: oklch(0.145 0 0); + --muted: oklch(0.969 0 0); --muted-foreground: oklch(0.556 0 0); - --accent: oklch(0.97 0 0); - --accent-foreground: oklch(0.205 0 0); - --destructive: oklch(0.577 0.245 27.325); - --border: oklch(0.922 0 0); - --input: oklch(0.922 0 0); - --ring: oklch(0.708 0 0); - --chart-1: oklch(0.646 0.222 41.116); - --chart-2: oklch(0.6 0.118 184.704); - --chart-3: oklch(0.398 0.07 227.392); - --chart-4: oklch(0.828 0.189 84.429); - --chart-5: oklch(0.769 0.188 70.08); - --sidebar: oklch(0.985 0 0); - --sidebar-foreground: oklch(0.145 0 0); - --sidebar-primary: oklch(0.205 0 0); + --accent: oklch(0.969 0 0); + --accent-foreground: oklch(0.145 0 0); + --destructive: oklch(0.627 0.265 303.9); + --destructive-foreground: oklch(0.985 0 0); + --border: oklch(0.898 0 0); + --input: oklch(0.898 0 0); + --ring: oklch(0.145 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.97 0 0); - --sidebar-accent-foreground: oklch(0.205 0 0); - --sidebar-border: oklch(0.922 0 0); - --sidebar-ring: oklch(0.708 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); } .dark { --background: oklch(0.145 0 0); --foreground: oklch(0.985 0 0); - --card: oklch(0.205 0 0); + --card: oklch(0.145 0 0); --card-foreground: oklch(0.985 0 0); - --popover: oklch(0.205 0 0); + --popover: oklch(0.145 0 0); --popover-foreground: oklch(0.985 0 0); - --primary: oklch(0.922 0 0); - --primary-foreground: oklch(0.205 0 0); - --secondary: oklch(0.269 0 0); + --primary: oklch(0.985 0 0); + --primary-foreground: oklch(0.145 0 0); + --secondary: oklch(0.205 0 0); --secondary-foreground: oklch(0.985 0 0); - --muted: oklch(0.269 0 0); - --muted-foreground: oklch(0.708 0 0); - --accent: oklch(0.269 0 0); + --muted: oklch(0.205 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.205 0 0); --accent-foreground: oklch(0.985 0 0); - --destructive: oklch(0.704 0.191 22.216); - --border: oklch(1 0 0 / 10%); - --input: oklch(1 0 0 / 15%); + --destructive: oklch(0.627 0.265 303.9); + --destructive-foreground: oklch(0.985 0 0); + --border: oklch(0.205 0 0); + --input: oklch(0.205 0 0); --ring: oklch(0.556 0 0); --chart-1: oklch(0.488 0.243 264.376); --chart-2: oklch(0.696 0.17 162.48); @@ -120,3 +122,31 @@ @apply bg-background text-foreground; } } + +@layer utilities { + @keyframes accordion-down { + from { + height: 0; + } + to { + height: var(--radix-accordion-content-height); + } + } + + @keyframes accordion-up { + from { + height: var(--radix-accordion-content-height); + } + to { + height: 0; + } + } + + .animate-accordion-down { + animation: accordion-down 0.2s ease-out; + } + + .animate-accordion-up { + animation: accordion-up 0.2s ease-out; + } +} diff --git a/app/settings/page.tsx b/app/settings/page.tsx index 9d14e7c..993d9aa 100644 --- a/app/settings/page.tsx +++ b/app/settings/page.tsx @@ -3,6 +3,7 @@ import { Badge } from "@/components/ui/badge"; import { Database, Server, Settings } from "lucide-react"; import AddCredits from "@/components/dashboard/add-credits"; +import UserManagement from "@/components/dashboard/user-management"; export default function SettingsPage() { return ( @@ -48,13 +49,13 @@ export default function SettingsPage() { Version Next.js - 14.x + 15.0.3
Version Node.js - 18.x + {process.version}
@@ -67,16 +68,21 @@ export default function SettingsPage() {
+ {/* Gestion des utilisateurs */} + + {/* Gestion des crédits */} -
-

- - Gestion des Crédits -

-
+ + + + + Gestion des Crédits + + + -
-
+ + ); } diff --git a/components/dashboard/add-credits.tsx b/components/dashboard/add-credits.tsx index dc88809..1aeaf48 100644 --- a/components/dashboard/add-credits.tsx +++ b/components/dashboard/add-credits.tsx @@ -2,7 +2,13 @@ import { useState } from "react"; import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Plus, DollarSign, Users, TrendingUp } from "lucide-react"; @@ -33,7 +39,7 @@ export default function AddCredits() { try { const response = await fetch("/api/add-credits"); const data = await response.json(); - + if (data.statistics) { setStats(data.statistics); } @@ -45,18 +51,22 @@ export default function AddCredits() { }; const addCreditsToAllUsers = async () => { - if (!confirm("Êtes-vous sûr de vouloir ajouter 5 millions de crédits à TOUS les utilisateurs ? Cette action est irréversible.")) { + if ( + !confirm( + "Êtes-vous sûr de vouloir ajouter 5 millions de crédits à TOUS les utilisateurs ? Cette action est irréversible." + ) + ) { return; } setLoading(true); try { const response = await fetch("/api/add-credits", { - method: "POST" + method: "POST", }); - + const data = await response.json(); - + if (data.success) { setResult(data.statistics); // Rafraîchir les stats @@ -86,7 +96,7 @@ export default function AddCredits() { {/* Bouton d'analyse */}
-
-

{stats.totalUsers}

+

+ {stats.totalUsers} +

- +
@@ -115,7 +127,7 @@ export default function AddCredits() { {stats.totalCredits.toLocaleString()}

- +
@@ -125,13 +137,15 @@ export default function AddCredits() { {stats.averageCredits.toLocaleString()}

- +
Sans Balance
-

{stats.usersWithoutBalance}

+

+ {stats.usersWithoutBalance} +

)} @@ -140,20 +154,26 @@ export default function AddCredits() { {stats && (
-

⚠️ Action Importante

+

+ ⚠️ Action Importante +

- Cette action va ajouter 5,000,000 crédits à chacun des {stats.totalUsers} utilisateurs. + Cette action va ajouter 5,000,000 crédits à + chacun des {stats.totalUsers} utilisateurs.
- Total de crédits qui seront ajoutés: {(stats.totalUsers * 5000000).toLocaleString()} + Total de crédits qui seront ajoutés:{" "} + {(stats.totalUsers * 3000000).toLocaleString()}

- -
)} @@ -161,23 +181,33 @@ export default function AddCredits() { {/* Résultats */} {result && (
-

✅ Crédits ajoutés avec succès !

+

+ ✅ Crédits ajoutés avec succès ! +

Balances mises Ă  jour: - {result.updatedBalances} + + {result.updatedBalances} +
Nouvelles balances: - {result.createdBalances} + + {result.createdBalances} +
Crédits par utilisateur: - {result.creditsPerUser.toLocaleString()} + + {result.creditsPerUser.toLocaleString()} +
Total ajouté: - {result.totalCreditsAdded.toLocaleString()} + + {result.totalCreditsAdded.toLocaleString()} +
@@ -185,4 +215,4 @@ export default function AddCredits() { ); -} \ No newline at end of file +} diff --git a/components/dashboard/create-user.tsx b/components/dashboard/create-user.tsx new file mode 100644 index 0000000..8800492 --- /dev/null +++ b/components/dashboard/create-user.tsx @@ -0,0 +1,274 @@ +"use client"; + +import { useState } from "react"; +import { Card, CardContent, 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 { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { UserPlus, Loader2, CheckCircle, AlertCircle } from "lucide-react"; + +interface CreateUserResult { + success: boolean; + message: string; + user?: { + id: string; + name: string; + email: string; + role: string; + createdAt: string; + }; + error?: string; +} + +export default function CreateUser() { + const [formData, setFormData] = useState({ + name: "", + email: "", + password: "", + confirmPassword: "", + role: "USER", + }); + const [isLoading, setIsLoading] = useState(false); + const [result, setResult] = useState(null); + + const handleInputChange = (field: string, value: string) => { + setFormData((prev) => ({ ...prev, [field]: value })); + // Réinitialiser le résultat quand l'utilisateur modifie le formulaire + if (result) { + setResult(null); + } + }; + + const validateForm = () => { + if (!formData.name.trim()) { + return "Le nom est requis"; + } + if (!formData.email.trim()) { + return "L'email est requis"; + } + if (!formData.password) { + return "Le mot de passe est requis"; + } + if (formData.password.length < 8) { + return "Le mot de passe doit contenir au moins 8 caractères"; + } + if (formData.password !== formData.confirmPassword) { + return "Les mots de passe ne correspondent pas"; + } + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(formData.email)) { + return "Format d'email invalide"; + } + return null; + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + const validationError = validateForm(); + if (validationError) { + setResult({ + success: false, + message: validationError, + }); + return; + } + + setIsLoading(true); + setResult(null); + + try { + const response = await fetch("/api/create-user", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name: formData.name.trim(), + email: formData.email.trim().toLowerCase(), + password: formData.password, + role: formData.role, + }), + }); + + const data = await response.json(); + + if (response.ok) { + setResult({ + success: true, + message: data.message, + user: data.user, + }); + // Réinitialiser le formulaire en cas de succès + setFormData({ + name: "", + email: "", + password: "", + confirmPassword: "", + role: "USER", + }); + } else { + setResult({ + success: false, + message: data.error || "Erreur lors de la création de l'utilisateur", + }); + } + } catch (error) { + console.error("Erreur lors de la création de l'utilisateur:", error); + setResult({ + success: false, + message: "Erreur de connexion au serveur", + }); + } finally { + setIsLoading(false); + } + }; + + return ( + + + + + Créer un nouvel utilisateur + + + +
+
+
+ + handleInputChange("name", e.target.value)} + disabled={isLoading} + required + /> +
+ +
+ + handleInputChange("email", e.target.value)} + disabled={isLoading} + required + /> +
+
+ +
+
+ + handleInputChange("password", e.target.value)} + disabled={isLoading} + required + /> +
+ +
+ + + handleInputChange("confirmPassword", e.target.value) + } + disabled={isLoading} + required + /> +
+
+ +
+ + +
+ + {result && ( + + {result.success ? ( + + ) : ( + + )} + + {result.message} + {result.success && result.user && ( +
+ Détails: +
+ • ID: {result.user.id} +
+ • Email: {result.user.email} +
+ • Rôle: {result.user.role} +
• Crédits initiaux: 5,000,000 tokens +
+ )} +
+
+ )} + +
+ +
+
+ +
+

Informations importantes :

+
    +
  • • L'utilisateur recevra automatiquement 5,000,000 tokens
  • +
  • • Le mot de passe sera hashĂ© de manière sĂ©curisĂ©e
  • +
  • • L'email doit ĂŞtre unique dans le système
  • +
  • • L'utilisateur pourra se connecter immĂ©diatement
  • +
+
+
+
+ ); +} \ No newline at end of file diff --git a/components/dashboard/dashboard-users-list.tsx b/components/dashboard/dashboard-users-list.tsx index fc6a376..e4892cb 100644 --- a/components/dashboard/dashboard-users-list.tsx +++ b/components/dashboard/dashboard-users-list.tsx @@ -175,7 +175,7 @@ export function DashboardUsersList() { const credits = latestBalance ? latestBalance.tokenCredits || 0 : 0; // Calculer les tokens consommés depuis les crédits - const INITIAL_CREDITS = 5000000; + const INITIAL_CREDITS = 3000000; const creditsUsed = INITIAL_CREDITS - credits; const tokensFromCredits = creditsUsed > 0 ? creditsUsed : 0; diff --git a/components/dashboard/delete-user.tsx b/components/dashboard/delete-user.tsx new file mode 100644 index 0000000..0f43b90 --- /dev/null +++ b/components/dashboard/delete-user.tsx @@ -0,0 +1,321 @@ +"use client"; + +import { useState } from "react"; +import { Card, CardContent, 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 { Alert, AlertDescription } from "@/components/ui/alert"; +import { UserMinus, Loader2, CheckCircle, AlertCircle, Trash2 } from "lucide-react"; + +interface DeleteUserResult { + success: boolean; + message: string; + deletedUser?: { + id: string; + name: string; + email: string; + role: string; + }; + balanceDeleted?: boolean; + error?: string; +} + +interface FoundUser { + _id: string; + name: string; + email: string; + role: string; + createdAt: string; +} + +export default function DeleteUser() { + const [searchData, setSearchData] = useState({ + email: "", + userId: "", + }); + const [isLoading, setIsLoading] = useState(false); + const [result, setResult] = useState(null); + const [confirmDelete, setConfirmDelete] = useState(false); + const [foundUser, setFoundUser] = useState(null); + + const handleInputChange = (field: string, value: string) => { + setSearchData((prev) => ({ ...prev, [field]: value })); + // Réinitialiser les résultats quand l'utilisateur modifie le formulaire + if (result) { + setResult(null); + } + if (confirmDelete) { + setConfirmDelete(false); + } + if (foundUser) { + setFoundUser(null); + } + }; + + const validateForm = () => { + if (!searchData.email.trim() && !searchData.userId.trim()) { + return "Email ou ID utilisateur requis"; + } + if (searchData.email && searchData.userId) { + return "Veuillez utiliser soit l'email soit l'ID, pas les deux"; + } + if (searchData.email) { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(searchData.email)) { + return "Format d'email invalide"; + } + } + return null; + }; + + const handleSearch = async (e: React.FormEvent) => { + e.preventDefault(); + + const validationError = validateForm(); + if (validationError) { + setResult({ + success: false, + message: validationError, + }); + return; + } + + setIsLoading(true); + setResult(null); + setFoundUser(null); + + try { + // D'abord, chercher l'utilisateur pour confirmation + const searchParams = new URLSearchParams(); + if (searchData.email) { + searchParams.append("email", searchData.email.trim().toLowerCase()); + } + if (searchData.userId) { + searchParams.append("id", searchData.userId.trim()); + } + + const response = await fetch(`/api/collections/users?${searchParams.toString()}`); + const data = await response.json(); + + if (response.ok && data.data && data.data.length > 0) { + setFoundUser(data.data[0]); + setResult({ + success: true, + message: "Utilisateur trouvé. Confirmez la suppression ci-dessous.", + }); + } else { + setResult({ + success: false, + message: "Utilisateur non trouvé", + }); + } + } catch (error) { + console.error("Erreur lors de la recherche:", error); + setResult({ + success: false, + message: "Erreur de connexion au serveur", + }); + } finally { + setIsLoading(false); + } + }; + + const handleDelete = async () => { + if (!foundUser) return; + + setIsLoading(true); + setResult(null); + + try { + const response = await fetch("/api/delete-user", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + userId: foundUser._id, + }), + }); + + const data = await response.json(); + + if (response.ok) { + setResult({ + success: true, + message: data.message, + deletedUser: data.deletedUser, + balanceDeleted: data.balanceDeleted, + }); + // Réinitialiser le formulaire + setSearchData({ + email: "", + userId: "", + }); + setFoundUser(null); + setConfirmDelete(false); + } else { + setResult({ + success: false, + message: data.error || "Erreur lors de la suppression de l'utilisateur", + }); + } + } catch (error) { + console.error("Erreur lors de la suppression:", error); + setResult({ + success: false, + message: "Erreur de connexion au serveur", + }); + } finally { + setIsLoading(false); + } + }; + + return ( + + + + + Supprimer un utilisateur + + + +
+
+
+ + handleInputChange("email", e.target.value)} + disabled={isLoading || !!searchData.userId} + /> +
+ +
+ + handleInputChange("userId", e.target.value)} + disabled={isLoading || !!searchData.email} + /> +
+
+ + {!foundUser && ( +
+ +
+ )} +
+ + {foundUser && ( +
+

Utilisateur trouvé :

+
+

ID: {foundUser._id}

+

Nom: {foundUser.name}

+

Email: {foundUser.email}

+

RĂ´le: {foundUser.role}

+

Créé le: {new Date(foundUser.createdAt).toLocaleDateString()}

+
+ +
+ {!confirmDelete ? ( + + ) : ( +
+ + +
+ )} +
+
+ )} + + {result && ( + + {result.success ? ( + + ) : ( + + )} + + {result.message} + {result.success && result.deletedUser && ( +
+ Utilisateur supprimé: +
+ • Nom: {result.deletedUser.name} +
+ • Email: {result.deletedUser.email} +
+ • Rôle: {result.deletedUser.role} +
+ • Solde supprimé: {result.balanceDeleted ? "Oui" : "Non"} +
+ )} +
+
+ )} + +
+

⚠️ Attention :

+
    +
  • • Cette action est irrĂ©versible
  • +
  • • L'utilisateur et son solde seront dĂ©finitivement supprimĂ©s
  • +
  • • Toutes les donnĂ©es associĂ©es seront perdues
  • +
  • • Utilisez cette fonction avec prĂ©caution
  • +
+
+
+
+ ); +} \ No newline at end of file diff --git a/components/dashboard/user-management.tsx b/components/dashboard/user-management.tsx new file mode 100644 index 0000000..2f064e1 --- /dev/null +++ b/components/dashboard/user-management.tsx @@ -0,0 +1,56 @@ +"use client"; + +import { UserPlus, UserMinus, Users } from "lucide-react"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import CreateUser from "./create-user"; +import DeleteUser from "./delete-user"; + +export default function UserManagement() { + return ( + + + + + Gestion des Utilisateurs + + + + + + +
+ + Créer un nouvel utilisateur +
+
+ +
+ +
+
+
+ + + +
+ + Supprimer un utilisateur +
+
+ +
+ +
+
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/components/ui/accordion.tsx b/components/ui/accordion.tsx new file mode 100644 index 0000000..266a031 --- /dev/null +++ b/components/ui/accordion.tsx @@ -0,0 +1,58 @@ +"use client"; + +import * as React from "react"; +import * as AccordionPrimitive from "@radix-ui/react-accordion"; +import { ChevronDown } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +const Accordion = AccordionPrimitive.Root; + +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AccordionItem.displayName = "AccordionItem"; + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + +)); +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +
{children}
+
+)); + +AccordionContent.displayName = AccordionPrimitive.Content.displayName; + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; diff --git a/components/ui/label.tsx b/components/ui/label.tsx new file mode 100644 index 0000000..860ad8f --- /dev/null +++ b/components/ui/label.tsx @@ -0,0 +1,26 @@ +"use client" + +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const labelVariants = cva( + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" +) + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, ...props }, ref) => ( + +)) +Label.displayName = LabelPrimitive.Root.displayName + +export { Label } \ No newline at end of file diff --git a/components/ui/select.tsx b/components/ui/select.tsx new file mode 100644 index 0000000..482a1e1 --- /dev/null +++ b/components/ui/select.tsx @@ -0,0 +1,160 @@ +"use client"; + +import * as React from "react"; +import * as SelectPrimitive from "@radix-ui/react-select"; +import { Check, ChevronDown, ChevronUp } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +const Select = SelectPrimitive.Root; + +const SelectGroup = SelectPrimitive.Group; + +const SelectValue = SelectPrimitive.Value; + +const SelectTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + span]:line-clamp-1", + className + )} + {...props} + > + {children} + + + + +)); +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; + +const SelectScrollUpButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName; + +const SelectScrollDownButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +SelectScrollDownButton.displayName = + SelectPrimitive.ScrollDownButton.displayName; + +const SelectContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, position = "popper", ...props }, ref) => ( + + + + + {children} + + + + +)); +SelectContent.displayName = SelectPrimitive.Content.displayName; + +const SelectLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SelectLabel.displayName = SelectPrimitive.Label.displayName; + +const SelectItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + + {children} + +)); +SelectItem.displayName = SelectPrimitive.Item.displayName; + +const SelectSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SelectSeparator.displayName = SelectPrimitive.Separator.displayName; + +export { + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectLabel, + SelectItem, + SelectSeparator, + SelectScrollUpButton, + SelectScrollDownButton, +}; diff --git a/package-lock.json b/package-lock.json index fab096f..76909b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,16 +8,21 @@ "name": "admin-dashboard", "version": "0.1.0", "dependencies": { + "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-navigation-menu": "^1.2.14", "@radix-ui/react-progress": "^1.1.7", + "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", "@supabase/ssr": "^0.7.0", "@supabase/supabase-js": "^2.58.0", + "@types/bcryptjs": "^2.4.6", "@types/mongodb": "^4.0.6", + "bcryptjs": "^3.0.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.544.0", @@ -1026,12 +1031,49 @@ "node": ">=12.4.0" } }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, "node_modules/@radix-ui/primitive": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", "license": "MIT" }, + "node_modules/@radix-ui/react-accordion": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", + "integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-arrow": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", @@ -1055,6 +1097,36 @@ } } }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", + "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-collection": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", @@ -1247,6 +1319,29 @@ } } }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", + "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-navigation-menu": { "version": "1.2.14", "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz", @@ -1441,6 +1536,49 @@ } } }, + "node_modules/@radix-ui/react-select": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", + "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-separator": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", @@ -2168,6 +2306,12 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/bcryptjs": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", + "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", + "license": "MIT" + }, "node_modules/@types/d3-array": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", @@ -3192,6 +3336,15 @@ "dev": true, "license": "MIT" }, + "node_modules/bcryptjs": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz", + "integrity": "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", diff --git a/package.json b/package.json index 96193f2..9ed05e7 100644 --- a/package.json +++ b/package.json @@ -9,16 +9,21 @@ "lint": "eslint" }, "dependencies": { + "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-navigation-menu": "^1.2.14", "@radix-ui/react-progress": "^1.1.7", + "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", "@supabase/ssr": "^0.7.0", "@supabase/supabase-js": "^2.58.0", + "@types/bcryptjs": "^2.4.6", "@types/mongodb": "^4.0.6", + "bcryptjs": "^3.0.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.544.0",