mirror of
https://github.com/R0m1k3/LogiFlow.git
synced 2026-10-12 09:47:54 +02:00
latest
This commit is contained in:
1 parent
e3fb29ab1d
commit
2485f71dd7
95 files changed
+41842
No files matched your search
@@ -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
@@ -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 };
|
||||
}
|
||||
@@ -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 };
|
||||
@@ -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 });
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -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();
|
||||
};
|
||||
@@ -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' });
|
||||
});
|
||||
}
|
||||
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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"));
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user