Compare commits
2
Commits
cee84bcd61
...
8154e904b9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8154e904b9 | ||
|
|
fcf6effb7b |
@@ -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);
|
||||
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
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 isLanIp(ip: string): boolean {
|
||||
return (
|
||||
/^127\./.test(ip) ||
|
||||
/^10\./.test(ip) ||
|
||||
/^192\.168\./.test(ip) ||
|
||||
/^172\.(1[6-9]|2\d|3[01])\./.test(ip)
|
||||
);
|
||||
}
|
||||
|
||||
function checkAccess(req: Request, res: Response): boolean {
|
||||
// Only allow requests from the local network
|
||||
const ip =
|
||||
(req.headers["x-real-ip"] as string) ||
|
||||
(req.headers["x-forwarded-for"] as string)?.split(",")[0].trim() ||
|
||||
req.socket.remoteAddress ||
|
||||
"";
|
||||
if (!isLanIp(ip)) {
|
||||
res.status(403).json({ error: "Access restricted to local network" });
|
||||
return false;
|
||||
}
|
||||
|
||||
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 (!checkAccess(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 (!checkAccess(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;
|
||||
Reference in New Issue
Block a user