mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-23 15:08:13 +02:00
feat(security): implement security fixes - Add AES-256-GCM encryption for Facebook/Instagram tokens - Add helmet for HTTP security headers - Add rate limiting (5 login attempts, 100 req/15min) - Add file upload validation (50MB max, MIME type whitelist) - Secure SQL console endpoint (disabled in production) - Enforce SESSION_SECRET in production
This commit is contained in:
1 parent
c3660f811b
commit
633875e8ec
7 files changed
+917
-450
No files matched your search
@@ -16,6 +16,10 @@ APP_URL=http://localhost:5555
|
|||||||
# Clé secrète pour les sessions (CHANGEZ CETTE VALEUR)
|
# Clé secrète pour les sessions (CHANGEZ CETTE VALEUR)
|
||||||
SESSION_SECRET=your-secret-key-change-me-to-random-string
|
SESSION_SECRET=your-secret-key-change-me-to-random-string
|
||||||
|
|
||||||
|
# Clé de chiffrement pour les tokens Facebook/Instagram (OBLIGATOIRE en production)
|
||||||
|
# Générer avec: openssl rand -hex 32
|
||||||
|
ENCRYPTION_KEY=your-32-byte-hex-key-here
|
||||||
|
|
||||||
# Clé API OpenRouter pour la génération de texte IA
|
# Clé API OpenRouter pour la génération de texte IA
|
||||||
# Obtenez votre clé sur https://openrouter.ai/
|
# Obtenez votre clé sur https://openrouter.ai/
|
||||||
OPENROUTER_API_KEY=your-openrouter-api-key-here
|
OPENROUTER_API_KEY=your-openrouter-api-key-here
|
||||||
|
|||||||
Generated
+569
-298
File diff suppressed because it is too large.
Load diff
+3
-1
@@ -64,8 +64,10 @@
|
|||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
"emoji-regex": "^10.6.0",
|
"emoji-regex": "^10.6.0",
|
||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
|
"express-rate-limit": "^7.5.1",
|
||||||
"express-session": "^1.18.1",
|
"express-session": "^1.18.1",
|
||||||
"framer-motion": "^11.13.1",
|
"framer-motion": "^11.13.1",
|
||||||
|
"helmet": "^8.1.0",
|
||||||
"html-to-image": "^1.11.13",
|
"html-to-image": "^1.11.13",
|
||||||
"html2canvas": "^1.4.1",
|
"html2canvas": "^1.4.1",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
@@ -125,4 +127,4 @@
|
|||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"bufferutil": "^4.0.8"
|
"bufferutil": "^4.0.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+61
-14
@@ -2,6 +2,9 @@ import express, { type Request, Response, NextFunction } from "express";
|
|||||||
import session from "express-session";
|
import session from "express-session";
|
||||||
import connectPgSimple from "connect-pg-simple";
|
import connectPgSimple from "connect-pg-simple";
|
||||||
import pg from "pg";
|
import pg from "pg";
|
||||||
|
import crypto from "crypto";
|
||||||
|
import helmet from "helmet";
|
||||||
|
import rateLimit from "express-rate-limit";
|
||||||
import passport from "./auth";
|
import passport from "./auth";
|
||||||
import { registerRoutes } from "./routes";
|
import { registerRoutes } from "./routes";
|
||||||
import { setupVite, serveStatic, log } from "./vite";
|
import { setupVite, serveStatic, log } from "./vite";
|
||||||
@@ -11,6 +14,42 @@ import { ensureAdminUserExists } from "./init-admin";
|
|||||||
const app = express();
|
const app = express();
|
||||||
const PgSession = connectPgSimple(session);
|
const PgSession = connectPgSimple(session);
|
||||||
|
|
||||||
|
// Headers de sécurité HTTP avec helmet
|
||||||
|
app.use(helmet({
|
||||||
|
contentSecurityPolicy: {
|
||||||
|
directives: {
|
||||||
|
defaultSrc: ["'self'"],
|
||||||
|
scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'"],
|
||||||
|
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
|
||||||
|
imgSrc: ["'self'", "data:", "https:", "blob:"],
|
||||||
|
fontSrc: ["'self'", "https://fonts.gstatic.com"],
|
||||||
|
connectSrc: ["'self'", "https://graph.facebook.com", "https://openrouter.ai", "https://res.cloudinary.com", "wss:", "ws:"],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
crossOriginEmbedderPolicy: false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Rate limiting global - 100 requêtes par 15 minutes par IP
|
||||||
|
const globalLimiter = rateLimit({
|
||||||
|
windowMs: 15 * 60 * 1000,
|
||||||
|
max: 100,
|
||||||
|
message: { error: 'Trop de requêtes, réessayez plus tard' },
|
||||||
|
standardHeaders: true,
|
||||||
|
legacyHeaders: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Rate limiting strict pour l'authentification - 5 tentatives par 15 minutes
|
||||||
|
const authLimiter = rateLimit({
|
||||||
|
windowMs: 15 * 60 * 1000,
|
||||||
|
max: 5,
|
||||||
|
message: { error: 'Trop de tentatives de connexion, réessayez dans 15 minutes' },
|
||||||
|
standardHeaders: true,
|
||||||
|
legacyHeaders: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use('/api/', globalLimiter);
|
||||||
|
app.use('/api/auth/login', authLimiter);
|
||||||
|
|
||||||
declare module 'http' {
|
declare module 'http' {
|
||||||
interface IncomingMessage {
|
interface IncomingMessage {
|
||||||
rawBody: unknown
|
rawBody: unknown
|
||||||
@@ -23,28 +62,36 @@ app.use(express.json({
|
|||||||
}));
|
}));
|
||||||
app.use(express.urlencoded({ extended: false }));
|
app.use(express.urlencoded({ extended: false }));
|
||||||
|
|
||||||
// Configuration des sessions
|
// Validation renforcée du SESSION_SECRET
|
||||||
if (!process.env.SESSION_SECRET) {
|
if (!process.env.SESSION_SECRET && process.env.NODE_ENV === 'production') {
|
||||||
console.warn('⚠️ SESSION_SECRET non défini. Utilisation d\'une clé par défaut (NON SÉCURISÉ en production)');
|
console.error('❌ SESSION_SECRET non défini en production. Arrêt du serveur.');
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!process.env.SESSION_SECRET) {
|
||||||
|
console.warn('⚠️ SESSION_SECRET non défini. Utilisation d\'une clé aléatoire pour le développement.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Générer un secret aléatoire pour le dev si non défini
|
||||||
|
const sessionSecret = process.env.SESSION_SECRET || crypto.randomBytes(32).toString('hex');
|
||||||
|
|
||||||
// Déterminer si on utilise HTTPS basé sur APP_URL
|
// Déterminer si on utilise HTTPS basé sur APP_URL
|
||||||
const isHttps = process.env.APP_URL?.startsWith('https://') || false;
|
const isHttps = process.env.APP_URL?.startsWith('https://') || false;
|
||||||
|
|
||||||
// Configuration du store de session pour production
|
// Configuration du store de session pour production
|
||||||
const sessionStore = process.env.NODE_ENV === 'production' && process.env.DATABASE_URL
|
const sessionStore = process.env.NODE_ENV === 'production' && process.env.DATABASE_URL
|
||||||
? new PgSession({
|
? new PgSession({
|
||||||
pool: new pg.Pool({
|
pool: new pg.Pool({
|
||||||
connectionString: process.env.DATABASE_URL,
|
connectionString: process.env.DATABASE_URL,
|
||||||
}),
|
}),
|
||||||
tableName: 'session',
|
tableName: 'session',
|
||||||
createTableIfMissing: true,
|
createTableIfMissing: true,
|
||||||
})
|
})
|
||||||
: undefined; // MemoryStore par défaut en dev
|
: undefined; // MemoryStore par défaut en dev
|
||||||
|
|
||||||
app.use(session({
|
app.use(session({
|
||||||
store: sessionStore,
|
store: sessionStore,
|
||||||
secret: process.env.SESSION_SECRET || 'your-secret-key-change-me',
|
secret: sessionSecret,
|
||||||
resave: false,
|
resave: false,
|
||||||
saveUninitialized: false,
|
saveUninitialized: false,
|
||||||
cookie: {
|
cookie: {
|
||||||
@@ -77,7 +124,7 @@ app.use((req, res, next) => {
|
|||||||
if (path === "/api/auth/session" && res.statusCode === 401) {
|
if (path === "/api/auth/session" && res.statusCode === 401) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let logLine = `${req.method} ${path} ${res.statusCode} in ${duration}ms`;
|
let logLine = `${req.method} ${path} ${res.statusCode} in ${duration}ms`;
|
||||||
if (capturedJsonResponse) {
|
if (capturedJsonResponse) {
|
||||||
logLine += ` :: ${JSON.stringify(capturedJsonResponse)}`;
|
logLine += ` :: ${JSON.stringify(capturedJsonResponse)}`;
|
||||||
@@ -119,17 +166,17 @@ app.use((req, res, next) => {
|
|||||||
// this serves both the API and the client.
|
// this serves both the API and the client.
|
||||||
// It is the only port that is not firewalled.
|
// It is the only port that is not firewalled.
|
||||||
const port = parseInt(process.env.PORT || '5000', 10);
|
const port = parseInt(process.env.PORT || '5000', 10);
|
||||||
|
|
||||||
// Initialiser l'utilisateur admin par défaut avant de démarrer le serveur
|
// Initialiser l'utilisateur admin par défaut avant de démarrer le serveur
|
||||||
await ensureAdminUserExists();
|
await ensureAdminUserExists();
|
||||||
|
|
||||||
server.listen({
|
server.listen({
|
||||||
port,
|
port,
|
||||||
host: "0.0.0.0",
|
host: "0.0.0.0",
|
||||||
reusePort: true,
|
reusePort: true,
|
||||||
}, () => {
|
}, () => {
|
||||||
log(`serving on port ${port}`);
|
log(`serving on port ${port}`);
|
||||||
|
|
||||||
// Démarrer le scheduler pour les publications programmées
|
// Démarrer le scheduler pour les publications programmées
|
||||||
schedulerService.start();
|
schedulerService.start();
|
||||||
});
|
});
|
||||||
|
|||||||
+154
-126
@@ -11,7 +11,27 @@ import { cloudinaryService } from "./services/cloudinary";
|
|||||||
import { insertPostSchema, insertScheduledPostSchema, insertSocialPageSchema, insertAiGenerationSchema, insertCloudinaryConfigSchema, updateCloudinaryConfigSchema, insertOpenrouterConfigSchema, updateOpenrouterConfigSchema, insertUserSchema, postMedia, type SocialPage } from "@shared/schema";
|
import { insertPostSchema, insertScheduledPostSchema, insertSocialPageSchema, insertAiGenerationSchema, insertCloudinaryConfigSchema, updateCloudinaryConfigSchema, insertOpenrouterConfigSchema, updateOpenrouterConfigSchema, insertUserSchema, postMedia, type SocialPage } from "@shared/schema";
|
||||||
import type { User, InsertUser, ScheduledPost } from "@shared/schema";
|
import type { User, InsertUser, ScheduledPost } from "@shared/schema";
|
||||||
|
|
||||||
const upload = multer({ storage: multer.memoryStorage() });
|
// Types MIME autorisés pour les uploads
|
||||||
|
const ALLOWED_MIME_TYPES = [
|
||||||
|
'image/jpeg', 'image/png', 'image/gif', 'image/webp',
|
||||||
|
'video/mp4', 'video/quicktime', 'video/webm'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Configuration multer avec validation de taille et type
|
||||||
|
const upload = multer({
|
||||||
|
storage: multer.memoryStorage(),
|
||||||
|
limits: {
|
||||||
|
fileSize: 50 * 1024 * 1024, // 50MB max
|
||||||
|
files: 10 // 10 fichiers max
|
||||||
|
},
|
||||||
|
fileFilter: (req, file, cb) => {
|
||||||
|
if (ALLOWED_MIME_TYPES.includes(file.mimetype)) {
|
||||||
|
cb(null, true);
|
||||||
|
} else {
|
||||||
|
cb(new Error(`Type de fichier non autorisé: ${file.mimetype}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Middleware pour vérifier l'authentification
|
// Middleware pour vérifier l'authentification
|
||||||
function requireAuth(req: Request, res: Response, next: NextFunction) {
|
function requireAuth(req: Request, res: Response, next: NextFunction) {
|
||||||
@@ -83,14 +103,14 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
app.get("/api/users", requireAdmin, async (req, res) => {
|
app.get("/api/users", requireAdmin, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const allUsers = await storage.getAllUsers();
|
const allUsers = await storage.getAllUsers();
|
||||||
|
|
||||||
// Ne pas envoyer les mots de passe
|
// Ne pas envoyer les mots de passe
|
||||||
const safeUsers = allUsers.map(user => ({
|
const safeUsers = allUsers.map(user => ({
|
||||||
id: user.id,
|
id: user.id,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
role: user.role,
|
role: user.role,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
res.json(safeUsers);
|
res.json(safeUsers);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching users:", error);
|
console.error("Error fetching users:", error);
|
||||||
@@ -143,7 +163,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
try {
|
try {
|
||||||
const userId = req.params.id;
|
const userId = req.params.id;
|
||||||
const { username, password, role } = req.body;
|
const { username, password, role } = req.body;
|
||||||
|
|
||||||
// Vérifier si l'utilisateur existe
|
// Vérifier si l'utilisateur existe
|
||||||
const existingUser = await storage.getUser(userId);
|
const existingUser = await storage.getUser(userId);
|
||||||
if (!existingUser) {
|
if (!existingUser) {
|
||||||
@@ -151,7 +171,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const updateData: Partial<InsertUser> = {};
|
const updateData: Partial<InsertUser> = {};
|
||||||
|
|
||||||
if (username && username !== existingUser.username) {
|
if (username && username !== existingUser.username) {
|
||||||
// Vérifier si le nouveau username est déjà pris
|
// Vérifier si le nouveau username est déjà pris
|
||||||
const userWithSameUsername = await storage.getUserByUsername(username);
|
const userWithSameUsername = await storage.getUserByUsername(username);
|
||||||
@@ -160,12 +180,12 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
}
|
}
|
||||||
updateData.username = username;
|
updateData.username = username;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (password) {
|
if (password) {
|
||||||
// Hasher le nouveau mot de passe
|
// Hasher le nouveau mot de passe
|
||||||
updateData.password = await bcrypt.hash(password, 10);
|
updateData.password = await bcrypt.hash(password, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (role && (role === "admin" || role === "user")) {
|
if (role && (role === "admin" || role === "user")) {
|
||||||
updateData.role = role;
|
updateData.role = role;
|
||||||
}
|
}
|
||||||
@@ -175,7 +195,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const updatedUser = await storage.updateUser(userId, updateData);
|
const updatedUser = await storage.updateUser(userId, updateData);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
id: updatedUser.id,
|
id: updatedUser.id,
|
||||||
username: updatedUser.username,
|
username: updatedUser.username,
|
||||||
@@ -192,7 +212,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
try {
|
try {
|
||||||
const userId = req.params.id;
|
const userId = req.params.id;
|
||||||
const currentUser = req.user as User;
|
const currentUser = req.user as User;
|
||||||
|
|
||||||
// Empêcher l'admin de se supprimer lui-même
|
// Empêcher l'admin de se supprimer lui-même
|
||||||
if (userId === currentUser.id) {
|
if (userId === currentUser.id) {
|
||||||
return res.status(400).json({ error: "Vous ne pouvez pas supprimer votre propre compte" });
|
return res.status(400).json({ error: "Vous ne pouvez pas supprimer votre propre compte" });
|
||||||
@@ -205,7 +225,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await storage.deleteUser(userId);
|
await storage.deleteUser(userId);
|
||||||
|
|
||||||
res.json({ success: true, message: "Utilisateur supprimé avec succès" });
|
res.json({ success: true, message: "Utilisateur supprimé avec succès" });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("Error deleting user:", error);
|
console.error("Error deleting user:", error);
|
||||||
@@ -240,7 +260,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
|
|
||||||
// Créer les nouvelles permissions
|
// Créer les nouvelles permissions
|
||||||
const permissions = await Promise.all(
|
const permissions = await Promise.all(
|
||||||
pageIds.map(pageId =>
|
pageIds.map(pageId =>
|
||||||
storage.createPagePermission({ userId, pageId })
|
storage.createPagePermission({ userId, pageId })
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -276,10 +296,10 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: `${migratedCount} permissions migrées avec succès`,
|
message: `${migratedCount} permissions migrées avec succès`,
|
||||||
migratedCount
|
migratedCount
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error migrating permissions:", error);
|
console.error("Error migrating permissions:", error);
|
||||||
@@ -291,14 +311,14 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
app.get("/api/auth/default-password-status", async (req, res) => {
|
app.get("/api/auth/default-password-status", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const adminUser = await storage.getUserByUsername("admin");
|
const adminUser = await storage.getUserByUsername("admin");
|
||||||
|
|
||||||
if (!adminUser) {
|
if (!adminUser) {
|
||||||
return res.json({ isDefault: false });
|
return res.json({ isDefault: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vérifier si le mot de passe correspond à "admin"
|
// Vérifier si le mot de passe correspond à "admin"
|
||||||
const isDefaultPassword = await bcrypt.compare("admin", adminUser.password);
|
const isDefaultPassword = await bcrypt.compare("admin", adminUser.password);
|
||||||
|
|
||||||
res.json({ isDefault: isDefaultPassword });
|
res.json({ isDefault: isDefaultPassword });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error checking default password:", error);
|
console.error("Error checking default password:", error);
|
||||||
@@ -306,8 +326,16 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Route SQL (réservée aux admins)
|
// Route SQL (réservée aux admins) - DÉSACTIVÉE EN PRODUCTION sauf si explicitement autorisée
|
||||||
app.post("/api/sql/execute", requireAdmin, async (req, res) => {
|
app.post("/api/sql/execute", requireAdmin, async (req, res) => {
|
||||||
|
// Vérification de sécurité: désactivé en production sauf si ALLOW_SQL_CONSOLE=true
|
||||||
|
if (process.env.NODE_ENV === 'production' && process.env.ALLOW_SQL_CONSOLE !== 'true') {
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
error: "Console SQL désactivée en production pour des raisons de sécurité"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Validation Zod
|
// Validation Zod
|
||||||
const sqlQuerySchema = z.object({
|
const sqlQuerySchema = z.object({
|
||||||
@@ -318,7 +346,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
|
|
||||||
const { db } = await import("./db");
|
const { db } = await import("./db");
|
||||||
const result = await db.execute(validatedData.query);
|
const result = await db.execute(validatedData.query);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
result,
|
result,
|
||||||
@@ -345,7 +373,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
const result = await db.execute<{ tablename: string }>(
|
const result = await db.execute<{ tablename: string }>(
|
||||||
`SELECT tablename FROM pg_tables WHERE schemaname = 'public' ORDER BY tablename;`
|
`SELECT tablename FROM pg_tables WHERE schemaname = 'public' ORDER BY tablename;`
|
||||||
);
|
);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
tables: result.rows || result,
|
tables: result.rows || result,
|
||||||
});
|
});
|
||||||
@@ -363,7 +391,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
try {
|
try {
|
||||||
const user = req.user as User;
|
const user = req.user as User;
|
||||||
const userId = user.id;
|
const userId = user.id;
|
||||||
|
|
||||||
const posts = await storage.getPosts(userId);
|
const posts = await storage.getPosts(userId);
|
||||||
const pages = await storage.getSocialPages(userId);
|
const pages = await storage.getSocialPages(userId);
|
||||||
const media = await storage.getMedia(userId);
|
const media = await storage.getMedia(userId);
|
||||||
@@ -379,7 +407,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
const yesterday = new Date(now);
|
const yesterday = new Date(now);
|
||||||
yesterday.setDate(yesterday.getDate() - 1);
|
yesterday.setDate(yesterday.getDate() - 1);
|
||||||
yesterday.setHours(0, 0, 0, 0);
|
yesterday.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
const lastMonth = new Date(now);
|
const lastMonth = new Date(now);
|
||||||
lastMonth.setMonth(lastMonth.getMonth() - 1);
|
lastMonth.setMonth(lastMonth.getMonth() - 1);
|
||||||
|
|
||||||
@@ -399,7 +427,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
}).length;
|
}).length;
|
||||||
|
|
||||||
// Calculer les variations en pourcentage
|
// Calculer les variations en pourcentage
|
||||||
const aiTextChange = aiTextsYesterday > 0
|
const aiTextChange = aiTextsYesterday > 0
|
||||||
? Math.round(((currentAiTexts - aiTextsYesterday) / aiTextsYesterday) * 100)
|
? Math.round(((currentAiTexts - aiTextsYesterday) / aiTextsYesterday) * 100)
|
||||||
: currentAiTexts > 0 ? 100 : 0;
|
: currentAiTexts > 0 ? 100 : 0;
|
||||||
|
|
||||||
@@ -469,10 +497,10 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
|
|
||||||
// Check if Cloudinary is configured (shared config used for all users)
|
// Check if Cloudinary is configured (shared config used for all users)
|
||||||
const cloudinaryConfig = await storage.getAnyCloudinaryConfig();
|
const cloudinaryConfig = await storage.getAnyCloudinaryConfig();
|
||||||
|
|
||||||
if (!cloudinaryConfig) {
|
if (!cloudinaryConfig) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
error: "Cloudinary not configured. Please ask an administrator to configure Cloudinary in Settings first."
|
error: "Cloudinary not configured. Please ask an administrator to configure Cloudinary in Settings first."
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -543,7 +571,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
// Load with Sharp to process
|
// Load with Sharp to process
|
||||||
const sharp = (await import("sharp")).default;
|
const sharp = (await import("sharp")).default;
|
||||||
let image = sharp(imageBuffer);
|
let image = sharp(imageBuffer);
|
||||||
|
|
||||||
// Get image metadata for dimensions
|
// Get image metadata for dimensions
|
||||||
const metadata = await image.metadata();
|
const metadata = await image.metadata();
|
||||||
const width = metadata.width!;
|
const width = metadata.width!;
|
||||||
@@ -562,33 +590,33 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
|
|
||||||
// Position mapping for each corner
|
// Position mapping for each corner
|
||||||
const positions: Record<string, { x: number; y: number; polygon: string; textX: number; textY: number; textRotation: number }> = {
|
const positions: Record<string, { x: number; y: number; polygon: string; textX: number; textY: number; textRotation: number }> = {
|
||||||
north_west: {
|
north_west: {
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
polygon: `0,0 ${ribbonSize},0 0,${ribbonSize}`, // Top-left triangle
|
polygon: `0,0 ${ribbonSize},0 0,${ribbonSize}`, // Top-left triangle
|
||||||
textX: ribbonSize * 0.3,
|
textX: ribbonSize * 0.3,
|
||||||
textY: ribbonSize * 0.3,
|
textY: ribbonSize * 0.3,
|
||||||
textRotation: -45
|
textRotation: -45
|
||||||
},
|
},
|
||||||
north_east: {
|
north_east: {
|
||||||
x: width - ribbonSize,
|
x: width - ribbonSize,
|
||||||
y: 0,
|
y: 0,
|
||||||
polygon: `0,0 ${ribbonSize},0 ${ribbonSize},${ribbonSize}`, // Top-right triangle
|
polygon: `0,0 ${ribbonSize},0 ${ribbonSize},${ribbonSize}`, // Top-right triangle
|
||||||
textX: ribbonSize * 0.7,
|
textX: ribbonSize * 0.7,
|
||||||
textY: ribbonSize * 0.3,
|
textY: ribbonSize * 0.3,
|
||||||
textRotation: 45
|
textRotation: 45
|
||||||
},
|
},
|
||||||
south_west: {
|
south_west: {
|
||||||
x: 0,
|
x: 0,
|
||||||
y: height - ribbonSize,
|
y: height - ribbonSize,
|
||||||
polygon: `0,0 0,${ribbonSize} ${ribbonSize},${ribbonSize}`, // Bottom-left triangle
|
polygon: `0,0 0,${ribbonSize} ${ribbonSize},${ribbonSize}`, // Bottom-left triangle
|
||||||
textX: ribbonSize * 0.3,
|
textX: ribbonSize * 0.3,
|
||||||
textY: ribbonSize * 0.7,
|
textY: ribbonSize * 0.7,
|
||||||
textRotation: -135
|
textRotation: -135
|
||||||
},
|
},
|
||||||
south_east: {
|
south_east: {
|
||||||
x: width - ribbonSize,
|
x: width - ribbonSize,
|
||||||
y: height - ribbonSize,
|
y: height - ribbonSize,
|
||||||
polygon: `${ribbonSize},0 0,${ribbonSize} ${ribbonSize},${ribbonSize}`, // Bottom-right triangle
|
polygon: `${ribbonSize},0 0,${ribbonSize} ${ribbonSize},${ribbonSize}`, // Bottom-right triangle
|
||||||
textX: ribbonSize * 0.7,
|
textX: ribbonSize * 0.7,
|
||||||
textY: ribbonSize * 0.7,
|
textY: ribbonSize * 0.7,
|
||||||
@@ -642,8 +670,8 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
const badgeSvg = `
|
const badgeSvg = `
|
||||||
<svg width="${badgeWidth}" height="${badgeHeight}">
|
<svg width="${badgeWidth}" height="${badgeHeight}">
|
||||||
<rect x="0" y="0" width="${badgeWidth}" height="${badgeHeight}"
|
<rect x="0" y="0" width="${badgeWidth}" height="${badgeHeight}"
|
||||||
rx="${badgeHeight/2}" ry="${badgeHeight/2}" fill="${color}"/>
|
rx="${badgeHeight / 2}" ry="${badgeHeight / 2}" fill="${color}"/>
|
||||||
<text x="${badgeWidth/2}" y="${badgeHeight/2}"
|
<text x="${badgeWidth / 2}" y="${badgeHeight / 2}"
|
||||||
font-family="Arial, sans-serif" font-size="${fontSize}" font-weight="bold"
|
font-family="Arial, sans-serif" font-size="${fontSize}" font-weight="bold"
|
||||||
fill="white" text-anchor="middle" dominant-baseline="middle">
|
fill="white" text-anchor="middle" dominant-baseline="middle">
|
||||||
${badgeText}
|
${badgeText}
|
||||||
@@ -661,7 +689,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
// Add logo overlay
|
// Add logo overlay
|
||||||
if (logo && logo.enabled) {
|
if (logo && logo.enabled) {
|
||||||
console.log("🏢 Adding logo overlay...");
|
console.log("🏢 Adding logo overlay...");
|
||||||
|
|
||||||
// Get Cloudinary config to get logo public ID
|
// Get Cloudinary config to get logo public ID
|
||||||
const cloudinaryConfig = await storage.getAnyCloudinaryConfig();
|
const cloudinaryConfig = await storage.getAnyCloudinaryConfig();
|
||||||
if (cloudinaryConfig && cloudinaryConfig.logoPublicId) {
|
if (cloudinaryConfig && cloudinaryConfig.logoPublicId) {
|
||||||
@@ -669,44 +697,44 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
// Build logo URL from Cloudinary
|
// Build logo URL from Cloudinary
|
||||||
const logoUrl = `https://res.cloudinary.com/${cloudinaryConfig.cloudName}/image/upload/${cloudinaryConfig.logoPublicId}`;
|
const logoUrl = `https://res.cloudinary.com/${cloudinaryConfig.cloudName}/image/upload/${cloudinaryConfig.logoPublicId}`;
|
||||||
console.log("📥 Downloading logo from:", logoUrl);
|
console.log("📥 Downloading logo from:", logoUrl);
|
||||||
|
|
||||||
// Download logo
|
// Download logo
|
||||||
const logoResponse = await fetch(logoUrl);
|
const logoResponse = await fetch(logoUrl);
|
||||||
const logoBuffer = Buffer.from(await logoResponse.arrayBuffer());
|
const logoBuffer = Buffer.from(await logoResponse.arrayBuffer());
|
||||||
|
|
||||||
// Determine logo size based on selection with safety cap
|
// Determine logo size based on selection with safety cap
|
||||||
const padding = 20;
|
const padding = 20;
|
||||||
const maxLogoWidth = width - (padding * 2); // Ensure logo fits within canvas
|
const maxLogoWidth = width - (padding * 2); // Ensure logo fits within canvas
|
||||||
|
|
||||||
const logoSizePercentages = {
|
const logoSizePercentages = {
|
||||||
small: 0.35, // 35% of image width
|
small: 0.35, // 35% of image width
|
||||||
medium: 0.50, // 50% of image width
|
medium: 0.50, // 50% of image width
|
||||||
large: 0.70 // 70% of image width
|
large: 0.70 // 70% of image width
|
||||||
};
|
};
|
||||||
|
|
||||||
const requestedWidth = Math.round(width * logoSizePercentages[logo.size as keyof typeof logoSizePercentages] || logoSizePercentages.medium);
|
const requestedWidth = Math.round(width * logoSizePercentages[logo.size as keyof typeof logoSizePercentages] || logoSizePercentages.medium);
|
||||||
const logoWidth = Math.min(requestedWidth, maxLogoWidth);
|
const logoWidth = Math.min(requestedWidth, maxLogoWidth);
|
||||||
|
|
||||||
// Resize logo preserving aspect ratio and apply opacity
|
// Resize logo preserving aspect ratio and apply opacity
|
||||||
const resizedLogo = await sharp(logoBuffer)
|
const resizedLogo = await sharp(logoBuffer)
|
||||||
.resize({ width: logoWidth, fit: 'contain' })
|
.resize({ width: logoWidth, fit: 'contain' })
|
||||||
.ensureAlpha()
|
.ensureAlpha()
|
||||||
.toBuffer();
|
.toBuffer();
|
||||||
|
|
||||||
// Get resized logo dimensions
|
// Get resized logo dimensions
|
||||||
const logoMetadata = await sharp(resizedLogo).metadata();
|
const logoMetadata = await sharp(resizedLogo).metadata();
|
||||||
const logoHeight = logoMetadata.height || logoWidth;
|
const logoHeight = logoMetadata.height || logoWidth;
|
||||||
|
|
||||||
// Ensure logo fits within height as well
|
// Ensure logo fits within height as well
|
||||||
const maxLogoHeight = height - (padding * 2);
|
const maxLogoHeight = height - (padding * 2);
|
||||||
if (logoHeight > maxLogoHeight) {
|
if (logoHeight > maxLogoHeight) {
|
||||||
console.warn(`⚠️ Logo height ${logoHeight}px exceeds max ${maxLogoHeight}px, would be clipped`);
|
console.warn(`⚠️ Logo height ${logoHeight}px exceeds max ${maxLogoHeight}px, would be clipped`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate position with padding
|
// Calculate position with padding
|
||||||
let logoX = padding;
|
let logoX = padding;
|
||||||
let logoY = padding;
|
let logoY = padding;
|
||||||
|
|
||||||
if (logo.position === 'north_east') {
|
if (logo.position === 'north_east') {
|
||||||
logoX = width - logoWidth - padding;
|
logoX = width - logoWidth - padding;
|
||||||
} else if (logo.position === 'south_west') {
|
} else if (logo.position === 'south_west') {
|
||||||
@@ -718,7 +746,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
logoX = Math.round((width - logoWidth) / 2);
|
logoX = Math.round((width - logoWidth) / 2);
|
||||||
logoY = Math.round((height - logoHeight) / 2);
|
logoY = Math.round((height - logoHeight) / 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply opacity by manipulating alpha channel
|
// Apply opacity by manipulating alpha channel
|
||||||
let logoWithOpacity = resizedLogo;
|
let logoWithOpacity = resizedLogo;
|
||||||
if (logo.opacity < 100) {
|
if (logo.opacity < 100) {
|
||||||
@@ -729,7 +757,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
.linear(opacityFactor, 0)
|
.linear(opacityFactor, 0)
|
||||||
.toBuffer();
|
.toBuffer();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add logo overlay
|
// Add logo overlay
|
||||||
overlays.push({
|
overlays.push({
|
||||||
input: logoWithOpacity,
|
input: logoWithOpacity,
|
||||||
@@ -737,7 +765,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
left: logoX,
|
left: logoX,
|
||||||
blend: 'over'
|
blend: 'over'
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`✅ Logo added at position ${logo.position} with ${logo.opacity}% opacity`);
|
console.log(`✅ Logo added at position ${logo.position} with ${logo.opacity}% opacity`);
|
||||||
} catch (logoError) {
|
} catch (logoError) {
|
||||||
console.error("⚠️ Failed to apply logo:", logoError);
|
console.error("⚠️ Failed to apply logo:", logoError);
|
||||||
@@ -761,8 +789,8 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
// Check if Cloudinary is configured (shared config used for all users)
|
// Check if Cloudinary is configured (shared config used for all users)
|
||||||
const cloudinaryConfig = await storage.getAnyCloudinaryConfig();
|
const cloudinaryConfig = await storage.getAnyCloudinaryConfig();
|
||||||
if (!cloudinaryConfig) {
|
if (!cloudinaryConfig) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
error: "Cloudinary not configured. Please ask an administrator to configure Cloudinary in Settings first."
|
error: "Cloudinary not configured. Please ask an administrator to configure Cloudinary in Settings first."
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -804,11 +832,11 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
const user = req.user as User;
|
const user = req.user as User;
|
||||||
const userId = user.id;
|
const userId = user.id;
|
||||||
const mediaId = req.params.id;
|
const mediaId = req.params.id;
|
||||||
|
|
||||||
// Get media to find cloudinary public ID
|
// Get media to find cloudinary public ID
|
||||||
const allMedia = await storage.getMedia(userId);
|
const allMedia = await storage.getMedia(userId);
|
||||||
const mediaToDelete = allMedia.find(m => m.id === mediaId);
|
const mediaToDelete = allMedia.find(m => m.id === mediaId);
|
||||||
|
|
||||||
if (!mediaToDelete) {
|
if (!mediaToDelete) {
|
||||||
return res.status(404).json({ error: "Media not found" });
|
return res.status(404).json({ error: "Media not found" });
|
||||||
}
|
}
|
||||||
@@ -816,7 +844,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
// Delete from Cloudinary
|
// Delete from Cloudinary
|
||||||
if (mediaToDelete.cloudinaryPublicId) {
|
if (mediaToDelete.cloudinaryPublicId) {
|
||||||
await cloudinaryService.deleteMedia(
|
await cloudinaryService.deleteMedia(
|
||||||
mediaToDelete.cloudinaryPublicId,
|
mediaToDelete.cloudinaryPublicId,
|
||||||
userId,
|
userId,
|
||||||
mediaToDelete.type
|
mediaToDelete.type
|
||||||
);
|
);
|
||||||
@@ -824,7 +852,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
|
|
||||||
// Delete from database
|
// Delete from database
|
||||||
await storage.deleteMedia(mediaId);
|
await storage.deleteMedia(mediaId);
|
||||||
|
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error deleting media:", error);
|
console.error("Error deleting media:", error);
|
||||||
@@ -850,10 +878,10 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
const user = req.user as User;
|
const user = req.user as User;
|
||||||
const userId = user.id;
|
const userId = user.id;
|
||||||
const { pageIds, postType, mediaId, mediaIds, ...postFields } = req.body;
|
const { pageIds, postType, mediaId, mediaIds, ...postFields } = req.body;
|
||||||
|
|
||||||
// Convert mediaIds to standardized format: array of { mediaId, displayOrder }
|
// Convert mediaIds to standardized format: array of { mediaId, displayOrder }
|
||||||
let finalMediaItems: Array<{ mediaId: string; displayOrder: number }> = [];
|
let finalMediaItems: Array<{ mediaId: string; displayOrder: number }> = [];
|
||||||
|
|
||||||
if (mediaIds && Array.isArray(mediaIds)) {
|
if (mediaIds && Array.isArray(mediaIds)) {
|
||||||
// New format: array of objects or strings
|
// New format: array of objects or strings
|
||||||
finalMediaItems = mediaIds.map((item: any, index: number) => {
|
finalMediaItems = mediaIds.map((item: any, index: number) => {
|
||||||
@@ -873,52 +901,52 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
// Legacy single mediaId
|
// Legacy single mediaId
|
||||||
finalMediaItems = [{ mediaId, displayOrder: 0 }];
|
finalMediaItems = [{ mediaId, displayOrder: 0 }];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate max 10 photos
|
// Validate max 10 photos
|
||||||
if (finalMediaItems.length > 10) {
|
if (finalMediaItems.length > 10) {
|
||||||
return res.status(400).json({ error: "Maximum 10 photos autorisées par publication" });
|
return res.status(400).json({ error: "Maximum 10 photos autorisées par publication" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate that stories require media
|
// Validate that stories require media
|
||||||
if ((postType === 'story' || postType === 'both') && finalMediaItems.length === 0) {
|
if ((postType === 'story' || postType === 'both') && finalMediaItems.length === 0) {
|
||||||
return res.status(400).json({ error: "Les stories nécessitent au moins un média (image ou vidéo)" });
|
return res.status(400).json({ error: "Les stories nécessitent au moins un média (image ou vidéo)" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Security: Verify user has access to all specified pages (unless admin)
|
// Security: Verify user has access to all specified pages (unless admin)
|
||||||
if (user.role !== 'admin' && pageIds && Array.isArray(pageIds) && pageIds.length > 0) {
|
if (user.role !== 'admin' && pageIds && Array.isArray(pageIds) && pageIds.length > 0) {
|
||||||
const accessiblePages = await storage.getUserAccessiblePages(userId);
|
const accessiblePages = await storage.getUserAccessiblePages(userId);
|
||||||
const accessiblePageIds = accessiblePages.map(p => p.id);
|
const accessiblePageIds = accessiblePages.map(p => p.id);
|
||||||
|
|
||||||
const hasAccessToAllPages = pageIds.every(pageId =>
|
const hasAccessToAllPages = pageIds.every(pageId =>
|
||||||
accessiblePageIds.includes(pageId)
|
accessiblePageIds.includes(pageId)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!hasAccessToAllPages) {
|
if (!hasAccessToAllPages) {
|
||||||
return res.status(403).json({
|
return res.status(403).json({
|
||||||
error: "Vous n'avez pas accès à certaines pages sélectionnées"
|
error: "Vous n'avez pas accès à certaines pages sélectionnées"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert scheduledFor string to Date if provided
|
// Convert scheduledFor string to Date if provided
|
||||||
if (postFields.scheduledFor && typeof postFields.scheduledFor === 'string') {
|
if (postFields.scheduledFor && typeof postFields.scheduledFor === 'string') {
|
||||||
postFields.scheduledFor = new Date(postFields.scheduledFor);
|
postFields.scheduledFor = new Date(postFields.scheduledFor);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set status to "scheduled" if scheduledFor is provided, otherwise "draft"
|
// Set status to "scheduled" if scheduledFor is provided, otherwise "draft"
|
||||||
if (postFields.scheduledFor) {
|
if (postFields.scheduledFor) {
|
||||||
postFields.status = "scheduled";
|
postFields.status = "scheduled";
|
||||||
|
|
||||||
// Validate that scheduled posts require at least one page
|
// Validate that scheduled posts require at least one page
|
||||||
if (!pageIds || !Array.isArray(pageIds) || pageIds.length === 0) {
|
if (!pageIds || !Array.isArray(pageIds) || pageIds.length === 0) {
|
||||||
return res.status(400).json({ error: "Les posts programmés nécessitent au moins une page cible" });
|
return res.status(400).json({ error: "Les posts programmés nécessitent au moins une page cible" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the post
|
// Create the post
|
||||||
const postData = insertPostSchema.parse({ ...postFields, userId });
|
const postData = insertPostSchema.parse({ ...postFields, userId });
|
||||||
const post = await storage.createPost(postData);
|
const post = await storage.createPost(postData);
|
||||||
|
|
||||||
// Link media to post if provided (with display order)
|
// Link media to post if provided (with display order)
|
||||||
if (finalMediaItems.length > 0) {
|
if (finalMediaItems.length > 0) {
|
||||||
const postMediaValues = finalMediaItems.map(item => ({
|
const postMediaValues = finalMediaItems.map(item => ({
|
||||||
@@ -928,15 +956,15 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
}));
|
}));
|
||||||
await db.insert(postMedia).values(postMediaValues);
|
await db.insert(postMedia).values(postMediaValues);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create scheduled posts for each selected page
|
// Create scheduled posts for each selected page
|
||||||
if (pageIds && Array.isArray(pageIds) && pageIds.length > 0) {
|
if (pageIds && Array.isArray(pageIds) && pageIds.length > 0) {
|
||||||
const scheduledAt = postFields.scheduledFor
|
const scheduledAt = postFields.scheduledFor
|
||||||
? new Date(postFields.scheduledFor)
|
? new Date(postFields.scheduledFor)
|
||||||
: new Date(); // Publish immediately if no date specified
|
: new Date(); // Publish immediately if no date specified
|
||||||
|
|
||||||
const finalPostType = postType || 'feed';
|
const finalPostType = postType || 'feed';
|
||||||
|
|
||||||
for (const pageId of pageIds) {
|
for (const pageId of pageIds) {
|
||||||
// If postType is "both", create two separate scheduled posts (story + feed)
|
// If postType is "both", create two separate scheduled posts (story + feed)
|
||||||
if (finalPostType === 'both') {
|
if (finalPostType === 'both') {
|
||||||
@@ -962,7 +990,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json(post);
|
res.json(post);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error creating post:", error);
|
console.error("Error creating post:", error);
|
||||||
@@ -997,7 +1025,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
if (scheduledPostsForPost.length > 0) {
|
if (scheduledPostsForPost.length > 0) {
|
||||||
const accessiblePages = await storage.getUserAccessiblePages(userId);
|
const accessiblePages = await storage.getUserAccessiblePages(userId);
|
||||||
const accessiblePageIds = accessiblePages.map(p => p.id);
|
const accessiblePageIds = accessiblePages.map(p => p.id);
|
||||||
|
|
||||||
// Vérifier si au moins une page du post est accessible
|
// Vérifier si au moins une page du post est accessible
|
||||||
const hasAccess = scheduledPostsForPost.some(sp => accessiblePageIds.includes(sp.pageId));
|
const hasAccess = scheduledPostsForPost.some(sp => accessiblePageIds.includes(sp.pageId));
|
||||||
if (hasAccess) {
|
if (hasAccess) {
|
||||||
@@ -1075,19 +1103,19 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
const user = req.user as User;
|
const user = req.user as User;
|
||||||
const userId = user.id;
|
const userId = user.id;
|
||||||
const { startDate, endDate } = req.query;
|
const { startDate, endDate } = req.query;
|
||||||
|
|
||||||
const start = startDate ? new Date(startDate as string) : undefined;
|
const start = startDate ? new Date(startDate as string) : undefined;
|
||||||
const end = endDate ? new Date(endDate as string) : undefined;
|
const end = endDate ? new Date(endDate as string) : undefined;
|
||||||
|
|
||||||
let scheduledPosts;
|
let scheduledPosts;
|
||||||
|
|
||||||
if (user.role === 'admin') {
|
if (user.role === 'admin') {
|
||||||
// Admin voit tous les posts programmés - on récupère toutes les pages
|
// Admin voit tous les posts programmés - on récupère toutes les pages
|
||||||
const allPages = await storage.getAllUsers().then(users =>
|
const allPages = await storage.getAllUsers().then(users =>
|
||||||
Promise.all(users.map(u => storage.getSocialPages(u.id)))
|
Promise.all(users.map(u => storage.getSocialPages(u.id)))
|
||||||
).then(pagesArrays => pagesArrays.flat());
|
).then(pagesArrays => pagesArrays.flat());
|
||||||
const allPageIds = allPages.map(p => p.id);
|
const allPageIds = allPages.map(p => p.id);
|
||||||
|
|
||||||
if (allPageIds.length > 0) {
|
if (allPageIds.length > 0) {
|
||||||
scheduledPosts = await storage.getScheduledPostsByPages(allPageIds, start, end);
|
scheduledPosts = await storage.getScheduledPostsByPages(allPageIds, start, end);
|
||||||
} else {
|
} else {
|
||||||
@@ -1097,14 +1125,14 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
// User voit uniquement les posts programmés sur les pages qui lui sont attribuées
|
// User voit uniquement les posts programmés sur les pages qui lui sont attribuées
|
||||||
const accessiblePages = await storage.getUserAccessiblePages(userId);
|
const accessiblePages = await storage.getUserAccessiblePages(userId);
|
||||||
const accessiblePageIds = accessiblePages.map(p => p.id);
|
const accessiblePageIds = accessiblePages.map(p => p.id);
|
||||||
|
|
||||||
if (accessiblePageIds.length > 0) {
|
if (accessiblePageIds.length > 0) {
|
||||||
scheduledPosts = await storage.getScheduledPostsByPages(accessiblePageIds, start, end);
|
scheduledPosts = await storage.getScheduledPostsByPages(accessiblePageIds, start, end);
|
||||||
} else {
|
} else {
|
||||||
scheduledPosts = [];
|
scheduledPosts = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json(scheduledPosts);
|
res.json(scheduledPosts);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching scheduled posts:", error);
|
console.error("Error fetching scheduled posts:", error);
|
||||||
@@ -1117,23 +1145,23 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
const user = req.user as User;
|
const user = req.user as User;
|
||||||
const userId = user.id;
|
const userId = user.id;
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
|
||||||
// Verify the scheduled post exists
|
// Verify the scheduled post exists
|
||||||
const scheduledPost = await storage.getScheduledPost(id);
|
const scheduledPost = await storage.getScheduledPost(id);
|
||||||
if (!scheduledPost) {
|
if (!scheduledPost) {
|
||||||
return res.status(404).json({ error: "Scheduled post not found" });
|
return res.status(404).json({ error: "Scheduled post not found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const post = await storage.getPost(scheduledPost.postId);
|
const post = await storage.getPost(scheduledPost.postId);
|
||||||
if (!post) {
|
if (!post) {
|
||||||
return res.status(404).json({ error: "Post not found" });
|
return res.status(404).json({ error: "Post not found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Admin peut tout supprimer, user peut supprimer uniquement ses propres posts
|
// Admin peut tout supprimer, user peut supprimer uniquement ses propres posts
|
||||||
if (user.role !== 'admin' && post.userId !== userId) {
|
if (user.role !== 'admin' && post.userId !== userId) {
|
||||||
return res.status(403).json({ error: "Unauthorized" });
|
return res.status(403).json({ error: "Unauthorized" });
|
||||||
}
|
}
|
||||||
|
|
||||||
await storage.deleteScheduledPost(id);
|
await storage.deleteScheduledPost(id);
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1147,37 +1175,37 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
const user = req.user as User;
|
const user = req.user as User;
|
||||||
const userId = user.id;
|
const userId = user.id;
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
|
||||||
// Verify the scheduled post exists
|
// Verify the scheduled post exists
|
||||||
const scheduledPost = await storage.getScheduledPost(id);
|
const scheduledPost = await storage.getScheduledPost(id);
|
||||||
if (!scheduledPost) {
|
if (!scheduledPost) {
|
||||||
return res.status(404).json({ error: "Scheduled post not found" });
|
return res.status(404).json({ error: "Scheduled post not found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const post = await storage.getPost(scheduledPost.postId);
|
const post = await storage.getPost(scheduledPost.postId);
|
||||||
if (!post) {
|
if (!post) {
|
||||||
return res.status(404).json({ error: "Post not found" });
|
return res.status(404).json({ error: "Post not found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Admin peut tout modifier, user peut modifier uniquement ses propres posts
|
// Admin peut tout modifier, user peut modifier uniquement ses propres posts
|
||||||
if (user.role !== 'admin' && post.userId !== userId) {
|
if (user.role !== 'admin' && post.userId !== userId) {
|
||||||
return res.status(403).json({ error: "Unauthorized" });
|
return res.status(403).json({ error: "Unauthorized" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only allow updating scheduledAt and pageId
|
// Only allow updating scheduledAt and pageId
|
||||||
const { scheduledAt, pageId } = req.body;
|
const { scheduledAt, pageId } = req.body;
|
||||||
const updateData: Partial<ScheduledPost> = {};
|
const updateData: Partial<ScheduledPost> = {};
|
||||||
|
|
||||||
if (scheduledAt) {
|
if (scheduledAt) {
|
||||||
updateData.scheduledAt = new Date(scheduledAt);
|
updateData.scheduledAt = new Date(scheduledAt);
|
||||||
// Synchroniser avec la table posts
|
// Synchroniser avec la table posts
|
||||||
await storage.updatePost(scheduledPost.postId, { scheduledFor: new Date(scheduledAt) });
|
await storage.updatePost(scheduledPost.postId, { scheduledFor: new Date(scheduledAt) });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pageId) {
|
if (pageId) {
|
||||||
updateData.pageId = pageId;
|
updateData.pageId = pageId;
|
||||||
}
|
}
|
||||||
|
|
||||||
const updated = await storage.updateScheduledPost(id, updateData);
|
const updated = await storage.updateScheduledPost(id, updateData);
|
||||||
res.json(updated);
|
res.json(updated);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1189,7 +1217,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
app.post("/api/scheduled-posts", requireAuth, async (req, res) => {
|
app.post("/api/scheduled-posts", requireAuth, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const scheduledPostData = insertScheduledPostSchema.parse(req.body);
|
const scheduledPostData = insertScheduledPostSchema.parse(req.body);
|
||||||
|
|
||||||
// If postType is "both", create two separate scheduled posts (story + feed)
|
// If postType is "both", create two separate scheduled posts (story + feed)
|
||||||
// This prevents retry loops - each post type is independent
|
// This prevents retry loops - each post type is independent
|
||||||
if (scheduledPostData.postType === 'both') {
|
if (scheduledPostData.postType === 'both') {
|
||||||
@@ -1220,9 +1248,9 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
try {
|
try {
|
||||||
const user = req.user as User;
|
const user = req.user as User;
|
||||||
const userId = user.id;
|
const userId = user.id;
|
||||||
|
|
||||||
let pages: SocialPage[];
|
let pages: SocialPage[];
|
||||||
|
|
||||||
if (user.role === 'admin') {
|
if (user.role === 'admin') {
|
||||||
// Les admins voient toutes les pages de tous les utilisateurs
|
// Les admins voient toutes les pages de tous les utilisateurs
|
||||||
const allUsers = await storage.getAllUsers();
|
const allUsers = await storage.getAllUsers();
|
||||||
@@ -1234,7 +1262,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
// Les utilisateurs normaux voient uniquement les pages auxquelles ils ont accès
|
// Les utilisateurs normaux voient uniquement les pages auxquelles ils ont accès
|
||||||
pages = await storage.getUserAccessiblePages(userId);
|
pages = await storage.getUserAccessiblePages(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json(pages);
|
res.json(pages);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching pages:", error);
|
console.error("Error fetching pages:", error);
|
||||||
@@ -1246,15 +1274,15 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
try {
|
try {
|
||||||
const user = req.user as User;
|
const user = req.user as User;
|
||||||
const userId = user.id;
|
const userId = user.id;
|
||||||
|
|
||||||
// Calculate token expiration date (60 days from now)
|
// Calculate token expiration date (60 days from now)
|
||||||
const tokenExpiresAt = new Date();
|
const tokenExpiresAt = new Date();
|
||||||
tokenExpiresAt.setDate(tokenExpiresAt.getDate() + 60);
|
tokenExpiresAt.setDate(tokenExpiresAt.getDate() + 60);
|
||||||
|
|
||||||
const pageData = insertSocialPageSchema.parse({
|
const pageData = insertSocialPageSchema.parse({
|
||||||
...req.body,
|
...req.body,
|
||||||
userId,
|
userId,
|
||||||
tokenExpiresAt
|
tokenExpiresAt
|
||||||
});
|
});
|
||||||
const page = await storage.createSocialPage(pageData);
|
const page = await storage.createSocialPage(pageData);
|
||||||
res.json(page);
|
res.json(page);
|
||||||
@@ -1269,21 +1297,21 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
const user = req.user as User;
|
const user = req.user as User;
|
||||||
const userId = user.id;
|
const userId = user.id;
|
||||||
const pageId = req.params.id;
|
const pageId = req.params.id;
|
||||||
|
|
||||||
const existingPage = await storage.getSocialPage(pageId);
|
const existingPage = await storage.getSocialPage(pageId);
|
||||||
if (!existingPage || existingPage.userId !== userId) {
|
if (!existingPage || existingPage.userId !== userId) {
|
||||||
return res.status(404).json({ error: "Page non trouvée" });
|
return res.status(404).json({ error: "Page non trouvée" });
|
||||||
}
|
}
|
||||||
|
|
||||||
let pageData = insertSocialPageSchema.partial().parse(req.body);
|
let pageData = insertSocialPageSchema.partial().parse(req.body);
|
||||||
|
|
||||||
// If accessToken is being updated, recalculate expiration date (60 days from now)
|
// If accessToken is being updated, recalculate expiration date (60 days from now)
|
||||||
if (pageData.accessToken) {
|
if (pageData.accessToken) {
|
||||||
const tokenExpiresAt = new Date();
|
const tokenExpiresAt = new Date();
|
||||||
tokenExpiresAt.setDate(tokenExpiresAt.getDate() + 60);
|
tokenExpiresAt.setDate(tokenExpiresAt.getDate() + 60);
|
||||||
pageData = { ...pageData, tokenExpiresAt };
|
pageData = { ...pageData, tokenExpiresAt };
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedPage = await storage.updateSocialPage(pageId, pageData);
|
const updatedPage = await storage.updateSocialPage(pageId, pageData);
|
||||||
res.json(updatedPage);
|
res.json(updatedPage);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1297,12 +1325,12 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
const user = req.user as User;
|
const user = req.user as User;
|
||||||
const userId = user.id;
|
const userId = user.id;
|
||||||
const pageId = req.params.id;
|
const pageId = req.params.id;
|
||||||
|
|
||||||
const existingPage = await storage.getSocialPage(pageId);
|
const existingPage = await storage.getSocialPage(pageId);
|
||||||
if (!existingPage || existingPage.userId !== userId) {
|
if (!existingPage || existingPage.userId !== userId) {
|
||||||
return res.status(404).json({ error: "Page non trouvée" });
|
return res.status(404).json({ error: "Page non trouvée" });
|
||||||
}
|
}
|
||||||
|
|
||||||
await storage.deleteSocialPage(pageId);
|
await storage.deleteSocialPage(pageId);
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1330,7 +1358,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
const user = req.user as User;
|
const user = req.user as User;
|
||||||
const userId = user.id;
|
const userId = user.id;
|
||||||
const config = await storage.getCloudinaryConfig(userId);
|
const config = await storage.getCloudinaryConfig(userId);
|
||||||
|
|
||||||
if (!config) {
|
if (!config) {
|
||||||
return res.json(null);
|
return res.json(null);
|
||||||
}
|
}
|
||||||
@@ -1348,10 +1376,10 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
try {
|
try {
|
||||||
const user = req.user as User;
|
const user = req.user as User;
|
||||||
const userId = user.id;
|
const userId = user.id;
|
||||||
|
|
||||||
// Check if config already exists
|
// Check if config already exists
|
||||||
const existingConfig = await storage.getCloudinaryConfig(userId);
|
const existingConfig = await storage.getCloudinaryConfig(userId);
|
||||||
|
|
||||||
let config;
|
let config;
|
||||||
if (existingConfig) {
|
if (existingConfig) {
|
||||||
// Pour les mises à jour, utiliser le schéma qui rend les secrets optionnels
|
// Pour les mises à jour, utiliser le schéma qui rend les secrets optionnels
|
||||||
@@ -1359,14 +1387,14 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
...req.body,
|
...req.body,
|
||||||
userId,
|
userId,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Si apiKey/apiSecret ne sont pas fournis, garder les anciens
|
// Si apiKey/apiSecret ne sont pas fournis, garder les anciens
|
||||||
const finalData = {
|
const finalData = {
|
||||||
...updateData,
|
...updateData,
|
||||||
apiKey: updateData.apiKey || existingConfig.apiKey,
|
apiKey: updateData.apiKey || existingConfig.apiKey,
|
||||||
apiSecret: updateData.apiSecret || existingConfig.apiSecret,
|
apiSecret: updateData.apiSecret || existingConfig.apiSecret,
|
||||||
};
|
};
|
||||||
|
|
||||||
config = await storage.updateCloudinaryConfig(userId, finalData);
|
config = await storage.updateCloudinaryConfig(userId, finalData);
|
||||||
} else {
|
} else {
|
||||||
// Pour les créations, exiger tous les champs
|
// Pour les créations, exiger tous les champs
|
||||||
@@ -1399,8 +1427,8 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
// Check if Cloudinary is configured
|
// Check if Cloudinary is configured
|
||||||
const cloudinaryConfig = await storage.getCloudinaryConfig(userId);
|
const cloudinaryConfig = await storage.getCloudinaryConfig(userId);
|
||||||
if (!cloudinaryConfig) {
|
if (!cloudinaryConfig) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
error: "Cloudinary not configured. Please configure Cloudinary first."
|
error: "Cloudinary not configured. Please configure Cloudinary first."
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1490,7 +1518,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
const user = req.user as User;
|
const user = req.user as User;
|
||||||
const userId = user.id;
|
const userId = user.id;
|
||||||
const config = await storage.getOpenrouterConfig(userId);
|
const config = await storage.getOpenrouterConfig(userId);
|
||||||
|
|
||||||
if (!config) {
|
if (!config) {
|
||||||
return res.json(null);
|
return res.json(null);
|
||||||
}
|
}
|
||||||
@@ -1508,10 +1536,10 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
try {
|
try {
|
||||||
const user = req.user as User;
|
const user = req.user as User;
|
||||||
const userId = user.id;
|
const userId = user.id;
|
||||||
|
|
||||||
// Check if config already exists
|
// Check if config already exists
|
||||||
const existingConfig = await storage.getOpenrouterConfig(userId);
|
const existingConfig = await storage.getOpenrouterConfig(userId);
|
||||||
|
|
||||||
let config;
|
let config;
|
||||||
if (existingConfig) {
|
if (existingConfig) {
|
||||||
// Pour les mises à jour, utiliser le schéma qui rend apiKey optionnel
|
// Pour les mises à jour, utiliser le schéma qui rend apiKey optionnel
|
||||||
@@ -1519,13 +1547,13 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
...req.body,
|
...req.body,
|
||||||
userId,
|
userId,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Si apiKey n'est pas fourni, garder l'ancien
|
// Si apiKey n'est pas fourni, garder l'ancien
|
||||||
const finalData = {
|
const finalData = {
|
||||||
...updateData,
|
...updateData,
|
||||||
apiKey: updateData.apiKey || existingConfig.apiKey,
|
apiKey: updateData.apiKey || existingConfig.apiKey,
|
||||||
};
|
};
|
||||||
|
|
||||||
config = await storage.updateOpenrouterConfig(userId, finalData);
|
config = await storage.updateOpenrouterConfig(userId, finalData);
|
||||||
} else {
|
} else {
|
||||||
// Pour les créations, exiger tous les champs
|
// Pour les créations, exiger tous les champs
|
||||||
|
|||||||
+34
-11
@@ -1,15 +1,15 @@
|
|||||||
import {
|
import {
|
||||||
users,
|
users,
|
||||||
socialPages,
|
socialPages,
|
||||||
media,
|
media,
|
||||||
posts,
|
posts,
|
||||||
postMedia,
|
postMedia,
|
||||||
scheduledPosts,
|
scheduledPosts,
|
||||||
aiGenerations,
|
aiGenerations,
|
||||||
cloudinaryConfig,
|
cloudinaryConfig,
|
||||||
openrouterConfig,
|
openrouterConfig,
|
||||||
userPagePermissions,
|
userPagePermissions,
|
||||||
type User,
|
type User,
|
||||||
type InsertUser,
|
type InsertUser,
|
||||||
type SocialPage,
|
type SocialPage,
|
||||||
type InsertSocialPage,
|
type InsertSocialPage,
|
||||||
@@ -31,6 +31,7 @@ import {
|
|||||||
} from "@shared/schema";
|
} from "@shared/schema";
|
||||||
import { db } from "./db";
|
import { db } from "./db";
|
||||||
import { eq, and, gte, lte, desc, asc, isNull, inArray } from "drizzle-orm";
|
import { eq, and, gte, lte, desc, asc, isNull, inArray } from "drizzle-orm";
|
||||||
|
import { encrypt, decrypt, isEncrypted } from "./utils/encryption";
|
||||||
|
|
||||||
export interface IStorage {
|
export interface IStorage {
|
||||||
// Users
|
// Users
|
||||||
@@ -131,21 +132,43 @@ export class DatabaseStorage implements IStorage {
|
|||||||
|
|
||||||
// Social Pages
|
// Social Pages
|
||||||
async getSocialPages(userId: string): Promise<SocialPage[]> {
|
async getSocialPages(userId: string): Promise<SocialPage[]> {
|
||||||
return await db.select().from(socialPages).where(eq(socialPages.userId, userId));
|
const pages = await db.select().from(socialPages).where(eq(socialPages.userId, userId));
|
||||||
|
// Déchiffrer les tokens pour chaque page
|
||||||
|
return pages.map(page => ({
|
||||||
|
...page,
|
||||||
|
accessToken: decrypt(page.accessToken)
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSocialPage(id: string): Promise<SocialPage | undefined> {
|
async getSocialPage(id: string): Promise<SocialPage | undefined> {
|
||||||
const [page] = await db.select().from(socialPages).where(eq(socialPages.id, id));
|
const [page] = await db.select().from(socialPages).where(eq(socialPages.id, id));
|
||||||
|
if (page) {
|
||||||
|
// Déchiffrer le token
|
||||||
|
page.accessToken = decrypt(page.accessToken);
|
||||||
|
}
|
||||||
return page || undefined;
|
return page || undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
async createSocialPage(page: InsertSocialPage): Promise<SocialPage> {
|
async createSocialPage(page: InsertSocialPage): Promise<SocialPage> {
|
||||||
const [newPage] = await db.insert(socialPages).values(page).returning();
|
// Chiffrer le token avant stockage
|
||||||
|
const encryptedPage = {
|
||||||
|
...page,
|
||||||
|
accessToken: encrypt(page.accessToken)
|
||||||
|
};
|
||||||
|
const [newPage] = await db.insert(socialPages).values(encryptedPage).returning();
|
||||||
|
// Retourner avec le token déchiffré
|
||||||
|
newPage.accessToken = decrypt(newPage.accessToken);
|
||||||
return newPage;
|
return newPage;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateSocialPage(id: string, page: Partial<InsertSocialPage>): Promise<SocialPage> {
|
async updateSocialPage(id: string, page: Partial<InsertSocialPage>): Promise<SocialPage> {
|
||||||
const [updated] = await db.update(socialPages).set(page).where(eq(socialPages.id, id)).returning();
|
// Chiffrer le token si présent dans la mise à jour
|
||||||
|
const updateData = page.accessToken
|
||||||
|
? { ...page, accessToken: encrypt(page.accessToken) }
|
||||||
|
: page;
|
||||||
|
const [updated] = await db.update(socialPages).set(updateData).where(eq(socialPages.id, id)).returning();
|
||||||
|
// Retourner avec le token déchiffré
|
||||||
|
updated.accessToken = decrypt(updated.accessToken);
|
||||||
return updated;
|
return updated;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,7 +242,7 @@ export class DatabaseStorage implements IStorage {
|
|||||||
|
|
||||||
async updatePostMedia(postId: string, mediaIds: string[]): Promise<void> {
|
async updatePostMedia(postId: string, mediaIds: string[]): Promise<void> {
|
||||||
await db.delete(postMedia).where(eq(postMedia.postId, postId));
|
await db.delete(postMedia).where(eq(postMedia.postId, postId));
|
||||||
|
|
||||||
if (mediaIds.length > 0) {
|
if (mediaIds.length > 0) {
|
||||||
const postMediaEntries = mediaIds.map((mediaId, index) => ({
|
const postMediaEntries = mediaIds.map((mediaId, index) => ({
|
||||||
postId,
|
postId,
|
||||||
@@ -423,7 +446,7 @@ export class DatabaseStorage implements IStorage {
|
|||||||
.from(userPagePermissions)
|
.from(userPagePermissions)
|
||||||
.innerJoin(socialPages, eq(userPagePermissions.pageId, socialPages.id))
|
.innerJoin(socialPages, eq(userPagePermissions.pageId, socialPages.id))
|
||||||
.where(eq(userPagePermissions.userId, userId));
|
.where(eq(userPagePermissions.userId, userId));
|
||||||
|
|
||||||
return permissions.map(p => p.social_pages);
|
return permissions.map(p => p.social_pages);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import crypto from 'crypto';
|
||||||
|
|
||||||
|
const ALGORITHM = 'aes-256-gcm';
|
||||||
|
const IV_LENGTH = 16;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère la clé de chiffrement depuis les variables d'environnement.
|
||||||
|
* Dérive une clé de 32 bytes pour AES-256.
|
||||||
|
*/
|
||||||
|
function getEncryptionKey(): Buffer {
|
||||||
|
const key = process.env.ENCRYPTION_KEY;
|
||||||
|
if (!key) {
|
||||||
|
// En développement, utiliser une clé par défaut (non sécurisé pour la production)
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
console.warn('⚠️ ENCRYPTION_KEY non défini. Utilisation d\'une clé par défaut (développement uniquement)');
|
||||||
|
return crypto.scryptSync('dev-default-key-not-secure', 'salt', 32);
|
||||||
|
}
|
||||||
|
throw new Error('ENCRYPTION_KEY non défini dans les variables d\'environnement');
|
||||||
|
}
|
||||||
|
// Dériver une clé de 32 bytes depuis la clé fournie
|
||||||
|
return crypto.scryptSync(key, 'socialflow-salt', 32);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chiffre une chaîne de texte avec AES-256-GCM.
|
||||||
|
* @param text - Le texte à chiffrer
|
||||||
|
* @returns Le texte chiffré au format: iv:authTag:encrypted (hex)
|
||||||
|
*/
|
||||||
|
export function encrypt(text: string): string {
|
||||||
|
const key = getEncryptionKey();
|
||||||
|
const iv = crypto.randomBytes(IV_LENGTH);
|
||||||
|
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
||||||
|
|
||||||
|
let encrypted = cipher.update(text, 'utf8', 'hex');
|
||||||
|
encrypted += cipher.final('hex');
|
||||||
|
const authTag = cipher.getAuthTag();
|
||||||
|
|
||||||
|
// Format: iv:authTag:encrypted
|
||||||
|
return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Déchiffre une chaîne chiffrée avec AES-256-GCM.
|
||||||
|
* @param encryptedText - Le texte chiffré au format iv:authTag:encrypted
|
||||||
|
* @returns Le texte déchiffré
|
||||||
|
*/
|
||||||
|
export function decrypt(encryptedText: string): string {
|
||||||
|
// Si le texte ne contient pas le format attendu, retourner tel quel
|
||||||
|
// (pour la rétrocompatibilité avec les tokens non chiffrés)
|
||||||
|
if (!encryptedText.includes(':')) {
|
||||||
|
return encryptedText;
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = getEncryptionKey();
|
||||||
|
const parts = encryptedText.split(':');
|
||||||
|
|
||||||
|
if (parts.length !== 3) {
|
||||||
|
// Format invalide, retourner tel quel (rétrocompatibilité)
|
||||||
|
return encryptedText;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [ivHex, authTagHex, encrypted] = parts;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const iv = Buffer.from(ivHex, 'hex');
|
||||||
|
const authTag = Buffer.from(authTagHex, 'hex');
|
||||||
|
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
|
||||||
|
decipher.setAuthTag(authTag);
|
||||||
|
|
||||||
|
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
||||||
|
decrypted += decipher.final('utf8');
|
||||||
|
|
||||||
|
return decrypted;
|
||||||
|
} catch (error) {
|
||||||
|
// Si le déchiffrement échoue, retourner tel quel (rétrocompatibilité)
|
||||||
|
console.warn('⚠️ Échec du déchiffrement, token probablement non chiffré');
|
||||||
|
return encryptedText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si un texte est déjà chiffré (format iv:authTag:encrypted).
|
||||||
|
* @param text - Le texte à vérifier
|
||||||
|
* @returns true si le texte semble être chiffré
|
||||||
|
*/
|
||||||
|
export function isEncrypted(text: string): boolean {
|
||||||
|
if (!text.includes(':')) return false;
|
||||||
|
const parts = text.split(':');
|
||||||
|
if (parts.length !== 3) return false;
|
||||||
|
// Vérifier que les parties ressemblent à du hex
|
||||||
|
return parts.every(part => /^[a-f0-9]+$/i.test(part));
|
||||||
|
}
|
||||||
Reference in new issue
Block a user