+ );
+}
diff --git a/client/src/pages/users-admin.tsx b/client/src/pages/users-admin.tsx
new file mode 100644
index 0000000..2f02c88
--- /dev/null
+++ b/client/src/pages/users-admin.tsx
@@ -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 (
+
+ {sidebarOpen && (
+
setSidebarOpen(false)}
+ />
+ )}
+
+
+
+
+
+
+ setSidebarOpen(!sidebarOpen)} />
+
+
+
+
+
+ Gestion des utilisateurs
+
+
+ Créez et gérez les utilisateurs de l'application
+
+
+
+
+
+
+
+
+ Créer un nouvel utilisateur
+
+
+ Seuls les administrateurs peuvent créer des utilisateurs
+
+
+
+
+
+
+
+
+
+ Commandes SQL utiles
+
+ Utilisez ces commandes dans l'onglet SQL pour gérer les utilisateurs
+
+
+
+
+
Voir tous les utilisateurs :
+
SELECT id, username, role FROM users;
+
+
+
+
Changer le rôle d'un utilisateur :
+
UPDATE users SET role = 'admin' WHERE username = 'nom_utilisateur';
+
+
+
+
Supprimer un utilisateur :
+
DELETE FROM users WHERE username = 'nom_utilisateur';
+
+
+
+
+
+
+
+ );
+}
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/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 9fdf8ab..ba89c3c 100644
--- a/server/routes.ts
+++ b/server/routes.ts
@@ -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
{
- // 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 {
});
// 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 {
});
// 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 {
});
// 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 {
});
// 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 {
});
// 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 {
}
});
- 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 {
});
// 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 {
}
});
- 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 {
});
// 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 {
}
});
- 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 {
});
// 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 {
});
// 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 {
}
});
- 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 {
});
// 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 {
});
// 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 {
}
});
- 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,
diff --git a/shared/schema.ts b/shared/schema.ts
index b07c9af..3a9e231 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", {
@@ -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({
From 14b4960e324e2e5ae398107e5e368a7fde275a28 Mon Sep 17 00:00:00 2001
From: michaelschal <35957947-michaelschal@users.noreply.replit.com>
Date: Wed, 1 Oct 2025 16:49:04 +0000
Subject: [PATCH 05/12] Improve user management and authentication system for
better security
Implement user roles (admin/user), role-based route protection, and update API endpoints for authentication and user management.
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: ae4037a0-2a6f-4530-9bac-79b543286bda
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/397bca8c-984f-43ff-841a-10897aeb8140/ae4037a0-2a6f-4530-9bac-79b543286bda/HPCBgsr
---
replit.md | 46 +++++++++++++++++++++++++++++++++++++++-------
1 file changed, 39 insertions(+), 7 deletions(-)
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
From ebc581e01538cbf33f3a4837e1568f402f826e1e Mon Sep 17 00:00:00 2001
From: michaelschal <35957947-michaelschal@users.noreply.replit.com>
Date: Wed, 1 Oct 2025 16:52:49 +0000
Subject: [PATCH 06/12] Transitioned from Plan to Build mode
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: ae4037a0-2a6f-4530-9bac-79b543286bda
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/397bca8c-984f-43ff-841a-10897aeb8140/ae4037a0-2a6f-4530-9bac-79b543286bda/HPCBgsr
From f6d82f67ba5eb97b874bdbd0aae571ae2a04895a Mon Sep 17 00:00:00 2001
From: michaelschal <35957947-michaelschal@users.noreply.replit.com>
Date: Wed, 1 Oct 2025 17:00:09 +0000
Subject: [PATCH 07/12] Add user management features for administrators to
control accounts
Implement CRUD operations for users, including listing, creating, editing, and deleting users, along with updates to authentication and UI components.
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: ae4037a0-2a6f-4530-9bac-79b543286bda
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/397bca8c-984f-43ff-841a-10897aeb8140/ae4037a0-2a6f-4530-9bac-79b543286bda/HPCBgsr
---
.replit | 4 +
client/src/components/sidebar.tsx | 8 +-
client/src/pages/login.tsx | 29 +-
client/src/pages/users-admin.tsx | 437 +++++++++++++++++++++++-------
server/routes.ts | 115 +++++++-
server/storage.ts | 16 ++
6 files changed, 507 insertions(+), 102 deletions(-)
diff --git a/.replit b/.replit
index 3238a51..983a4bc 100644
--- a/.replit
+++ b/.replit
@@ -14,6 +14,10 @@ run = ["npm", "run", "start"]
localPort = 5000
externalPort = 80
+[[ports]]
+localPort = 46679
+externalPort = 3000
+
[env]
PORT = "5000"
diff --git a/client/src/components/sidebar.tsx b/client/src/components/sidebar.tsx
index 31ca9ac..657e772 100644
--- a/client/src/components/sidebar.tsx
+++ b/client/src/components/sidebar.tsx
@@ -11,12 +11,12 @@ export default function Sidebar() {
const { toast } = useToast();
// Charger la session utilisateur
- const { data: session } = useQuery({
+ const { data: session } = useQuery<{ id: string; username: string; role: string }>({
queryKey: ["/api/auth/session"],
retry: false,
});
- const isAdmin = (session as any)?.role === "admin";
+ const isAdmin = session?.role === "admin";
// Mutation pour déconnexion
const logoutMutation = useMutation({
@@ -191,8 +191,8 @@ export default function Sidebar() {
{session && (
Connecté en tant que
-
{(session as any).username}
-
{(session as any).role}
+
{session.username}
+
{session.role}
)}
diff --git a/client/src/pages/login.tsx b/client/src/pages/login.tsx
index ab24eee..605242e 100644
--- a/client/src/pages/login.tsx
+++ b/client/src/pages/login.tsx
@@ -1,4 +1,4 @@
-import { useState } from "react";
+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";
@@ -14,6 +14,22 @@ export default function Login() {
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();
@@ -102,10 +118,13 @@ export default function Login() {
-