From fcf6effb7bb03b90ebcf7ffdbb0541bebf1e1e86 Mon Sep 17 00:00:00 2001 From: Summit Dental Care Date: Mon, 22 Jun 2026 23:50:19 -0400 Subject: [PATCH] fix: move network-backup endpoints outside JWT auth middleware The /network-backup and /network-backup-files routes use their own X-Network-Backup-Key authentication. Mounting them behind authenticateJWT blocked all receiver sync requests before they could be validated. Co-Authored-By: Claude Sonnet 4.6 --- apps/Backend/src/app.ts | 3 + .../src/routes/network-backup-public.ts | 81 +++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 apps/Backend/src/routes/network-backup-public.ts diff --git a/apps/Backend/src/app.ts b/apps/Backend/src/app.ts index 3c67e1f4..ebd9d774 100755 --- a/apps/Backend/src/app.ts +++ b/apps/Backend/src/app.ts @@ -7,6 +7,7 @@ import authRoutes from "./routes/auth"; import twilioWebhookRoutes from "./routes/twilio-webhooks"; import greetingRoutes from "./routes/greeting"; import { authenticateJWT } from "./middlewares/auth.middleware"; +import networkBackupPublicRoutes from "./routes/network-backup-public"; import dotenv from "dotenv"; import { startBackupCron } from "./cron/backupCheck"; import path from "path"; @@ -77,6 +78,8 @@ app.use("/api/auth", authRoutes); app.use("/api/greeting", greetingRoutes); // Twilio webhooks are public — Twilio sends no JWT token app.use("/api/twilio", express.urlencoded({ extended: false }), twilioWebhookRoutes); +// Network backup endpoints use their own API key auth — must be before authenticateJWT +app.use("/api/database-management", networkBackupPublicRoutes); // All other API routes require JWT app.use("/api", authenticateJWT, routes); diff --git a/apps/Backend/src/routes/network-backup-public.ts b/apps/Backend/src/routes/network-backup-public.ts new file mode 100644 index 00000000..7e3065bf --- /dev/null +++ b/apps/Backend/src/routes/network-backup-public.ts @@ -0,0 +1,81 @@ +import { Router, Request, Response } from "express"; +import { spawn } from "child_process"; +import path from "path"; +import fs from "fs"; +import archiver from "archiver"; +import { getOrCreateApiKey } from "../services/networkSyncConfigService"; + +const router = Router(); + +const UPLOADS_DIR = path.resolve(process.cwd(), "uploads"); + +function checkApiKey(req: Request, res: Response): boolean { + const providedKey = req.headers["x-network-backup-key"] as string | undefined; + if (!providedKey) { + res.status(401).json({ error: "Missing X-Network-Backup-Key header" }); + return false; + } + const storedKey = getOrCreateApiKey(); + if (providedKey !== storedKey) { + res.status(401).json({ error: "Invalid API key" }); + return false; + } + return true; +} + +// GET /api/database-management/network-backup +router.get("/network-backup", async (req: Request, res: Response): Promise => { + if (!checkApiKey(req, res)) return; + + const pg = spawn( + "pg_dump", + [ + "--no-acl", "--no-owner", + "-h", process.env.DB_HOST || "localhost", + "-U", process.env.DB_USER || "postgres", + process.env.DB_NAME || "dental_db", + ], + { env: { ...process.env, PGPASSWORD: process.env.DB_PASSWORD } } + ); + + res.setHeader("Content-Type", "application/octet-stream"); + res.setHeader( + "Content-Disposition", + `attachment; filename="network_backup_${Date.now()}.sql"` + ); + + pg.stdout.pipe(res); + + let stderr = ""; + pg.stderr.on("data", (d) => (stderr += d.toString())); + pg.on("error", (err) => { + if (!res.headersSent) + res.status(500).json({ error: "pg_dump failed", details: err.message }); + }); + pg.on("close", (code) => { + if (code !== 0) console.error("pg_dump for network backup failed:", stderr); + }); +}); + +// GET /api/database-management/network-backup-files +router.get("/network-backup-files", (req: Request, res: Response): any => { + if (!checkApiKey(req, res)) return; + + if (!fs.existsSync(UPLOADS_DIR)) { + return res.status(200).end(); + } + + res.setHeader("Content-Type", "application/zip"); + res.setHeader("Content-Disposition", `attachment; filename="network_uploads_${Date.now()}.zip"`); + + const archive = archiver("zip", { zlib: { level: 6 } }); + archive.on("error", (err) => { + if (!res.headersSent) res.status(500).json({ error: "Failed to create archive", details: err.message }); + }); + + archive.pipe(res); + archive.directory(UPLOADS_DIR, false); + archive.finalize(); +}); + +export default router;