Checkpoint: Ajout onglet Sauvegarde DB dans ImportSettings : endpoint POST /api/db-backup (mysqldump), GET /api/db-backup/:filename, procédures tRPC backup.list et backup.delete, composant BackupSection avec liste des sauvegardes et téléchargement.
This commit is contained in:
@@ -5,6 +5,9 @@ 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";
|
||||
const execAsync = promisify(execCb);
|
||||
import { createExpressMiddleware } from "@trpc/server/adapters/express";
|
||||
import { registerOAuthRoutes } from "./oauth";
|
||||
import { appRouter } from "../routers";
|
||||
@@ -225,6 +228,70 @@ async function startServer() {
|
||||
});
|
||||
|
||||
// ============= WEB IMPORT SOURCES - Endpoint pour script cron externe =============
|
||||
|
||||
// ============= 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 token = req.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; }
|
||||
|
||||
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);
|
||||
|
||||
// Construire la commande mysqldump
|
||||
const sslFlag = dbUrl.searchParams.get("ssl-mode") === "DISABLED" ? "" : "--ssl-mode=REQUIRED";
|
||||
const cmd = `mysqldump ${sslFlag} -h "${host}" -P ${port} -u "${username}" --password="${password}" "${database}" > "${filePath}"`;
|
||||
|
||||
console.log(`[Backup] Generating dump for database ${database}...`);
|
||||
await execAsync(cmd);
|
||||
console.log(`[Backup] Dump saved to ${filePath}`);
|
||||
|
||||
// Retourner le fichier en téléchargement
|
||||
const encodedName = encodeURIComponent(fileName);
|
||||
res.setHeader("Content-Disposition", `attachment; filename="${encodedName}"; filename*=UTF-8''${encodedName}`);
|
||||
res.setHeader("Content-Type", "application/sql");
|
||||
res.sendFile(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 });
|
||||
}
|
||||
});
|
||||
|
||||
// Télécharger une sauvegarde existante
|
||||
app.get("/api/db-backup/:filename", async (req, res) => {
|
||||
const { verifyToken } = await import("../auth");
|
||||
const token = req.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; }
|
||||
|
||||
const fileName = path.basename(req.params.filename);
|
||||
const filePath = path.join(path.resolve("backups"), fileName);
|
||||
if (!fs.existsSync(filePath)) { res.status(404).json({ error: "Fichier introuvable" }); return; }
|
||||
const encodedName = encodeURIComponent(fileName);
|
||||
res.setHeader("Content-Disposition", `attachment; filename="${encodedName}"; filename*=UTF-8''${encodedName}`);
|
||||
res.setHeader("Content-Type", "application/sql");
|
||||
res.sendFile(filePath);
|
||||
});
|
||||
|
||||
app.post("/api/web-import/push-invoice", async (req, res) => {
|
||||
try {
|
||||
const { apiToken, fileName, fileBase64, mimeType } = req.body;
|
||||
|
||||
@@ -85,6 +85,11 @@ import {
|
||||
updateWebImportSourceStatus,
|
||||
} from "./db";
|
||||
import { loginLocal, hashPassword, getAzureAuthUrl, isAzureAdConfigured, generateToken } from "./auth";
|
||||
import { exec as execCb } from "child_process";
|
||||
import { promisify } from "util";
|
||||
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";
|
||||
@@ -2678,5 +2683,33 @@ export const appRouter = router({
|
||||
return { apiToken: source.apiToken };
|
||||
}),
|
||||
}),
|
||||
|
||||
// ============= BACKUP ROUTES =============
|
||||
backup: router({
|
||||
// Liste les sauvegardes existantes dans le dossier backups/
|
||||
list: adminProcedure.query(async () => {
|
||||
const backupDir = pathSync.resolve("backups");
|
||||
if (!fsSync.existsSync(backupDir)) return [];
|
||||
const files = fsSync.readdirSync(backupDir)
|
||||
.filter(f => f.endsWith(".sql") || f.endsWith(".sql.gz"))
|
||||
.map(f => {
|
||||
const stat = fsSync.statSync(pathSync.join(backupDir, f));
|
||||
return { name: f, size: stat.size, createdAt: stat.mtime };
|
||||
})
|
||||
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
||||
return files;
|
||||
}),
|
||||
|
||||
// Supprime une sauvegarde
|
||||
delete: adminProcedure
|
||||
.input(z.object({ name: z.string() }))
|
||||
.mutation(async ({ input }) => {
|
||||
const backupDir = pathSync.resolve("backups");
|
||||
const filePath = pathSync.join(backupDir, pathSync.basename(input.name));
|
||||
if (!fsSync.existsSync(filePath)) throw new TRPCError({ code: 'NOT_FOUND', message: 'Fichier introuvable' });
|
||||
fsSync.unlinkSync(filePath);
|
||||
return { success: true };
|
||||
}),
|
||||
}),
|
||||
});
|
||||
export type AppRouter = typeof appRouter;
|
||||
|
||||
Reference in New Issue
Block a user