mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-11 17:26:45 +02:00
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:
1 parent
ebc581e015
commit
f6d82f67ba
6 files changed
+507
-102
No files matched your search
@@ -14,6 +14,10 @@ run = ["npm", "run", "start"]
|
|||||||
localPort = 5000
|
localPort = 5000
|
||||||
externalPort = 80
|
externalPort = 80
|
||||||
|
|
||||||
|
[[ports]]
|
||||||
|
localPort = 46679
|
||||||
|
externalPort = 3000
|
||||||
|
|
||||||
[env]
|
[env]
|
||||||
PORT = "5000"
|
PORT = "5000"
|
||||||
|
|
||||||
|
|||||||
@@ -11,12 +11,12 @@ export default function Sidebar() {
|
|||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
// Charger la session utilisateur
|
// Charger la session utilisateur
|
||||||
const { data: session } = useQuery({
|
const { data: session } = useQuery<{ id: string; username: string; role: string }>({
|
||||||
queryKey: ["/api/auth/session"],
|
queryKey: ["/api/auth/session"],
|
||||||
retry: false,
|
retry: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const isAdmin = (session as any)?.role === "admin";
|
const isAdmin = session?.role === "admin";
|
||||||
|
|
||||||
// Mutation pour déconnexion
|
// Mutation pour déconnexion
|
||||||
const logoutMutation = useMutation({
|
const logoutMutation = useMutation({
|
||||||
@@ -191,8 +191,8 @@ export default function Sidebar() {
|
|||||||
{session && (
|
{session && (
|
||||||
<div className="mt-4 px-4 py-3 bg-muted rounded-lg">
|
<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-xs text-muted-foreground">Connecté en tant que</p>
|
||||||
<p className="text-sm font-semibold truncate">{(session as any).username}</p>
|
<p className="text-sm font-semibold truncate">{session.username}</p>
|
||||||
<p className="text-xs text-muted-foreground capitalize">{(session as any).role}</p>
|
<p className="text-xs text-muted-foreground capitalize">{session.role}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useLocation } from "wouter";
|
import { useLocation } from "wouter";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
@@ -14,6 +14,22 @@ export default function Login() {
|
|||||||
const [username, setUsername] = useState("");
|
const [username, setUsername] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
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) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -102,10 +118,13 @@ export default function Login() {
|
|||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div className="mt-6 text-center text-sm text-muted-foreground">
|
{showDefaultPassword && (
|
||||||
<p>Identifiants par défaut :</p>
|
<div className="mt-6 text-center text-sm text-muted-foreground">
|
||||||
<p>admin / admin</p>
|
<p>Identifiants par défaut :</p>
|
||||||
</div>
|
<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>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,16 +7,44 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
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 { useToast } from "@/hooks/use-toast";
|
||||||
import { apiRequest, queryClient } from "@/lib/queryClient";
|
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() {
|
export default function UsersAdmin() {
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
// Form states
|
||||||
const [username, setUsername] = useState("");
|
const [username, setUsername] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [role, setRole] = useState<"admin" | "user">("user");
|
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
|
// Mutation pour créer un utilisateur
|
||||||
const createUserMutation = useMutation({
|
const createUserMutation = useMutation({
|
||||||
@@ -29,10 +57,10 @@ export default function UsersAdmin() {
|
|||||||
title: "Utilisateur créé",
|
title: "Utilisateur créé",
|
||||||
description: `L'utilisateur ${data.username} a été créé avec succès`,
|
description: `L'utilisateur ${data.username} a été créé avec succès`,
|
||||||
});
|
});
|
||||||
// Réinitialiser le formulaire
|
|
||||||
setUsername("");
|
setUsername("");
|
||||||
setPassword("");
|
setPassword("");
|
||||||
setRole("user");
|
setRole("user");
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["/api/users"] });
|
||||||
},
|
},
|
||||||
onError: (error: any) => {
|
onError: (error: any) => {
|
||||||
toast({
|
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) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
@@ -67,6 +143,54 @@ export default function UsersAdmin() {
|
|||||||
createUserMutation.mutate({ username, password, role });
|
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 (
|
return (
|
||||||
<div className="flex h-screen overflow-hidden bg-background">
|
<div className="flex h-screen overflow-hidden bg-background">
|
||||||
{sidebarOpen && (
|
{sidebarOpen && (
|
||||||
@@ -97,103 +221,232 @@ export default function UsersAdmin() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="max-w-2xl">
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
<Card>
|
{/* Liste des utilisateurs */}
|
||||||
<CardHeader>
|
<div className="lg:col-span-2">
|
||||||
<CardTitle className="flex items-center gap-2">
|
<Card>
|
||||||
<UserPlus className="w-5 h-5" />
|
<CardHeader>
|
||||||
Créer un nouvel utilisateur
|
<CardTitle>Liste des utilisateurs</CardTitle>
|
||||||
</CardTitle>
|
<CardDescription>
|
||||||
<CardDescription>
|
{users?.length || 0} utilisateur(s) enregistré(s)
|
||||||
Seuls les administrateurs peuvent créer des utilisateurs
|
</CardDescription>
|
||||||
</CardDescription>
|
</CardHeader>
|
||||||
</CardHeader>
|
<CardContent>
|
||||||
<CardContent>
|
{isLoading ? (
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
<div className="text-center py-8">
|
||||||
<div className="space-y-2">
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto"></div>
|
||||||
<Label htmlFor="username">Nom d'utilisateur</Label>
|
<p className="text-muted-foreground mt-4">Chargement...</p>
|
||||||
<Input
|
</div>
|
||||||
id="username"
|
) : users && users.length > 0 ? (
|
||||||
type="text"
|
<Table>
|
||||||
placeholder="utilisateur123"
|
<TableHeader>
|
||||||
value={username}
|
<TableRow>
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
<TableHead>Nom d'utilisateur</TableHead>
|
||||||
required
|
<TableHead>Rôle</TableHead>
|
||||||
data-testid="input-new-username"
|
<TableHead className="text-right">Actions</TableHead>
|
||||||
/>
|
</TableRow>
|
||||||
</div>
|
</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">
|
{/* Formulaire de création */}
|
||||||
<Label htmlFor="password">Mot de passe</Label>
|
<div>
|
||||||
<Input
|
<Card>
|
||||||
id="password"
|
<CardHeader>
|
||||||
type="password"
|
<CardTitle className="flex items-center gap-2">
|
||||||
placeholder="••••••••"
|
<UserPlus className="w-5 h-5" />
|
||||||
value={password}
|
Créer un utilisateur
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
</CardTitle>
|
||||||
required
|
<CardDescription>
|
||||||
data-testid="input-new-password"
|
Ajouter un nouvel utilisateur
|
||||||
/>
|
</CardDescription>
|
||||||
<p className="text-xs text-muted-foreground">
|
</CardHeader>
|
||||||
Minimum 4 caractères
|
<CardContent>
|
||||||
</p>
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
</div>
|
<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">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="role">Rôle</Label>
|
<Label htmlFor="password">Mot de passe</Label>
|
||||||
<Select value={role} onValueChange={(value: "admin" | "user") => setRole(value)}>
|
<Input
|
||||||
<SelectTrigger id="role" data-testid="select-user-role">
|
id="password"
|
||||||
<SelectValue />
|
type="password"
|
||||||
</SelectTrigger>
|
placeholder="••••••••"
|
||||||
<SelectContent>
|
value={password}
|
||||||
<SelectItem value="user">Utilisateur</SelectItem>
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
<SelectItem value="admin">Administrateur</SelectItem>
|
required
|
||||||
</SelectContent>
|
data-testid="input-new-password"
|
||||||
</Select>
|
/>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Les administrateurs ont accès à tous les paramètres et à la gestion SQL
|
Minimum 4 caractères
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<div className="space-y-2">
|
||||||
type="submit"
|
<Label htmlFor="role">Rôle</Label>
|
||||||
className="w-full"
|
<Select value={role} onValueChange={(value: "admin" | "user") => setRole(value)}>
|
||||||
disabled={createUserMutation.isPending}
|
<SelectTrigger id="role" data-testid="select-user-role">
|
||||||
data-testid="button-create-user"
|
<SelectValue />
|
||||||
>
|
</SelectTrigger>
|
||||||
{createUserMutation.isPending ? "Création en cours..." : "Créer l'utilisateur"}
|
<SelectContent>
|
||||||
</Button>
|
<SelectItem value="user">Utilisateur</SelectItem>
|
||||||
</form>
|
<SelectItem value="admin">Administrateur</SelectItem>
|
||||||
</CardContent>
|
</SelectContent>
|
||||||
</Card>
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Card className="mt-6">
|
<Button
|
||||||
<CardHeader>
|
type="submit"
|
||||||
<CardTitle>Commandes SQL utiles</CardTitle>
|
className="w-full"
|
||||||
<CardDescription>
|
disabled={createUserMutation.isPending}
|
||||||
Utilisez ces commandes dans l'onglet SQL pour gérer les utilisateurs
|
data-testid="button-create-user"
|
||||||
</CardDescription>
|
>
|
||||||
</CardHeader>
|
{createUserMutation.isPending ? "Création..." : "Créer"}
|
||||||
<CardContent className="space-y-4">
|
</Button>
|
||||||
<div className="bg-muted p-4 rounded-lg">
|
</form>
|
||||||
<p className="text-sm font-semibold mb-2">Voir tous les utilisateurs :</p>
|
</CardContent>
|
||||||
<code className="text-xs">SELECT id, username, role FROM users;</code>
|
</Card>
|
||||||
</div>
|
</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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+114
-1
@@ -8,7 +8,7 @@ import { z } from "zod";
|
|||||||
import { openRouterService } from "./services/openrouter";
|
import { openRouterService } from "./services/openrouter";
|
||||||
import { cloudinaryService } from "./services/cloudinary";
|
import { cloudinaryService } from "./services/cloudinary";
|
||||||
import { insertPostSchema, insertScheduledPostSchema, insertSocialPageSchema, insertAiGenerationSchema, insertCloudinaryConfigSchema, insertOpenrouterConfigSchema, insertUserSchema } from "@shared/schema";
|
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() });
|
const upload = multer({ storage: multer.memoryStorage() });
|
||||||
|
|
||||||
@@ -78,6 +78,25 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 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)
|
// Route pour créer un nouvel utilisateur (réservée aux admins)
|
||||||
app.post("/api/users", requireAdmin, async (req, res) => {
|
app.post("/api/users", requireAdmin, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
@@ -118,6 +137,100 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 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<InsertUser> = {};
|
||||||
|
|
||||||
|
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)
|
// Route SQL (réservée aux admins)
|
||||||
app.post("/api/sql/execute", requireAdmin, async (req, res) => {
|
app.post("/api/sql/execute", requireAdmin, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -31,7 +31,10 @@ export interface IStorage {
|
|||||||
// Users
|
// Users
|
||||||
getUser(id: string): Promise<User | undefined>;
|
getUser(id: string): Promise<User | undefined>;
|
||||||
getUserByUsername(username: string): Promise<User | undefined>;
|
getUserByUsername(username: string): Promise<User | undefined>;
|
||||||
|
getAllUsers(): Promise<User[]>;
|
||||||
createUser(user: InsertUser): Promise<User>;
|
createUser(user: InsertUser): Promise<User>;
|
||||||
|
updateUser(id: string, user: Partial<InsertUser>): Promise<User>;
|
||||||
|
deleteUser(id: string): Promise<void>;
|
||||||
|
|
||||||
// Social Pages
|
// Social Pages
|
||||||
getSocialPages(userId: string): Promise<SocialPage[]>;
|
getSocialPages(userId: string): Promise<SocialPage[]>;
|
||||||
@@ -88,11 +91,24 @@ export class DatabaseStorage implements IStorage {
|
|||||||
return user || undefined;
|
return user || undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getAllUsers(): Promise<User[]> {
|
||||||
|
return await db.select().from(users);
|
||||||
|
}
|
||||||
|
|
||||||
async createUser(insertUser: InsertUser): Promise<User> {
|
async createUser(insertUser: InsertUser): Promise<User> {
|
||||||
const [user] = await db.insert(users).values(insertUser).returning();
|
const [user] = await db.insert(users).values(insertUser).returning();
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateUser(id: string, updateData: Partial<InsertUser>): Promise<User> {
|
||||||
|
const [updated] = await db.update(users).set(updateData).where(eq(users.id, id)).returning();
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteUser(id: string): Promise<void> {
|
||||||
|
await db.delete(users).where(eq(users.id, id));
|
||||||
|
}
|
||||||
|
|
||||||
// Social Pages
|
// Social Pages
|
||||||
async getSocialPages(userId: string): Promise<SocialPage[]> {
|
async getSocialPages(userId: string): Promise<SocialPage[]> {
|
||||||
return await db.select().from(socialPages).where(eq(socialPages.userId, userId));
|
return await db.select().from(socialPages).where(eq(socialPages.userId, userId));
|
||||||
|
|||||||
Reference in new issue
Block a user