From d4fe15c23d4567516b0069b4e11a5e3d2cad7b79 Mon Sep 17 00:00:00 2001 From: Manus Date: Sun, 12 Apr 2026 10:31:18 -0400 Subject: [PATCH] =?UTF-8?q?Checkpoint:=20Correction=20du=20bouton=20t?= =?UTF-8?q?=C3=A9l=C3=A9chargement=20BAP=20:=20la=20route=20/api/download-?= =?UTF-8?q?bap=20utilise=20maintenant=20un=20query=20param=20pdfPath=20pou?= =?UTF-8?q?r=20accepter=20les=20chemins=20complets=20avec=20sous-dossiers?= =?UTF-8?q?=20(ex:=20/storage/2026-04/BAP=5Fxxx.pdf).=20Ajout=20d'une=20ro?= =?UTF-8?q?ute=20de=20compatibilit=C3=A9=20qui=20cherche=20dans=20tous=20l?= =?UTF-8?q?es=20sous-dossiers.=20S=C3=A9curit=C3=A9=20anti-path-traversal?= =?UTF-8?q?=20maintenue.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/pages/BapHistory.tsx | 4 ++- client/src/pages/InvoicesBAP.tsx | 4 ++- server/_core/index.ts | 47 +++++++++++++++++++++++++++++--- 3 files changed, 49 insertions(+), 6 deletions(-) diff --git a/client/src/pages/BapHistory.tsx b/client/src/pages/BapHistory.tsx index 28d663d..e037840 100644 --- a/client/src/pages/BapHistory.tsx +++ b/client/src/pages/BapHistory.tsx @@ -23,9 +23,11 @@ import { } from "lucide-react"; // Helper : télécharge un PDF annoté BAP depuis son URL de stockage +// pdfUrl peut être : '/storage/2026-04/BAP_xxx.pdf' ou '/storage/BAP_xxx.pdf' function downloadBapPdf(pdfUrl: string, supplierName?: string | null) { const filename = pdfUrl.split('/').pop() || 'BAP.pdf'; - const downloadUrl = `/api/download-bap/${filename}`; + // Utiliser le query param pdfPath pour passer le chemin complet au serveur + const downloadUrl = `/api/download-bap?pdfPath=${encodeURIComponent(pdfUrl)}`; const a = document.createElement('a'); a.href = downloadUrl; a.download = supplierName ? `BAP_${supplierName.replace(/[^a-zA-Z0-9]/g, '_')}.pdf` : filename; diff --git a/client/src/pages/InvoicesBAP.tsx b/client/src/pages/InvoicesBAP.tsx index 8038ade..9f767d7 100644 --- a/client/src/pages/InvoicesBAP.tsx +++ b/client/src/pages/InvoicesBAP.tsx @@ -35,9 +35,11 @@ import { trpc } from "@/lib/trpc"; import { Search, FileText, Download, FileSpreadsheet, Trash2, Edit, Trash, CheckCircle, CheckCircle2, ShieldCheck } from "lucide-react"; // Helper : télécharge un PDF annoté BAP depuis son URL de stockage +// pdfUrl peut être : '/storage/2026-04/BAP_xxx.pdf' ou '/storage/BAP_xxx.pdf' function downloadBapPdf(pdfUrl: string, supplierName?: string) { const filename = pdfUrl.split('/').pop() || 'BAP.pdf'; - const downloadUrl = `/api/download-bap/${filename}`; + // Utiliser le query param pdfPath pour passer le chemin complet au serveur + const downloadUrl = `/api/download-bap?pdfPath=${encodeURIComponent(pdfUrl)}`; const a = document.createElement('a'); a.href = downloadUrl; a.download = supplierName ? `BAP_${supplierName.replace(/[^a-zA-Z0-9]/g, '_')}.pdf` : filename; diff --git a/server/_core/index.ts b/server/_core/index.ts index e1c12a4..45b6ed9 100644 --- a/server/_core/index.ts +++ b/server/_core/index.ts @@ -41,18 +41,57 @@ async function startServer() { app.use("/storage", express.static("storage")); // Route de téléchargement forcé du PDF annoté BAP - app.get("/api/download-bap/:filename", (req, res) => { - const filename = path.basename(req.params.filename); // sécurité : pas de path traversal - const storagePath = path.resolve("storage", filename); + // Accepte les chemins avec sous-dossiers : /api/download-bap/2026-04/filename.pdf + // ou via query param pdfPath : /api/download-bap/file.pdf?pdfPath=/storage/2026-04/file.pdf + app.get("/api/download-bap", (req, res) => { + // Mode 1 : query param pdfPath (chemin complet depuis /storage/...) + const pdfPath = req.query.pdfPath as string | undefined; + if (!pdfPath) { + res.status(400).json({ error: "Paramètre pdfPath manquant" }); + return; + } + // Sécurité : s'assurer que le chemin est bien dans le dossier storage + const normalized = path.normalize(pdfPath).replace(/^\/+/, ''); + if (normalized.startsWith('..') || !normalized.startsWith('storage')) { + res.status(403).json({ error: "Accès refusé" }); + return; + } + const storagePath = path.resolve(normalized); if (!fs.existsSync(storagePath)) { res.status(404).json({ error: "Fichier introuvable" }); return; } - res.setHeader("Content-Disposition", `attachment; filename="${filename}"`); + const filename = path.basename(storagePath); + res.setHeader("Content-Disposition", `attachment; filename="${encodeURIComponent(filename)}"`); res.setHeader("Content-Type", "application/pdf"); res.sendFile(storagePath); }); + // Compat. ancienne route avec :filename (sans sous-dossier) + app.get("/api/download-bap/:filename", (req, res) => { + const filename = path.basename(req.params.filename); + // Chercher dans tous les sous-dossiers de storage + const storageRoot = path.resolve("storage"); + let found: string | null = null; + try { + const subdirs = fs.readdirSync(storageRoot); + for (const sub of subdirs) { + const candidate = path.join(storageRoot, sub, filename); + if (fs.existsSync(candidate)) { found = candidate; break; } + } + // Aussi essayer directement dans storage/ + const direct = path.join(storageRoot, filename); + if (!found && fs.existsSync(direct)) found = direct; + } catch { /* ignore */ } + if (!found) { + res.status(404).json({ error: "Fichier introuvable" }); + return; + } + res.setHeader("Content-Disposition", `attachment; filename="${encodeURIComponent(filename)}"`); + res.setHeader("Content-Type", "application/pdf"); + res.sendFile(found); + }); + // tRPC API app.use( "/api/trpc",