diff --git a/.replit b/.replit index 3238a51..22a2415 100644 --- a/.replit +++ b/.replit @@ -14,6 +14,18 @@ run = ["npm", "run", "start"] localPort = 5000 externalPort = 80 +[[ports]] +localPort = 35739 +externalPort = 3002 + +[[ports]] +localPort = 40537 +externalPort = 3000 + +[[ports]] +localPort = 43245 +externalPort = 3001 + [env] PORT = "5000" diff --git a/client/src/App.tsx b/client/src/App.tsx index 5ea27ba..07ba43a 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -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 ( +
+
+
+

Chargement...

+
+
+ ); + } + + if (!session) { + return null; + } + + if (adminOnly && (session as any).role !== "admin") { + return ( +
+
+

Accès refusé

+

Cette page est réservée aux administrateurs.

+
+
+ ); + } + + return ; +} function Router() { return ( - - - - - - - - - + + {() => } + {() => } + {() => } + {() => } + {() => } + {() => } + {() => } + {() => } + {() => } + {() => } + {() => } ); diff --git a/client/src/components/sidebar.tsx b/client/src/components/sidebar.tsx index 6247a70..657e772 100644 --- a/client/src/components/sidebar.tsx +++ b/client/src/components/sidebar.tsx @@ -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<{ id: string; username: string; role: string }>({ + queryKey: ["/api/auth/session"], + retry: false, + }); + + const isAdmin = session?.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() { -
- - - Paramètres - +
+ {isAdmin && ( + <> +
+

Administration

+
+ + + Utilisateurs + + + + SQL + + + + Paramètres + + + )} + +
+ +
+ + {session && ( +
+

Connecté en tant que

+

{session.username}

+

{session.role}

+
+ )}
); diff --git a/client/src/pages/login.tsx b/client/src/pages/login.tsx new file mode 100644 index 0000000..605242e --- /dev/null +++ b/client/src/pages/login.tsx @@ -0,0 +1,132 @@ +import { useState, useEffect } from "react"; +import { useLocation } from "wouter"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +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 [showDefaultPassword, setShowDefaultPassword] = useState(false); + + // Vérifier si le mot de passe admin par défaut est toujours actif + useEffect(() => { + const checkDefaultPassword = async () => { + try { + const response = await fetch("/api/auth/default-password-status"); + const data = await response.json(); + setShowDefaultPassword(data.isDefault === true); + } catch (error) { + console.error("Error checking default password status:", error); + } + }; + + checkDefaultPassword(); + }, []); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + 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 ( +
+ + + Social Flow + + Connectez-vous à votre compte pour gérer vos publications + + + +
+
+ +
+ + setUsername(e.target.value)} + className="pl-10" + required + disabled={isLoading} + data-testid="input-username" + /> +
+
+ +
+ +
+ + setPassword(e.target.value)} + className="pl-10" + required + disabled={isLoading} + data-testid="input-password" + /> +
+
+ + +
+ + {showDefaultPassword && ( +
+

Identifiants par défaut :

+

admin / admin

+

⚠️ Changez ce mot de passe après la première connexion

+
+ )} +
+
+
+ ); +} diff --git a/client/src/pages/settings.tsx b/client/src/pages/settings.tsx index 606011c..d1bcf1a 100644 --- a/client/src/pages/settings.tsx +++ b/client/src/pages/settings.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from "react"; -import { Settings as SettingsIcon, Bell, Key, Shield, Cloud } from "lucide-react"; +import { Settings as SettingsIcon, Bell, Key, Shield, Cloud, Brain } from "lucide-react"; import { useQuery, useMutation } from "@tanstack/react-query"; import Sidebar from "@/components/sidebar"; import TopBar from "@/components/topbar"; @@ -7,6 +7,8 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Button } from "@/components/ui/button"; import { useToast } from "@/hooks/use-toast"; import { apiRequest, queryClient } from "@/lib/queryClient"; @@ -18,12 +20,23 @@ export default function Settings() { const [cloudName, setCloudName] = useState(""); const [apiKey, setApiKey] = useState(""); const [apiSecret, setApiSecret] = useState(""); + const [openrouterApiKey, setOpenrouterApiKey] = useState(""); + const [openrouterModel, setOpenrouterModel] = useState("anthropic/claude-3.5-sonnet"); + const [openrouterSystemPrompt, setOpenrouterSystemPrompt] = useState("Tu es un expert en marketing des réseaux sociaux. Génère 3 variations de textes engageants pour des publications Facebook et Instagram à partir des informations produit fournies. Chaque variation doit être unique, captivante et optimisée pour l'engagement."); const { toast } = useToast(); const { data: cloudinaryConfig } = useQuery({ queryKey: ['/api/cloudinary/config'], }); + const { data: openrouterConfig } = useQuery({ + queryKey: ['/api/openrouter/config'], + }); + + const { data: openrouterModels } = useQuery({ + queryKey: ['/api/openrouter/models'], + }); + useEffect(() => { if (cloudinaryConfig) { setCloudName((cloudinaryConfig as any).cloudName || ""); @@ -31,6 +44,16 @@ export default function Settings() { } }, [cloudinaryConfig]); + useEffect(() => { + if (openrouterConfig) { + setOpenrouterModel((openrouterConfig as any).model || "anthropic/claude-3.5-sonnet"); + setOpenrouterSystemPrompt((openrouterConfig as any).systemPrompt || "Tu es un expert en marketing des réseaux sociaux. Génère 3 variations de textes engageants pour des publications Facebook et Instagram à partir des informations produit fournies. Chaque variation doit être unique, captivante et optimisée pour l'engagement."); + } + }, [openrouterConfig]); + + // Vérifier si une config OpenRouter existe déjà + const hasExistingConfig = !!openrouterConfig; + const saveCloudinaryMutation = useMutation({ mutationFn: () => apiRequest('POST', '/api/cloudinary/config', { @@ -55,6 +78,30 @@ export default function Settings() { }, }); + const saveOpenrouterMutation = useMutation({ + mutationFn: () => + apiRequest('POST', '/api/openrouter/config', { + apiKey: openrouterApiKey, + model: openrouterModel, + systemPrompt: openrouterSystemPrompt, + }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['/api/openrouter/config'] }); + toast({ + title: "Configuration sauvegardée", + description: "Vos paramètres OpenRouter ont été enregistrés", + }); + setOpenrouterApiKey(""); // Clear the API key after saving + }, + onError: () => { + toast({ + title: "Erreur", + description: "Impossible de sauvegarder la configuration OpenRouter", + variant: "destructive", + }); + }, + }); + return (
{sidebarOpen && ( @@ -128,26 +175,84 @@ export default function Settings() { - - Clés API + + Configuration OpenRouter (IA) - Configurez vos clés API pour les intégrations + Configurez OpenRouter pour la génération de texte avec l'IA
- + setOpenrouterApiKey(e.target.value)} + placeholder={hasExistingConfig ? "••••••••••••••••" : "sk-or-v1-..."} + data-testid="input-openrouter-api-key" />

- Utilisée pour la génération de texte IA + {hasExistingConfig ? ( + <> + ✓ Clé API configurée - Laissez vide pour garder la clé actuelle + + ) : ( + <> + Obtenez votre clé API sur{" "} + + openrouter.ai + + + )}

+
+ + +

+ Choisissez le modèle d'IA pour générer vos publications +

+
+
+ +