Files
demat-facturation/server/_core/index.ts

199 lines
7.9 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 archiver from "archiver";
import { createExpressMiddleware } from "@trpc/server/adapters/express";
import { registerOAuthRoutes } from "./oauth";
import { appRouter } from "../routers";
import { createContext } from "./context";
import { serveStatic, setupVite } from "./vite";
import { getAllUsers } from "../db";
import { startEmailImportService } from "../emailImportService";
import { startFolderImportService } from "../folderImportService";
import { getImportSettingsByUser } from "../db";
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);
});
// Route de téléchargement groupé ZIP des PDFs annotés BAP
// POST /api/download-bap-zip avec body { files: Array<{ pdfPath: string, filename: string }> }
app.post("/api/download-bap-zip", (req, res) => {
const files: Array<{ pdfPath: string; filename: string }> = req.body.files || [];
if (!files.length) {
res.status(400).json({ error: "Aucun fichier spécifié" });
return;
}
// Vérifier que tous les chemins sont dans storage/
const resolvedFiles: Array<{ absPath: string; filename: string }> = [];
for (const f of files) {
const normalized = path.normalize(f.pdfPath).replace(/^\/+/, '');
if (normalized.startsWith('..') || !normalized.startsWith('storage')) continue;
const absPath = path.resolve(normalized);
if (fs.existsSync(absPath)) {
resolvedFiles.push({ absPath, filename: f.filename });
}
}
if (!resolvedFiles.length) {
res.status(404).json({ error: "Aucun fichier trouvé" });
return;
}
const zipFilename = `BAP_export_${new Date().toLocaleDateString('fr-CA')}.zip`;
const encodedZip = encodeURIComponent(zipFilename);
res.setHeader("Content-Type", "application/zip");
res.setHeader("Content-Disposition", `attachment; filename="${encodedZip}"; filename*=UTF-8''${encodedZip}`);
const archive = archiver('zip', { zlib: { level: 6 } });
archive.on('error', (err) => { console.error('ZIP error:', err); res.destroy(); });
archive.pipe(res);
// Gérer les doublons de noms de fichiers
const usedNames = new Map<string, number>();
for (const { absPath, filename } of resolvedFiles) {
const base = filename.replace(/\.pdf$/i, '');
const count = usedNames.get(base) || 0;
usedNames.set(base, count + 1);
const finalName = count === 0 ? filename : `${base} (${count}).pdf`;
archive.file(absPath, { name: finalName });
}
archive.finalize();
});
// 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}/`);
});
// Redémarrer automatiquement les services actifs (IMAP, dossier) après redémarrage du serveur
setTimeout(async () => {
try {
const users = await getAllUsers();
for (const user of users) {
const settings = await getImportSettingsByUser(user.id);
if (!settings) continue;
if (settings.emailImportEnabled === 1) {
console.log(`[AutoRestart] Restarting email import service for user ${user.id}...`);
await startEmailImportService(user.id).catch(e =>
console.error(`[AutoRestart] Failed to restart email service for user ${user.id}:`, e.message)
);
}
if ((settings as any).autoImportEnabled === 1) {
console.log(`[AutoRestart] Restarting folder import service for user ${user.id}...`);
await startFolderImportService(user.id).catch(e =>
console.error(`[AutoRestart] Failed to restart folder service for user ${user.id}:`, e.message)
);
}
}
} catch (e: any) {
console.error('[AutoRestart] Error during service auto-restart:', e.message);
}
}, 5000); // Attendre 5s que le serveur soit prêt
}
startServer().catch(console.error);