diff --git a/.replit b/.replit index 3238a51..983a4bc 100644 --- a/.replit +++ b/.replit @@ -14,6 +14,10 @@ run = ["npm", "run", "start"] localPort = 5000 externalPort = 80 +[[ports]] +localPort = 46679 +externalPort = 3000 + [env] PORT = "5000" diff --git a/client/src/components/sidebar.tsx b/client/src/components/sidebar.tsx index 31ca9ac..657e772 100644 --- a/client/src/components/sidebar.tsx +++ b/client/src/components/sidebar.tsx @@ -11,12 +11,12 @@ export default function Sidebar() { const { toast } = useToast(); // Charger la session utilisateur - const { data: session } = useQuery({ + const { data: session } = useQuery<{ id: string; username: string; role: string }>({ queryKey: ["/api/auth/session"], retry: false, }); - const isAdmin = (session as any)?.role === "admin"; + const isAdmin = session?.role === "admin"; // Mutation pour déconnexion const logoutMutation = useMutation({ @@ -191,8 +191,8 @@ export default function Sidebar() { {session && (

Connecté en tant que

-

{(session as any).username}

-

{(session as any).role}

+

{session.username}

+

{session.role}

)} diff --git a/client/src/pages/login.tsx b/client/src/pages/login.tsx index ab24eee..605242e 100644 --- a/client/src/pages/login.tsx +++ b/client/src/pages/login.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; import { useLocation } from "wouter"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; @@ -14,6 +14,22 @@ export default function Login() { const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); const [isLoading, setIsLoading] = useState(false); + const [showDefaultPassword, setShowDefaultPassword] = useState(false); + + // Vérifier si le mot de passe admin par défaut est toujours actif + useEffect(() => { + const checkDefaultPassword = async () => { + try { + const response = await fetch("/api/auth/default-password-status"); + const data = await response.json(); + setShowDefaultPassword(data.isDefault === true); + } catch (error) { + console.error("Error checking default password status:", error); + } + }; + + checkDefaultPassword(); + }, []); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -102,10 +118,13 @@ export default function Login() { -
-

Identifiants par défaut :

-

admin / admin

-
+ {showDefaultPassword && ( +
+

Identifiants par défaut :

+

admin / admin

+

⚠️ Changez ce mot de passe après la première connexion

+
+ )} diff --git a/client/src/pages/users-admin.tsx b/client/src/pages/users-admin.tsx index 2f02c88..a3af7d0 100644 --- a/client/src/pages/users-admin.tsx +++ b/client/src/pages/users-admin.tsx @@ -7,16 +7,44 @@ 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 { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { Badge } from "@/components/ui/badge"; import { useToast } from "@/hooks/use-toast"; import { apiRequest, queryClient } from "@/lib/queryClient"; -import { Users, UserPlus } from "lucide-react"; +import { Users, UserPlus, Pencil, Trash2, Shield, User as UserIcon } from "lucide-react"; + +type UserData = { + id: string; + username: string; + role: "admin" | "user"; +}; export default function UsersAdmin() { const [sidebarOpen, setSidebarOpen] = useState(false); const { toast } = useToast(); + + // Form states const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); const [role, setRole] = useState<"admin" | "user">("user"); + + // Edit dialog states + const [editDialogOpen, setEditDialogOpen] = useState(false); + const [editingUser, setEditingUser] = useState(null); + const [editUsername, setEditUsername] = useState(""); + const [editPassword, setEditPassword] = useState(""); + const [editRole, setEditRole] = useState<"admin" | "user">("user"); + + // Delete dialog states + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [userToDelete, setUserToDelete] = useState(null); + + // Charger la liste des utilisateurs + const { data: users, isLoading } = useQuery({ + queryKey: ["/api/users"], + }); // Mutation pour créer un utilisateur const createUserMutation = useMutation({ @@ -29,10 +57,10 @@ export default function UsersAdmin() { title: "Utilisateur créé", description: `L'utilisateur ${data.username} a été créé avec succès`, }); - // Réinitialiser le formulaire setUsername(""); setPassword(""); setRole("user"); + queryClient.invalidateQueries({ queryKey: ["/api/users"] }); }, onError: (error: any) => { toast({ @@ -43,6 +71,54 @@ export default function UsersAdmin() { }, }); + // Mutation pour modifier un utilisateur + const updateUserMutation = useMutation({ + mutationFn: async ({ id, data }: { id: string; data: any }) => { + const res = await apiRequest("PATCH", `/api/users/${id}`, data); + return await res.json(); + }, + onSuccess: (data) => { + toast({ + title: "Utilisateur modifié", + description: `L'utilisateur ${data.username} a été modifié avec succès`, + }); + setEditDialogOpen(false); + setEditingUser(null); + queryClient.invalidateQueries({ queryKey: ["/api/users"] }); + }, + onError: (error: any) => { + toast({ + variant: "destructive", + title: "Erreur", + description: error.message || "Erreur lors de la modification de l'utilisateur", + }); + }, + }); + + // Mutation pour supprimer un utilisateur + const deleteUserMutation = useMutation({ + mutationFn: async (id: string) => { + const res = await apiRequest("DELETE", `/api/users/${id}`, {}); + return await res.json(); + }, + onSuccess: () => { + toast({ + title: "Utilisateur supprimé", + description: "L'utilisateur a été supprimé avec succès", + }); + setDeleteDialogOpen(false); + setUserToDelete(null); + queryClient.invalidateQueries({ queryKey: ["/api/users"] }); + }, + onError: (error: any) => { + toast({ + variant: "destructive", + title: "Erreur", + description: error.message || "Erreur lors de la suppression de l'utilisateur", + }); + }, + }); + const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); @@ -67,6 +143,54 @@ export default function UsersAdmin() { createUserMutation.mutate({ username, password, role }); }; + const handleEditClick = (user: UserData) => { + setEditingUser(user); + setEditUsername(user.username); + setEditPassword(""); + setEditRole(user.role); + setEditDialogOpen(true); + }; + + const handleEditSubmit = () => { + if (!editingUser) return; + + const updateData: any = {}; + + if (editUsername !== editingUser.username) { + updateData.username = editUsername; + } + + if (editPassword) { + updateData.password = editPassword; + } + + if (editRole !== editingUser.role) { + updateData.role = editRole; + } + + if (Object.keys(updateData).length === 0) { + toast({ + variant: "destructive", + title: "Erreur", + description: "Aucune modification détectée", + }); + return; + } + + updateUserMutation.mutate({ id: editingUser.id, data: updateData }); + }; + + const handleDeleteClick = (user: UserData) => { + setUserToDelete(user); + setDeleteDialogOpen(true); + }; + + const handleDeleteConfirm = () => { + if (userToDelete) { + deleteUserMutation.mutate(userToDelete.id); + } + }; + return (
{sidebarOpen && ( @@ -97,103 +221,232 @@ export default function UsersAdmin() {

-
- - - - - Créer un nouvel utilisateur - - - Seuls les administrateurs peuvent créer des utilisateurs - - - -
-
- - setUsername(e.target.value)} - required - data-testid="input-new-username" - /> -
+
+ {/* Liste des utilisateurs */} +
+ + + Liste des utilisateurs + + {users?.length || 0} utilisateur(s) enregistré(s) + + + + {isLoading ? ( +
+
+

Chargement...

+
+ ) : users && users.length > 0 ? ( + + + + Nom d'utilisateur + Rôle + Actions + + + + {users.map((user) => ( + + + {user.role === "admin" ? ( + + ) : ( + + )} + {user.username} + + + + {user.role === "admin" ? "Administrateur" : "Utilisateur"} + + + +
+ + +
+
+
+ ))} +
+
+ ) : ( +
+

Aucun utilisateur trouvé

+
+ )} +
+
+
-
- - setPassword(e.target.value)} - required - data-testid="input-new-password" - /> -

- Minimum 4 caractères -

-
+ {/* Formulaire de création */} +
+ + + + + Créer un utilisateur + + + Ajouter un nouvel utilisateur + + + + +
+ + setUsername(e.target.value)} + required + data-testid="input-new-username" + /> +
-
- - -

- Les administrateurs ont accès à tous les paramètres et à la gestion SQL -

-
+
+ + setPassword(e.target.value)} + required + data-testid="input-new-password" + /> +

+ Minimum 4 caractères +

+
- - -
-
+
+ + +
- - - Commandes SQL utiles - - Utilisez ces commandes dans l'onglet SQL pour gérer les utilisateurs - - - -
-

Voir tous les utilisateurs :

- SELECT id, username, role FROM users; -
- -
-

Changer le rôle d'un utilisateur :

- UPDATE users SET role = 'admin' WHERE username = 'nom_utilisateur'; -
- -
-

Supprimer un utilisateur :

- DELETE FROM users WHERE username = 'nom_utilisateur'; -
-
-
+ + + + +
+ + {/* Dialog de modification */} + + + + Modifier l'utilisateur + + Modifiez les informations de l'utilisateur. Laissez le mot de passe vide pour ne pas le changer. + + +
+
+ + setEditUsername(e.target.value)} + data-testid="input-edit-username" + /> +
+
+ + setEditPassword(e.target.value)} + data-testid="input-edit-password" + /> +
+
+ + +
+
+ + + + +
+
+ + {/* Dialog de confirmation de suppression */} + + + + Confirmer la suppression + + Êtes-vous sûr de vouloir supprimer l'utilisateur {userToDelete?.username} ? + Cette action est irréversible. + + + + Annuler + + {deleteUserMutation.isPending ? "Suppression..." : "Supprimer"} + + + + ); } diff --git a/server/routes.ts b/server/routes.ts index ba89c3c..c5bfd81 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -8,7 +8,7 @@ import { z } from "zod"; import { openRouterService } from "./services/openrouter"; import { cloudinaryService } from "./services/cloudinary"; import { insertPostSchema, insertScheduledPostSchema, insertSocialPageSchema, insertAiGenerationSchema, insertCloudinaryConfigSchema, insertOpenrouterConfigSchema, insertUserSchema } from "@shared/schema"; -import type { User } from "@shared/schema"; +import type { User, InsertUser } from "@shared/schema"; const upload = multer({ storage: multer.memoryStorage() }); @@ -78,6 +78,25 @@ export async function registerRoutes(app: Express): Promise { } }); + // Route pour obtenir la liste de tous les utilisateurs (réservée aux admins) + app.get("/api/users", requireAdmin, async (req, res) => { + try { + const allUsers = await storage.getAllUsers(); + + // Ne pas envoyer les mots de passe + const safeUsers = allUsers.map(user => ({ + id: user.id, + username: user.username, + role: user.role, + })); + + res.json(safeUsers); + } catch (error) { + console.error("Error fetching users:", error); + res.status(500).json({ error: "Erreur lors de la récupération des utilisateurs" }); + } + }); + // Route pour créer un nouvel utilisateur (réservée aux admins) app.post("/api/users", requireAdmin, async (req, res) => { try { @@ -118,6 +137,100 @@ export async function registerRoutes(app: Express): Promise { } }); + // Route pour modifier un utilisateur (réservée aux admins) + app.patch("/api/users/:id", requireAdmin, async (req, res) => { + try { + const userId = req.params.id; + const { username, password, role } = req.body; + + // Vérifier si l'utilisateur existe + const existingUser = await storage.getUser(userId); + if (!existingUser) { + return res.status(404).json({ error: "Utilisateur non trouvé" }); + } + + const updateData: Partial = {}; + + if (username && username !== existingUser.username) { + // Vérifier si le nouveau username est déjà pris + const userWithSameUsername = await storage.getUserByUsername(username); + if (userWithSameUsername && userWithSameUsername.id !== userId) { + return res.status(409).json({ error: "Ce nom d'utilisateur est déjà utilisé" }); + } + updateData.username = username; + } + + if (password) { + // Hasher le nouveau mot de passe + updateData.password = await bcrypt.hash(password, 10); + } + + if (role && (role === "admin" || role === "user")) { + updateData.role = role; + } + + if (Object.keys(updateData).length === 0) { + return res.status(400).json({ error: "Aucune modification fournie" }); + } + + const updatedUser = await storage.updateUser(userId, updateData); + + res.json({ + id: updatedUser.id, + username: updatedUser.username, + role: updatedUser.role, + }); + } catch (error: any) { + console.error("Error updating user:", error); + res.status(500).json({ error: "Erreur lors de la modification de l'utilisateur" }); + } + }); + + // Route pour supprimer un utilisateur (réservée aux admins) + app.delete("/api/users/:id", requireAdmin, async (req, res) => { + try { + const userId = req.params.id; + const currentUser = req.user as User; + + // Empêcher l'admin de se supprimer lui-même + if (userId === currentUser.id) { + return res.status(400).json({ error: "Vous ne pouvez pas supprimer votre propre compte" }); + } + + // Vérifier si l'utilisateur existe + const existingUser = await storage.getUser(userId); + if (!existingUser) { + return res.status(404).json({ error: "Utilisateur non trouvé" }); + } + + await storage.deleteUser(userId); + + res.json({ success: true, message: "Utilisateur supprimé avec succès" }); + } catch (error: any) { + console.error("Error deleting user:", error); + res.status(500).json({ error: "Erreur lors de la suppression de l'utilisateur" }); + } + }); + + // Route pour vérifier si le mot de passe admin par défaut a été changé + app.get("/api/auth/default-password-status", async (req, res) => { + try { + const adminUser = await storage.getUserByUsername("admin"); + + if (!adminUser) { + return res.json({ isDefault: false }); + } + + // Vérifier si le mot de passe correspond à "admin" + const isDefaultPassword = await bcrypt.compare("admin", adminUser.password); + + res.json({ isDefault: isDefaultPassword }); + } catch (error) { + console.error("Error checking default password:", error); + res.status(500).json({ error: "Erreur lors de la vérification" }); + } + }); + // Route SQL (réservée aux admins) app.post("/api/sql/execute", requireAdmin, async (req, res) => { try { diff --git a/server/storage.ts b/server/storage.ts index d3485b2..4f5a9b1 100644 --- a/server/storage.ts +++ b/server/storage.ts @@ -31,7 +31,10 @@ export interface IStorage { // Users getUser(id: string): Promise; getUserByUsername(username: string): Promise; + getAllUsers(): Promise; createUser(user: InsertUser): Promise; + updateUser(id: string, user: Partial): Promise; + deleteUser(id: string): Promise; // Social Pages getSocialPages(userId: string): Promise; @@ -88,11 +91,24 @@ export class DatabaseStorage implements IStorage { return user || undefined; } + async getAllUsers(): Promise { + return await db.select().from(users); + } + async createUser(insertUser: InsertUser): Promise { const [user] = await db.insert(users).values(insertUser).returning(); return user; } + async updateUser(id: string, updateData: Partial): Promise { + const [updated] = await db.update(users).set(updateData).where(eq(users.id, id)).returning(); + return updated; + } + + async deleteUser(id: string): Promise { + await db.delete(users).where(eq(users.id, id)); + } + // Social Pages async getSocialPages(userId: string): Promise { return await db.select().from(socialPages).where(eq(socialPages.userId, userId));