Checkpoint: Audit technique : suppression des composants, scripts et dépendances inutilisés ; chargement différé des pages ; génération de sauvegardes SQL robuste par lots ; centralisation des contrôles d’accès ; sécurisation des cookies Azure et imports web ; documentation de maintenance et 5 tests de non-régression ajoutés.

This commit is contained in:
Manus
2026-08-17 20:21:58 +00:00
parent 6b83361056
commit 54164b1b33
27 changed files with 371 additions and 4369 deletions

View File

@@ -0,0 +1,30 @@
import { describe, expect, it } from "vitest";
import type { Request } from "express";
import { getSessionCookieOptions } from "./cookies";
function requestFor(protocol: "http" | "https", forwardedProto?: string): Request {
return {
protocol,
headers: forwardedProto ? { "x-forwarded-proto": forwardedProto } : {},
} as Request;
}
describe("getSessionCookieOptions", () => {
it("utilise des cookies sécurisés derrière le proxy HTTPS", () => {
expect(getSessionCookieOptions(requestFor("http", "https"))).toMatchObject({
httpOnly: true,
path: "/",
secure: true,
sameSite: "none",
});
});
it("reste compatible avec lenvironnement HTTP local", () => {
expect(getSessionCookieOptions(requestFor("http"))).toMatchObject({
httpOnly: true,
path: "/",
secure: false,
sameSite: "lax",
});
});
});

View File

@@ -1,13 +1,6 @@
import type { CookieOptions, Request } from "express";
const LOCAL_HOSTS = new Set(["localhost", "127.0.0.1", "::1"]);
function isIpAddress(host: string) {
// Basic IPv4 check and IPv6 presence detection.
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(host)) return true;
return host.includes(":");
}
/** Detects HTTPS after direct access or a reverse proxy such as Traefik. */
function isSecureRequest(req: Request) {
if (req.protocol === "https") return true;
@@ -24,25 +17,13 @@ function isSecureRequest(req: Request) {
export function getSessionCookieOptions(
req: Request
): Pick<CookieOptions, "domain" | "httpOnly" | "path" | "sameSite" | "secure"> {
// const hostname = req.hostname;
// const shouldSetDomain =
// hostname &&
// !LOCAL_HOSTS.has(hostname) &&
// !isIpAddress(hostname) &&
// hostname !== "127.0.0.1" &&
// hostname !== "::1";
// const domain =
// shouldSetDomain && !hostname.startsWith(".")
// ? `.${hostname}`
// : shouldSetDomain
// ? hostname
// : undefined;
const secure = isSecureRequest(req);
return {
httpOnly: true,
path: "/",
sameSite: "none",
secure: isSecureRequest(req),
// Browsers reject SameSite=None without Secure; use Lax for local HTTP.
sameSite: secure ? "none" : "lax",
secure,
};
}

View File

@@ -5,20 +5,43 @@ import net from "net";
import path from "path";
import fs from "fs";
import archiver from "archiver";
import { exec as execCb } from "child_process";
import { promisify } from "util";
import { parse as parseCookies } from "cookie";
const execAsync = promisify(execCb);
import { createExpressMiddleware } from "@trpc/server/adapters/express";
import { registerOAuthRoutes } from "./oauth";
import { appRouter } from "../routers";
import { createContext } from "./context";
import { getSessionCookieOptions } from "./cookies";
import { serveStatic, setupVite } from "./vite";
import { getAllUsers, getUserByAzureAdId, getUserByEmail, upsertUser } from "../db";
import { getAllUsers, getImportSettingsByUser, getUserByAzureAdId, getUserByEmail, getUserSettings, upsertUser } from "../db";
import { startEmailImportService } from "../emailImportService";
import { startFolderImportService } from "../folderImportService";
import { getImportSettingsByUser } from "../db";
import { handleAzureCallback, isAzureAdConfigured, generateToken } from "../auth";
import { handleAzureCallback, isAzureAdConfigured, generateToken, verifyToken } from "../auth";
import { createDatabaseBackup } from "../databaseBackup";
import { generateStorageKey, localStoragePut } from "../localStorage";
const MAX_WEB_IMPORT_BYTES = 20 * 1024 * 1024;
/** Returns the signed local session or sends the appropriate HTTP error. */
function requireAuthenticatedUser(req: express.Request, res: express.Response) {
const token = parseCookies(req.headers.cookie || "").auth_token;
const user = token ? verifyToken(token) : null;
if (!user) {
res.status(401).json({ error: "Non authentifié" });
return null;
}
return user;
}
/** Restricts a sensitive endpoint to administrators. */
function requireAdmin(req: express.Request, res: express.Response) {
const user = requireAuthenticatedUser(req, res);
if (!user) return null;
if (user.role !== "admin") {
res.status(403).json({ error: "Accès réservé aux administrateurs" });
return null;
}
return user;
}
function isPortAvailable(port: number): Promise<boolean> {
return new Promise(resolve => {
@@ -54,6 +77,7 @@ async function startServer() {
// 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) => {
if (!requireAuthenticatedUser(req, res)) return;
// Mode 1 : query param pdfPath (chemin complet depuis /storage/...)
const pdfPath = req.query.pdfPath as string | undefined;
if (!pdfPath) {
@@ -84,6 +108,7 @@ async function startServer() {
// Compat. ancienne route avec :filename (sans sous-dossier)
app.get("/api/download-bap/:filename", (req, res) => {
if (!requireAuthenticatedUser(req, res)) return;
const filename = path.basename(req.params.filename);
// Chercher dans tous les sous-dossiers de storage
const storageRoot = path.resolve("storage");
@@ -110,6 +135,7 @@ async function startServer() {
// 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) => {
if (!requireAuthenticatedUser(req, res)) return;
const files: Array<{ pdfPath: string; filename: string }> = req.body.files || [];
if (!files.length) {
res.status(400).json({ error: "Aucun fichier spécifié" });
@@ -213,10 +239,7 @@ async function startServer() {
// Générer le token JWT et poser le cookie
const token = generateToken(user);
res.cookie("auth_token", token, {
httpOnly: true,
secure: false,
sameSite: "lax",
path: "/",
...getSessionCookieOptions(req),
maxAge: 7 * 24 * 60 * 60 * 1000,
});
@@ -232,94 +255,29 @@ async function startServer() {
// ============= DB BACKUP - Génération et téléchargement dump MySQL =============
app.post("/api/db-backup", async (req, res) => {
// Vérifier l'auth JWT
const { verifyToken } = await import("../auth");
const cookies = parseCookies(req.headers.cookie || "");
const token = cookies.auth_token;
if (!token) { res.status(401).json({ error: "Non authentifié" }); return; }
const user = verifyToken(token);
if (!user || user.role !== "admin") { res.status(403).json({ error: "Accès réservé aux admins" }); return; }
if (!requireAdmin(req, res)) return;
try {
const dbUrl = new URL(process.env.DATABASE_URL || "");
const host = dbUrl.hostname;
const port = dbUrl.port || "3306";
const username = dbUrl.username;
const password = dbUrl.password;
const database = dbUrl.pathname.slice(1);
// Créer le dossier backups/
const backupDir = path.resolve("backups");
if (!fs.existsSync(backupDir)) fs.mkdirSync(backupDir, { recursive: true });
const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
const fileName = `backup-${database}-${timestamp}.sql`;
const filePath = path.join(backupDir, fileName);
// Dump SQL via mysql2 (pas besoin de mysqldump)
console.log(`[Backup] Generating SQL dump for database ${database}...`);
const mysql = await import("mysql2/promise");
const sslRequired = !dbUrl.searchParams.get("ssl-mode")?.includes("DISABLED");
const conn = await mysql.createConnection({
host, port: parseInt(port), user: username, password: decodeURIComponent(password),
database, ssl: sslRequired ? { rejectUnauthorized: false } : undefined,
});
let sql = `-- Backup généré le ${new Date().toISOString()}\n-- Base : ${database}\nSET FOREIGN_KEY_CHECKS=0;\n\n`;
// Lister les tables
const [tables] = await conn.query<any[]>(`SHOW TABLES`);
const tableNames: string[] = tables.map((r: any) => Object.values(r)[0] as string);
for (const table of tableNames) {
// CREATE TABLE
const [createRows] = await conn.query<any[]>(`SHOW CREATE TABLE \`${table}\``);
const createSql: string = createRows[0]['Create Table'] || createRows[0][`Create Table`];
sql += `\n-- Table: ${table}\nDROP TABLE IF EXISTS \`${table}\`;\n${createSql};\n\n`;
// INSERT DATA
const [rows] = await conn.query<any[]>(`SELECT * FROM \`${table}\``);
if (rows.length > 0) {
const cols = Object.keys(rows[0]).map(c => `\`${c}\``).join(", ");
const values = rows.map(row =>
"(" + Object.values(row).map(v =>
v === null ? "NULL" :
v instanceof Date ? `'${v.toISOString().replace('T', ' ').replace('Z', '')}'` :
typeof v === "number" ? v :
`'${String(v).replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`
).join(", ") + ")"
).join(",\n");
sql += `INSERT INTO \`${table}\` (${cols}) VALUES\n${values};\n\n`;
}
}
sql += `\nSET FOREIGN_KEY_CHECKS=1;\n-- Fin du dump\n`;
await conn.end();
fs.writeFileSync(filePath, sql, "utf8");
console.log(`[Backup] Dump saved to ${filePath} (${(sql.length / 1024).toFixed(1)} Ko)`);
const backup = await createDatabaseBackup(process.env.DATABASE_URL, backupDir);
console.log(`[Backup] Dump saved to ${backup.filePath} (${backup.size} bytes)`);
// Retourner le fichier en téléchargement
const encodedName = encodeURIComponent(fileName);
const encodedName = encodeURIComponent(backup.fileName);
res.setHeader("Content-Disposition", `attachment; filename="${encodedName}"; filename*=UTF-8''${encodedName}`);
res.setHeader("Content-Type", "application/sql");
res.sendFile(filePath, (err) => {
res.sendFile(backup.filePath, (err) => {
if (err) console.error("[Backup] Error sending file:", err);
});
} catch (err: any) {
console.error("[Backup] Error:", err.message);
res.status(500).json({ error: "Erreur lors de la génération du dump : " + err.message });
res.status(500).json({ error: "La sauvegarde na pas pu être générée. Consultez les journaux serveur." });
}
});
// Télécharger une sauvegarde existante
app.get("/api/db-backup/:filename", async (req, res) => {
const { verifyToken } = await import("../auth");
const cookies2 = parseCookies(req.headers.cookie || "");
const token = cookies2.auth_token;
if (!token) { res.status(401).json({ error: "Non authentifié" }); return; }
const user = verifyToken(token);
if (!user || user.role !== "admin") { res.status(403).json({ error: "Accès réservé aux admins" }); return; }
if (!requireAdmin(req, res)) return;
const fileName = path.basename(req.params.filename);
const filePath = path.join(path.resolve("backups"), fileName);
@@ -332,35 +290,52 @@ async function startServer() {
app.post("/api/web-import/push-invoice", async (req, res) => {
try {
const { apiToken, fileName, fileBase64, mimeType } = req.body;
if (!apiToken || !fileName || !fileBase64) {
const { apiToken, fileName, fileBase64 } = req.body;
if (typeof apiToken !== "string" || typeof fileName !== "string" || typeof fileBase64 !== "string") {
res.status(400).json({ error: "apiToken, fileName et fileBase64 sont requis" });
return;
}
const { getWebImportSourceByToken, getImportSettingsByUser, createInvoice, findDuplicateInvoice, isInvoiceBlacklisted, updateWebImportSourceStatus, createSourceFile } = await import('../db');
const safeFileName = path.basename(fileName);
if (!safeFileName.toLowerCase().endsWith(".pdf")) {
res.status(400).json({ error: "Seuls les fichiers PDF sont acceptés" });
return;
}
if (Buffer.byteLength(fileBase64, "utf8") > Math.ceil(MAX_WEB_IMPORT_BYTES * 1.34)) {
res.status(413).json({ error: "Le fichier dépasse la taille maximale autorisée" });
return;
}
const { getWebImportSourceByToken, createInvoice, findDuplicateInvoice, isInvoiceBlacklisted, updateWebImportSourceStatus, createSourceFile } = await import('../db');
const source = await getWebImportSourceByToken(apiToken);
if (!source) {
res.status(401).json({ error: "Token invalide" });
return;
}
const pdfBuffer = Buffer.from(fileBase64, 'base64');
const fileMime = mimeType || 'application/pdf';
// Stocker le fichier source en DB
if (pdfBuffer.length === 0 || pdfBuffer.length > MAX_WEB_IMPORT_BYTES || !pdfBuffer.subarray(0, 4).equals(Buffer.from("%PDF"))) {
res.status(400).json({ error: "Le contenu reçu nest pas un PDF valide" });
return;
}
// Stocker d'abord le PDF de façon persistante, comme les autres sources d'import.
const storageKey = generateStorageKey(source.userId, safeFileName);
const { url: fileUrl } = await localStoragePut(storageKey, pdfBuffer, "application/pdf");
const sourceFile = await createSourceFile({
userId: source.userId,
fileName,
fileKey: `web-import/${source.userId}/${Date.now()}-${fileName}`,
fileUrl: '',
fileName: safeFileName,
fileKey: storageKey,
fileUrl,
});
const importSettings = await getImportSettingsByUser(source.userId);
const userSettings = await getUserSettings(source.userId);
const aiSettings = {
aiProvider: importSettings?.aiProvider || 'manus',
mistralApiKey: importSettings?.mistralApiKey || undefined,
manusForgeApiUrl: importSettings?.manusForgeApiUrl || undefined,
manusForgeApiKey: importSettings?.manusForgeApiKey || undefined,
aiProvider: userSettings?.aiProvider || "manus",
mistralApiKey: userSettings?.mistralApiKey || undefined,
manusForgeApiUrl: userSettings?.manusForgeApiUrl || undefined,
manusForgeApiKey: userSettings?.manusForgeApiKey || undefined,
geminiApiKey: userSettings?.geminiApiKey || undefined,
};
const { extractInvoicesWithMistral } = await import('../invoiceExtractor');
const extractResult = await extractInvoicesWithMistral(pdfBuffer, source.userId, sourceFile.id, 'mistral-large-latest', undefined, aiSettings);
const extractResult = await extractInvoicesWithMistral(pdfBuffer, source.userId, sourceFile.id, userSettings?.llmModel || "mistral-large-latest", undefined, aiSettings);
let imported = 0;
let duplicates = 0;
for (const inv of extractResult.invoices || []) {
@@ -375,7 +350,7 @@ async function startServer() {
res.json({ success: true, imported, duplicates, total: (extractResult.invoices || []).length });
} catch (err: any) {
console.error('[WebImport] Erreur push-invoice:', err.message);
res.status(500).json({ error: err.message });
res.status(500).json({ error: "Limport web a échoué. Consultez les journaux serveur." });
}
});

View File

@@ -329,7 +329,6 @@ export async function invokeLLMWithUserSettings(
}
const provider = userSettings?.aiProvider || "mistral";
const isMistral = provider === "mistral";
const {
messages,

View File

@@ -1,6 +1,6 @@
import bcrypt from "bcrypt";
import { ConfidentialClientApplication } from "@azure/msal-node";
import { getUserByEmail, getUserByAzureAdId } from "./db";
import { getUserByEmail } from "./db";
import jwt from "jsonwebtoken";
const SALT_ROUNDS = 10;

View File

@@ -0,0 +1,21 @@
import { describe, expect, it } from "vitest";
import { toSqlLiteral } from "./databaseBackup";
describe("toSqlLiteral", () => {
it("sérialise les valeurs primitives de manière importable", () => {
expect(toSqlLiteral(null)).toBe("NULL");
expect(toSqlLiteral(undefined)).toBe("NULL");
expect(toSqlLiteral(42.5)).toBe("42.5");
expect(toSqlLiteral(true)).toBe("1");
expect(toSqlLiteral(false)).toBe("0");
});
it("échappe les caractères sensibles dune chaîne SQL", () => {
expect(toSqlLiteral("O'Hara\\facture\nligne")).toBe("'O\\'Hara\\\\facture\\nligne'");
});
it("conserve les données binaires et dates sans conversion ambiguë", () => {
expect(toSqlLiteral(Buffer.from([0, 255]))).toBe("X'00ff'");
expect(toSqlLiteral(new Date("2026-08-17T12:34:56.000Z"))).toBe("'2026-08-17 12:34:56.000'");
});
});

170
server/databaseBackup.ts Normal file
View File

@@ -0,0 +1,170 @@
import fs from "fs/promises";
import path from "path";
import mysql, { type RowDataPacket } from "mysql2/promise";
/** Number of rows exported per INSERT statement to bound memory usage. */
const EXPORT_BATCH_SIZE = 500;
/** Keep a short local history while preventing unbounded disk growth. */
const MAX_BACKUP_FILES = 10;
export type DatabaseBackupResult = {
fileName: string;
filePath: string;
size: number;
tableCount: number;
};
/**
* Converts one MySQL value to a portable SQL literal.
* Binary values, booleans, dates, quotes and backslashes are handled explicitly
* so a generated dump can be imported without corrupting invoice data.
*/
export function toSqlLiteral(value: unknown): string {
if (value === null || value === undefined) return "NULL";
if (Buffer.isBuffer(value)) return `X'${value.toString("hex")}'`;
if (value instanceof Date) {
return `'${value.toISOString().replace("T", " ").replace("Z", "")}'`;
}
if (typeof value === "number") return Number.isFinite(value) ? String(value) : "NULL";
if (typeof value === "boolean") return value ? "1" : "0";
return `'${String(value)
.replace(/\\/g, "\\\\")
.replace(/'/g, "\\'")
.replace(/\u0000/g, "\\0")
.replace(/\n/g, "\\n")
.replace(/\r/g, "\\r")}'`;
}
function quoteIdentifier(identifier: string): string {
if (!/^[A-Za-z0-9_$]+$/.test(identifier)) {
throw new Error("Identifiant SQL inattendu lors de la sauvegarde");
}
return `\`${identifier}\``;
}
function buildFileName(database: string, now = new Date()): string {
const safeDatabase = database.replace(/[^A-Za-z0-9_-]/g, "_");
const timestamp = now.toISOString().replace(/[:.]/g, "-").slice(0, 19);
return `backup-${safeDatabase}-${timestamp}.sql`;
}
function buildSslConfig(databaseUrl: URL) {
const sslMode = databaseUrl.searchParams.get("ssl-mode")?.toUpperCase();
const sslParameter = databaseUrl.searchParams.get("ssl");
if (sslMode === "REQUIRED" || sslMode === "VERIFY_CA" || sslMode === "VERIFY_IDENTITY") {
return { rejectUnauthorized: sslMode === "VERIFY_IDENTITY" };
}
if (sslParameter && sslParameter !== "false") {
try {
return JSON.parse(sslParameter) as { rejectUnauthorized?: boolean };
} catch {
return { rejectUnauthorized: false };
}
}
return undefined;
}
async function pruneOldBackups(backupDir: string): Promise<void> {
const entries = await fs.readdir(backupDir, { withFileTypes: true });
const backups = await Promise.all(
entries
.filter(entry => entry.isFile() && entry.name.endsWith(".sql"))
.map(async entry => ({
name: entry.name,
modifiedAt: (await fs.stat(path.join(backupDir, entry.name))).mtimeMs,
}))
);
backups.sort((a, b) => b.modifiedAt - a.modifiedAt);
await Promise.all(
backups.slice(MAX_BACKUP_FILES).map(backup => fs.unlink(path.join(backupDir, backup.name)))
);
}
/**
* Creates a self-contained SQL dump without requiring the mysqldump binary.
* Rows are exported in batches to avoid keeping the entire database in memory.
*/
export async function createDatabaseBackup(
databaseUrlValue: string | undefined,
backupDir: string
): Promise<DatabaseBackupResult> {
if (!databaseUrlValue) {
throw new Error("DATABASE_URL est absente");
}
const databaseUrl = new URL(databaseUrlValue);
if (!databaseUrl.protocol.startsWith("mysql")) {
throw new Error("La sauvegarde requiert une base de données MySQL compatible");
}
const database = databaseUrl.pathname.replace(/^\//, "");
if (!database) {
throw new Error("Nom de base de données absent de DATABASE_URL");
}
await fs.mkdir(backupDir, { recursive: true });
const fileName = buildFileName(database);
const filePath = path.join(backupDir, fileName);
const connection = await mysql.createConnection({
host: databaseUrl.hostname,
port: Number(databaseUrl.port || "3306"),
user: decodeURIComponent(databaseUrl.username),
password: decodeURIComponent(databaseUrl.password),
database,
ssl: buildSslConfig(databaseUrl),
});
try {
await fs.writeFile(
filePath,
`-- Backup généré le ${new Date().toISOString()}\n-- Base : ${database}\nSET FOREIGN_KEY_CHECKS=0;\n\n`,
"utf8"
);
const [tables] = await connection.query<RowDataPacket[]>("SHOW TABLES");
const tableNames = tables.map(row => String(Object.values(row)[0]));
for (const tableName of tableNames) {
const table = quoteIdentifier(tableName);
const [createRows] = await connection.query<RowDataPacket[]>(`SHOW CREATE TABLE ${table}`);
const createStatement = String(createRows[0]?.["Create Table"] ?? "");
if (!createStatement) throw new Error(`Structure introuvable pour la table ${tableName}`);
await fs.appendFile(
filePath,
`-- Table: ${tableName}\nDROP TABLE IF EXISTS ${table};\n${createStatement};\n\n`,
"utf8"
);
let offset = 0;
while (true) {
const [rows] = await connection.query<RowDataPacket[]>(
`SELECT * FROM ${table} LIMIT ${EXPORT_BATCH_SIZE} OFFSET ${offset}`
);
if (rows.length === 0) break;
const columns = Object.keys(rows[0]!).map(quoteIdentifier).join(", ");
const values = rows
.map(row => `(${Object.values(row).map(toSqlLiteral).join(", ")})`)
.join(",\n");
await fs.appendFile(filePath, `INSERT INTO ${table} (${columns}) VALUES\n${values};\n\n`, "utf8");
offset += rows.length;
}
}
await fs.appendFile(filePath, "SET FOREIGN_KEY_CHECKS=1;\n-- Fin du dump\n", "utf8");
await pruneOldBackups(backupDir);
const { size } = await fs.stat(filePath);
return { fileName, filePath, size, tableCount: tableNames.length };
} catch (error) {
await fs.rm(filePath, { force: true });
throw error;
} finally {
await connection.end();
}
}

View File

@@ -43,11 +43,8 @@ import {
InsertBapHistory,
BapHistory,
invoiceLearnings,
InsertInvoiceLearning,
InvoiceLearning,
deletedInvoices,
InsertDeletedInvoice,
DeletedInvoice,
webImportSources,
InsertWebImportSource,
WebImportSource

View File

@@ -343,7 +343,7 @@ async function checkEmailsForPDFs(config: EmailImportConfig): Promise<void> {
imap.once("ready", () => {
console.log(`[EmailImport] Connected to IMAP server for user ${config.userId} (mode: ${config.authMode || "basic"})`);
openInbox((err, box) => {
openInbox((err) => {
if (err) {
console.error("[EmailImport] Error opening inbox:", err);
imap.end();

View File

@@ -38,7 +38,6 @@ interface AutoImportResult {
// ── Constantes ─────────────────────────────────────────────────────────────
const FREEPRO_BASE_URL = "https://pro.free.fr";
const LOGIN_URL = `${FREEPRO_BASE_URL}/espace-client/connexion/#/`;
const LOGIN_FORM_URL = `${FREEPRO_BASE_URL}/espace-client/connexion/`;
// Endpoint réel capturé via analyse réseau du portail FreePro (XHR POST)
const DO_LOGIN_URL = `${FREEPRO_BASE_URL}/account/security/do_login`;
@@ -77,17 +76,6 @@ function extractCookies(setCookieHeader: string | null): string {
.join("; ");
}
/**
* Calcule le label du mois précédent (les factures FreePro arrivent en début de mois suivant)
*/
function previousMoisLabel(): string {
const now = new Date();
now.setMonth(now.getMonth() - 1);
const m = String(now.getMonth() + 1).padStart(2, "0");
const y = String(now.getFullYear());
return `${m}/${y}`;
}
// ── Connexion au portail FreePro ───────────────────────────────────────────
/**

View File

@@ -1,4 +1,4 @@
import { invokeLLM, invokeLLMWithUserSettings } from "./_core/llm";
import { invokeLLMWithUserSettings } from "./_core/llm";
import { PDFDocument } from "pdf-lib";
import { createLlmLog } from "./db";
import PDFParser from "pdf2json";

View File

@@ -40,7 +40,7 @@ export function generateStorageKey(userId: number, fileName: string): string {
export async function localStoragePut(
fileKey: string,
buffer: Buffer,
contentType?: string
_contentType?: string
): Promise<{ key: string; url: string }> {
try {
const fullPath = path.join(STORAGE_BASE_PATH, fileKey);

View File

@@ -16,7 +16,6 @@ import { promisify } from "util";
import * as os from "os";
import * as path from "path";
import * as fs from "fs/promises";
import * as fsSync from "fs";
const execFileAsync = promisify(execFile);

View File

@@ -82,14 +82,10 @@ import {
createWebImportSource,
updateWebImportSource,
deleteWebImportSource,
updateWebImportSourceStatus,
} from "./db";
import { loginLocal, hashPassword, getAzureAuthUrl, isAzureAdConfigured, generateToken } from "./auth";
import { exec as execCb } from "child_process";
import { promisify } from "util";
import { loginLocal, hashPassword, getAzureAuthUrl, isAzureAdConfigured } from "./auth";
import fsSync from "fs";
import pathSync from "path";
const execAsync = promisify(execCb);
import { extractInvoicesWithMistral, generateMetadataJSON } from "./invoiceExtractor";
import { localStoragePut, generateStorageKey } from "./localStorage";
import { testSftpConnection, exportInvoiceToSftp, getUserSftpConfig } from "./sftpExport";