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:
michaelschal committed 2025-10-01 16:48:20 +00:00
1 parent 7b97fb4839
commit 5ee75edf8c
12 files changed
+1081 -61

No files matched your search

+61 -11
View File
@@ -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>
);
+102 -10
View File
@@ -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>
);
+113
View File
@@ -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>
);
}
+248
View File
@@ -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>
);
}
+199
View File
@@ -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>
);
}
+39
View File
@@ -0,0 +1,39 @@
-- Script d'initialisation de la base de données Social Flow
-- Ce fichier contient les commandes SQL pour créer les tables et l'utilisateur admin par défaut
-- Créer l'enum pour les rôles utilisateur (si pas déjà créé par drizzle-kit push)
DO $$ BEGIN
CREATE TYPE user_role AS ENUM ('admin', 'user');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- Créer l'utilisateur admin par défaut (username: admin, password: admin)
-- Note: Le hash bcrypt pour "admin" est généré avec un salt de 10
INSERT INTO users (id, username, password, role)
VALUES (
'admin-user-id',
'admin',
'$2b$10$mw7B1qBoTxxT7BT.Mv9uTeExX27y5nPVN4/e6L7W1VqkTNe/tGw9G',
'admin'
) ON CONFLICT (username) DO NOTHING;
-- Créer un utilisateur demo pour les tests
INSERT INTO users (id, username, password, role)
VALUES (
'demo-user',
'demo',
'$2b$10$mw7B1qBoTxxT7BT.Mv9uTeExX27y5nPVN4/e6L7W1VqkTNe/tGw9G',
'user'
) ON CONFLICT (username) DO NOTHING;
-- Liste des tables créées automatiquement par Drizzle ORM :
-- - users (utilisateurs avec rôles)
-- - social_pages (pages Facebook/Instagram connectées)
-- - cloudinary_config (configuration Cloudinary par utilisateur)
-- - openrouter_config (configuration OpenRouter par utilisateur)
-- - media (fichiers médias stockés dans Cloudinary)
-- - posts (publications créées)
-- - scheduled_posts (publications programmées)
-- - ai_generations (historique des générations IA)
-- - post_media (relation many-to-many entre posts et media)
+37 -4
View File
@@ -40,9 +40,11 @@
"@radix-ui/react-toggle-group": "^1.1.3",
"@radix-ui/react-tooltip": "^1.2.0",
"@tanstack/react-query": "^5.60.5",
"@types/bcrypt": "^6.0.0",
"@types/multer": "^2.0.0",
"@types/node-cron": "^3.0.11",
"@types/pg": "^8.15.5",
"bcrypt": "^6.0.0",
"class-variance-authority": "^0.7.1",
"cloudinary": "^2.7.0",
"clsx": "^2.1.1",
@@ -3862,6 +3864,15 @@
"@babel/types": "^7.20.7"
}
},
"node_modules/@types/bcrypt": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-6.0.0.tgz",
"integrity": "sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/body-parser": {
"version": "1.19.5",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
@@ -4308,6 +4319,20 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"license": "MIT"
},
"node_modules/bcrypt": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz",
"integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"node-addon-api": "^8.3.0",
"node-gyp-build": "^4.8.4"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@@ -6474,6 +6499,15 @@
"react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc"
}
},
"node_modules/node-addon-api": {
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz",
"integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==",
"license": "MIT",
"engines": {
"node": "^18 || ^20 || >= 21"
}
},
"node_modules/node-cron": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.2.1.tgz",
@@ -6484,11 +6518,10 @@
}
},
"node_modules/node-gyp-build": {
"version": "4.8.3",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.3.tgz",
"integrity": "sha512-EMS95CMJzdoSKoIiXo8pxKoL8DYxwIZXYlLmgPb8KUv794abpnLK6ynsCAWNliOjREKruYKdzbh76HHYUHX7nw==",
"version": "4.8.4",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
"license": "MIT",
"optional": true,
"bin": {
"node-gyp-build": "bin.js",
"node-gyp-build-optional": "optional.js",
+2
View File
@@ -42,9 +42,11 @@
"@radix-ui/react-toggle-group": "^1.1.3",
"@radix-ui/react-tooltip": "^1.2.0",
"@tanstack/react-query": "^5.60.5",
"@types/bcrypt": "^6.0.0",
"@types/multer": "^2.0.0",
"@types/node-cron": "^3.0.11",
"@types/pg": "^8.15.5",
"bcrypt": "^6.0.0",
"class-variance-authority": "^0.7.1",
"cloudinary": "^2.7.0",
"clsx": "^2.1.1",
+45
View File
@@ -0,0 +1,45 @@
import passport from "passport";
import { Strategy as LocalStrategy } from "passport-local";
import bcrypt from "bcrypt";
import { storage } from "./storage";
import type { User } from "@shared/schema";
// Configuration de la stratégie locale
passport.use(
new LocalStrategy(async (username, password, done) => {
try {
const user = await storage.getUserByUsername(username);
if (!user) {
return done(null, false, { message: "Nom d'utilisateur ou mot de passe incorrect" });
}
const isValidPassword = await bcrypt.compare(password, user.password);
if (!isValidPassword) {
return done(null, false, { message: "Nom d'utilisateur ou mot de passe incorrect" });
}
return done(null, user);
} catch (error) {
return done(error);
}
})
);
// Sérialisation de l'utilisateur pour la session
passport.serializeUser((user: Express.User, done) => {
done(null, (user as User).id);
});
// Désérialisation de l'utilisateur depuis la session
passport.deserializeUser(async (id: string, done) => {
try {
const user = await storage.getUser(id);
done(null, user);
} catch (error) {
done(error);
}
});
export default passport;
+22
View File
@@ -1,4 +1,6 @@
import express, { type Request, Response, NextFunction } from "express";
import session from "express-session";
import passport from "./auth";
import { registerRoutes } from "./routes";
import { setupVite, serveStatic, log } from "./vite";
import { schedulerService } from "./services/scheduler";
@@ -17,6 +19,26 @@ app.use(express.json({
}));
app.use(express.urlencoded({ extended: false }));
// Configuration des sessions
if (!process.env.SESSION_SECRET) {
console.warn('⚠️ SESSION_SECRET non défini. Utilisation d\'une clé par défaut (NON SÉCURISÉ en production)');
}
app.use(session({
secret: process.env.SESSION_SECRET || 'your-secret-key-change-me',
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
sameSite: 'lax',
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 jours
}
}));
// Initialisation de Passport
app.use(passport.initialize());
app.use(passport.session());
app.use((req, res, next) => {
const start = Date.now();
const path = req.path;
+210 -36
View File
@@ -1,20 +1,180 @@
import type { Express } from "express";
import type { Express, Request, Response, NextFunction } from "express";
import { createServer, type Server } from "http";
import { storage } from "./storage";
import multer from "multer";
import bcrypt from "bcrypt";
import passport from "./auth";
import { z } from "zod";
import { openRouterService } from "./services/openrouter";
import { cloudinaryService } from "./services/cloudinary";
import { insertPostSchema, insertScheduledPostSchema, insertSocialPageSchema, insertAiGenerationSchema, insertCloudinaryConfigSchema, insertOpenrouterConfigSchema } from "@shared/schema";
import { insertPostSchema, insertScheduledPostSchema, insertSocialPageSchema, insertAiGenerationSchema, insertCloudinaryConfigSchema, insertOpenrouterConfigSchema, insertUserSchema } from "@shared/schema";
import type { User } from "@shared/schema";
const upload = multer({ storage: multer.memoryStorage() });
// Middleware pour vérifier l'authentification
function requireAuth(req: Request, res: Response, next: NextFunction) {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Non authentifié" });
}
next();
}
// Middleware pour vérifier le rôle admin
function requireAdmin(req: Request, res: Response, next: NextFunction) {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Non authentifié" });
}
const user = req.user as User;
if (user.role !== "admin") {
return res.status(403).json({ error: "Accès refusé. Réservé aux administrateurs." });
}
next();
}
export async function registerRoutes(app: Express): Promise<Server> {
// Stats endpoint
app.get("/api/stats", async (req, res) => {
// Routes d'authentification
app.post("/api/auth/login", (req, res, next) => {
passport.authenticate("local", (err: any, user: User | false, info: any) => {
if (err) {
return next(err);
}
if (!user) {
return res.status(401).json({ error: info?.message || "Authentification échouée" });
}
req.logIn(user, (err) => {
if (err) {
return next(err);
}
return res.json({
id: user.id,
username: user.username,
role: user.role,
});
});
})(req, res, next);
});
app.post("/api/auth/logout", (req, res) => {
req.logout((err) => {
if (err) {
return res.status(500).json({ error: "Erreur lors de la déconnexion" });
}
res.json({ message: "Déconnecté avec succès" });
});
});
app.get("/api/auth/session", (req, res) => {
if (req.isAuthenticated()) {
const user = req.user as User;
res.json({
id: user.id,
username: user.username,
role: user.role,
});
} else {
res.status(401).json({ error: "Non authentifié" });
}
});
// Route pour créer un nouvel utilisateur (réservée aux admins)
app.post("/api/users", requireAdmin, async (req, res) => {
try {
// For now, return mock stats. In a real app, calculate from database
const userId = "demo-user"; // In real app, get from session/auth
// Validation Zod
const createUserSchema = insertUserSchema.extend({
username: insertUserSchema.shape.username.min(3, "Le nom d'utilisateur doit contenir au moins 3 caractères"),
password: insertUserSchema.shape.password.min(4, "Le mot de passe doit contenir au moins 4 caractères"),
});
const validatedData = createUserSchema.parse(req.body);
// Vérifier si l'utilisateur existe déjà
const existingUser = await storage.getUserByUsername(validatedData.username);
if (existingUser) {
return res.status(409).json({ error: "Ce nom d'utilisateur existe déjà" });
}
// Hasher le mot de passe
const hashedPassword = await bcrypt.hash(validatedData.password, 10);
const newUser = await storage.createUser({
username: validatedData.username,
password: hashedPassword,
role: validatedData.role || "user",
});
res.json({
id: newUser.id,
username: newUser.username,
role: newUser.role,
});
} catch (error: any) {
console.error("Error creating user:", error);
if (error.name === 'ZodError') {
return res.status(400).json({ error: error.errors[0]?.message || "Données invalides" });
}
res.status(500).json({ error: "Erreur lors de la création de l'utilisateur" });
}
});
// Route SQL (réservée aux admins)
app.post("/api/sql/execute", requireAdmin, async (req, res) => {
try {
// Validation Zod
const sqlQuerySchema = z.object({
query: z.string().min(1, "La requête SQL ne peut pas être vide"),
});
const validatedData = sqlQuerySchema.parse(req.body);
const { db } = await import("./db");
const result = await db.execute(validatedData.query);
res.json({
success: true,
result,
});
} catch (error: any) {
console.error("Error executing SQL:", error);
if (error.name === 'ZodError') {
return res.status(400).json({
success: false,
error: error.errors[0]?.message || "Données invalides",
});
}
res.status(500).json({
success: false,
error: error.message || "Erreur lors de l'exécution de la requête SQL",
});
}
});
// Route pour obtenir la liste des tables (réservée aux admins)
app.get("/api/sql/tables", requireAdmin, async (req, res) => {
try {
const { db } = await import("./db");
const result = await db.execute<{ tablename: string }>(
`SELECT tablename FROM pg_tables WHERE schemaname = 'public' ORDER BY tablename;`
);
res.json({
tables: result.rows || result,
});
} catch (error: any) {
console.error("Error fetching tables:", error);
res.status(500).json({
error: error.message || "Erreur lors de la récupération des tables",
});
}
});
// Stats endpoint
app.get("/api/stats", requireAuth, async (req, res) => {
try {
const user = req.user as User;
const userId = user.id;
const posts = await storage.getPosts(userId);
const pages = await storage.getSocialPages(userId);
@@ -36,10 +196,11 @@ export async function registerRoutes(app: Express): Promise<Server> {
});
// AI text generation
app.post("/api/ai/generate", async (req, res) => {
app.post("/api/ai/generate", requireAuth, async (req, res) => {
try {
const user = req.user as User;
const userId = user.id;
const productInfo = req.body;
const userId = "demo-user"; // In real app, get from session/auth
const generatedTexts = await openRouterService.generatePostText(productInfo, userId);
@@ -58,13 +219,14 @@ export async function registerRoutes(app: Express): Promise<Server> {
});
// Media upload
app.post("/api/media/upload", upload.single("file"), async (req, res) => {
app.post("/api/media/upload", requireAuth, upload.single("file"), async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({ error: "No file uploaded" });
}
const userId = "demo-user"; // In real app, get from session/auth
const user = req.user as User;
const userId = user.id;
// Check if Cloudinary is configured
const cloudinaryConfig = await storage.getCloudinaryConfig(userId);
@@ -104,9 +266,10 @@ export async function registerRoutes(app: Express): Promise<Server> {
});
// Get media
app.get("/api/media", async (req, res) => {
app.get("/api/media", requireAuth, async (req, res) => {
try {
const userId = "demo-user"; // In real app, get from session/auth
const user = req.user as User;
const userId = user.id;
const media = await storage.getMedia(userId);
res.json(media);
} catch (error) {
@@ -116,9 +279,10 @@ export async function registerRoutes(app: Express): Promise<Server> {
});
// Delete media
app.delete("/api/media/:id", async (req, res) => {
app.delete("/api/media/:id", requireAuth, async (req, res) => {
try {
const userId = "demo-user"; // In real app, get from session/auth
const user = req.user as User;
const userId = user.id;
const mediaId = req.params.id;
// Get media to find cloudinary public ID
@@ -149,9 +313,10 @@ export async function registerRoutes(app: Express): Promise<Server> {
});
// Posts
app.get("/api/posts", async (req, res) => {
app.get("/api/posts", requireAuth, async (req, res) => {
try {
const userId = "demo-user"; // In real app, get from session/auth
const user = req.user as User;
const userId = user.id;
const posts = await storage.getPosts(userId);
res.json(posts);
} catch (error) {
@@ -160,9 +325,10 @@ export async function registerRoutes(app: Express): Promise<Server> {
}
});
app.post("/api/posts", async (req, res) => {
app.post("/api/posts", requireAuth, async (req, res) => {
try {
const userId = "demo-user"; // In real app, get from session/auth
const user = req.user as User;
const userId = user.id;
const postData = insertPostSchema.parse({ ...req.body, userId });
const post = await storage.createPost(postData);
res.json(post);
@@ -173,9 +339,10 @@ export async function registerRoutes(app: Express): Promise<Server> {
});
// Scheduled posts
app.get("/api/scheduled-posts", async (req, res) => {
app.get("/api/scheduled-posts", requireAuth, async (req, res) => {
try {
const userId = "demo-user"; // In real app, get from session/auth
const user = req.user as User;
const userId = user.id;
const { startDate, endDate } = req.query;
const start = startDate ? new Date(startDate as string) : undefined;
@@ -189,7 +356,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
}
});
app.post("/api/scheduled-posts", async (req, res) => {
app.post("/api/scheduled-posts", requireAuth, async (req, res) => {
try {
const scheduledPostData = insertScheduledPostSchema.parse(req.body);
const scheduledPost = await storage.createScheduledPost(scheduledPostData);
@@ -201,9 +368,10 @@ export async function registerRoutes(app: Express): Promise<Server> {
});
// Social pages
app.get("/api/pages", async (req, res) => {
app.get("/api/pages", requireAuth, async (req, res) => {
try {
const userId = "demo-user"; // In real app, get from session/auth
const user = req.user as User;
const userId = user.id;
const pages = await storage.getSocialPages(userId);
res.json(pages);
} catch (error) {
@@ -212,9 +380,10 @@ export async function registerRoutes(app: Express): Promise<Server> {
}
});
app.post("/api/pages", async (req, res) => {
app.post("/api/pages", requireAuth, async (req, res) => {
try {
const userId = "demo-user"; // In real app, get from session/auth
const user = req.user as User;
const userId = user.id;
const pageData = insertSocialPageSchema.parse({ ...req.body, userId });
const page = await storage.createSocialPage(pageData);
res.json(page);
@@ -225,9 +394,10 @@ export async function registerRoutes(app: Express): Promise<Server> {
});
// AI Generations
app.get("/api/ai/generations", async (req, res) => {
app.get("/api/ai/generations", requireAuth, async (req, res) => {
try {
const userId = "demo-user"; // In real app, get from session/auth
const user = req.user as User;
const userId = user.id;
const generations = await storage.getAiGenerations(userId);
res.json(generations);
} catch (error) {
@@ -237,9 +407,10 @@ export async function registerRoutes(app: Express): Promise<Server> {
});
// Cloudinary Config
app.get("/api/cloudinary/config", async (req, res) => {
app.get("/api/cloudinary/config", requireAdmin, async (req, res) => {
try {
const userId = "demo-user"; // In real app, get from session/auth
const user = req.user as User;
const userId = user.id;
const config = await storage.getCloudinaryConfig(userId);
if (!config) {
@@ -255,9 +426,10 @@ export async function registerRoutes(app: Express): Promise<Server> {
}
});
app.post("/api/cloudinary/config", async (req, res) => {
app.post("/api/cloudinary/config", requireAdmin, async (req, res) => {
try {
const userId = "demo-user"; // In real app, get from session/auth
const user = req.user as User;
const userId = user.id;
const configData = insertCloudinaryConfigSchema.parse({
...req.body,
@@ -284,7 +456,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
});
// OpenRouter models list
app.get("/api/openrouter/models", async (req, res) => {
app.get("/api/openrouter/models", requireAuth, async (req, res) => {
try {
const response = await fetch("https://openrouter.ai/api/v1/models", {
headers: {
@@ -305,9 +477,10 @@ export async function registerRoutes(app: Express): Promise<Server> {
});
// OpenRouter configuration
app.get("/api/openrouter/config", async (req, res) => {
app.get("/api/openrouter/config", requireAdmin, async (req, res) => {
try {
const userId = "demo-user"; // In real app, get from session/auth
const user = req.user as User;
const userId = user.id;
const config = await storage.getOpenrouterConfig(userId);
if (!config) {
@@ -323,9 +496,10 @@ export async function registerRoutes(app: Express): Promise<Server> {
}
});
app.post("/api/openrouter/config", async (req, res) => {
app.post("/api/openrouter/config", requireAdmin, async (req, res) => {
try {
const userId = "demo-user"; // In real app, get from session/auth
const user = req.user as User;
const userId = user.id;
const configData = insertOpenrouterConfigSchema.parse({
...req.body,
+3
View File
@@ -8,11 +8,13 @@ export const platformEnum = pgEnum("platform", ["facebook", "instagram"]);
export const postTypeEnum = pgEnum("post_type", ["feed", "story"]);
export const postStatusEnum = pgEnum("post_status", ["draft", "scheduled", "published", "failed"]);
export const mediaTypeEnum = pgEnum("media_type", ["image", "video"]);
export const userRoleEnum = pgEnum("user_role", ["admin", "user"]);
export const users = pgTable("users", {
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
username: text("username").notNull().unique(),
password: text("password").notNull(),
role: userRoleEnum("role").notNull().default("user"),
});
export const socialPages = pgTable("social_pages", {
@@ -181,6 +183,7 @@ export const aiGenerationsRelations = relations(aiGenerations, ({ one }) => ({
export const insertUserSchema = createInsertSchema(users).pick({
username: true,
password: true,
role: true,
});
export const insertSocialPageSchema = createInsertSchema(socialPages).omit({