128 lines
4.6 KiB
TypeScript
128 lines
4.6 KiB
TypeScript
import "dotenv/config";
|
|
import express from "express";
|
|
import { createServer } from "http";
|
|
import net from "net";
|
|
import path from "path";
|
|
import fs from "fs";
|
|
import { createExpressMiddleware } from "@trpc/server/adapters/express";
|
|
import { registerOAuthRoutes } from "./oauth";
|
|
import { appRouter } from "../routers";
|
|
import { createContext } from "./context";
|
|
import { serveStatic, setupVite } from "./vite";
|
|
|
|
function isPortAvailable(port: number): Promise<boolean> {
|
|
return new Promise(resolve => {
|
|
const server = net.createServer();
|
|
server.listen(port, () => {
|
|
server.close(() => resolve(true));
|
|
});
|
|
server.on("error", () => resolve(false));
|
|
});
|
|
}
|
|
|
|
async function findAvailablePort(startPort: number = 3000): Promise<number> {
|
|
for (let port = startPort; port < startPort + 20; port++) {
|
|
if (await isPortAvailable(port)) {
|
|
return port;
|
|
}
|
|
}
|
|
throw new Error(`No available port found starting from ${startPort}`);
|
|
}
|
|
|
|
async function startServer() {
|
|
const app = express();
|
|
const server = createServer(app);
|
|
// Configure body parser with larger size limit for file uploads
|
|
app.use(express.json({ limit: "50mb" }));
|
|
app.use(express.urlencoded({ limit: "50mb", extended: true }));
|
|
// OAuth callback under /api/oauth/callback
|
|
registerOAuthRoutes(app);
|
|
// Serve local storage files
|
|
app.use("/storage", express.static("storage"));
|
|
|
|
// Route de téléchargement forcé du PDF annoté BAP
|
|
// 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;
|
|
}
|
|
// Utiliser le nom de fichier fourni par le client (format Date - Fournisseur - N°Facture.pdf)
|
|
const customFilename = req.query.filename as string | undefined;
|
|
const fallbackFilename = path.basename(storagePath);
|
|
const finalFilename = customFilename ? customFilename : fallbackFilename;
|
|
// RFC 5987 : utiliser filename* pour les caractères non-ASCII
|
|
const encodedFilename = encodeURIComponent(finalFilename);
|
|
res.setHeader("Content-Disposition", `attachment; filename="${encodedFilename}"; filename*=UTF-8''${encodedFilename}`);
|
|
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",
|
|
createExpressMiddleware({
|
|
router: appRouter,
|
|
createContext,
|
|
})
|
|
);
|
|
// development mode uses Vite, production mode uses static files
|
|
if (process.env.NODE_ENV === "development") {
|
|
await setupVite(app, server);
|
|
} else {
|
|
serveStatic(app);
|
|
}
|
|
|
|
const preferredPort = parseInt(process.env.PORT || "3000");
|
|
const port = await findAvailablePort(preferredPort);
|
|
|
|
if (port !== preferredPort) {
|
|
console.log(`Port ${preferredPort} is busy, using port ${port} instead`);
|
|
}
|
|
|
|
server.listen(port, () => {
|
|
console.log(`Server running on http://localhost:${port}/`);
|
|
});
|
|
}
|
|
|
|
startServer().catch(console.error);
|