From 0f2adca44a0a385f22e1d6bcb88a66bafc7d9028 Mon Sep 17 00:00:00 2001 From: nBiqoz Date: Mon, 6 Oct 2025 19:16:20 +0200 Subject: [PATCH] new --- app/analytics/page.tsx | 3 +- app/api/add-credits/route.ts | 124 +++++++ app/api/cleanup/balances/route.ts | 104 ++++++ app/api/metrics/route.ts | 37 +- app/api/stats/route.ts | 72 +++- app/api/user-activity/route.ts | 8 +- app/login/page.tsx | 194 +++++++++++ app/settings/page.tsx | 67 ++-- components/collections/users-table.tsx | 6 +- components/dashboard/add-credits.tsx | 188 ++++++++++ .../charts/model-distribution-chart.tsx | 214 ++++++++++-- .../charts/real-user-activity-chart.tsx | 21 +- .../dashboard/charts/simple-stats-chart.tsx | 15 +- .../dashboard/charts/user-activity-chart.tsx | 24 +- components/dashboard/real-time-stats.tsx | 8 +- components/dashboard/usage-analytics.tsx | 98 ++++-- components/layout/sidebar.tsx | 326 ++++++++++++------ components/ui/alert.tsx | 46 +++ lib/supabase/client.ts | 8 + lib/supabase/server.ts | 29 ++ middleware.ts | 65 ++++ package-lock.json | 158 ++++++++- package.json | 2 + 23 files changed, 1569 insertions(+), 248 deletions(-) create mode 100644 app/api/add-credits/route.ts create mode 100644 app/api/cleanup/balances/route.ts create mode 100644 app/login/page.tsx create mode 100644 components/dashboard/add-credits.tsx create mode 100644 components/ui/alert.tsx create mode 100644 lib/supabase/client.ts create mode 100644 lib/supabase/server.ts create mode 100644 middleware.ts diff --git a/app/analytics/page.tsx b/app/analytics/page.tsx index a92dfde..ec75cf0 100644 --- a/app/analytics/page.tsx +++ b/app/analytics/page.tsx @@ -2,6 +2,7 @@ import { Suspense } from "react"; import { Card, CardContent } from "@/components/ui/card"; import { UsageAnalytics } from "@/components/dashboard/usage-analytics"; import { RecentTransactions } from "@/components/dashboard/recent-transactions"; + import { BarChart3 } from "lucide-react"; function AnalyticsSkeleton() { @@ -39,7 +40,7 @@ export default function AnalyticsPage() {
{/* Analytics des utilisateurs */} - + {/* Transactions récentes - toute la largeur */}
diff --git a/app/api/add-credits/route.ts b/app/api/add-credits/route.ts new file mode 100644 index 0000000..483c8c9 --- /dev/null +++ b/app/api/add-credits/route.ts @@ -0,0 +1,124 @@ +import { NextResponse } from "next/server"; +import { getDatabase } from "@/lib/db/mongodb"; + +export async function POST() { + try { + const db = await getDatabase(); + const CREDITS_TO_ADD = 5000000; // 5 millions de tokens + + 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(); + console.log(`👥 Utilisateurs trouvés: ${users.length}`); + + if (users.length === 0) { + return NextResponse.json({ + success: false, + 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()) + ); + + console.log(`💰 Balances existantes: ${existingBalances.length}`); + + let updatedCount = 0; + let createdCount = 0; + let totalCreditsAdded = 0; + + // 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() } + } + ); + + if (updateResult.modifiedCount > 0) { + updatedCount++; + totalCreditsAdded += CREDITS_TO_ADD; + 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 + const newBalance = { + user: userId, + tokenCredits: CREDITS_TO_ADD, + autoRefillEnabled: false, + lastRefill: new Date(), + refillAmount: 0, + refillIntervalUnit: "month", + refillIntervalValue: 1, + __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(`✅ TERMINÉ:`); + console.log(`- Balances mises à jour: ${updatedCount}`); + console.log(`- Nouvelles balances créées: ${createdCount}`); + console.log(`- Total crédits ajoutés: ${totalCreditsAdded.toLocaleString()}`); + + return NextResponse.json({ + success: true, + statistics: { + totalUsers: users.length, + updatedBalances: updatedCount, + createdBalances: createdCount, + creditsPerUser: CREDITS_TO_ADD, + totalCreditsAdded + }, + 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 }); + } +} + +// Endpoint pour vérifier les crédits actuels +export async function GET() { + try { + const db = await getDatabase(); + + 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; + + return NextResponse.json({ + statistics: { + totalUsers: users.length, + totalBalances: balances.length, + totalCredits, + averageCredits: Math.round(averageCredits), + 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/cleanup/balances/route.ts b/app/api/cleanup/balances/route.ts new file mode 100644 index 0000000..47155b0 --- /dev/null +++ b/app/api/cleanup/balances/route.ts @@ -0,0 +1,104 @@ +import { NextResponse } from "next/server"; +import { getDatabase } from "@/lib/db/mongodb"; + +export async function POST() { + try { + const db = await getDatabase(); + + // Récupérer tous les utilisateurs existants + const users = await db.collection("users").find({}).toArray(); + const userIds = new Set(users.map(user => user._id.toString())); + + // Récupérer toutes les balances + const balances = await db.collection("balances").find({}).toArray(); + + // Identifier les balances fantômes + const phantomBalances = balances.filter(balance => + !userIds.has(balance.user.toString()) + ); + + // Calculer les statistiques avant nettoyage + const totalBalances = balances.length; + const phantomCount = phantomBalances.length; + const phantomCredits = phantomBalances.reduce( + (sum, balance) => sum + (balance.tokenCredits || 0), + 0 + ); + + console.log(`🗑️ SUPPRESSION: ${phantomCount} balances fantômes détectées`); + console.log(`💰 CRÉDITS FANTÔMES: ${phantomCredits}`); + + // SUPPRESSION DÉFINITIVE des balances fantômes + const deleteResult = await db.collection("balances").deleteMany({ + user: { $nin: Array.from(userIds) } + }); + + console.log(`✅ SUPPRIMÉES: ${deleteResult.deletedCount} balances`); + + return NextResponse.json({ + success: true, + statistics: { + totalBalances, + phantomCount, + phantomCredits, + cleanedCount: deleteResult.deletedCount + }, + message: `${deleteResult.deletedCount} balances fantômes supprimées définitivement` + }); + + } catch (error) { + console.error("Erreur lors du nettoyage des balances:", error); + return NextResponse.json({ error: "Erreur serveur" }, { status: 500 }); + } +} + +// Endpoint pour analyser sans nettoyer +export async function GET() { + try { + const db = await getDatabase(); + + const users = await db.collection("users").find({}).toArray(); + const userIds = new Set(users.map(user => user._id.toString())); + + const balances = await db.collection("balances").find({}).toArray(); + + const phantomBalances = balances.filter(balance => + !userIds.has(balance.user.toString()) + ); + + const phantomCredits = phantomBalances.reduce( + (sum, balance) => sum + (balance.tokenCredits || 0), + 0 + ); + + // Analyser les doublons aussi + const userCounts = new Map(); + balances.forEach(balance => { + const userId = balance.user.toString(); + userCounts.set(userId, (userCounts.get(userId) || 0) + 1); + }); + + const duplicates = Array.from(userCounts.entries()) + .filter(([, count]) => count > 1) + .map(([userId, count]) => ({ + userId, + count, + isPhantom: !userIds.has(userId) + })); + + return NextResponse.json({ + analysis: { + totalBalances: balances.length, + totalUsers: users.length, + phantomBalances: phantomBalances.length, + phantomCredits, + duplicateUsers: duplicates.length, + duplicates: duplicates.slice(0, 10) // Premiers 10 exemples + } + }); + + } catch (error) { + console.error("Erreur lors de l'analyse des balances:", error); + return NextResponse.json({ error: "Erreur serveur" }, { status: 500 }); + } +} \ No newline at end of file diff --git a/app/api/metrics/route.ts b/app/api/metrics/route.ts index 3fbe1a7..115c7d8 100644 --- a/app/api/metrics/route.ts +++ b/app/api/metrics/route.ts @@ -13,21 +13,21 @@ export async function GET() { db.collection("balances").find({}).toArray(), ]); - // Calculer les utilisateurs actifs (dernière semaine) - const oneWeekAgo = new Date(); - oneWeekAgo.setDate(oneWeekAgo.getDate() - 7); + // Calculer les utilisateurs actifs (30 derniers jours) + const thirtyDaysAgo = new Date(); + thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); const activeUsers = users.filter((user) => { const lastActivity = new Date(user.updatedAt || user.createdAt); - return lastActivity >= oneWeekAgo; + return lastActivity >= thirtyDaysAgo; }).length; // Calculer les administrateurs const totalAdmins = users.filter(user => user.role === 'ADMIN').length; - // Calculer les conversations actives (dernière semaine) + // Calculer les conversations actives (30 derniers jours) const activeConversations = conversations.filter((conv) => { const lastActivity = new Date(conv.updatedAt || conv.createdAt); - return lastActivity >= oneWeekAgo; + return lastActivity >= thirtyDaysAgo; }).length; // Calculer le total des messages @@ -41,32 +41,27 @@ export async function GET() { return sum + Math.abs(Number(transaction.rawAmount) || 0); }, 0); - // Calculer le total des crédits depuis balances - const totalCredits = balances.reduce((sum, balance) => { - return sum + (Number(balance.tokenCredits) || 0); - }, 0); + // Calculer le total des crédits + const totalCreditsUsed = balances.reduce( + (sum, balance) => sum + (balance.tokenCredits || 0), + 0 + ); // Récupérer les transactions récentes (dernières 10) const recentTransactions = transactions .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) - .slice(0, 10) - .map(transaction => ({ - _id: transaction._id, - description: `Transaction ${transaction.tokenType} - ${transaction.model}`, - amount: transaction.rawAmount, - type: transaction.rawAmount > 0 ? 'credit' : 'debit', - createdAt: transaction.createdAt - })); + .slice(0, 10); return NextResponse.json({ totalUsers: users.length, activeUsers, totalAdmins, - totalCredits, + totalCredits: totalCreditsUsed, activeConversations, - totalMessages: totalMessages, + totalMessages, totalTokensConsumed, - recentTransactions + totalCreditsUsed, + recentTransactions, }); } catch (error) { console.error("Erreur lors du calcul des métriques:", error); diff --git a/app/api/stats/route.ts b/app/api/stats/route.ts index 59083d5..16ffb09 100644 --- a/app/api/stats/route.ts +++ b/app/api/stats/route.ts @@ -7,6 +7,22 @@ export async function GET() { // Récupérer toutes les transactions const transactions = await db.collection("transactions").find({}).toArray(); + + console.log(`Total transactions trouvées: ${transactions.length}`); + + // Vérifier les champs de date disponibles dans les transactions + if (transactions.length > 0) { + const sampleTransaction = transactions[0]; + console.log("Exemple de transaction:", { + _id: sampleTransaction._id, + createdAt: sampleTransaction.createdAt, + updatedAt: sampleTransaction.updatedAt, + date: sampleTransaction.date, + timestamp: sampleTransaction.timestamp, + rawAmount: sampleTransaction.rawAmount, + model: sampleTransaction.model + }); + } // Calculer les tokens par jour (7 derniers jours) const dailyStats = []; @@ -22,14 +38,44 @@ export async function GET() { nextDate.setDate(nextDate.getDate() + 1); const dayTransactions = transactions.filter(transaction => { - const transactionDate = new Date(transaction.createdAt); + // Essayer différents champs de date + let transactionDate = null; + + if (transaction.createdAt) { + transactionDate = new Date(transaction.createdAt); + } else if (transaction.updatedAt) { + transactionDate = new Date(transaction.updatedAt); + } else if (transaction.date) { + transactionDate = new Date(transaction.date); + } else if (transaction.timestamp) { + transactionDate = new Date(transaction.timestamp); + } else if (transaction._id && transaction._id.getTimestamp) { + // Utiliser le timestamp de l'ObjectId MongoDB + transactionDate = transaction._id.getTimestamp(); + } + + if (!transactionDate || isNaN(transactionDate.getTime())) { + return false; + } + return transactionDate >= date && transactionDate < nextDate; }); const totalTokens = dayTransactions.reduce((sum, transaction) => { - return sum + Math.abs(Number(transaction.rawAmount) || 0); + // Essayer différents champs pour les tokens + let tokens = 0; + if (transaction.rawAmount) { + tokens = Math.abs(Number(transaction.rawAmount) || 0); + } else if (transaction.amount) { + tokens = Math.abs(Number(transaction.amount) || 0); + } else if (transaction.tokens) { + tokens = Math.abs(Number(transaction.tokens) || 0); + } + return sum + tokens; }, 0); + console.log(`${dayNames[date.getDay()]} (${date.toISOString().split('T')[0]}): ${dayTransactions.length} transactions, ${totalTokens} tokens`); + dailyStats.push({ name: dayNames[date.getDay()], value: totalTokens @@ -40,9 +86,19 @@ export async function GET() { const modelStats = new Map(); transactions.forEach(transaction => { - const model = transaction.model || "Inconnu"; - const tokens = Math.abs(Number(transaction.rawAmount) || 0); - modelStats.set(model, (modelStats.get(model) || 0) + tokens); + const model = transaction.model || transaction.modelName || "Inconnu"; + let tokens = 0; + if (transaction.rawAmount) { + tokens = Math.abs(Number(transaction.rawAmount) || 0); + } else if (transaction.amount) { + tokens = Math.abs(Number(transaction.amount) || 0); + } else if (transaction.tokens) { + tokens = Math.abs(Number(transaction.tokens) || 0); + } + + if (tokens > 0) { + modelStats.set(model, (modelStats.get(model) || 0) + tokens); + } }); // Convertir en array et trier par usage @@ -50,6 +106,12 @@ export async function GET() { .map(([name, value]) => ({ name, value })) .sort((a, b) => b.value - a.value); + console.log("Statistiques calculées:", { + dailyStats, + totalModels: modelData.length, + topModel: modelData[0] + }); + return NextResponse.json({ dailyTokens: dailyStats, modelDistribution: modelData diff --git a/app/api/user-activity/route.ts b/app/api/user-activity/route.ts index 85b5563..b0ae9a3 100644 --- a/app/api/user-activity/route.ts +++ b/app/api/user-activity/route.ts @@ -8,16 +8,16 @@ export async function GET() { // Récupérer tous les utilisateurs const users = await db.collection("users").find({}).toArray(); - // Calculer les utilisateurs actifs (dernière semaine) - const oneWeekAgo = new Date(); - oneWeekAgo.setDate(oneWeekAgo.getDate() - 7); + // Calculer les utilisateurs actifs (30 derniers jours) + const thirtyDaysAgo = new Date(); + thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); let activeUsers = 0; let inactiveUsers = 0; users.forEach(user => { const lastActivity = new Date(user.updatedAt || user.createdAt); - if (lastActivity >= oneWeekAgo) { + if (lastActivity >= thirtyDaysAgo) { activeUsers++; } else { inactiveUsers++; diff --git a/app/login/page.tsx b/app/login/page.tsx new file mode 100644 index 0000000..ba908c6 --- /dev/null +++ b/app/login/page.tsx @@ -0,0 +1,194 @@ +"use client"; + +import { useState } from "react"; +import { createClient } from "@/lib/supabase/client"; +import { useRouter } from "next/navigation"; +import Image from "next/image"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Loader2, Mail, Lock, Shield } from "lucide-react"; + +export default function LoginPage() { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + const router = useRouter(); + + // Fonction pour traduire les erreurs Supabase en français + const getErrorMessage = (error: string) => { + const errorMessages: { [key: string]: string } = { + "Invalid login credentials": "Identifiants de connexion invalides", + "Email not confirmed": "Email non confirmé", + "Too many requests": "Trop de tentatives de connexion", + "User not found": "Utilisateur non trouvé", + "Invalid email": "Adresse email invalide", + "Password should be at least 6 characters": "Le mot de passe doit contenir au moins 6 caractères", + "Email rate limit exceeded": "Limite de tentatives dépassée", + "Invalid email or password": "Email ou mot de passe incorrect", + "Account not found": "Compte non trouvé", + "Invalid credentials": "Identifiants incorrects", + "Authentication failed": "Échec de l'authentification", + "Access denied": "Accès refusé", + "Unauthorized": "Non autorisé", + }; + + // Chercher une correspondance exacte + if (errorMessages[error]) { + return errorMessages[error]; + } + + // Chercher une correspondance partielle + for (const [englishError, frenchError] of Object.entries(errorMessages)) { + if (error.toLowerCase().includes(englishError.toLowerCase())) { + return frenchError; + } + } + + // Message par défaut si aucune correspondance + return "Erreur de connexion. Vérifiez vos identifiants."; + }; + + const handleLogin = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setError(""); + + // Validation côté client + if (!email || !password) { + setError("Veuillez remplir tous les champs"); + setLoading(false); + return; + } + + if (!email.includes("@")) { + setError("Veuillez entrer une adresse email valide"); + setLoading(false); + return; + } + + const supabase = createClient(); + + try { + const { data, error: authError } = await supabase.auth.signInWithPassword({ + email, + password, + }); + + if (authError) { + setError(getErrorMessage(authError.message)); + return; + } + + if (data.user) { + router.push("/"); + router.refresh(); + } + } catch (error) { + console.error('Login error:', error); + setError("Une erreur inattendue est survenue. Veuillez réessayer."); + } finally { + setLoading(false); + } + }; + + return ( +
+ + +
+
+ Logo +
+
+
+ + + Admin Dashboard + +

+ Connectez-vous pour accéder au tableau de bord +

+
+
+ + + {error && ( + + + {error} + + + )} + +
+
+ +
+ + setEmail(e.target.value)} + className="pl-10 border-gray-300 focus:border-gray-900 focus:ring-gray-900" + required + /> +
+
+ +
+ +
+ + setPassword(e.target.value)} + className="pl-10 border-gray-300 focus:border-gray-900 focus:ring-gray-900" + required + /> +
+
+ + +
+ +
+

+ Accès réservé aux administrateurs autorisés +

+
+
+
+
+ ); +} diff --git a/app/settings/page.tsx b/app/settings/page.tsx index 55126f7..f2fa0c6 100644 --- a/app/settings/page.tsx +++ b/app/settings/page.tsx @@ -1,5 +1,14 @@ -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; +import { Database, Server, Settings } from "lucide-react"; + +import AddCredits from "@/components/dashboard/add-credits"; export default function SettingsPage() { return ( @@ -7,65 +16,73 @@ export default function SettingsPage() {

Paramètres

- Configuration du dashboard Cercle GPT + Configuration et maintenance du système

+ {/* Informations système */}
- - Connexion MongoDB + + + + Base de données +
- Statut: + + Statut MongoDB + Connecté
-
- - Base de données: - - Cercle GPT -
-
- - Collections: - - 29 collections -
- - Informations système + + + + Informations système +
- Version Next.js: + Version Next.js - 15.5.4 + 14.x
- Version Node.js: + Version Node.js - {process.version} + 18.x
- Environnement: + Environnement - {process.env.NODE_ENV} + Development
+ + {/* Gestion des crédits */} +
+

+ + Gestion des Crédits +

+
+ +
+
); } diff --git a/components/collections/users-table.tsx b/components/collections/users-table.tsx index 1279c30..8f7194a 100644 --- a/components/collections/users-table.tsx +++ b/components/collections/users-table.tsx @@ -96,7 +96,7 @@ export function UsersTable() { {users.map((user) => { const userCredits = creditsMap.get(user._id) || 0; const isActive = new Date(user.updatedAt || user.createdAt) > - new Date(Date.now() - 5 * 60 * 1000); // 5 minutes en millisecondes + new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); // 30 jours en millisecondes return ( @@ -122,13 +122,13 @@ export function UsersTable() { - + {isActive ? 'Actif' : 'Inactif'} - {formatDate(new Date(user.createdAt))} + {formatDate(user.createdAt)} diff --git a/components/dashboard/add-credits.tsx b/components/dashboard/add-credits.tsx new file mode 100644 index 0000000..dc88809 --- /dev/null +++ b/components/dashboard/add-credits.tsx @@ -0,0 +1,188 @@ +"use client"; + +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Plus, DollarSign, Users, TrendingUp } from "lucide-react"; + +interface AddCreditsStats { + totalUsers: number; + totalBalances: number; + totalCredits: number; + averageCredits: number; + usersWithoutBalance: number; +} + +interface AddCreditsResult { + totalUsers: number; + updatedBalances: number; + createdBalances: number; + creditsPerUser: number; + totalCreditsAdded: number; +} + +export default function AddCredits() { + const [stats, setStats] = useState(null); + const [result, setResult] = useState(null); + const [loading, setLoading] = useState(false); + const [analyzing, setAnalyzing] = useState(false); + + const analyzeCurrentCredits = async () => { + setAnalyzing(true); + try { + const response = await fetch("/api/add-credits"); + const data = await response.json(); + + if (data.statistics) { + setStats(data.statistics); + } + } catch (error) { + console.error("Erreur lors de l'analyse:", error); + } finally { + setAnalyzing(false); + } + }; + + 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.")) { + return; + } + + setLoading(true); + try { + const response = await fetch("/api/add-credits", { + method: "POST" + }); + + const data = await response.json(); + + if (data.success) { + setResult(data.statistics); + // Rafraîchir les stats + await analyzeCurrentCredits(); + } else { + alert("Erreur: " + (data.error || data.message)); + } + } catch (error) { + console.error("Erreur lors de l'ajout des crédits:", error); + alert("Erreur lors de l'ajout des crédits"); + } finally { + setLoading(false); + } + }; + + return ( + + + + + Ajouter des Crédits + + + Ajouter 5 millions de tokens à tous les utilisateurs existants + + + + {/* Bouton d'analyse */} +
+ +
+ + {/* Statistiques actuelles */} + {stats && ( +
+
+
+ + Utilisateurs +
+

{stats.totalUsers}

+
+ +
+
+ + Total Crédits +
+

+ {stats.totalCredits.toLocaleString()} +

+
+ +
+
+ + Moyenne +
+

+ {stats.averageCredits.toLocaleString()} +

+
+ +
+
+ + Sans Balance +
+

{stats.usersWithoutBalance}

+
+
+ )} + + {/* Bouton d'ajout de crédits */} + {stats && ( +
+
+

⚠️ Action Importante

+

+ 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()} +

+
+ + +
+ )} + + {/* Résultats */} + {result && ( +
+

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

+
+
+ Balances mises Ă  jour: + {result.updatedBalances} +
+
+ Nouvelles balances: + {result.createdBalances} +
+
+ Crédits par utilisateur: + {result.creditsPerUser.toLocaleString()} +
+
+ Total ajouté: + {result.totalCreditsAdded.toLocaleString()} +
+
+
+ )} +
+
+ ); +} \ No newline at end of file diff --git a/components/dashboard/charts/model-distribution-chart.tsx b/components/dashboard/charts/model-distribution-chart.tsx index aeeee0a..0c6ee3f 100644 --- a/components/dashboard/charts/model-distribution-chart.tsx +++ b/components/dashboard/charts/model-distribution-chart.tsx @@ -9,14 +9,23 @@ import { CartesianGrid, Tooltip, ResponsiveContainer, + Cell, } from "recharts"; interface ModelDistributionChartProps { title: string; + subtitle?: string; data: Array<{ name: string; value: number; + color?: string; + models?: Array<{ + name: string; + value: number; + }>; }>; + showLegend?: boolean; + totalTokens?: number; } interface TooltipPayload { @@ -24,6 +33,11 @@ interface TooltipPayload { payload: { name: string; value: number; + color?: string; + models?: Array<{ + name: string; + value: number; + }>; }; } @@ -32,63 +46,211 @@ interface CustomTooltipProps { payload?: TooltipPayload[]; } -const CustomTooltip = ({ active, payload }: CustomTooltipProps) => { - if (active && payload && payload.length) { - return ( -
-

- {`${payload[0].value.toLocaleString()} tokens`} -

-

- {payload[0].payload.name} -

-
- ); - } +// Couleurs par fournisseur selon l'image +const providerColors: { [key: string]: string } = { + Anthropic: "#7C3AED", // Violet vif + OpenAI: "#059669", // Vert turquoise vif + "Mistral AI": "#D97706", // Orange vif + Meta: "#DB2777", // Rose/Magenta vif + Google: "#2563EB", // Bleu vif + Cohere: "#0891B2", // Cyan vif +}; + +// Fonction pour regrouper les modèles par fournisseur +const groupByProvider = (modelData: Array<{ name: string; value: number }>) => { + const providerMap: { + [key: string]: { + value: number; + models: Array<{ name: string; value: number }>; + }; + } = {}; + + modelData.forEach((model) => { + let provider = ""; + + // Déterminer le fournisseur basé sur le nom du modèle + if ( + model.name.toLowerCase().includes("claude") || + model.name.toLowerCase().includes("anthropic") + ) { + provider = "Anthropic"; + } else if ( + model.name.toLowerCase().includes("gpt") || + model.name.toLowerCase().includes("openai") + ) { + provider = "OpenAI"; + } else if (model.name.toLowerCase().includes("mistral")) { + provider = "Mistral AI"; + } else if ( + model.name.toLowerCase().includes("llama") || + model.name.toLowerCase().includes("meta") + ) { + provider = "Meta"; + } else if ( + model.name.toLowerCase().includes("palm") || + model.name.toLowerCase().includes("gemini") || + model.name.toLowerCase().includes("google") + ) { + provider = "Google"; + } else if (model.name.toLowerCase().includes("cohere")) { + provider = "Cohere"; + } else { + provider = "Autres"; + } + + if (!providerMap[provider]) { + providerMap[provider] = { value: 0, models: [] }; + } + + providerMap[provider].value += model.value; + providerMap[provider].models.push(model); + }); + + return Object.entries(providerMap).map(([name, data]) => ({ + name, + value: data.value, + models: data.models, + color: providerColors[name] || "#6B7280", + })); +}; + +const CustomTooltip = () => { return null; }; export function ModelDistributionChart({ title, + subtitle, data, + totalTokens, }: ModelDistributionChartProps) { + // Si les données sont déjà groupées par fournisseur, les utiliser directement + // Sinon, les regrouper automatiquement + const groupedData = data[0]?.models ? data : groupByProvider(data); + + // Créer une liste de tous les modèles avec leurs couleurs + const allModels = groupedData.flatMap((provider) => + provider.models?.map((model) => ({ + name: model.name, + color: provider.color, + value: model.value + })) || [] + ); + return ( {title} + {subtitle && ( +

{subtitle}

+ )}
- + { + if (value >= 1000000) return `${(value / 1000000).toFixed(1)}M`; + if (value >= 1000) return `${(value / 1000).toFixed(1)}K`; + return value.toString(); + }} /> } /> - + + {groupedData.map((entry, index) => ( + + ))} + + + {/* Petites cartes légères pour chaque provider */} +
+ {groupedData.map((item, index) => ( +
+
+
+

+ {item.name} +

+
+

+ {item.value.toLocaleString()} +

+

tokens

+
+ ))} +
+ + {/* Total général */} + {totalTokens && ( +
+

+ Total général:{" "} + + {totalTokens.toLocaleString()} + {" "} + tokens +

+
+ )} + + {/* Légende dynamique des modèles */} + {allModels.length > 0 && ( +
+

+ Modèles utilisés +

+
+ {allModels + .sort((a, b) => b.value - a.value) // Trier par usage décroissant + .map((model, index) => ( +
+
+ + {model.name} + +
+ ))} +
+
+ )}
); diff --git a/components/dashboard/charts/real-user-activity-chart.tsx b/components/dashboard/charts/real-user-activity-chart.tsx index a8cec6a..683661b 100644 --- a/components/dashboard/charts/real-user-activity-chart.tsx +++ b/components/dashboard/charts/real-user-activity-chart.tsx @@ -44,12 +44,12 @@ export function RealUserActivityChart() { { name: "Utilisateurs actifs", value: activity.activeUsers, - color: "#22c55e", // Vert clair pour actifs + color: "#000000", // Noir pour actifs }, { name: "Utilisateurs inactifs", value: activity.inactiveUsers, - color: "#ef4444", // Rouge pour inactifs + color: "#666666", // Gris pour inactifs }, ]; @@ -62,7 +62,7 @@ export function RealUserActivityChart() { Activité des utilisateurs

- Actifs = connectés dans les 7 derniers jours + Actifs = connectés dans les 30 derniers jours

@@ -74,8 +74,10 @@ export function RealUserActivityChart() { cy="50%" innerRadius={60} outerRadius={100} - paddingAngle={5} + paddingAngle={2} dataKey="value" + stroke="#ffffff" + strokeWidth={2} > {data.map((entry, index) => ( @@ -86,17 +88,20 @@ export function RealUserActivityChart() { backgroundColor: "hsl(var(--background))", border: "1px solid hsl(var(--border))", borderRadius: "8px", + fontSize: "12px" }} formatter={(value: number) => [ - `${value} utilisateurs (${((value / total) * 100).toFixed( - 1 - )}%)`, + `${value} utilisateurs (${((value / total) * 100).toFixed(1)}%)`, "", ]} /> ( - + {value}: {entry.payload?.value} ( {((entry.payload?.value / total) * 100).toFixed(1)}%) diff --git a/components/dashboard/charts/simple-stats-chart.tsx b/components/dashboard/charts/simple-stats-chart.tsx index 7b2d06c..1560699 100644 --- a/components/dashboard/charts/simple-stats-chart.tsx +++ b/components/dashboard/charts/simple-stats-chart.tsx @@ -31,8 +31,8 @@ export function SimpleStatsChart({ title, data, color = "hsl(var(--primary))" }: - - + + @@ -46,6 +46,11 @@ export function SimpleStatsChart({ title, data, color = "hsl(var(--primary))" }: axisLine={false} tickLine={false} className="text-xs fill-muted-foreground" + tickFormatter={(value) => { + if (value >= 1000000) return `${(value / 1000000).toFixed(1)}M`; + if (value >= 1000) return `${(value / 1000).toFixed(1)}K`; + return value.toString(); + }} /> [ + value >= 1000 ? `${(value / 1000).toFixed(1)}K tokens` : `${value} tokens`, + 'Tokens consommés' + ]} /> diff --git a/components/dashboard/charts/user-activity-chart.tsx b/components/dashboard/charts/user-activity-chart.tsx index 20441c4..fa9b603 100644 --- a/components/dashboard/charts/user-activity-chart.tsx +++ b/components/dashboard/charts/user-activity-chart.tsx @@ -20,12 +20,12 @@ export function UserActivityChart({ activeUsers, inactiveUsers }: UserActivityCh { name: 'Utilisateurs actifs', value: activeUsers, - color: '#22c55e' // Vert clair pour actifs + color: '#000000' // Noir pour actifs }, { name: 'Utilisateurs inactifs', value: inactiveUsers, - color: '#ef4444' // Rouge pour inactifs + color: '#666666' // Gris pour inactifs }, ]; @@ -36,7 +36,7 @@ export function UserActivityChart({ activeUsers, inactiveUsers }: UserActivityCh Activité des utilisateurs

- Actifs = connectés dans les 7 derniers jours + Actifs = connectés dans les 30 derniers jours

@@ -48,8 +48,10 @@ export function UserActivityChart({ activeUsers, inactiveUsers }: UserActivityCh cy="50%" innerRadius={60} outerRadius={100} - paddingAngle={5} + paddingAngle={2} dataKey="value" + stroke="#ffffff" + strokeWidth={2} > {data.map((entry, index) => ( @@ -60,16 +62,22 @@ export function UserActivityChart({ activeUsers, inactiveUsers }: UserActivityCh backgroundColor: 'hsl(var(--background))', border: '1px solid hsl(var(--border))', borderRadius: '8px', + fontSize: '12px' }} formatter={(value: number) => [ `${value} utilisateurs (${((value / total) * 100).toFixed(1)}%)`, - '' + '', ]} /> - ( - - {value}: {entry.payload?.value} ({((entry.payload?.value / total) * 100).toFixed(1)}%) + + {value}: {entry.payload?.value} ( + {((entry.payload?.value / total) * 100).toFixed(1)}%) )} /> diff --git a/components/dashboard/real-time-stats.tsx b/components/dashboard/real-time-stats.tsx index e575aad..51bf1bb 100644 --- a/components/dashboard/real-time-stats.tsx +++ b/components/dashboard/real-time-stats.tsx @@ -11,7 +11,7 @@ export function RealTimeStats() { if (loading) { return ( -
+
@@ -20,7 +20,7 @@ export function RealTimeStats() { if (error) { return ( -
+
@@ -47,7 +47,7 @@ export function RealTimeStats() { if (!stats) { return ( -
+

@@ -67,7 +67,7 @@ export function RealTimeStats() { } return ( -

+
(); - balances.forEach(balance => { + balances.forEach((balance) => { const userId = balance.user; userCounts.set(userId, (userCounts.get(userId) || 0) + 1); }); - - const duplicateUsers = Array.from(userCounts.entries()).filter(([_, count]) => count > 1); + + const duplicateUsers = Array.from(userCounts.entries()).filter(([, count]) => count > 1); console.log("Utilisateurs avec plusieurs entrées:", duplicateUsers); // Afficher quelques exemples d'entrées @@ -73,6 +69,29 @@ export function UsageAnalytics() { const totalBrut = balances.reduce((sum, balance) => sum + (balance.tokenCredits || 0), 0); console.log("Total brut (avec doublons potentiels):", totalBrut); + // Ajouter des logs détaillés pour comprendre le problème + console.log("=== DIAGNOSTIC DÉTAILLÉ ==="); + + // Analyser les doublons + const duplicateDetails = Array.from(userCounts.entries()) + .filter(([, count]) => count > 1) + .map(([userId, count]) => { + const userBalances = balances.filter(b => b.user === userId); + const totalCredits = userBalances.reduce((sum, b) => sum + (b.tokenCredits || 0), 0); + return { + userId, + count, + totalCredits, + balances: userBalances.map(b => ({ + credits: b.tokenCredits, + createdAt: b.createdAt, + updatedAt: b.updatedAt + })) + }; + }); + + console.log("Détails des doublons:", duplicateDetails); + // NOUVEAU : Identifier les utilisateurs fantômes console.log("=== ANALYSE DES UTILISATEURS FANTÔMES ==="); const userIds = new Set(users.map(user => user._id)); @@ -90,41 +109,58 @@ export function UsageAnalytics() { console.log("Crédits des utilisateurs fantômes:", phantomCredits); console.log("Crédits des vrais utilisateurs:", totalBrut - phantomCredits); + + // Analyser les utilisateurs fantômes + const phantomDetails = uniquePhantomUsers.map(userId => { + const userBalances = balances.filter(b => b.user === userId); + const totalCredits = userBalances.reduce((sum, b) => sum + (b.tokenCredits || 0), 0); + return { userId, totalCredits, count: userBalances.length }; + }); + + console.log("Détails des utilisateurs fantômes:", phantomDetails); - // Calculer les utilisateurs actifs (5 dernières minutes) - const fiveMinutesAgo = new Date(); - fiveMinutesAgo.setMinutes(fiveMinutesAgo.getMinutes() - 5); + // Calculer les utilisateurs actifs (30 derniers jours) + const thirtyDaysAgo = new Date(); + thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); const activeUsers = users.filter((user) => { const lastActivity = new Date(user.updatedAt || user.createdAt); - return lastActivity >= fiveMinutesAgo; + return lastActivity >= thirtyDaysAgo; }).length; - // CORRECTION : Créer une map des crédits par utilisateur en évitant les doublons + // CORRECTION AMÉLIORÉE : Créer une map des crédits par utilisateur const creditsMap = new Map(); // Grouper les balances par utilisateur const balancesByUser = new Map(); balances.forEach((balance) => { const userId = balance.user; - if (!balancesByUser.has(userId)) { - balancesByUser.set(userId, []); + // Ignorer les utilisateurs fantômes (qui n'existent plus) + if (users.some(user => user._id === userId)) { + if (!balancesByUser.has(userId)) { + balancesByUser.set(userId, []); + } + balancesByUser.get(userId)!.push(balance); } - balancesByUser.get(userId)!.push(balance); }); - // Pour chaque utilisateur, prendre seulement la dernière entrée + // Pour chaque utilisateur, calculer les crédits selon votre logique métier balancesByUser.forEach((userBalances, userId) => { if (userBalances.length > 0) { - // Trier par date de mise à jour (plus récent en premier) + // OPTION A: Prendre la balance la plus récente const sortedBalances = userBalances.sort((a, b) => { const aDate = new Date((a.updatedAt as string) || (a.createdAt as string) || 0); const bDate = new Date((b.updatedAt as string) || (b.createdAt as string) || 0); return bDate.getTime() - aDate.getTime(); }); + creditsMap.set(userId, sortedBalances[0].tokenCredits || 0); - // Prendre la plus récente - const latestBalance = sortedBalances[0]; - creditsMap.set(userId, latestBalance.tokenCredits || 0); + // OPTION B: Sommer toutes les balances (si c'est votre logique) + // const totalCredits = userBalances.reduce((sum, balance) => sum + (balance.tokenCredits || 0), 0); + // creditsMap.set(userId, totalCredits); + + // OPTION C: Prendre la balance avec le plus de crédits + // const maxCredits = Math.max(...userBalances.map(b => b.tokenCredits || 0)); + // creditsMap.set(userId, maxCredits); } }); @@ -167,12 +203,16 @@ export function UsageAnalytics() { } }); - // CORRECTION : Calculer le total des crédits depuis la map corrigée + // Calculer le total des crédits depuis la map corrigée (sans doublons ni fantômes) const totalCreditsUsed = Array.from(creditsMap.values()).reduce( (sum, credits) => sum + credits, 0 ); + console.log("=== RÉSULTATS CORRIGÉS ==="); + console.log("Crédits totaux (sans doublons ni fantômes):", totalCreditsUsed); + console.log("Utilisateurs avec crédits:", creditsMap.size); + // Tous les utilisateurs triés par tokens puis conversations const allUsers = Array.from(userStats.entries()) .map(([userId, stats]) => ({ @@ -242,7 +282,7 @@ export function UsageAnalytics() {
{stats.totalUsers}

- {stats.activeUsers} actifs cette semaine + {stats.activeUsers} actifs ce mois

diff --git a/components/layout/sidebar.tsx b/components/layout/sidebar.tsx index b0e8966..861977c 100644 --- a/components/layout/sidebar.tsx +++ b/components/layout/sidebar.tsx @@ -1,12 +1,11 @@ "use client"; -import { useState } from "react"; +import { useState, useEffect } from "react"; import Link from "next/link"; -import { usePathname } from "next/navigation"; +import { usePathname, useRouter } from "next/navigation"; import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; -import { Badge } from "@/components/ui/badge"; -import { Separator } from "@/components/ui/separator"; +import { createClient } from "@/lib/supabase/client"; import Image from "next/image"; import { LayoutDashboard, @@ -21,135 +20,164 @@ import { ChevronLeft, ChevronRight, BarChart3, - Activity, + LogOut, + User, + Mail, } from "lucide-react"; +import type { User as SupabaseUser } from "@supabase/supabase-js"; -interface NavigationItem { - name: string; - href: string; - icon: React.ElementType; - badge?: string | null; -} - -const navigation: NavigationItem[] = [ +const topLevelNavigation = [ { - name: "Vue d'ensemble", + name: "Dashboard", href: "/", icon: LayoutDashboard, - badge: null, }, { name: "Analytics", href: "/analytics", icon: BarChart3, - badge: "Nouveau", }, ]; -const dataNavigation: NavigationItem[] = [ - { name: "Utilisateurs", href: "/users", icon: Users, badge: null }, +const navigationGroups = [ { - name: "Conversations", - href: "/conversations", - icon: MessageSquare, - badge: null, + name: "Données", + items: [ + { + name: "Utilisateurs", + href: "/users", + icon: Users, + }, + { + name: "Conversations", + href: "/conversations", + icon: MessageSquare, + }, + { + name: "Messages", + href: "/messages", + icon: FileText, + }, + { + name: "Transactions", + href: "/transactions", + icon: CreditCard, + }, + ], }, - { name: "Messages", href: "/messages", icon: FileText, badge: null }, { - name: "Transactions", - href: "/transactions", - icon: CreditCard, - badge: null, + name: "Système", + items: [ + { + name: "Collections", + href: "/collections", + icon: Database, + }, + { + name: "Agents", + href: "/agents", + icon: Bot, + }, + { + name: "Rôles", + href: "/roles", + icon: Shield, + }, + { + name: "Paramètres", + href: "/settings", + icon: Settings, + }, + ], }, ]; -const systemNavigation: NavigationItem[] = [ - { name: "Agents", href: "/agents", icon: Bot, badge: null }, - { name: "Rôles", href: "/roles", icon: Shield, badge: null }, - { name: "Collections", href: "/collections", icon: Database, badge: null }, - { name: "Paramètres", href: "/settings", icon: Settings, badge: null }, -]; - export function Sidebar() { - const [collapsed, setCollapsed] = useState(false); const pathname = usePathname(); + const router = useRouter(); + const [collapsed, setCollapsed] = useState(false); + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + const supabase = createClient(); - const NavSection = ({ - title, - items, - showTitle = true, - }: { - title: string; - items: NavigationItem[]; - showTitle?: boolean; - }) => ( -
- {!collapsed && showTitle && ( -

- {title} -

- )} - {items.map((item) => { - const isActive = pathname === item.href; - return ( - - - + useEffect(() => { + const getUser = async () => { + try { + const { + data: { user }, + } = await supabase.auth.getUser(); + setUser(user); + } catch (error) { + console.error( + "Erreur lors de la récupération de l'utilisateur:", + error ); - })} -
- ); + setUser(null); + } finally { + setLoading(false); + } + }; + + getUser(); + + // Écouter les changements d'authentification + const { + data: { subscription }, + } = supabase.auth.onAuthStateChange((event, session) => { + setUser(session?.user || null); + setLoading(false); + }); + + return () => subscription.unsubscribe(); + }, [supabase.auth]); + + const handleLogout = async () => { + try { + await supabase.auth.signOut(); + router.push("/login"); + router.refresh(); + } catch (error) { + console.error("Erreur lors de la déconnexion:", error); + } + }; + + // Ne pas afficher la sidebar si l'utilisateur n'est pas connecté ou en cours de chargement + if (loading || !user) { + return null; + } return (
{/* Header */} -
+
{!collapsed && ( -
-
+
+
Cercle GPT Logo
-

Cercle GPT

-

Admin Dashboard

+

+ Cercle GPT +

+

Admin Dashboard

)}
{/* Navigation */} - + + {/* Section utilisateur connecté */} +
+ {/* Informations utilisateur */} +
+
+
+ +
+
+ {!collapsed && ( +
+

+ Administrateur +

+
+ +

{user.email}

+
+
+ )}
- )} + + {/* Bouton de déconnexion */} + +
); } diff --git a/components/ui/alert.tsx b/components/ui/alert.tsx new file mode 100644 index 0000000..16b823b --- /dev/null +++ b/components/ui/alert.tsx @@ -0,0 +1,46 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", + { + variants: { + variant: { + default: "bg-background text-foreground", + destructive: + "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +
+)) +Alert.displayName = "Alert" + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertDescription.displayName = "AlertDescription" + +export { Alert, AlertDescription } \ No newline at end of file diff --git a/lib/supabase/client.ts b/lib/supabase/client.ts new file mode 100644 index 0000000..78ff395 --- /dev/null +++ b/lib/supabase/client.ts @@ -0,0 +1,8 @@ +import { createBrowserClient } from '@supabase/ssr' + +export function createClient() { + return createBrowserClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! + ) +} \ No newline at end of file diff --git a/lib/supabase/server.ts b/lib/supabase/server.ts new file mode 100644 index 0000000..a627460 --- /dev/null +++ b/lib/supabase/server.ts @@ -0,0 +1,29 @@ +import { createServerClient as createSupabaseServerClient } from '@supabase/ssr' +import { cookies } from 'next/headers' + +export async function createServerClient() { + const cookieStore = await cookies() + + return createSupabaseServerClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + { + cookies: { + getAll() { + return cookieStore.getAll() + }, + setAll(cookiesToSet: Array<{ name: string; value: string; options?: Record }>) { + try { + cookiesToSet.forEach(({ name, value, options }) => + cookieStore.set(name, value, options) + ) + } catch { + // The `setAll` method was called from a Server Component. + // This can be ignored if you have middleware refreshing + // user sessions. + } + }, + }, + } + ) +} \ No newline at end of file diff --git a/middleware.ts b/middleware.ts new file mode 100644 index 0000000..ebae12d --- /dev/null +++ b/middleware.ts @@ -0,0 +1,65 @@ +import { createServerClient } from '@supabase/ssr' +import { NextResponse, type NextRequest } from 'next/server' + +export async function middleware(request: NextRequest) { + let supabaseResponse = NextResponse.next({ + request, + }) + + const supabase = createServerClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + { + cookies: { + getAll() { + return request.cookies.getAll() + }, + setAll(cookiesToSet: Array<{ name: string; value: string; options?: Record }>) { + cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value)) + supabaseResponse = NextResponse.next({ + request, + }) + cookiesToSet.forEach(({ name, value, options }) => + supabaseResponse.cookies.set(name, value, options) + ) + }, + }, + } + ) + + // Rafraîchir la session + const { data: { user } } = await supabase.auth.getUser() + + // Si l'utilisateur est sur la page de login + if (request.nextUrl.pathname === '/login') { + if (user) { + // Utilisateur connecté, rediriger vers le dashboard + const redirectUrl = new URL('/', request.url) + return NextResponse.redirect(redirectUrl) + } + // Utilisateur non connecté, autoriser l'accès à la page de login + return supabaseResponse + } + + // Pour toutes les autres pages, vérifier l'authentification + if (!user) { + // Utilisateur non connecté, rediriger vers login + const redirectUrl = new URL('/login', request.url) + return NextResponse.redirect(redirectUrl) + } + + return supabaseResponse +} + +export const config = { + matcher: [ + /* + * Match all request paths except for the ones starting with: + * - _next/static (static files) + * - _next/image (image optimization files) + * - favicon.ico (favicon file) + * - public folder + */ + '/((?!_next/static|_next/image|favicon.ico|public|img).*)', + ], +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index cade4d0..fab096f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,8 @@ "@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/mongodb": "^4.0.6", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -1761,6 +1763,115 @@ "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", "license": "MIT" }, + "node_modules/@supabase/auth-js": { + "version": "2.72.0", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.72.0.tgz", + "integrity": "sha512-4+bnUrtTDK1YD0/FCx2YtMiQH5FGu9Jlf4IQi5kcqRwRwqp2ey39V61nHNdH86jm3DIzz0aZKiWfTW8qXk1swQ==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.5.0.tgz", + "integrity": "sha512-SXBx6Jvp+MOBekeKFu+G11YLYPeVeGQl23eYyAG9+Ro0pQ1aIP0UZNIBxHKNHqxzR0L0n6gysNr2KT3841NATw==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/node-fetch": { + "version": "2.6.15", + "resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz", + "integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + } + }, + "node_modules/@supabase/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/@supabase/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/@supabase/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/@supabase/postgrest-js": { + "version": "1.21.4", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.21.4.tgz", + "integrity": "sha512-TxZCIjxk6/dP9abAi89VQbWWMBbybpGWyvmIzTd79OeravM13OjR/YEYeyUOPcM1C3QyvXkvPZhUfItvmhY1IQ==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.15.5", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.15.5.tgz", + "integrity": "sha512-/Rs5Vqu9jejRD8ZeuaWXebdkH+J7V6VySbCZ/zQM93Ta5y3mAmocjioa/nzlB6qvFmyylUgKVS1KpE212t30OA==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.13", + "@types/phoenix": "^1.6.6", + "@types/ws": "^8.18.1", + "ws": "^8.18.2" + } + }, + "node_modules/@supabase/ssr": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@supabase/ssr/-/ssr-0.7.0.tgz", + "integrity": "sha512-G65t5EhLSJ5c8hTCcXifSL9Q/ZRXvqgXeNo+d3P56f4U1IxwTqjB64UfmfixvmMcjuxnq2yGqEWVJqUcO+AzAg==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.2" + }, + "peerDependencies": { + "@supabase/supabase-js": "^2.43.4" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.12.2.tgz", + "integrity": "sha512-SiySHxi3q7gia7NBYpsYRu8gyI0NhFwSORMxbZIxJ/zAVkN6QpwDRan158CJ+UdzD4WB/rQMAGRqIJQP+7ccAQ==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.58.0", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.58.0.tgz", + "integrity": "sha512-Tm1RmQpoAKdQr4/8wiayGti/no+If7RtveVZjHR8zbO7hhQjmPW2Ok5ZBPf1MGkt5c+9R85AVMsTfSaqAP1sUg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@supabase/auth-js": "2.72.0", + "@supabase/functions-js": "2.5.0", + "@supabase/node-fetch": "2.6.15", + "@supabase/postgrest-js": "1.21.4", + "@supabase/realtime-js": "2.15.5", + "@supabase/storage-js": "2.12.2" + } + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -2154,12 +2265,17 @@ "version": "20.19.19", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.19.tgz", "integrity": "sha512-pb1Uqj5WJP7wrcbLU7Ru4QtA0+3kAXrkutGiD26wUKzSMgNNaPARTUDQmElUXp64kh3cWdou3Q0C7qwwxqSFmg==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } }, + "node_modules/@types/phoenix": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz", + "integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==", + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.2.0", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.0.tgz", @@ -2203,6 +2319,15 @@ "@types/webidl-conversions": "*" } }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.45.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.45.0.tgz", @@ -3261,6 +3386,15 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -7280,7 +7414,6 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, "license": "MIT" }, "node_modules/unrs-resolver": { @@ -7539,6 +7672,27 @@ "node": ">=0.10.0" } }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/yallist": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", diff --git a/package.json b/package.json index 532fca4..96193f2 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,8 @@ "@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/mongodb": "^4.0.6", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1",