+ {sidebarOpen && (
+
setSidebarOpen(false)}
+ />
+ )}
+
+
+
+
+
+
+ setSidebarOpen(!sidebarOpen)} />
+
+
+
+
+
+ Gestion des utilisateurs
+
+
+ Créez et gérez les utilisateurs de l'application
+
+
+
+
+ {/* Liste des utilisateurs */}
+
+
+
+ Liste des utilisateurs
+
+ {users?.length || 0} utilisateur(s) enregistré(s)
+
+
+
+ {isLoading ? (
+
+ ) : users && users.length > 0 ? (
+
+
+
+ Nom d'utilisateur
+ Rôle
+ Actions
+
+
+
+ {users.map((user) => (
+
+
+ {user.role === "admin" ? (
+
+ ) : (
+
+ )}
+ {user.username}
+
+
+
+ {user.role === "admin" ? "Administrateur" : "Utilisateur"}
+
+
+
+
+
handleEditClick(user)}
+ data-testid={`button-edit-user-${user.id}`}
+ >
+
+
+
handleDeleteClick(user)}
+ data-testid={`button-delete-user-${user.id}`}
+ >
+
+
+
+
+
+ ))}
+
+
+ ) : (
+
+
Aucun utilisateur trouvé
+
+ )}
+
+
+
+
+ {/* Formulaire de création */}
+
+
+
+
+
+ Créer un utilisateur
+
+
+ Ajouter un nouvel utilisateur
+
+
+
+
+
+
+
+
+
+
+
+ {/* Dialog de modification */}
+
+
+
+ Modifier l'utilisateur
+
+ Modifiez les informations de l'utilisateur. Laissez le mot de passe vide pour ne pas le changer.
+
+
+
+
+ Nom d'utilisateur
+ setEditUsername(e.target.value)}
+ data-testid="input-edit-username"
+ />
+
+
+ Nouveau mot de passe (optionnel)
+ setEditPassword(e.target.value)}
+ data-testid="input-edit-password"
+ />
+
+
+ Rôle
+ setEditRole(value)}>
+
+
+
+
+ Utilisateur
+ Administrateur
+
+
+
+
+
+ setEditDialogOpen(false)}>
+ Annuler
+
+
+ {updateUserMutation.isPending ? "Enregistrement..." : "Enregistrer"}
+
+
+
+
+
+ {/* Dialog de confirmation de suppression */}
+
+
+
+ Confirmer la suppression
+
+ Êtes-vous sûr de vouloir supprimer l'utilisateur {userToDelete?.username} ?
+ Cette action est irréversible.
+
+
+
+ Annuler
+
+ {deleteUserMutation.isPending ? "Suppression..." : "Supprimer"}
+
+
+
+
+
+ );
+}
diff --git a/init.sql b/init.sql
new file mode 100644
index 0000000..6ef5940
--- /dev/null
+++ b/init.sql
@@ -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)
diff --git a/package-lock.json b/package-lock.json
index b4f2e73..9961c5f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index 89788c2..fe661ff 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/replit.md b/replit.md
index 17103d5..50e51ee 100644
--- a/replit.md
+++ b/replit.md
@@ -18,13 +18,40 @@ Social Flow is a comprehensive social media automation platform designed to stre
Preferred communication style: Simple, everyday language.
+## Authentication & Authorization
+
+**System**: Passport.js avec stratégie locale et bcrypt pour le hachage des mots de passe
+
+**Rôles utilisateur**:
+- `admin` - Accès complet à tous les paramètres, SQL, et gestion des utilisateurs
+- `user` - Accès aux fonctionnalités de publication uniquement (pas de paramètres)
+
+**Utilisateur par défaut**:
+- Username: `admin`
+- Password: `admin`
+- Rôle: admin
+- **Important**: Changez ce mot de passe en production
+
+**Session Management**:
+- express-session avec cookies HTTP-only
+- sameSite: 'lax' pour prévention CSRF
+- Durée de session: 7 jours
+- Secret de session via variable d'environnement `SESSION_SECRET`
+
+**Protection des routes**:
+- Backend: Middleware `requireAuth` pour routes utilisateur, `requireAdmin` pour routes admin
+- Frontend: Composant `ProtectedRoute` avec vérification de session et redirection vers /login
+
## System Architecture
### Frontend Architecture
**Framework**: React with TypeScript using Vite as the build tool
-**Routing**: Wouter for lightweight client-side routing
+**Routing**: Wouter for lightweight client-side routing with protected routes
+- Public: `/login`
+- Protected (authenticated): Dashboard, Media, AI, Posts, Calendar, Pages, Analytics, History
+- Protected (admin only): `/settings`, `/sql`, `/users`
**State Management**:
- TanStack Query (React Query) for server state management and caching
@@ -49,11 +76,16 @@ Preferred communication style: Simple, everyday language.
**Type Safety**: Full TypeScript implementation across frontend, backend, and shared schema
**API Design**: RESTful endpoints with conventional HTTP methods
-- `/api/stats` - Dashboard statistics
-- `/api/ai/generate` - AI text generation
-- `/api/media/*` - Media upload and management
-- `/api/pages/*` - Social page management
-- `/api/posts/*` - Post and scheduling management
+- `/api/auth/*` - Authentication (login, logout, session)
+- `/api/users` - User management (admin only)
+- `/api/sql/*` - SQL administration (admin only)
+- `/api/stats` - Dashboard statistics (authenticated)
+- `/api/ai/generate` - AI text generation (authenticated)
+- `/api/media/*` - Media upload and management (authenticated)
+- `/api/pages/*` - Social page management (authenticated)
+- `/api/posts/*` - Post and scheduling management (authenticated)
+- `/api/cloudinary/config` - Cloudinary configuration (admin only)
+- `/api/openrouter/config` - OpenRouter configuration (admin only)
**File Upload**: Multer middleware for handling multipart/form-data with in-memory storage strategy, integrated with Cloudinary for cloud storage
@@ -79,7 +111,7 @@ Preferred communication style: Simple, everyday language.
**Schema Design**:
Core Tables:
-- `users` - User authentication and profiles
+- `users` - User authentication and profiles with role-based access (admin/user)
- `cloudinary_config` - Cloudinary configuration per user (cloud name, API key, API secret)
- `social_pages` - Connected Facebook/Instagram pages with access tokens
- `media` - Uploaded media files with Cloudinary public IDs and transformation URLs
diff --git a/server/auth.ts b/server/auth.ts
new file mode 100644
index 0000000..7d35fcc
--- /dev/null
+++ b/server/auth.ts
@@ -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;
diff --git a/server/index.ts b/server/index.ts
index 22e3c13..7f5ab2a 100644
--- a/server/index.ts
+++ b/server/index.ts
@@ -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;
diff --git a/server/routes.ts b/server/routes.ts
index 3643989..2a1bc0a 100644
--- a/server/routes.ts
+++ b/server/routes.ts
@@ -1,20 +1,293 @@
-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 } from "@shared/schema";
+import { insertPostSchema, insertScheduledPostSchema, insertSocialPageSchema, insertAiGenerationSchema, insertCloudinaryConfigSchema, insertOpenrouterConfigSchema, updateOpenrouterConfigSchema, insertUserSchema } from "@shared/schema";
+import type { User, InsertUser } 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
{
- // 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 obtenir la liste de tous les utilisateurs (réservée aux admins)
+ app.get("/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
+ 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)
+ app.post("/api/users", requireAdmin, async (req, res) => {
+ try {
+ // 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 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 = {};
+
+ 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)
+ 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,12 +309,13 @@ export async function registerRoutes(app: Express): Promise {
});
// 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);
+ const generatedTexts = await openRouterService.generatePostText(productInfo, userId);
// Save to database
await storage.createAiGeneration({
@@ -58,13 +332,14 @@ export async function registerRoutes(app: Express): Promise {
});
// 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 +379,10 @@ export async function registerRoutes(app: Express): Promise {
});
// 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 +392,10 @@ export async function registerRoutes(app: Express): Promise {
});
// 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 +426,10 @@ export async function registerRoutes(app: Express): Promise {
});
// 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 +438,10 @@ export async function registerRoutes(app: Express): Promise {
}
});
- 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 +452,10 @@ export async function registerRoutes(app: Express): Promise {
});
// 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 +469,7 @@ export async function registerRoutes(app: Express): Promise {
}
});
- 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 +481,10 @@ export async function registerRoutes(app: Express): Promise {
});
// 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 +493,10 @@ export async function registerRoutes(app: Express): Promise {
}
});
- 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 +507,10 @@ export async function registerRoutes(app: Express): Promise {
});
// 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 +520,10 @@ export async function registerRoutes(app: Express): Promise {
});
// 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 +539,10 @@ export async function registerRoutes(app: Express): Promise {
}
});
- 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,
@@ -283,6 +568,88 @@ export async function registerRoutes(app: Express): Promise {
}
});
+ // OpenRouter models list
+ app.get("/api/openrouter/models", requireAuth, async (req, res) => {
+ try {
+ const response = await fetch("https://openrouter.ai/api/v1/models", {
+ headers: {
+ "Content-Type": "application/json",
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error(`OpenRouter API error: ${response.status}`);
+ }
+
+ const data = await response.json();
+ res.json(data);
+ } catch (error) {
+ console.error("Error fetching OpenRouter models:", error);
+ res.status(500).json({ error: "Failed to fetch OpenRouter models" });
+ }
+ });
+
+ // OpenRouter configuration
+ app.get("/api/openrouter/config", requireAdmin, async (req, res) => {
+ try {
+ const user = req.user as User;
+ const userId = user.id;
+ const config = await storage.getOpenrouterConfig(userId);
+
+ if (!config) {
+ return res.json(null);
+ }
+
+ // Don't send the API key to the client
+ const { apiKey, ...safeConfig } = config;
+ res.json(safeConfig);
+ } catch (error) {
+ console.error("Error fetching OpenRouter config:", error);
+ res.status(500).json({ error: "Failed to fetch OpenRouter config" });
+ }
+ });
+
+ app.post("/api/openrouter/config", requireAdmin, async (req, res) => {
+ try {
+ const user = req.user as User;
+ const userId = user.id;
+
+ // Check if config already exists
+ const existingConfig = await storage.getOpenrouterConfig(userId);
+
+ let config;
+ if (existingConfig) {
+ // Pour les mises à jour, utiliser le schéma qui rend apiKey optionnel
+ const updateData = updateOpenrouterConfigSchema.parse({
+ ...req.body,
+ userId,
+ });
+
+ // Si apiKey n'est pas fourni, garder l'ancien
+ const finalData = {
+ ...updateData,
+ apiKey: updateData.apiKey || existingConfig.apiKey,
+ };
+
+ config = await storage.updateOpenrouterConfig(userId, finalData);
+ } else {
+ // Pour les créations, exiger tous les champs
+ const configData = insertOpenrouterConfigSchema.parse({
+ ...req.body,
+ userId,
+ });
+ config = await storage.createOpenrouterConfig(configData);
+ }
+
+ // Don't send the API key back
+ const { apiKey, ...safeConfig } = config;
+ res.json(safeConfig);
+ } catch (error) {
+ console.error("Error saving OpenRouter config:", error);
+ res.status(500).json({ error: "Failed to save OpenRouter config" });
+ }
+ });
+
const httpServer = createServer(app);
return httpServer;
diff --git a/server/services/openrouter.ts b/server/services/openrouter.ts
index ac97545..88bedab 100644
--- a/server/services/openrouter.ts
+++ b/server/services/openrouter.ts
@@ -12,35 +12,32 @@ interface GeneratedText {
characterCount: number;
}
+import { storage } from '../storage';
+
export class OpenRouterService {
- private apiKey: string;
private baseUrl = "https://openrouter.ai/api/v1/chat/completions";
- constructor() {
- this.apiKey = process.env.OPENROUTER_API_KEY || "";
- if (!this.apiKey) {
- console.warn("OPENROUTER_API_KEY not set. AI text generation will not work.");
- }
- }
-
- async generatePostText(productInfo: ProductInfo): Promise {
- if (!this.apiKey) {
- throw new Error("OpenRouter API key not configured");
+ async generatePostText(productInfo: ProductInfo, userId: string): Promise {
+ // Get user's OpenRouter configuration
+ const config = await storage.getOpenrouterConfig(userId);
+
+ if (!config) {
+ throw new Error('Configuration OpenRouter non trouvée. Veuillez configurer OpenRouter dans les paramètres.');
}
- const prompt = this.buildPrompt(productInfo);
+ const prompt = this.buildPrompt(productInfo, config.systemPrompt);
try {
const response = await fetch(this.baseUrl, {
method: "POST",
headers: {
- "Authorization": `Bearer ${this.apiKey}`,
+ "Authorization": `Bearer ${config.apiKey}`,
"Content-Type": "application/json",
- "HTTP-Referer": process.env.APP_URL || "http://localhost:5000",
+ "HTTP-Referer": process.env.APP_URL || "http://localhost:5555",
"X-Title": "Social Flow"
},
body: JSON.stringify({
- model: "anthropic/claude-3.5-sonnet",
+ model: config.model,
messages: [
{
role: "user",
@@ -66,26 +63,18 @@ export class OpenRouterService {
}
}
- private buildPrompt(productInfo: ProductInfo): string {
+ private buildPrompt(productInfo: ProductInfo, systemPrompt: string): string {
const features = productInfo.features?.join(", ") || "";
- return `Tu es un expert en marketing sur les réseaux sociaux. Génère 3 variantes de texte pour une publication Facebook/Instagram pour ce produit:
-
-Nom: ${productInfo.name}
+ const productDetails = `Nom: ${productInfo.name}
${productInfo.description ? `Description: ${productInfo.description}` : ''}
${productInfo.price ? `Prix: ${productInfo.price}` : ''}
-${features ? `Caractéristiques: ${features}` : ''}
+${features ? `Caractéristiques: ${features}` : ''}`;
-Crée 3 versions différentes:
-1. Version Dynamique - Énergique et engageante avec des emojis
-2. Version Informative - Factuelle et professionnelle
-3. Version Émotionnelle - Axée sur les bénéfices émotionnels
+ return `${systemPrompt}
-Chaque version doit:
-- Être en français
-- Faire entre 150-250 caractères
-- Être optimisée pour les réseaux sociaux
-- Inclure des emojis pertinents (sauf la version informative)
+Informations produit:
+${productDetails}
Format de réponse:
VERSION 1 - DYNAMIQUE:
diff --git a/server/storage.ts b/server/storage.ts
index c967f6d..4f5a9b1 100644
--- a/server/storage.ts
+++ b/server/storage.ts
@@ -6,6 +6,7 @@ import {
scheduledPosts,
aiGenerations,
cloudinaryConfig,
+ openrouterConfig,
type User,
type InsertUser,
type SocialPage,
@@ -20,6 +21,8 @@ import {
type InsertAiGeneration,
type CloudinaryConfig,
type InsertCloudinaryConfig,
+ type OpenrouterConfig,
+ type InsertOpenrouterConfig,
} from "@shared/schema";
import { db } from "./db";
import { eq, and, gte, lte, desc, isNull } from "drizzle-orm";
@@ -28,7 +31,10 @@ export interface IStorage {
// Users
getUser(id: string): Promise;
getUserByUsername(username: string): Promise;
+ getAllUsers(): Promise;
createUser(user: InsertUser): Promise;
+ updateUser(id: string, user: Partial): Promise;
+ deleteUser(id: string): Promise;
// Social Pages
getSocialPages(userId: string): Promise;
@@ -66,6 +72,11 @@ export interface IStorage {
getCloudinaryConfig(userId: string): Promise;
createCloudinaryConfig(config: InsertCloudinaryConfig): Promise;
updateCloudinaryConfig(userId: string, config: Partial): Promise;
+
+ // OpenRouter Config
+ getOpenrouterConfig(userId: string): Promise;
+ createOpenrouterConfig(config: InsertOpenrouterConfig): Promise;
+ updateOpenrouterConfig(userId: string, config: Partial): Promise;
}
export class DatabaseStorage implements IStorage {
@@ -80,11 +91,24 @@ export class DatabaseStorage implements IStorage {
return user || undefined;
}
+ async getAllUsers(): Promise {
+ return await db.select().from(users);
+ }
+
async createUser(insertUser: InsertUser): Promise {
const [user] = await db.insert(users).values(insertUser).returning();
return user;
}
+ async updateUser(id: string, updateData: Partial): Promise {
+ const [updated] = await db.update(users).set(updateData).where(eq(users.id, id)).returning();
+ return updated;
+ }
+
+ async deleteUser(id: string): Promise {
+ await db.delete(users).where(eq(users.id, id));
+ }
+
// Social Pages
async getSocialPages(userId: string): Promise {
return await db.select().from(socialPages).where(eq(socialPages.userId, userId));
@@ -235,6 +259,25 @@ export class DatabaseStorage implements IStorage {
.returning();
return updated;
}
+
+ // OpenRouter Config
+ async getOpenrouterConfig(userId: string): Promise {
+ const [config] = await db.select().from(openrouterConfig).where(eq(openrouterConfig.userId, userId));
+ return config || undefined;
+ }
+
+ async createOpenrouterConfig(config: InsertOpenrouterConfig): Promise {
+ const [newConfig] = await db.insert(openrouterConfig).values(config).returning();
+ return newConfig;
+ }
+
+ async updateOpenrouterConfig(userId: string, config: Partial): Promise {
+ const [updated] = await db.update(openrouterConfig)
+ .set({ ...config, updatedAt: new Date() })
+ .where(eq(openrouterConfig.userId, userId))
+ .returning();
+ return updated;
+ }
}
export const storage = new DatabaseStorage();
diff --git a/shared/schema.ts b/shared/schema.ts
index bdf10e9..144f27d 100644
--- a/shared/schema.ts
+++ b/shared/schema.ts
@@ -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", {
@@ -37,6 +39,16 @@ export const cloudinaryConfig = pgTable("cloudinary_config", {
updatedAt: timestamp("updated_at").defaultNow(),
});
+export const openrouterConfig = pgTable("openrouter_config", {
+ id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
+ userId: varchar("user_id").notNull().unique().references(() => users.id, { onDelete: "cascade" }),
+ apiKey: text("api_key").notNull(),
+ model: text("model").notNull().default("anthropic/claude-3.5-sonnet"),
+ systemPrompt: text("system_prompt").notNull().default("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."),
+ createdAt: timestamp("created_at").defaultNow(),
+ updatedAt: timestamp("updated_at").defaultNow(),
+});
+
export const media = pgTable("media", {
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
userId: varchar("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
@@ -96,6 +108,7 @@ export const usersRelations = relations(users, ({ many, one }) => ({
posts: many(posts),
aiGenerations: many(aiGenerations),
cloudinaryConfig: one(cloudinaryConfig),
+ openrouterConfig: one(openrouterConfig),
}));
export const cloudinaryConfigRelations = relations(cloudinaryConfig, ({ one }) => ({
@@ -105,6 +118,13 @@ export const cloudinaryConfigRelations = relations(cloudinaryConfig, ({ one }) =
}),
}));
+export const openrouterConfigRelations = relations(openrouterConfig, ({ one }) => ({
+ user: one(users, {
+ fields: [openrouterConfig.userId],
+ references: [users.id],
+ }),
+}));
+
export const socialPagesRelations = relations(socialPages, ({ one, many }) => ({
user: one(users, {
fields: [socialPages.userId],
@@ -163,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({
@@ -199,6 +220,16 @@ export const insertCloudinaryConfigSchema = createInsertSchema(cloudinaryConfig)
updatedAt: true,
});
+export const insertOpenrouterConfigSchema = createInsertSchema(openrouterConfig).omit({
+ id: true,
+ createdAt: true,
+ updatedAt: true,
+});
+
+export const updateOpenrouterConfigSchema = insertOpenrouterConfigSchema.partial({
+ apiKey: true,
+});
+
// Types
export type User = typeof users.$inferSelect;
export type InsertUser = z.infer;
@@ -220,3 +251,6 @@ export type InsertAiGeneration = z.infer;
export type CloudinaryConfig = typeof cloudinaryConfig.$inferSelect;
export type InsertCloudinaryConfig = z.infer;
+
+export type OpenrouterConfig = typeof openrouterConfig.$inferSelect;
+export type InsertOpenrouterConfig = z.infer;