mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-21 05:58:12 +02:00
Add user authentication and admin roles to manage application access
Implement user login, session management, and role-based access control for users and administrators. Includes new admin pages for user management and SQL query execution. 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
7b97fb4839
commit
5ee75edf8c
12 files changed
+1081
-61
No files matched your search
+61
-11
@@ -1,9 +1,11 @@
|
||||
import { Switch, Route } from "wouter";
|
||||
import { Switch, Route, Redirect, useLocation } from "wouter";
|
||||
import { useEffect } from "react";
|
||||
import { queryClient } from "./lib/queryClient";
|
||||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import { QueryClientProvider, useQuery } from "@tanstack/react-query";
|
||||
import { Toaster } from "@/components/ui/toaster";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import NotFound from "@/pages/not-found";
|
||||
import Login from "@/pages/login";
|
||||
import Dashboard from "@/pages/dashboard";
|
||||
import NewPost from "@/pages/new-post";
|
||||
import Calendar from "@/pages/calendar";
|
||||
@@ -13,19 +15,67 @@ import AI from "@/pages/ai";
|
||||
import Analytics from "@/pages/analytics";
|
||||
import History from "@/pages/history";
|
||||
import Settings from "@/pages/settings";
|
||||
import SqlAdmin from "@/pages/sql";
|
||||
import UsersAdmin from "@/pages/users-admin";
|
||||
|
||||
function ProtectedRoute({ component: Component, adminOnly = false }: { component: React.ComponentType; adminOnly?: boolean }) {
|
||||
const [, setLocation] = useLocation();
|
||||
|
||||
const { data: session, isLoading } = useQuery({
|
||||
queryKey: ["/api/auth/session"],
|
||||
retry: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && !session) {
|
||||
setLocation("/login");
|
||||
}
|
||||
}, [isLoading, session, setLocation]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-screen bg-background">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto mb-4"></div>
|
||||
<p className="text-muted-foreground">Chargement...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (adminOnly && (session as any).role !== "admin") {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-screen bg-background">
|
||||
<div className="text-center">
|
||||
<h1 className="text-2xl font-bold mb-2">Accès refusé</h1>
|
||||
<p className="text-muted-foreground">Cette page est réservée aux administrateurs.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <Component />;
|
||||
}
|
||||
|
||||
function Router() {
|
||||
return (
|
||||
<Switch>
|
||||
<Route path="/" component={Dashboard} />
|
||||
<Route path="/new" component={NewPost} />
|
||||
<Route path="/calendar" component={Calendar} />
|
||||
<Route path="/media" component={Media} />
|
||||
<Route path="/pages" component={PagesManagement} />
|
||||
<Route path="/ai" component={AI} />
|
||||
<Route path="/analytics" component={Analytics} />
|
||||
<Route path="/history" component={History} />
|
||||
<Route path="/settings" component={Settings} />
|
||||
<Route path="/login" component={Login} />
|
||||
<Route path="/">{() => <ProtectedRoute component={Dashboard} />}</Route>
|
||||
<Route path="/new">{() => <ProtectedRoute component={NewPost} />}</Route>
|
||||
<Route path="/calendar">{() => <ProtectedRoute component={Calendar} />}</Route>
|
||||
<Route path="/media">{() => <ProtectedRoute component={Media} />}</Route>
|
||||
<Route path="/pages">{() => <ProtectedRoute component={PagesManagement} />}</Route>
|
||||
<Route path="/ai">{() => <ProtectedRoute component={AI} />}</Route>
|
||||
<Route path="/analytics">{() => <ProtectedRoute component={Analytics} />}</Route>
|
||||
<Route path="/history">{() => <ProtectedRoute component={History} />}</Route>
|
||||
<Route path="/settings">{() => <ProtectedRoute component={Settings} adminOnly />}</Route>
|
||||
<Route path="/sql">{() => <ProtectedRoute component={SqlAdmin} adminOnly />}</Route>
|
||||
<Route path="/users">{() => <ProtectedRoute component={UsersAdmin} adminOnly />}</Route>
|
||||
<Route component={NotFound} />
|
||||
</Switch>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,38 @@
|
||||
import { Home, PlusCircle, Calendar, Images, Users, Bot, BarChart3, Clock, Settings } from "lucide-react";
|
||||
import { Home, PlusCircle, Calendar, Images, Users, Bot, BarChart3, Clock, Settings, Database, UserCog, LogOut } from "lucide-react";
|
||||
import { Link, useLocation } from "wouter";
|
||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||
import { apiRequest, queryClient } from "@/lib/queryClient";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export default function Sidebar() {
|
||||
const [location] = useLocation();
|
||||
const [, setLocation] = useLocation();
|
||||
const { toast } = useToast();
|
||||
|
||||
// Charger la session utilisateur
|
||||
const { data: session } = useQuery({
|
||||
queryKey: ["/api/auth/session"],
|
||||
retry: false,
|
||||
});
|
||||
|
||||
const isAdmin = (session as any)?.role === "admin";
|
||||
|
||||
// Mutation pour déconnexion
|
||||
const logoutMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
const res = await apiRequest("POST", "/api/auth/logout", {});
|
||||
return await res.json();
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.clear();
|
||||
setLocation("/login");
|
||||
toast({
|
||||
title: "Déconnecté",
|
||||
description: "Vous avez été déconnecté avec succès",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const navItems = [
|
||||
{ icon: Home, label: "Tableau de bord", href: "/", badge: null },
|
||||
@@ -94,15 +124,77 @@ export default function Sidebar() {
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div className="p-4 border-t border-border">
|
||||
<Link
|
||||
href="/settings"
|
||||
className="flex items-center gap-3 px-4 py-3 rounded-lg text-muted-foreground hover:bg-accent hover:text-accent-foreground transition-all"
|
||||
data-testid="link-settings"
|
||||
>
|
||||
<Settings className="w-5 h-5" />
|
||||
<span>Paramètres</span>
|
||||
</Link>
|
||||
<div className="p-4 border-t border-border space-y-2">
|
||||
{isAdmin && (
|
||||
<>
|
||||
<div className="mb-2">
|
||||
<p className="text-xs text-muted-foreground mb-2 px-4 uppercase tracking-wider">Administration</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/users"
|
||||
className={`
|
||||
flex items-center gap-3 px-4 py-3 rounded-lg transition-all
|
||||
${location === "/users"
|
||||
? 'bg-accent text-accent-foreground'
|
||||
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
|
||||
}
|
||||
`}
|
||||
data-testid="link-users"
|
||||
>
|
||||
<UserCog className="w-5 h-5" />
|
||||
<span>Utilisateurs</span>
|
||||
</Link>
|
||||
<Link
|
||||
href="/sql"
|
||||
className={`
|
||||
flex items-center gap-3 px-4 py-3 rounded-lg transition-all
|
||||
${location === "/sql"
|
||||
? 'bg-accent text-accent-foreground'
|
||||
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
|
||||
}
|
||||
`}
|
||||
data-testid="link-sql"
|
||||
>
|
||||
<Database className="w-5 h-5" />
|
||||
<span>SQL</span>
|
||||
</Link>
|
||||
<Link
|
||||
href="/settings"
|
||||
className={`
|
||||
flex items-center gap-3 px-4 py-3 rounded-lg transition-all
|
||||
${location === "/settings"
|
||||
? 'bg-accent text-accent-foreground'
|
||||
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
|
||||
}
|
||||
`}
|
||||
data-testid="link-settings"
|
||||
>
|
||||
<Settings className="w-5 h-5" />
|
||||
<span>Paramètres</span>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="pt-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="w-full justify-start text-muted-foreground hover:text-accent-foreground"
|
||||
onClick={() => logoutMutation.mutate()}
|
||||
disabled={logoutMutation.isPending}
|
||||
data-testid="button-logout"
|
||||
>
|
||||
<LogOut className="w-5 h-5 mr-3" />
|
||||
{logoutMutation.isPending ? "Déconnexion..." : "Déconnexion"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{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>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import { useState } from "react";
|
||||
import { useLocation } from "wouter";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { apiRequest } from "@/lib/queryClient";
|
||||
import { Lock, User } from "lucide-react";
|
||||
|
||||
export default function Login() {
|
||||
const [, setLocation] = useLocation();
|
||||
const { toast } = useToast();
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const response = await apiRequest("POST", "/api/auth/login", { username, password });
|
||||
const data = await response.json();
|
||||
|
||||
toast({
|
||||
title: "Connexion réussie",
|
||||
description: `Bienvenue ${data.username}`,
|
||||
});
|
||||
|
||||
// Rediriger vers le dashboard
|
||||
setLocation("/");
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Erreur de connexion",
|
||||
description: error.message || "Nom d'utilisateur ou mot de passe incorrect",
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-background via-background to-muted/20 p-4">
|
||||
<Card className="w-full max-w-md">
|
||||
<CardHeader className="space-y-1 text-center">
|
||||
<CardTitle className="text-3xl font-bold">Social Flow</CardTitle>
|
||||
<CardDescription>
|
||||
Connectez-vous à votre compte pour gérer vos publications
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="username" data-testid="label-username">
|
||||
Nom d'utilisateur
|
||||
</Label>
|
||||
<div className="relative">
|
||||
<User className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
id="username"
|
||||
type="text"
|
||||
placeholder="admin"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
className="pl-10"
|
||||
required
|
||||
disabled={isLoading}
|
||||
data-testid="input-username"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password" data-testid="label-password">
|
||||
Mot de passe
|
||||
</Label>
|
||||
<div className="relative">
|
||||
<Lock className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder="••••••••"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="pl-10"
|
||||
required
|
||||
disabled={isLoading}
|
||||
data-testid="input-password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
disabled={isLoading}
|
||||
data-testid="button-login"
|
||||
>
|
||||
{isLoading ? "Connexion en cours..." : "Se connecter"}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<div className="mt-6 text-center text-sm text-muted-foreground">
|
||||
<p>Identifiants par défaut :</p>
|
||||
<p>admin / admin</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
import { useState } from "react";
|
||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||
import Sidebar from "@/components/sidebar";
|
||||
import TopBar from "@/components/topbar";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { apiRequest, queryClient } from "@/lib/queryClient";
|
||||
import { Database, Download, Play } from "lucide-react";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
|
||||
export default function SqlAdmin() {
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
const { toast } = useToast();
|
||||
const [sqlQuery, setSqlQuery] = useState("");
|
||||
const [queryResult, setQueryResult] = useState<any>(null);
|
||||
|
||||
// Charger la liste des tables
|
||||
const { data: tablesData } = useQuery({
|
||||
queryKey: ["/api/sql/tables"],
|
||||
});
|
||||
|
||||
// Mutation pour exécuter une requête SQL
|
||||
const executeSqlMutation = useMutation({
|
||||
mutationFn: async (query: string) => {
|
||||
const res = await apiRequest("POST", "/api/sql/execute", { query });
|
||||
return await res.json();
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
setQueryResult(data);
|
||||
if (data.success) {
|
||||
toast({
|
||||
title: "Requête exécutée",
|
||||
description: "La requête SQL a été exécutée avec succès",
|
||||
});
|
||||
}
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Erreur SQL",
|
||||
description: error.message || "Erreur lors de l'exécution de la requête",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const handleExecuteQuery = () => {
|
||||
if (!sqlQuery.trim()) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Erreur",
|
||||
description: "Veuillez entrer une requête SQL",
|
||||
});
|
||||
return;
|
||||
}
|
||||
executeSqlMutation.mutate(sqlQuery);
|
||||
};
|
||||
|
||||
const handleDownloadResult = () => {
|
||||
if (!queryResult?.result) return;
|
||||
|
||||
const json = JSON.stringify(queryResult.result, null, 2);
|
||||
const blob = new Blob([json], { type: "application/json" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = `sql-result-${Date.now()}.json`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const insertTable = (tableName: string) => {
|
||||
setSqlQuery(`SELECT * FROM ${tableName} LIMIT 10;`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-screen overflow-hidden bg-background">
|
||||
{sidebarOpen && (
|
||||
<div
|
||||
className="fixed inset-0 bg-black/50 z-40 lg:hidden"
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className={`
|
||||
fixed lg:static inset-y-0 left-0 z-50 transform transition-transform duration-300 ease-in-out
|
||||
${sidebarOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'}
|
||||
`}>
|
||||
<Sidebar />
|
||||
</div>
|
||||
|
||||
<main className="flex-1 overflow-y-auto">
|
||||
<TopBar onMenuClick={() => setSidebarOpen(!sidebarOpen)} />
|
||||
|
||||
<div className="p-6 space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold flex items-center gap-2">
|
||||
<Database className="w-8 h-8" />
|
||||
Administration SQL
|
||||
</h1>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
Exécutez des requêtes SQL et gérez votre base de données
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
||||
{/* Liste des tables */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Tables disponibles</CardTitle>
|
||||
<CardDescription>
|
||||
Cliquez pour insérer dans la requête
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
{(tablesData as any)?.tables?.map((table: any) => (
|
||||
<Button
|
||||
key={table.tablename}
|
||||
variant="outline"
|
||||
className="w-full justify-start text-left"
|
||||
onClick={() => insertTable(table.tablename)}
|
||||
data-testid={`button-table-${table.tablename}`}
|
||||
>
|
||||
<Database className="w-4 h-4 mr-2" />
|
||||
{table.tablename}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Éditeur SQL et résultats */}
|
||||
<div className="lg:col-span-3 space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Éditeur SQL</CardTitle>
|
||||
<CardDescription>
|
||||
Entrez votre requête SQL ci-dessous
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<Textarea
|
||||
placeholder="SELECT * FROM users LIMIT 10;"
|
||||
value={sqlQuery}
|
||||
onChange={(e) => setSqlQuery(e.target.value)}
|
||||
className="font-mono min-h-[200px]"
|
||||
data-testid="textarea-sql-query"
|
||||
/>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={handleExecuteQuery}
|
||||
disabled={executeSqlMutation.isPending}
|
||||
data-testid="button-execute-sql"
|
||||
>
|
||||
<Play className="w-4 h-4 mr-2" />
|
||||
{executeSqlMutation.isPending ? "Exécution..." : "Exécuter"}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setSqlQuery("")}
|
||||
data-testid="button-clear-sql"
|
||||
>
|
||||
Effacer
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Résultats */}
|
||||
{queryResult && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<CardTitle>Résultats</CardTitle>
|
||||
<CardDescription>
|
||||
{queryResult.success ? "Requête exécutée avec succès" : "Erreur lors de l'exécution"}
|
||||
</CardDescription>
|
||||
</div>
|
||||
{queryResult.success && queryResult.result && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleDownloadResult}
|
||||
data-testid="button-download-result"
|
||||
>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
Télécharger JSON
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{queryResult.success ? (
|
||||
<div className="overflow-x-auto">
|
||||
{Array.isArray(queryResult.result) && queryResult.result.length > 0 ? (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
{Object.keys(queryResult.result[0]).map((key) => (
|
||||
<TableHead key={key}>{key}</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{queryResult.result.map((row: any, idx: number) => (
|
||||
<TableRow key={idx}>
|
||||
{Object.values(row).map((value: any, cellIdx: number) => (
|
||||
<TableCell key={cellIdx} className="font-mono text-xs">
|
||||
{value === null ? (
|
||||
<span className="text-muted-foreground italic">null</span>
|
||||
) : typeof value === 'object' ? (
|
||||
JSON.stringify(value)
|
||||
) : (
|
||||
String(value)
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
) : (
|
||||
<pre className="bg-muted p-4 rounded-lg overflow-auto max-h-96">
|
||||
<code>{JSON.stringify(queryResult.result, null, 2)}</code>
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-destructive/10 border border-destructive/20 text-destructive p-4 rounded-lg">
|
||||
<p className="font-semibold">Erreur :</p>
|
||||
<p className="font-mono text-sm mt-2">{queryResult.error}</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
import { useState } from "react";
|
||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||
import Sidebar from "@/components/sidebar";
|
||||
import TopBar from "@/components/topbar";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
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 { useToast } from "@/hooks/use-toast";
|
||||
import { apiRequest, queryClient } from "@/lib/queryClient";
|
||||
import { Users, UserPlus } from "lucide-react";
|
||||
|
||||
export default function UsersAdmin() {
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
const { toast } = useToast();
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [role, setRole] = useState<"admin" | "user">("user");
|
||||
|
||||
// Mutation pour créer un utilisateur
|
||||
const createUserMutation = useMutation({
|
||||
mutationFn: async (userData: { username: string; password: string; role: string }) => {
|
||||
const res = await apiRequest("POST", "/api/users", userData);
|
||||
return await res.json();
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
toast({
|
||||
title: "Utilisateur créé",
|
||||
description: `L'utilisateur ${data.username} a été créé avec succès`,
|
||||
});
|
||||
// Réinitialiser le formulaire
|
||||
setUsername("");
|
||||
setPassword("");
|
||||
setRole("user");
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Erreur",
|
||||
description: error.message || "Erreur lors de la création de l'utilisateur",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!username || !password) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Erreur",
|
||||
description: "Veuillez remplir tous les champs",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (password.length < 4) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Erreur",
|
||||
description: "Le mot de passe doit contenir au moins 4 caractères",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
createUserMutation.mutate({ username, password, role });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-screen overflow-hidden bg-background">
|
||||
{sidebarOpen && (
|
||||
<div
|
||||
className="fixed inset-0 bg-black/50 z-40 lg:hidden"
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className={`
|
||||
fixed lg:static inset-y-0 left-0 z-50 transform transition-transform duration-300 ease-in-out
|
||||
${sidebarOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'}
|
||||
`}>
|
||||
<Sidebar />
|
||||
</div>
|
||||
|
||||
<main className="flex-1 overflow-y-auto">
|
||||
<TopBar onMenuClick={() => setSidebarOpen(!sidebarOpen)} />
|
||||
|
||||
<div className="p-6 space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold flex items-center gap-2">
|
||||
<Users className="w-8 h-8" />
|
||||
Gestion des utilisateurs
|
||||
</h1>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
Créez et gérez les utilisateurs de l'application
|
||||
</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="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>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user