Mobile cameras produce 8-12MB photos. Now: - Canvas-based resize to max 2000x1500 before upload - JPEG compression at 0.7-0.85 quality - Express body limit increased to 5MB for id-upload route - Falls back to raw upload for small images and PDFs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
192 lines
7 KiB
TypeScript
192 lines
7 KiB
TypeScript
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 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";
|
|
|
|
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(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(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);
|
|
});
|