This commit is contained in:
LogiFlow committed 2025-08-11 14:43:48 +00:00
1 parent e3fb29ab1d
commit 2485f71dd7
95 files changed
+41842

No files matched your search

+73
View File
@@ -0,0 +1,73 @@
import crypto from 'crypto';
/**
* Alternative à bcrypt pour production Docker Alpine
* Utilise crypto natif Node.js (pas de compilation nécessaire)
*/
const SALT_ROUNDS = 10;
export async function hashPassword(password: string): Promise<string> {
// Générer un salt aléatoire
const salt = crypto.randomBytes(16).toString('hex');
// Créer le hash avec PBKDF2 (sécurisé et natif)
const hash = crypto.pbkdf2Sync(password, salt, 100000, 64, 'sha512').toString('hex');
// Retourner salt + hash combinés
return `${salt}:${hash}`;
}
export async function comparePasswords(password: string, hashedPassword: string): Promise<boolean> {
try {
// Vérifier si c'est un hash de développement (format scrypt avec point)
if (hashedPassword.includes('.')) {
console.log('🔧 Detected development hash format - attempting migration');
return compareScryptPassword(password, hashedPassword);
}
// Séparer le salt du hash (format production PBKDF2)
const [salt, originalHash] = hashedPassword.split(':');
if (!salt || !originalHash) {
console.log('❌ Invalid PBKDF2 hash format');
return false;
}
// Recalculer le hash avec le même salt
const hash = crypto.pbkdf2Sync(password, salt, 100000, 64, 'sha512').toString('hex');
// Comparaison sécurisée
return crypto.timingSafeEqual(Buffer.from(originalHash, 'hex'), Buffer.from(hash, 'hex'));
} catch (error) {
console.error('Error comparing passwords:', error);
return false;
}
}
// Fonction pour comparer les mots de passe de développement (format scrypt)
function compareScryptPassword(password: string, hashedPassword: string): boolean {
try {
const [hashed, salt] = hashedPassword.split('.');
if (!hashed || !salt) {
return false;
}
const hashedBuf = Buffer.from(hashed, 'hex');
const suppliedBuf = crypto.scryptSync(password, salt, 64) as Buffer;
return crypto.timingSafeEqual(hashedBuf, suppliedBuf);
} catch (error) {
console.error('Error comparing scrypt password:', error);
return false;
}
}
// Fonction pour migrer les anciens hashes bcrypt si nécessaire
export function isBcryptHash(hash: string): boolean {
return hash.startsWith('$2a$') || hash.startsWith('$2b$') || hash.startsWith('$2y$');
}
// Fonction pour générer le hash par défaut de l'admin
export async function getDefaultAdminHash(): Promise<string> {
return await hashPassword('admin');
}
+119
View File
@@ -0,0 +1,119 @@
import { Request, Response, NextFunction } from 'express';
// Cache en mémoire simple pour les données fréquemment utilisées
class MemoryCache {
private cache: Map<string, { data: any; timestamp: number; ttl: number }> = new Map();
private readonly DEFAULT_TTL = 5 * 60 * 1000; // 5 minutes
set(key: string, data: any, ttl: number = this.DEFAULT_TTL): void {
this.cache.set(key, {
data,
timestamp: Date.now(),
ttl
});
}
get(key: string): any | null {
const item = this.cache.get(key);
if (!item) return null;
if (Date.now() - item.timestamp > item.ttl) {
this.cache.delete(key);
return null;
}
return item.data;
}
delete(key: string): void {
this.cache.delete(key);
}
clear(): void {
this.cache.clear();
}
// Nettoyer les éléments expirés
cleanup(): void {
const now = Date.now();
for (const [key, item] of this.cache.entries()) {
if (now - item.timestamp > item.ttl) {
this.cache.delete(key);
}
}
}
}
export const cache = new MemoryCache();
// Nettoyage automatique toutes les 10 minutes
setInterval(() => cache.cleanup(), 10 * 60 * 1000);
// Middleware de mise en cache pour les réponses API
export function cacheMiddleware(ttl: number = 5 * 60 * 1000) {
return (req: Request, res: Response, next: NextFunction) => {
// Ne pas mettre en cache les requêtes POST/PUT/DELETE
if (req.method !== 'GET') {
return next();
}
const key = `${req.originalUrl}:${req.user?.id || 'anonymous'}`;
const cachedData = cache.get(key);
if (cachedData) {
res.setHeader('X-Cache', 'HIT');
return res.json(cachedData);
}
res.setHeader('X-Cache', 'MISS');
// Intercepter la réponse pour la mettre en cache
const originalSend = res.json;
res.json = function(data: any) {
if (res.statusCode === 200) {
cache.set(key, data, ttl);
}
return originalSend.call(this, data);
};
next();
};
}
// Invalidation de cache par pattern
export function invalidateCache(pattern: string): void {
for (const key of cache['cache'].keys()) {
if (key.includes(pattern)) {
cache.delete(key);
}
}
}
// Middleware de compression des réponses
export function setupCompression(app: any) {
app.use((req: Request, res: Response, next: NextFunction) => {
// Activer la compression pour les réponses JSON
if (req.accepts('gzip') && res.getHeader('Content-Type')?.includes('json')) {
res.setHeader('Content-Encoding', 'gzip');
}
next();
});
}
// Optimisation des requêtes base de données
export function createOptimizedQuery(baseQuery: string, params: any[]): { query: string; params: any[] } {
// Optimiser les requêtes en évitant les SELECT * inutiles
let optimizedQuery = baseQuery;
// Limiter les résultats pour éviter les grosses requêtes
if (!optimizedQuery.includes('LIMIT') && !optimizedQuery.includes('COUNT')) {
optimizedQuery += ' LIMIT 1000';
}
// Ajouter des index hints si nécessaire
if (optimizedQuery.includes('ORDER BY created_at')) {
optimizedQuery = optimizedQuery.replace('ORDER BY created_at', 'ORDER BY created_at DESC');
}
return { query: optimizedQuery, params };
}
+15
View File
@@ -0,0 +1,15 @@
import { Pool } from 'pg';
import { drizzle } from 'drizzle-orm/node-postgres';
import * as schema from '../shared/schema';
// Production database configuration using standard PostgreSQL
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl: false, // Docker internal connection doesn't need SSL
max: 20, // Maximum number of connections
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
});
export const db = drizzle(pool, { schema });
export { pool };
+15
View File
@@ -0,0 +1,15 @@
import { Pool, neonConfig } from '@neondatabase/serverless';
import { drizzle } from 'drizzle-orm/neon-serverless';
import ws from "ws";
import * as schema from "@shared/schema";
neonConfig.webSocketConstructor = ws;
if (!process.env.DATABASE_URL) {
throw new Error(
"DATABASE_URL must be set. Did you forget to provision a database?",
);
}
export const pool = new Pool({ connectionString: process.env.DATABASE_URL });
export const db = drizzle({ client: pool, schema });
+141
View File
@@ -0,0 +1,141 @@
import express, { type Request, Response, NextFunction } from "express";
import { setupSecurityHeaders, setupRateLimiting, setupInputSanitization } from "./security";
import { setupCompression } from "./cache";
import { monitor, setupMonitoringEndpoints } from "./monitoring";
import { initDatabase } from "./initDatabase.production";
import path from "path";
import fs from "fs";
const app = express();
// Configuration trust proxy sécurisée pour Docker
// Faire confiance seulement au premier proxy (Docker/nginx)
app.set('trust proxy', 1);
// Fonction de log pour la production
function log(message: string, source = "express") {
const formattedTime = new Date().toLocaleTimeString("en-US", {
hour: "numeric",
minute: "2-digit",
second: "2-digit",
hour12: true,
});
console.log(`${formattedTime} [${source}] ${message}`);
}
// Fonction pour servir les fichiers statiques en production
function serveStatic(app: express.Express) {
// Essayer plusieurs chemins possibles pour le build frontend
const possiblePaths = [
path.resolve("dist", "public"),
path.resolve("dist"),
path.resolve(".", "dist", "public"),
];
let distPath = null;
for (const testPath of possiblePaths) {
if (fs.existsSync(testPath) && fs.existsSync(path.join(testPath, "index.html"))) {
distPath = testPath;
break;
}
}
if (!distPath) {
console.log("Available directories:");
console.log("- dist/:", fs.existsSync("dist") ? fs.readdirSync("dist") : "NOT FOUND");
console.log("- dist/public/:", fs.existsSync("dist/public") ? fs.readdirSync("dist/public") : "NOT FOUND");
throw new Error(
`Could not find the build directory with index.html. Checked: ${possiblePaths.join(", ")}`,
);
}
console.log(`✅ Serving static files from: ${distPath}`);
app.use(express.static(distPath));
// fall through to index.html if the file doesn't exist (seulement pour les routes non-API)
app.get("*", (req, res) => {
// Ne pas rediriger les routes API vers index.html
if (req.path.startsWith('/api/')) {
return res.status(404).json({ message: 'API route not found' });
}
// Log pour debug du routing
console.log(`📍 Serving index.html for path: ${req.path}`);
res.sendFile(path.resolve(distPath, "index.html"));
});
}
// Sécurité et optimisation
setupSecurityHeaders(app);
setupRateLimiting(app);
setupInputSanitization(app);
setupCompression(app);
// Monitoring des performances
app.use(monitor.middleware());
setupMonitoringEndpoints(app);
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: false, limit: '10mb' }));
// Logging optimisé (sans détails de réponse sensibles)
app.use((req, res, next) => {
const start = Date.now();
const path = req.path;
res.on("finish", () => {
const duration = Date.now() - start;
if (path.startsWith("/api")) {
// Logging sécurisé sans données sensibles
let logLine = `${req.method} ${path} ${res.statusCode} in ${duration}ms`;
// Ne pas logger les données sensibles
if (path.includes('/login') || path.includes('/password')) {
logLine += ' :: [SENSITIVE DATA HIDDEN]';
}
if (logLine.length > 80) {
logLine = logLine.slice(0, 79) + "…";
}
log(logLine);
}
});
next();
});
(async () => {
// Initialiser la base de données en premier
try {
await initDatabase();
} catch (error) {
console.error('❌ Failed to initialize database:', error);
process.exit(1);
}
const { registerRoutes } = await import('./routes.production');
const server = await registerRoutes(app);
app.use((err: any, _req: Request, res: Response, _next: NextFunction) => {
const status = err.status || err.statusCode || 500;
const message = err.message || "Internal Server Error";
res.status(status).json({ message });
throw err;
});
// En production, servir les fichiers statiques uniquement
serveStatic(app);
// Port configuré pour la production
const port = process.env.PORT || 3000;
server.listen({
port,
host: "0.0.0.0",
}, () => {
log(`Server running on port ${port}`);
});
})();
+106
View File
@@ -0,0 +1,106 @@
// Environment setup for production deployment
process.env.STORAGE_MODE = 'production'; // Force production mode for debugging
// Keep NODE_ENV as development to avoid static file serving issues
// process.env.NODE_ENV = 'production'; // Force production environment for debugging
import express, { type Request, Response, NextFunction } from "express";
import { registerRoutes } from "./routes";
import { setupVite, serveStatic, log } from "./vite";
import { setupSecurityHeaders, setupRateLimiting, setupInputSanitization } from "./security";
import { setupCompression } from "./cache";
import { monitor, setupMonitoringEndpoints } from "./monitoring";
import { initRolesAndPermissions } from "./initRolesAndPermissions";
import { initDatabase } from "./initDatabase.production";
const app = express();
// Sécurité et optimisation
setupSecurityHeaders(app);
setupRateLimiting(app);
setupInputSanitization(app);
setupCompression(app);
// Monitoring des performances
app.use(monitor.middleware());
setupMonitoringEndpoints(app);
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: false, limit: '10mb' }));
// Logging optimisé (sans détails de réponse sensibles)
app.use((req, res, next) => {
const start = Date.now();
const path = req.path;
res.on("finish", () => {
const duration = Date.now() - start;
if (path.startsWith("/api")) {
// Logging sécurisé sans données sensibles
let logLine = `${req.method} ${path} ${res.statusCode} in ${duration}ms`;
// Ne pas logger les données sensibles
if (path.includes('/login') || path.includes('/password')) {
logLine += ' :: [SENSITIVE DATA HIDDEN]';
}
if (logLine.length > 80) {
logLine = logLine.slice(0, 79) + "…";
}
log(logLine);
}
});
next();
});
(async () => {
// Initialize roles and permissions on startup
try {
await initRolesAndPermissions();
} catch (error) {
console.error("Failed to initialize roles and permissions:", error);
// Continue startup even if role initialization fails
}
// Initialize production database and permissions when using production storage
if (process.env.STORAGE_MODE === 'production') {
try {
await initDatabase();
} catch (error) {
console.error("Failed to initialize production database:", error);
// Continue startup even if production initialization fails
}
}
const server = await registerRoutes(app);
app.use((err: any, _req: Request, res: Response, _next: NextFunction) => {
const status = err.status || err.statusCode || 500;
const message = err.message || "Internal Server Error";
res.status(status).json({ message });
throw err;
});
// importantly only setup vite in development and after
// setting up all the other routes so the catch-all route
// doesn't interfere with the other routes
if (app.get("env") === "development") {
await setupVite(app, server);
} else {
serveStatic(app);
}
// ALWAYS serve the app on port 5000
// this serves both the API and the client.
// It is the only port that is not firewalled.
const port = 5000;
server.listen({
port,
host: "0.0.0.0",
reusePort: true,
}, () => {
log(`serving on port ${port}`);
});
})();
File diff suppressed because it is too large. Load diff
+179
View File
@@ -0,0 +1,179 @@
import { storage } from "./storage";
export async function initRolesAndPermissions() {
console.log("Initializing roles and permissions...");
try {
// Check if roles already exist
const existingRoles = await storage.getRoles();
if (existingRoles.length > 0) {
console.log("Roles already exist, skipping initialization");
return;
}
// Create default roles
const adminRole = await storage.createRole({
name: "admin",
displayName: "Administrateur",
description: "Accès complet à toutes les fonctionnalités du système",
color: "#dc2626",
isSystem: true,
isActive: true,
});
const managerRole = await storage.createRole({
name: "manager",
displayName: "Manager",
description: "Accès à la gestion des commandes, livraisons et fournisseurs",
color: "#2563eb",
isSystem: true,
isActive: true,
});
const employeeRole = await storage.createRole({
name: "employee",
displayName: "Employé",
description: "Accès en lecture aux données et publicités",
color: "#16a34a",
isSystem: true,
isActive: true,
});
const directeurRole = await storage.createRole({
name: "directeur",
displayName: "Directeur",
description: "Supervision générale et gestion stratégique",
color: "#7c3aed",
isSystem: true,
isActive: true,
});
console.log("Created default roles:", { adminRole, managerRole, employeeRole, directeurRole });
// Create default permissions
const permissions = [
// Dashboard permissions
{ category: "tableau_de_bord", name: "dashboard_read", displayName: "Voir tableau de bord", description: "Accès en lecture au tableau de bord", action: "read", resource: "dashboard", isSystem: true },
// Groups/Stores permissions
{ category: "magasins", name: "groups_read", displayName: "Voir magasins", description: "Accès en lecture aux magasins", action: "read", resource: "groups", isSystem: true },
{ category: "magasins", name: "groups_create", displayName: "Créer magasins", description: "Création de nouveaux magasins", action: "create", resource: "groups", isSystem: true },
{ category: "magasins", name: "groups_update", displayName: "Modifier magasins", description: "Modification des magasins existants", action: "update", resource: "groups", isSystem: true },
{ category: "magasins", name: "groups_delete", displayName: "Supprimer magasins", description: "Suppression de magasins", action: "delete", resource: "groups", isSystem: true },
// Suppliers permissions
{ category: "fournisseurs", name: "suppliers_read", displayName: "Voir fournisseurs", description: "Accès en lecture aux fournisseurs", action: "read", resource: "suppliers", isSystem: true },
{ category: "fournisseurs", name: "suppliers_create", displayName: "Créer fournisseurs", description: "Création de nouveaux fournisseurs", action: "create", resource: "suppliers", isSystem: true },
{ category: "fournisseurs", name: "suppliers_update", displayName: "Modifier fournisseurs", description: "Modification des fournisseurs existants", action: "update", resource: "suppliers", isSystem: true },
{ category: "fournisseurs", name: "suppliers_delete", displayName: "Supprimer fournisseurs", description: "Suppression de fournisseurs", action: "delete", resource: "fournisseurs", isSystem: true },
// Orders permissions
{ category: "commandes", name: "orders_read", displayName: "Voir commandes", description: "Accès en lecture aux commandes", action: "read", resource: "orders", isSystem: true },
{ category: "commandes", name: "orders_create", displayName: "Créer commandes", description: "Création de nouvelles commandes", action: "create", resource: "orders", isSystem: true },
{ category: "commandes", name: "orders_update", displayName: "Modifier commandes", description: "Modification des commandes existantes", action: "update", resource: "orders", isSystem: true },
{ category: "commandes", name: "orders_delete", displayName: "Supprimer commandes", description: "Suppression de commandes", action: "delete", resource: "orders", isSystem: true },
// Deliveries permissions
{ category: "livraisons", name: "deliveries_read", displayName: "Voir livraisons", description: "Accès en lecture aux livraisons", action: "read", resource: "deliveries", isSystem: true },
{ category: "livraisons", name: "deliveries_create", displayName: "Créer livraisons", description: "Création de nouvelles livraisons", action: "create", resource: "deliveries", isSystem: true },
{ category: "livraisons", name: "deliveries_update", displayName: "Modifier livraisons", description: "Modification des livraisons existantes", action: "update", resource: "deliveries", isSystem: true },
{ category: "livraisons", name: "deliveries_delete", displayName: "Supprimer livraisons", description: "Suppression de livraisons", action: "delete", resource: "deliveries", isSystem: true },
{ category: "livraisons", name: "deliveries_validate", displayName: "Valider livraisons", description: "Validation des livraisons avec BL", action: "validate", resource: "deliveries", isSystem: true },
// Calendar permissions
{ category: "calendrier", name: "calendar_read", displayName: "Voir calendrier", description: "Accès en lecture au calendrier", action: "read", resource: "calendar", isSystem: true },
// BL Reconciliation permissions
{ category: "rapprochement", name: "reconciliation_read", displayName: "Voir rapprochement", description: "Accès en lecture au rapprochement BL/Factures", action: "read", resource: "reconciliation", isSystem: true },
{ category: "rapprochement", name: "reconciliation_update", displayName: "Modifier rapprochement", description: "Modification des données de rapprochement", action: "update", resource: "reconciliation", isSystem: true },
// Publicities permissions
{ category: "publicites", name: "publicities_read", displayName: "Voir publicités", description: "Accès en lecture aux publicités", action: "read", resource: "publicities", isSystem: true },
{ category: "publicites", name: "publicities_create", displayName: "Créer publicités", description: "Création de nouvelles publicités", action: "create", resource: "publicities", isSystem: true },
{ category: "publicites", name: "publicities_update", displayName: "Modifier publicités", description: "Modification des publicités existantes", action: "update", resource: "publicities", isSystem: true },
{ category: "publicites", name: "publicities_delete", displayName: "Supprimer publicités", description: "Suppression de publicités", action: "delete", resource: "publicities", isSystem: true },
// Customer Orders permissions
{ category: "commandes_clients", name: "customer_orders_read", displayName: "Voir commandes clients", description: "Accès en lecture aux commandes clients", action: "read", resource: "customer_orders", isSystem: true },
{ category: "commandes_clients", name: "customer_orders_create", displayName: "Créer commandes clients", description: "Création de nouvelles commandes clients", action: "create", resource: "customer_orders", isSystem: true },
{ category: "commandes_clients", name: "customer_orders_update", displayName: "Modifier commandes clients", description: "Modification des commandes clients existantes", action: "update", resource: "customer_orders", isSystem: true },
{ category: "commandes_clients", name: "customer_orders_delete", displayName: "Supprimer commandes clients", description: "Suppression de commandes clients", action: "delete", resource: "customer_orders", isSystem: true },
{ category: "commandes_clients", name: "customer_orders_print", displayName: "Imprimer étiquettes", description: "Impression d'étiquettes de commandes clients", action: "print", resource: "customer_orders", isSystem: true },
{ category: "commandes_clients", name: "customer_orders_notify", displayName: "Notifier clients", description: "Envoi de notifications aux clients", action: "notify", resource: "customer_orders", isSystem: true },
// DLC Management permissions
{ category: "gestion_dlc", name: "dlc_read", displayName: "Voir produits DLC", description: "Accès en lecture aux produits avec date limite de consommation", action: "read", resource: "dlc", isSystem: true },
{ category: "gestion_dlc", name: "dlc_create", displayName: "Créer produits DLC", description: "Création de nouveaux produits DLC", action: "create", resource: "dlc", isSystem: true },
{ category: "gestion_dlc", name: "dlc_update", displayName: "Modifier produits DLC", description: "Modification des produits DLC existants", action: "update", resource: "dlc", isSystem: true },
{ category: "gestion_dlc", name: "dlc_delete", displayName: "Supprimer produits DLC", description: "Suppression de produits DLC", action: "delete", resource: "dlc", isSystem: true },
{ category: "gestion_dlc", name: "dlc_validate", displayName: "Valider produits DLC", description: "Validation et vérification des produits DLC", action: "validate", resource: "dlc", isSystem: true },
{ category: "gestion_dlc", name: "dlc_print", displayName: "Imprimer étiquettes DLC", description: "Impression d'étiquettes pour produits DLC", action: "print", resource: "dlc", isSystem: true },
{ category: "gestion_dlc", name: "dlc_stats", displayName: "Voir statistiques DLC", description: "Accès aux statistiques des produits DLC", action: "stats", resource: "dlc", isSystem: true },
// Task Management permissions
{ category: "gestion_taches", name: "tasks_read", displayName: "Voir tâches", description: "Accès en lecture aux tâches", action: "read", resource: "tasks", isSystem: true },
{ category: "gestion_taches", name: "tasks_create", displayName: "Créer tâches", description: "Création de nouvelles tâches", action: "create", resource: "tasks", isSystem: true },
{ category: "gestion_taches", name: "tasks_update", displayName: "Modifier tâches", description: "Modification des tâches existantes", action: "update", resource: "tasks", isSystem: true },
{ category: "gestion_taches", name: "tasks_delete", displayName: "Supprimer tâches", description: "Suppression de tâches", action: "delete", resource: "tasks", isSystem: true },
{ category: "gestion_taches", name: "tasks_assign", displayName: "Assigner tâches", description: "Attribution de tâches aux utilisateurs", action: "assign", resource: "tasks", isSystem: true },
// Users permissions
{ category: "utilisateurs", name: "users_read", displayName: "Voir utilisateurs", description: "Accès en lecture aux utilisateurs", action: "read", resource: "users", isSystem: true },
{ category: "utilisateurs", name: "users_create", displayName: "Créer utilisateurs", description: "Création de nouveaux utilisateurs", action: "create", resource: "users", isSystem: true },
{ category: "utilisateurs", name: "users_update", displayName: "Modifier utilisateurs", description: "Modification des utilisateurs existants", action: "update", resource: "users", isSystem: true },
{ category: "utilisateurs", name: "users_delete", displayName: "Supprimer utilisateurs", description: "Suppression d'utilisateurs", action: "delete", resource: "users", isSystem: true },
// Role Management permissions
{ category: "gestion_roles", name: "roles_read", displayName: "Voir rôles", description: "Accès en lecture aux rôles", action: "read", resource: "roles", isSystem: true },
{ category: "gestion_roles", name: "roles_create", displayName: "Créer rôles", description: "Création de nouveaux rôles", action: "create", resource: "roles", isSystem: true },
{ category: "gestion_roles", name: "roles_update", displayName: "Modifier rôles", description: "Modification des rôles existants", action: "update", resource: "roles", isSystem: true },
{ category: "gestion_roles", name: "roles_delete", displayName: "Supprimer rôles", description: "Suppression de rôles", action: "delete", resource: "roles", isSystem: true },
{ category: "gestion_roles", name: "roles_assign", displayName: "Assigner rôles", description: "Attribution de rôles aux utilisateurs", action: "assign", resource: "roles", isSystem: true },
// System administration permissions
{ category: "administration", name: "system_admin", displayName: "Administration système", description: "Accès complet à l'administration du système", action: "admin", resource: "system", isSystem: true },
{ category: "administration", name: "nocodb_config", displayName: "Configuration NocoDB", description: "Gestion de la configuration NocoDB", action: "config", resource: "nocodb", isSystem: true },
];
// Create all permissions
const createdPermissions = [];
for (const permData of permissions) {
const permission = await storage.createPermission(permData);
createdPermissions.push(permission);
}
console.log(`Created ${createdPermissions.length} permissions`);
// Assign permissions to roles
// Super Admin gets all permissions
const allPermissionIds = createdPermissions.map(p => p.id);
await storage.setRolePermissions(adminRole.id, allPermissionIds);
console.log(`Assigned all permissions to Super Admin role`);
// Manager gets most permissions except system admin and role management, but includes DLC validate
const managerPermissions = createdPermissions.filter(p =>
!p.name.startsWith('roles_') &&
!p.name.startsWith('system_') &&
p.name !== 'users_delete'
).map(p => p.id);
await storage.setRolePermissions(managerRole.id, managerPermissions);
console.log(`Assigned ${managerPermissions.length} permissions to Manager role`);
// Employee gets basic read permissions, customer orders and DLC read/create/update
const employeePermissions = createdPermissions.filter(p =>
p.action === 'read' ||
p.name.startsWith('customer_orders_') ||
p.name.startsWith('dlc_') && (p.action === 'read' || p.action === 'create' || p.action === 'update') ||
(p.category === 'livraisons' && (p.action === 'create' || p.action === 'update'))
).map(p => p.id);
await storage.setRolePermissions(employeeRole.id, employeePermissions);
console.log(`Assigned ${employeePermissions.length} permissions to Employee role`);
console.log("✅ Roles and permissions initialization completed successfully");
} catch (error) {
console.error("❌ Error initializing roles and permissions:", error);
throw error;
}
}
+253
View File
@@ -0,0 +1,253 @@
import passport from 'passport';
import { Strategy as LocalStrategy } from 'passport-local';
import session from 'express-session';
import { pool } from './initDatabase.production';
import type { Express } from 'express';
// Import connect-pg-simple using ES6 import
import connectPgSimple from 'connect-pg-simple';
const PgSession = connectPgSimple(session);
interface User {
id: string;
username: string;
email: string;
name: string;
firstName: string;
lastName: string;
profileImageUrl?: string;
password: string;
role: string;
passwordChanged: boolean;
}
declare global {
namespace Express {
interface User extends User {}
}
}
// Import des fonctions de hachage (une seule fois)
import { hashPassword, comparePasswords } from './auth-utils.production';
export function setupLocalAuth(app: Express) {
// Configure session with PostgreSQL store
app.use(session({
store: new PgSession({
pool: pool,
tableName: 'session',
createTableIfMissing: true
}),
secret: process.env.SESSION_SECRET || 'LogiFlow_Super_Secret_Session_Key_2025_Production',
resave: false,
saveUninitialized: false,
rolling: true,
cookie: {
secure: false, // Set to true if using HTTPS
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000, // 24 hours
sameSite: 'lax'
}
}));
app.use(passport.initialize());
app.use(passport.session());
// Configure local strategy
passport.use(new LocalStrategy({
usernameField: 'username',
passwordField: 'password'
}, async (username, password, done) => {
try {
const result = await pool.query(
'SELECT * FROM users WHERE username = $1',
[username]
);
const user = result.rows[0];
if (!user) {
console.log('❌ Login failed: User not found:', username);
return done(null, false, { message: 'Invalid username or password.' });
}
const isMatch = await comparePasswords(password, user.password);
if (!isMatch) {
console.log('❌ Login failed: Invalid password for user:', username);
return done(null, false, { message: 'Invalid username or password.' });
}
// Migrer le mot de passe vers le nouveau format si nécessaire
if (user.password.includes('.')) {
console.log('🔧 Migrating password to production format for user:', username);
try {
const newHashedPassword = await hashPassword(password);
await pool.query(
'UPDATE users SET password = $1 WHERE id = $2',
[newHashedPassword, user.id]
);
console.log('✅ Password migrated to production format');
} catch (error) {
console.error('❌ Failed to migrate password:', error);
// Continue with login even if migration fails
}
}
console.log('✅ Login successful for user:', username);
return done(null, {
id: user.id,
username: user.username,
email: user.email,
name: user.name,
firstName: user.first_name,
lastName: user.last_name,
profileImageUrl: user.profile_image_url,
password: user.password,
role: user.role,
passwordChanged: user.password_changed
});
} catch (error) {
console.error('❌ Authentication error:', error);
return done(error);
}
}));
passport.serializeUser((user: any, done) => {
done(null, user.id);
});
passport.deserializeUser(async (id: string, done) => {
try {
const result = await pool.query(
'SELECT * FROM users WHERE id = $1',
[id]
);
const user = result.rows[0];
if (user) {
done(null, {
id: user.id,
username: user.username,
email: user.email,
name: user.name,
firstName: user.first_name,
lastName: user.last_name,
profileImageUrl: user.profile_image_url,
password: user.password,
role: user.role,
passwordChanged: user.password_changed
});
} else {
done(new Error('User not found'), null);
}
} catch (error) {
done(error, null);
}
});
// Authentication routes
app.post('/api/login', passport.authenticate('local'), (req: any, res) => {
if (req.user) {
console.log('✅ User authenticated successfully:', req.user.username);
res.json({
success: true,
user: {
id: req.user.id,
username: req.user.username,
email: req.user.email,
name: req.user.name,
firstName: req.user.firstName,
lastName: req.user.lastName,
profileImageUrl: req.user.profileImageUrl,
role: req.user.role,
passwordChanged: req.user.passwordChanged
}
});
} else {
res.status(401).json({ success: false, message: 'Authentication failed' });
}
});
app.get('/api/user', (req: any, res) => {
if (req.isAuthenticated()) {
res.json({
id: req.user.id,
username: req.user.username,
email: req.user.email,
name: req.user.name,
firstName: req.user.firstName,
lastName: req.user.lastName,
profileImageUrl: req.user.profileImageUrl,
role: req.user.role,
passwordChanged: req.user.passwordChanged
});
} else {
res.status(401).json({ message: 'Not authenticated' });
}
});
app.post('/api/logout', (req: any, res) => {
req.logout((err: any) => {
if (err) {
console.error('Logout error:', err);
return res.status(500).json({ message: 'Logout failed' });
}
req.session.destroy((err: any) => {
if (err) {
console.error('Session destroy error:', err);
return res.status(500).json({ message: 'Session destroy failed' });
}
res.clearCookie('connect.sid');
res.json({ message: 'Logged out successfully' });
});
});
});
// Check if default credentials should be shown
app.get("/api/default-credentials-check", async (req, res) => {
try {
const result = await pool.query(
'SELECT password_changed FROM users WHERE username = $1',
['admin']
);
const adminUser = result.rows[0];
const showDefault = adminUser && !adminUser.password_changed;
res.json({ showDefault: !!showDefault });
} catch (error) {
console.error('Error checking default credentials:', error);
res.json({ showDefault: true }); // Default to showing credentials if error
}
});
console.log('✅ Local authentication configured');
}
export const requireAuth = (req: any, res: any, next: any) => {
// 🔍 DEBUG PRODUCTION: Diagnostiquer l'authentification
console.log('🔍 PRODUCTION AUTH DEBUG:', {
url: req.url,
method: req.method,
isAuthenticated: req.isAuthenticated ? req.isAuthenticated() : 'NO_FUNCTION',
hasUser: !!req.user,
userId: req.user?.id,
username: req.user?.username,
sessionId: req.sessionID,
hasSession: !!req.session,
sessionData: req.session ? Object.keys(req.session) : 'NO_SESSION',
cookies: req.headers.cookie ? 'HAS_COOKIES' : 'NO_COOKIES'
});
if (req.isAuthenticated && req.isAuthenticated()) {
console.log('✅ PRODUCTION AUTH: User authenticated, proceeding');
return next();
}
console.log('❌ PRODUCTION AUTH: Authentication failed, returning 401');
res.status(401).json({
message: 'Authentication required',
debug: {
isAuthenticated: req.isAuthenticated ? req.isAuthenticated() : false,
hasUser: !!req.user,
hasSession: !!req.session
}
});
};
+229
View File
@@ -0,0 +1,229 @@
import passport from "passport";
import { Strategy as LocalStrategy } from "passport-local";
import { Express } from "express";
import session from "express-session";
import { scrypt, randomBytes, timingSafeEqual } from "crypto";
import { promisify } from "util";
import { storage } from "./storage";
import { User as SelectUser } from "@shared/schema";
import connectPg from "connect-pg-simple";
declare global {
namespace Express {
interface User extends SelectUser {}
}
}
const scryptAsync = promisify(scrypt);
export async function hashPassword(password: string) {
const salt = randomBytes(16).toString("hex");
const buf = (await scryptAsync(password, salt, 64)) as Buffer;
return `${buf.toString("hex")}.${salt}`;
}
async function comparePasswords(supplied: string, stored: string) {
console.log('🔐 comparePasswords:', { supplied: 'HIDDEN', stored: stored?.substring(0, 20) + '...', hasFormat: stored?.includes('.') });
// Vérifier le format du mot de passe stocké
if (!stored || !stored.includes('.')) {
console.error('❌ Invalid password format:', { stored });
return false;
}
const [hashed, salt] = stored.split(".");
if (!hashed || !salt) {
console.error('❌ Missing hash or salt:', { hasHash: !!hashed, hasSalt: !!salt });
return false;
}
try {
const hashedBuf = Buffer.from(hashed, "hex");
const suppliedBuf = (await scryptAsync(supplied, salt, 64)) as Buffer;
const result = timingSafeEqual(hashedBuf, suppliedBuf);
console.log('🔐 Password comparison result:', result);
return result;
} catch (error) {
console.error('❌ Error comparing passwords:', error);
return false;
}
}
async function createDefaultAdminUser() {
try {
const existingAdmin = await storage.getUserByUsername('admin');
if (!existingAdmin) {
const hashedPassword = await hashPassword('admin');
await storage.createUser({
id: 'admin_local',
username: 'admin',
email: 'admin@logiflow.com',
firstName: 'Administrateur',
lastName: 'Système',
password: hashedPassword,
role: 'admin',
passwordChanged: false,
});
console.log('✅ Admin user created: admin / admin');
} else {
// Corriger le mot de passe admin si le format est incorrect
if (!existingAdmin.password || !existingAdmin.password.includes('.')) {
console.log('🔧 Fixing admin password format...');
const hashedPassword = await hashPassword('admin');
await storage.updateUser(existingAdmin.id, {
password: hashedPassword,
passwordChanged: false
});
console.log('✅ Admin password format fixed');
}
}
} catch (error) {
console.error('Error managing admin user:', error);
}
}
export function setupLocalAuth(app: Express) {
// Create admin user on startup
createDefaultAdminUser();
const PostgresSessionStore = connectPg(session);
const sessionStore = new PostgresSessionStore({
conString: process.env.DATABASE_URL,
createTableIfMissing: false,
tableName: 'session',
});
const sessionSettings: session.SessionOptions = {
secret: process.env.SESSION_SECRET || 'fallback-secret-key',
resave: false,
saveUninitialized: false,
store: sessionStore,
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
maxAge: 24 * 60 * 60 * 1000, // 24 hours
},
};
app.set("trust proxy", 1);
app.use(session(sessionSettings));
app.use(passport.initialize());
app.use(passport.session());
passport.use(
new LocalStrategy(
{
usernameField: 'username',
passwordField: 'password',
},
async (username, password, done) => {
try {
const user = await storage.getUserByUsername(username);
if (!user || !user.password) {
return done(null, false, { message: 'Identifiant ou mot de passe incorrect' });
}
const isValidPassword = await comparePasswords(password, user.password);
if (!isValidPassword) {
return done(null, false, { message: 'Identifiant ou mot de passe incorrect' });
}
return done(null, user);
} catch (error) {
return done(error);
}
}
)
);
passport.serializeUser((user, done) => done(null, user.id));
passport.deserializeUser(async (id: string, done) => {
try {
const user = await storage.getUserWithGroups(id);
done(null, user);
} catch (error) {
done(error);
}
});
// Login route
app.post("/api/login", (req, res, next) => {
passport.authenticate("local", (err: any, user: any, info: any) => {
if (err) return next(err);
if (!user) {
return res.status(401).json({ message: info?.message || "Authentification échouée" });
}
req.login(user, (err) => {
if (err) return next(err);
res.json({
id: user.id,
username: user.username,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
role: user.role,
passwordChanged: user.passwordChanged
});
});
})(req, res, next);
});
// Logout routes (both GET and POST for compatibility)
const logoutHandler = (req: any, res: any, next: any) => {
req.logout((err: any) => {
if (err) return next(err);
req.session.destroy((err: any) => {
if (err) return next(err);
res.clearCookie('connect.sid', { path: '/' });
// Force redirect to login page for both GET and POST
res.redirect('/auth');
});
});
};
app.post("/api/logout", logoutHandler);
app.get("/api/logout", logoutHandler);
// Get current user
app.get("/api/user", (req, res) => {
if (!req.isAuthenticated()) {
return res.status(401).json({ message: "Non authentifié" });
}
const user = req.user as SelectUser;
res.json({
id: user.id,
username: user.username,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
role: user.role,
passwordChanged: user.passwordChanged
});
});
// Check if default credentials should be shown
app.get("/api/default-credentials-check", async (req, res) => {
try {
const adminUser = await storage.getUserByUsername('admin');
const showDefault = adminUser && !adminUser.passwordChanged;
res.json({ showDefault: !!showDefault });
} catch (error) {
res.json({ showDefault: true }); // Default to showing credentials if error
}
});
// Health check endpoint
app.get("/api/health", (req, res) => {
res.status(200).json({ status: "ok", auth: "local" });
});
}
export const requireAuth = (req: any, res: any, next: any) => {
if (!req.isAuthenticated()) {
return res.status(401).json({ message: "Authentification requise" });
}
next();
};
+151
View File
@@ -0,0 +1,151 @@
import { Request, Response, NextFunction } from 'express';
// Interface pour les métriques
interface Metrics {
requestCount: number;
errorCount: number;
responseTime: number[];
memoryUsage: NodeJS.MemoryUsage;
activeConnections: number;
lastReset: Date;
}
class PerformanceMonitor {
private metrics: Metrics = {
requestCount: 0,
errorCount: 0,
responseTime: [],
memoryUsage: process.memoryUsage(),
activeConnections: 0,
lastReset: new Date()
};
private slowQueries: Array<{ query: string; duration: number; timestamp: Date }> = [];
private MAX_RESPONSE_TIMES = 1000; // Garder les 1000 derniers temps de réponse
// Middleware pour mesurer les performances
middleware() {
return (req: Request, res: Response, next: NextFunction) => {
const startTime = Date.now();
this.metrics.requestCount++;
this.metrics.activeConnections++;
// Capturer le temps de réponse
res.on('finish', () => {
const duration = Date.now() - startTime;
this.metrics.responseTime.push(duration);
// Garder seulement les N derniers temps de réponse
if (this.metrics.responseTime.length > this.MAX_RESPONSE_TIMES) {
this.metrics.responseTime.shift();
}
// Détecter les requêtes lentes
if (duration > 1000) {
this.logSlowQuery(req.originalUrl, duration);
}
// Compter les erreurs
if (res.statusCode >= 400) {
this.metrics.errorCount++;
}
this.metrics.activeConnections--;
this.metrics.memoryUsage = process.memoryUsage();
});
next();
};
}
// Enregistrer une requête lente
private logSlowQuery(query: string, duration: number) {
this.slowQueries.push({
query,
duration,
timestamp: new Date()
});
// Garder seulement les 100 dernières requêtes lentes
if (this.slowQueries.length > 100) {
this.slowQueries.shift();
}
console.warn(`⚠️ Slow query detected: ${query} (${duration}ms)`);
}
// Obtenir les statistiques
getStats() {
const responseTimes = this.metrics.responseTime;
const avgResponseTime = responseTimes.length > 0
? responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length
: 0;
const p95ResponseTime = responseTimes.length > 0
? responseTimes.sort((a, b) => a - b)[Math.floor(responseTimes.length * 0.95)]
: 0;
return {
uptime: process.uptime(),
requestCount: this.metrics.requestCount,
errorCount: this.metrics.errorCount,
errorRate: this.metrics.requestCount > 0 ? (this.metrics.errorCount / this.metrics.requestCount) * 100 : 0,
avgResponseTime: Math.round(avgResponseTime),
p95ResponseTime: Math.round(p95ResponseTime),
activeConnections: this.metrics.activeConnections,
memoryUsage: {
rss: `${Math.round(this.metrics.memoryUsage.rss / 1024 / 1024)}MB`,
heapTotal: `${Math.round(this.metrics.memoryUsage.heapTotal / 1024 / 1024)}MB`,
heapUsed: `${Math.round(this.metrics.memoryUsage.heapUsed / 1024 / 1024)}MB`,
external: `${Math.round(this.metrics.memoryUsage.external / 1024 / 1024)}MB`
},
slowQueries: this.slowQueries.slice(-10), // 10 dernières requêtes lentes
lastReset: this.metrics.lastReset
};
}
// Réinitialiser les métriques
reset() {
this.metrics = {
requestCount: 0,
errorCount: 0,
responseTime: [],
memoryUsage: process.memoryUsage(),
activeConnections: 0,
lastReset: new Date()
};
this.slowQueries = [];
}
// Alerte si l'utilisation mémoire est trop élevée
checkMemoryUsage() {
const memUsage = process.memoryUsage();
const heapUsedMB = memUsage.heapUsed / 1024 / 1024;
if (heapUsedMB > 500) { // Plus de 500MB
console.warn(`⚠️ High memory usage: ${Math.round(heapUsedMB)}MB`);
return true;
}
return false;
}
}
// Instance globale
export const monitor = new PerformanceMonitor();
// Vérification périodique de la mémoire
setInterval(() => {
monitor.checkMemoryUsage();
}, 5 * 60 * 1000); // Toutes les 5 minutes
// Endpoint pour les métriques
export function setupMonitoringEndpoints(app: any) {
app.get('/api/metrics', (req: Request, res: Response) => {
res.json(monitor.getStats());
});
app.post('/api/metrics/reset', (req: Request, res: Response) => {
monitor.reset();
res.json({ message: 'Metrics reset successfully' });
});
}
+139
View File
@@ -0,0 +1,139 @@
// Service for interacting with NocoDB
import { storage } from "./storage";
export interface InvoiceVerificationResult {
exists: boolean;
error?: string;
}
export async function verifyInvoiceReference(
groupId: number,
invoiceReference: string,
supplierName?: string
): Promise<InvoiceVerificationResult> {
try {
// Get group with NocoDB configuration
const group = await storage.getGroup(groupId);
if (!group || !group.nocodbConfigId || !group.nocodbTableId) {
return { exists: false, error: "No NocoDB configuration for this group" };
}
// Get NocoDB configuration
const nocodbConfig = await storage.getNocodbConfig(group.nocodbConfigId);
if (!nocodbConfig) {
return { exists: false, error: "NocoDB configuration not found" };
}
const columnName = group.invoiceColumnName || "Ref Facture";
// Normalize search term (case insensitive)
const normalizedInvoiceRef = invoiceReference.trim().toLowerCase();
// Make request to NocoDB API to get all records and filter them case-insensitively
// URL format: {baseUrl}/api/v1/db/data/noco/{projectId}/{tableId}
const url = `${nocodbConfig.baseUrl}/api/v1/db/data/noco/${nocodbConfig.projectId}/${group.nocodbTableId}`;
console.log(`🔍 NocoDB API Request for ${invoiceReference} (supplier: ${supplierName || 'any'}):`, {
url,
columnName,
groupId,
projectId: nocodbConfig.projectId,
tableId: group.nocodbTableId,
normalizedSearch: normalizedInvoiceRef
});
const response = await fetch(url, {
method: "GET",
headers: {
"xc-token": nocodbConfig.apiToken,
"Content-Type": "application/json",
},
});
if (!response.ok) {
return { exists: false, error: `NocoDB API error: ${response.status}` };
}
const data = await response.json();
console.log(`📋 NocoDB Response:`, {
totalRows: data.pageInfo?.totalRows || 0,
returnedRows: data.list?.length || 0,
columnName,
searchValue: invoiceReference.trim()
});
// Filter results case-insensitively for invoice reference
let matchingRecords = [];
if (data.list && data.list.length > 0) {
matchingRecords = data.list.filter((record: any) => {
const recordInvoiceRef = record[columnName];
if (!recordInvoiceRef) return false;
// Case-insensitive comparison for invoice reference
const normalizedRecordRef = recordInvoiceRef.toString().toLowerCase();
const invoiceMatches = normalizedRecordRef === normalizedInvoiceRef;
// If supplier name is provided, also check supplier match (case-insensitive)
if (invoiceMatches && supplierName) {
const supplierColumn = record['Fournisseur'] || record['Supplier'] || record['fournisseur'] || record['supplier'];
if (supplierColumn) {
const normalizedRecordSupplier = supplierColumn.toString().toLowerCase();
const normalizedSearchSupplier = supplierName.toLowerCase();
const supplierMatches = normalizedRecordSupplier.includes(normalizedSearchSupplier) ||
normalizedSearchSupplier.includes(normalizedRecordSupplier);
console.log(`🏢 Supplier check:`, {
recordSupplier: supplierColumn,
searchSupplier: supplierName,
matches: supplierMatches
});
return supplierMatches;
}
}
return invoiceMatches;
});
}
const exists = matchingRecords.length > 0;
if (exists) {
console.log(`✅ Found matching invoice:`, {
found: matchingRecords[0][columnName],
searchValue: invoiceReference.trim(),
totalMatches: matchingRecords.length,
supplierMatch: supplierName ? 'verified' : 'not checked'
});
} else {
console.log(`❌ No matching invoice found for: ${invoiceReference}${supplierName ? ` (supplier: ${supplierName})` : ''}`);
}
console.log(`🔍 Search result for ${invoiceReference}:`, { exists });
return { exists };
} catch (error) {
console.error("Error verifying invoice reference:", error);
return { exists: false, error: error instanceof Error ? error.message : "Unknown error" };
}
}
// Bulk verify multiple invoice references
export async function verifyMultipleInvoiceReferences(
invoiceReferences: { groupId: number; invoiceReference: string; deliveryId: number; supplierName?: string }[]
): Promise<Record<number, InvoiceVerificationResult>> {
const results: Record<number, InvoiceVerificationResult> = {};
// Process in parallel for better performance
const promises = invoiceReferences.map(async ({ groupId, invoiceReference, deliveryId, supplierName }) => {
const result = await verifyInvoiceReference(groupId, invoiceReference, supplierName);
results[deliveryId] = result;
});
await Promise.all(promises);
return results;
}
File diff suppressed because it is too large. Load diff
+2723
View File
File diff suppressed because it is too large. Load diff
+143
View File
@@ -0,0 +1,143 @@
import { Express, Request, Response, NextFunction } from 'express';
import rateLimit from 'express-rate-limit';
// Headers de sécurité
export function setupSecurityHeaders(app: Express) {
app.use((req: Request, res: Response, next: NextFunction) => {
// Protection contre les attaques XSS
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-XSS-Protection', '1; mode=block');
// Protection HTTPS
if (process.env.NODE_ENV === 'production') {
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
}
// Politique de sécurité du contenu
res.setHeader('Content-Security-Policy',
"default-src 'self'; " +
"script-src 'self' 'unsafe-inline' 'unsafe-eval'; " +
"style-src 'self' 'unsafe-inline'; " +
"img-src 'self' data: https:; " +
"connect-src 'self' ws: wss:; " +
"font-src 'self' data:;"
);
// Protection contre les attaques de référence
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
// Protection des données sensibles
res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
next();
});
}
// Limitation du taux de requêtes
export function setupRateLimiting(app: Express) {
// Limiteur général
const generalLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 1000, // limite chaque IP à 1000 requêtes par fenêtre
message: {
error: 'Trop de requêtes depuis cette IP, veuillez réessayer plus tard.',
},
standardHeaders: true,
legacyHeaders: false,
trustProxy: 1, // Configuration sécurisée pour Docker (1 proxy de confiance)
skip: (req) => {
// Skip rate limiting for health checks
return req.path === '/api/health';
}
});
// Limiteur pour l'authentification
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // limite les tentatives de connexion
message: {
error: 'Trop de tentatives de connexion, veuillez réessayer plus tard.',
},
standardHeaders: true,
legacyHeaders: false,
trustProxy: 1, // Configuration sécurisée pour Docker (1 proxy de confiance)
});
// Limiteur pour l'API - adapté pour une utilisation normale
const apiLimiter = rateLimit({
windowMs: 1 * 60 * 1000, // 1 minute
max: process.env.NODE_ENV === 'development' ? 500 : 300, // 500 en dev, 300 en prod
message: {
error: 'Limite API atteinte, veuillez ralentir vos requêtes.',
},
standardHeaders: true,
legacyHeaders: false,
trustProxy: 1, // Configuration sécurisée pour Docker (1 proxy de confiance)
// Exclure certaines routes critiques du rate limiting strict
skip: (req) => {
return req.path === '/api/health' || req.path === '/api/user';
},
handler: (req, res) => {
console.warn(`🚨 Rate limit reached for IP: ${req.ip} on path: ${req.path} at ${new Date().toISOString()}`);
res.status(429).json({
error: 'Limite API atteinte, veuillez ralentir vos requêtes.',
});
}
});
app.use(generalLimiter);
app.use('/api/login', authLimiter);
app.use('/api/', apiLimiter);
}
// Validation et nettoyage des entrées
export function sanitizeInput(input: any): any {
if (typeof input === 'string') {
// Supprimer les caractères dangereux
return input.replace(/[<>]/g, '').trim();
}
if (typeof input === 'object' && input !== null) {
const sanitized: any = {};
for (const key in input) {
sanitized[key] = sanitizeInput(input[key]);
}
return sanitized;
}
return input;
}
// Middleware de nettoyage des requêtes
export function setupInputSanitization(app: Express) {
app.use((req: Request, res: Response, next: NextFunction) => {
if (req.body) {
req.body = sanitizeInput(req.body);
}
if (req.query) {
req.query = sanitizeInput(req.query);
}
if (req.params) {
req.params = sanitizeInput(req.params);
}
next();
});
}
// Middleware de logging sécurisé
export function secureLog(message: string, data?: any) {
const timestamp = new Date().toISOString();
const logData = data ? JSON.stringify(data, null, 2) : '';
// En production, ne pas logger les données sensibles
if (process.env.NODE_ENV === 'production') {
if (message.includes('password') || message.includes('token')) {
console.log(`[${timestamp}] ${message} - [SENSITIVE DATA HIDDEN]`);
} else {
console.log(`[${timestamp}] ${message}`, logData);
}
} else {
console.log(`[${timestamp}] ${message}`, logData);
}
}
File diff suppressed because it is too large. Load diff
+1806
View File
File diff suppressed because it is too large. Load diff
+85
View File
@@ -0,0 +1,85 @@
import express, { type Express } from "express";
import fs from "fs";
import path from "path";
import { createServer as createViteServer, createLogger } from "vite";
import { type Server } from "http";
import viteConfig from "../vite.config";
import { nanoid } from "nanoid";
const viteLogger = createLogger();
export function log(message: string, source = "express") {
const formattedTime = new Date().toLocaleTimeString("en-US", {
hour: "numeric",
minute: "2-digit",
second: "2-digit",
hour12: true,
});
console.log(`${formattedTime} [${source}] ${message}`);
}
export async function setupVite(app: Express, server: Server) {
const serverOptions = {
middlewareMode: true,
hmr: { server },
allowedHosts: true as const,
};
const vite = await createViteServer({
...viteConfig,
configFile: false,
customLogger: {
...viteLogger,
error: (msg, options) => {
viteLogger.error(msg, options);
process.exit(1);
},
},
server: serverOptions,
appType: "custom",
});
app.use(vite.middlewares);
app.use("*", async (req, res, next) => {
const url = req.originalUrl;
try {
const clientTemplate = path.resolve(
import.meta.dirname,
"..",
"client",
"index.html",
);
// always reload the index.html file from disk incase it changes
let template = await fs.promises.readFile(clientTemplate, "utf-8");
template = template.replace(
`src="/src/main.tsx"`,
`src="/src/main.tsx?v=${nanoid()}"`,
);
const page = await vite.transformIndexHtml(url, template);
res.status(200).set({ "Content-Type": "text/html" }).end(page);
} catch (e) {
vite.ssrFixStacktrace(e as Error);
next(e);
}
});
}
export function serveStatic(app: Express) {
const distPath = path.resolve(import.meta.dirname, "public");
if (!fs.existsSync(distPath)) {
throw new Error(
`Could not find the build directory: ${distPath}, make sure to build the client first`,
);
}
app.use(express.static(distPath));
// fall through to index.html if the file doesn't exist
app.use("*", (_req, res) => {
res.sendFile(path.resolve(distPath, "index.html"));
});
}