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
This commit is contained in:
michaelschal committed 2025-10-01 17:00:09 +00:00
1 parent ebc581e015
commit f6d82f67ba
6 files changed
+507 -102

No files matched your search

+4 -4
View File
@@ -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 && (
<div className="mt-4 px-4 py-3 bg-muted rounded-lg">
<p className="text-xs text-muted-foreground">Connecté en tant que</p>
<p className="text-sm font-semibold truncate">{(session as any).username}</p>
<p className="text-xs text-muted-foreground capitalize">{(session as any).role}</p>
<p className="text-sm font-semibold truncate">{session.username}</p>
<p className="text-xs text-muted-foreground capitalize">{session.role}</p>
</div>
)}
</div>
+24 -5
View File
@@ -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() {
</Button>
</form>
<div className="mt-6 text-center text-sm text-muted-foreground">
<p>Identifiants par défaut :</p>
<p>admin / admin</p>
</div>
{showDefaultPassword && (
<div className="mt-6 text-center text-sm text-muted-foreground">
<p>Identifiants par défaut :</p>
<p className="font-semibold">admin / admin</p>
<p className="text-xs mt-2 text-orange-500">⚠️ Changez ce mot de passe après la première connexion</p>
</div>
)}
</CardContent>
</Card>
</div>
+345 -92
View File
@@ -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<UserData | null>(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<UserData | null>(null);
// Charger la liste des utilisateurs
const { data: users, isLoading } = useQuery<UserData[]>({
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 (
<div className="flex h-screen overflow-hidden bg-background">
{sidebarOpen && (
@@ -97,103 +221,232 @@ export default function UsersAdmin() {
</p>
</div>
<div className="max-w-2xl">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<UserPlus className="w-5 h-5" />
Créer un nouvel utilisateur
</CardTitle>
<CardDescription>
Seuls les administrateurs peuvent créer des utilisateurs
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="username">Nom d'utilisateur</Label>
<Input
id="username"
type="text"
placeholder="utilisateur123"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
data-testid="input-new-username"
/>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Liste des utilisateurs */}
<div className="lg:col-span-2">
<Card>
<CardHeader>
<CardTitle>Liste des utilisateurs</CardTitle>
<CardDescription>
{users?.length || 0} utilisateur(s) enregistré(s)
</CardDescription>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="text-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto"></div>
<p className="text-muted-foreground mt-4">Chargement...</p>
</div>
) : users && users.length > 0 ? (
<Table>
<TableHeader>
<TableRow>
<TableHead>Nom d'utilisateur</TableHead>
<TableHead>Rôle</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{users.map((user) => (
<TableRow key={user.id}>
<TableCell className="font-medium flex items-center gap-2">
{user.role === "admin" ? (
<Shield className="w-4 h-4 text-primary" />
) : (
<UserIcon className="w-4 h-4 text-muted-foreground" />
)}
{user.username}
</TableCell>
<TableCell>
<Badge variant={user.role === "admin" ? "default" : "secondary"}>
{user.role === "admin" ? "Administrateur" : "Utilisateur"}
</Badge>
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2">
<Button
variant="outline"
size="sm"
onClick={() => handleEditClick(user)}
data-testid={`button-edit-user-${user.id}`}
>
<Pencil className="w-4 h-4" />
</Button>
<Button
variant="destructive"
size="sm"
onClick={() => handleDeleteClick(user)}
data-testid={`button-delete-user-${user.id}`}
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
) : (
<div className="text-center py-8">
<p className="text-muted-foreground">Aucun utilisateur trouvé</p>
</div>
)}
</CardContent>
</Card>
</div>
<div className="space-y-2">
<Label htmlFor="password">Mot de passe</Label>
<Input
id="password"
type="password"
placeholder="••••••••"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
data-testid="input-new-password"
/>
<p className="text-xs text-muted-foreground">
Minimum 4 caractères
</p>
</div>
{/* Formulaire de création */}
<div>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<UserPlus className="w-5 h-5" />
Créer un utilisateur
</CardTitle>
<CardDescription>
Ajouter un nouvel utilisateur
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="username">Nom d'utilisateur</Label>
<Input
id="username"
type="text"
placeholder="utilisateur123"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
data-testid="input-new-username"
/>
</div>
<div className="space-y-2">
<Label htmlFor="role">Rôle</Label>
<Select value={role} onValueChange={(value: "admin" | "user") => setRole(value)}>
<SelectTrigger id="role" data-testid="select-user-role">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="user">Utilisateur</SelectItem>
<SelectItem value="admin">Administrateur</SelectItem>
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">
Les administrateurs ont accès à tous les paramètres et à la gestion SQL
</p>
</div>
<div className="space-y-2">
<Label htmlFor="password">Mot de passe</Label>
<Input
id="password"
type="password"
placeholder="••••••••"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
data-testid="input-new-password"
/>
<p className="text-xs text-muted-foreground">
Minimum 4 caractères
</p>
</div>
<Button
type="submit"
className="w-full"
disabled={createUserMutation.isPending}
data-testid="button-create-user"
>
{createUserMutation.isPending ? "Création en cours..." : "Créer l'utilisateur"}
</Button>
</form>
</CardContent>
</Card>
<div className="space-y-2">
<Label htmlFor="role">Rôle</Label>
<Select value={role} onValueChange={(value: "admin" | "user") => setRole(value)}>
<SelectTrigger id="role" data-testid="select-user-role">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="user">Utilisateur</SelectItem>
<SelectItem value="admin">Administrateur</SelectItem>
</SelectContent>
</Select>
</div>
<Card className="mt-6">
<CardHeader>
<CardTitle>Commandes SQL utiles</CardTitle>
<CardDescription>
Utilisez ces commandes dans l'onglet SQL pour gérer les utilisateurs
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="bg-muted p-4 rounded-lg">
<p className="text-sm font-semibold mb-2">Voir tous les utilisateurs :</p>
<code className="text-xs">SELECT id, username, role FROM users;</code>
</div>
<div className="bg-muted p-4 rounded-lg">
<p className="text-sm font-semibold mb-2">Changer le rôle d'un utilisateur :</p>
<code className="text-xs">UPDATE users SET role = 'admin' WHERE username = 'nom_utilisateur';</code>
</div>
<div className="bg-muted p-4 rounded-lg">
<p className="text-sm font-semibold mb-2">Supprimer un utilisateur :</p>
<code className="text-xs">DELETE FROM users WHERE username = 'nom_utilisateur';</code>
</div>
</CardContent>
</Card>
<Button
type="submit"
className="w-full"
disabled={createUserMutation.isPending}
data-testid="button-create-user"
>
{createUserMutation.isPending ? "Création..." : "Créer"}
</Button>
</form>
</CardContent>
</Card>
</div>
</div>
</div>
</main>
{/* Dialog de modification */}
<Dialog open={editDialogOpen} onOpenChange={setEditDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Modifier l'utilisateur</DialogTitle>
<DialogDescription>
Modifiez les informations de l'utilisateur. Laissez le mot de passe vide pour ne pas le changer.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="edit-username">Nom d'utilisateur</Label>
<Input
id="edit-username"
value={editUsername}
onChange={(e) => setEditUsername(e.target.value)}
data-testid="input-edit-username"
/>
</div>
<div className="space-y-2">
<Label htmlFor="edit-password">Nouveau mot de passe (optionnel)</Label>
<Input
id="edit-password"
type="password"
placeholder="Laisser vide pour ne pas changer"
value={editPassword}
onChange={(e) => setEditPassword(e.target.value)}
data-testid="input-edit-password"
/>
</div>
<div className="space-y-2">
<Label htmlFor="edit-role">Rôle</Label>
<Select value={editRole} onValueChange={(value: "admin" | "user") => setEditRole(value)}>
<SelectTrigger id="edit-role" data-testid="select-edit-role">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="user">Utilisateur</SelectItem>
<SelectItem value="admin">Administrateur</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setEditDialogOpen(false)}>
Annuler
</Button>
<Button
onClick={handleEditSubmit}
disabled={updateUserMutation.isPending}
data-testid="button-save-edit"
>
{updateUserMutation.isPending ? "Enregistrement..." : "Enregistrer"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Dialog de confirmation de suppression */}
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Confirmer la suppression</AlertDialogTitle>
<AlertDialogDescription>
Êtes-vous sûr de vouloir supprimer l'utilisateur <strong>{userToDelete?.username}</strong> ?
Cette action est irréversible.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Annuler</AlertDialogCancel>
<AlertDialogAction
onClick={handleDeleteConfirm}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
data-testid="button-confirm-delete"
>
{deleteUserMutation.isPending ? "Suppression..." : "Supprimer"}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
}