import express from "express"; import cookieParser from "cookie-parser"; import { config } from "./config.js"; import { pool, pgHealthy } from "./db.js"; import { securityHeaders, extractClientIp } from "./middleware/security.js"; import { corsMiddleware } from "./middleware/cors.js"; import { globalLimiter } from "./middleware/rate-limit.js"; import { errorHandler } from "./middleware/error-handler.js"; import { accessLog } from "./middleware/access-log.js"; import healthRouter from "./routes/health.js"; import subscribeRouter from "./routes/subscribe.js"; import ticketsRouter from "./routes/tickets.js"; import quotesRouter from "./routes/quotes.js"; import formationsRouter from "./routes/formations.js"; import discountsRouter from "./routes/discounts.js"; import iftaRouter from "./routes/ifta.js"; import adminRouter from "./routes/admin.js"; import webhooksRouter from "./routes/webhooks.js"; import identityRouter from "./routes/identity.js"; import refundsRouter from "./routes/refunds.js"; import agentsRouter from "./routes/agents.js"; import bundlesRouter from "./routes/bundles.js"; import entitiesRouter from "./routes/entities.js"; import idUploadRouter from "./routes/id-upload.js"; import canadaCrtcRouter from "./routes/canada-crtc.js"; import checkoutRouter from "./routes/checkout.js"; import ambLocationsRouter from "./routes/amb-locations.js"; import paymentMethodsRouter from "./routes/payment-methods.js"; import paypalRouter from "./routes/paypal.js"; import portalAuthRouter from "./routes/portal-auth.js"; import portalRouter from "./routes/portal.js"; import portalSetupRouter from "./routes/portal-setup.js"; import portalEsignRouter from "./routes/portal-esign.js"; import fccLookupRouter from "./routes/fcc-lookup.js"; import telecomEntitiesRouter from "./routes/telecom-entities.js"; import complianceOrdersRouter from "./routes/compliance-orders.js"; import cdrRouter from "./routes/cdr.js"; import iccRouter from "./routes/icc.js"; import resellerCertsRouter from "./routes/reseller-certs.js"; import lnpaRegionsRouter from "./routes/lnpa-regions.js"; import fccFilingsRouter from "./routes/fcc-filings.js"; import adminCryptoRouter from "./routes/admin-crypto.js"; import foreignQualRouter from "./routes/foreign-qualification.js"; import corpStatusRouter from "./routes/corp-status.js"; import portalRmdReviewRouter from "./routes/portal-rmd-review.js"; import portalEsignGenericRouter from "./routes/portal-esign-generic.js"; import pucRouter from "./routes/puc.js"; import fccCarrierRegRouter from "./routes/fcc-carrier-registration.js"; import dotLookupRouter from "./routes/dot-lookup.js"; import npiLookupRouter from "./routes/npi-lookup.js"; import surveyRouter from "./routes/survey.js"; import orderTimelineRouter from "./routes/order-timeline.js"; const app = express(); // Trust first proxy (nginx) in production if (config.nodeEnv === "production") { app.set("trust proxy", 1); } // --- Middleware stack (order matters) --- app.use(securityHeaders); app.use(extractClientIp); app.use(accessLog); app.use(corsMiddleware); app.use(cookieParser()); app.use(globalLimiter); // Stripe webhook — raw body MUST be preserved for signature verification. // Mount BEFORE express.json() so the Buffer is not parsed away. app.use("/api/v1/webhooks/stripe", express.raw({ type: "application/json" })); app.use(identityRouter); // identity webhook uses raw() internally on its specific route // Photo ID upload needs larger body (resized images up to ~3MB as base64) app.use("/api/v1/id-upload", express.json({ limit: "5mb" })); app.use(express.json({ limit: "512kb" })); // 512kb for eSign signature PNG base64 // Reject non-JSON content types on POST/PUT/PATCH app.use((req, res, next) => { if (["POST", "PUT", "PATCH"].includes(req.method) && !req.is("json")) { res.status(415).json({ error: "Content-Type must be application/json" }); return; } next(); }); // --- Routes --- app.use(healthRouter); app.use(subscribeRouter); app.use(ticketsRouter); app.use(quotesRouter); app.use(formationsRouter); app.use(discountsRouter); app.use(iftaRouter); app.use(adminRouter); app.use(webhooksRouter); app.use(refundsRouter); app.use(agentsRouter); app.use(bundlesRouter); app.use(entitiesRouter); app.use(idUploadRouter); app.use(canadaCrtcRouter); app.use(checkoutRouter); app.use(ambLocationsRouter); app.use(paymentMethodsRouter); app.use(paypalRouter); app.use("/api/v1/auth", portalAuthRouter); app.use(portalSetupRouter); app.use(portalEsignRouter); app.use(portalRmdReviewRouter); app.use(portalEsignGenericRouter); app.use("/api/v1/portal", portalRouter); // Must be AFTER specific portal routes (uses catch-all customer-auth) app.use(fccLookupRouter); app.use(corpStatusRouter); app.use(telecomEntitiesRouter); app.use(complianceOrdersRouter); app.use(cdrRouter); app.use(iccRouter); app.use(resellerCertsRouter); app.use(lnpaRegionsRouter); app.use(fccFilingsRouter); app.use(foreignQualRouter); app.use(pucRouter); app.use(fccCarrierRegRouter); app.use(dotLookupRouter); app.use(npiLookupRouter); app.use(surveyRouter); app.use(orderTimelineRouter); app.use(adminCryptoRouter); // Note: identityRouter mounted above express.json() for webhook route, // but also handles non-webhook routes (create-session, poll) which work fine with json() // --- Error handler (must be last) --- app.use(errorHandler); // --- Start --- async function start() { // Verify database connection const dbOk = await pgHealthy(); if (dbOk) { console.log(`[db] PostgreSQL connected`); } else { console.warn(`[db] PostgreSQL unreachable — API will start but DB-dependent routes will fail`); } // Pre-warm FX rate cache import("./fx.js").then(fx => fx.warmFxCache()).catch(err => console.warn("[fx] warmup failed:", err)); // Log ERPNext/Listmonk configuration status (non-blocking) if (config.erpnext.apiKey) { console.log(`[erpnext] Configured at ${config.erpnext.url}`); try { const r = await fetch(`${config.erpnext.url.replace(/\/$/, "")}/api/method/ping`, { headers: { Authorization: `token ${config.erpnext.apiKey}:${config.erpnext.apiSecret}`, "X-Frappe-Site-Name": config.erpnext.siteName, }, }); if (r.ok) { console.log("[erpnext] Connectivity check passed"); } else { console.warn(`[erpnext] Connectivity check failed (HTTP ${r.status})`); } } catch (err) { console.warn("[erpnext] Connectivity check failed (network error):", err); } } else { console.warn(`[erpnext] No API key configured — ERPNext integration disabled`); } if (config.listmonk.password) { console.log(`[listmonk] Configured at ${config.listmonk.url}`); } else { console.warn(`[listmonk] No credentials configured — Listmonk integration disabled`); } const host = "0.0.0.0"; // bind all interfaces — nginx on host proxies to Docker container port app.listen(config.port, host, () => { console.log(`[api] Performance West API listening on ${host}:${config.port} (${config.nodeEnv})`); }); } start().catch((err) => { console.error("[api] Fatal startup error:", err); process.exit(1); }); // Graceful shutdown process.on("SIGTERM", async () => { console.log("[api] SIGTERM received, shutting down..."); await pool.end(); process.exit(0); }); process.on("SIGINT", async () => { console.log("[api] SIGINT received, shutting down..."); await pool.end(); process.exit(0); });