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 */}
-
Utilisateurs
- {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
+
+
+
+
+
+
+
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
+
+
+
+
+
+ {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",