From f6d82f67ba5eb97b874bdbd0aae571ae2a04895a Mon Sep 17 00:00:00 2001
From: michaelschal <35957947-michaelschal@users.noreply.replit.com>
Date: Wed, 1 Oct 2025 17:00:09 +0000
Subject: [PATCH] Add user management features for administrators to control
accounts
Implement CRUD operations for users, including listing, creating, editing, and deleting users, along with updates to authentication and UI components.
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: ae4037a0-2a6f-4530-9bac-79b543286bda
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/397bca8c-984f-43ff-841a-10897aeb8140/ae4037a0-2a6f-4530-9bac-79b543286bda/HPCBgsr
---
.replit | 4 +
client/src/components/sidebar.tsx | 8 +-
client/src/pages/login.tsx | 29 +-
client/src/pages/users-admin.tsx | 437 +++++++++++++++++++++++-------
server/routes.ts | 115 +++++++-
server/storage.ts | 16 ++
6 files changed, 507 insertions(+), 102 deletions(-)
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
-
-
-
-
+
+ {/* Dialog de modification */}
+
+
+ {/* 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));