perf(serveur): réponses API allégées, compression, cache des assets et index

- Fichiers statiques servis avant la session (plus de requêtes SQL par
  asset), /assets en cache immuable 1 an, index.html en no-cache, et
  compression gzip/brotli des réponses (dépendance compression, externe
  dans le bundle esbuild du Dockerfile).
- req.user (déjà chargé par deserializeUser) réutilisé dans les handlers
  au lieu de relire l'utilisateur et ses magasins à chaque appel ;
  GET /api/user ne refait plus de requête.
- Listes : le magasin joint est réduit aux champs lus par l'interface
  (plus de logo base64 ni de configuration SMTP/NocoDB dans chaque ligne),
  plus aucune empreinte de mot de passe dans les créateurs/auteurs ni dans
  /api/users.
- N+1 supprimés (/api/users, annonces, historique SAV, caches de
  vérification des factures), requêtes indépendantes en parallèle (stats,
  analytics, météo, getDelivery, getUserWithGroups), jointure
  multiplicative des statistiques par magasin corrigée.
- Échéancier limité au magasin demandé ; filtre status sur
  GET /api/deliveries.
- Index de performance créés en arrière-plan au démarrage
  (CREATE INDEX CONCURRENTLY, reliquats invalides purgés sans verrou
  exclusif).
- La connexion n'attend plus la sauvegarde quotidienne ; purge du cache
  des factures active en production ; logs volumineux retirés des
  chemins chauds ; NODE_ENV fixé dans l'image.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MsDJjQrAggcJwbbBtKhgyb
This commit is contained in:
Claude committed 2026-10-03 04:29:51 +00:00
1 parent 3f2660c506
commit a5785f0244
13 files changed
+1113 -1116

No files matched your search

+35 -37
View File
@@ -5,7 +5,7 @@ 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 { User as SelectUser, type UserWithGroups } from "@shared/schema";
import connectPg from "connect-pg-simple";
import { backupService } from "./backupService";
@@ -141,7 +141,9 @@ export function setupLocalAuth(app: Express) {
passport.deserializeUser(async (id: string, done) => {
try {
const user = await storage.getUserWithGroups(id);
done(null, user);
// Utilisateur supprimé depuis la connexion : false vide la session (401)
// au lieu de l'erreur « Failed to deserialize user out of session » (500)
done(null, user || false);
} catch (error) {
done(error);
}
@@ -157,20 +159,9 @@ export function setupLocalAuth(app: Express) {
return res.status(400).json({ message: info?.message || "Invalid credentials" });
}
req.login(user, async (err) => {
req.login(user, (err) => {
if (err) return next(err);
// Vérifier et effectuer une sauvegarde quotidienne si nécessaire
try {
const backupResult = await backupService.checkAndPerformDailyBackup(user.id);
if (backupResult.backupPerformed) {
console.log('💾 Sauvegarde quotidienne effectuée lors de la connexion de', user.username);
}
} catch (error) {
console.error('⚠️ Erreur lors de la vérification de sauvegarde:', error);
// Ne pas faire échouer la connexion si la sauvegarde échoue
}
res.json({
id: user.id,
username: user.username,
@@ -180,6 +171,21 @@ export function setupLocalAuth(app: Express) {
role: user.role,
passwordChanged: user.passwordChanged
});
// Vérifier et effectuer une sauvegarde quotidienne si nécessaire, après
// la réponse : la connexion n'attend plus le pg_dump
setImmediate(() => {
backupService.checkAndPerformDailyBackup(user.id)
.then((backupResult) => {
if (backupResult.backupPerformed) {
console.log('💾 Sauvegarde quotidienne effectuée lors de la connexion de', user.username);
}
})
.catch((error) => {
// Ne pas faire échouer la connexion si la sauvegarde échoue
console.error('⚠️ Erreur lors de la vérification de sauvegarde:', error);
});
});
});
})(req, res, next);
});
@@ -201,33 +207,25 @@ export function setupLocalAuth(app: Express) {
app.get("/api/logout", logoutHandler);
// Get current user
app.get("/api/user", async (req, res) => {
app.get("/api/user", (req, res) => {
if (!req.isAuthenticated()) {
return res.status(401).json({ message: "Non authentifié" });
}
try {
const userId = (req.user as SelectUser).id;
const userWithGroups = await storage.getUserWithGroups(userId);
if (!userWithGroups) {
return res.status(404).json({ message: "Utilisateur non trouvé" });
}
res.json({
id: userWithGroups.id,
username: userWithGroups.username,
email: userWithGroups.email,
firstName: userWithGroups.firstName,
lastName: userWithGroups.lastName,
role: userWithGroups.role,
passwordChanged: userWithGroups.passwordChanged,
userGroups: userWithGroups.userGroups || []
});
} catch (error) {
console.error("Error fetching user with groups:", error);
res.status(500).json({ message: "Erreur lors de la récupération des données utilisateur" });
}
// req.user est déjà rechargé avec ses magasins par deserializeUser à chaque
// requête : inutile de relire l'utilisateur en base
const userWithGroups = req.user as UserWithGroups;
res.json({
id: userWithGroups.id,
username: userWithGroups.username,
email: userWithGroups.email,
firstName: userWithGroups.firstName,
lastName: userWithGroups.lastName,
role: userWithGroups.role,
passwordChanged: userWithGroups.passwordChanged,
userGroups: userWithGroups.userGroups || []
});
});
// Check if default credentials should be shown