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 <noreply@anthropic.com>
This commit is contained in:
Summit Dental Care
2026-06-22 23:50:19 -04:00
parent cee84bcd61
commit fcf6effb7b
2 changed files with 84 additions and 0 deletions

View File

@@ -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);

View File

@@ -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<any> => {
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;