const express = require("express"); const helmet = require("helmet"); const cors = require("cors"); const compression = require("compression"); const morgan = require("morgan"); const mongoSanitize = require("express-mongo-sanitize"); const hpp = require("hpp"); const { env } = require("./config/env"); const { apiLimiter } = require("./middleware/rateLimiters"); const { errorHandler, notFound } = require("./middleware/errorHandler"); const logger = require("./utils/logger"); const authRoutes = require("./routes/auth.routes"); const courseRoutes = require("./routes/course.routes"); const orderRoutes = require("./routes/order.routes"); const adminRoutes = require("./routes/admin.routes"); const voucherRoutes = require("./routes/voucher.routes"); const webhookRoutes = require("./routes/webhook.routes"); const app = express(); // Trust the first proxy hop (Render/Heroku/Nginx/etc.) so req.ip and the // "secure" cookie/HSTS logic reflect the real client over HTTPS. app.set("trust proxy", 1); // --- Security headers ------------------------------------------------- app.use( helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'"], styleSrc: ["'self'", "'unsafe-inline'"], imgSrc: ["'self'", "data:", "https:"], objectSrc: ["'none'"], frameAncestors: ["'none'"], }, }, crossOriginResourcePolicy: { policy: "same-site" }, hsts: { maxAge: 63072000, includeSubDomains: true, preload: true }, }) ); // --- CORS: only the configured frontend origins may call the API. // `credentials: false` is deliberate — this API is designed to run // cross-site from its frontend (e.g. a Hugging Face Space backend + // a separate Vercel-hosted frontend), and some hosting proxies in that // kind of setup intercept CORS preflight requests before they reach // this app and strip the Access-Control-Allow-Credentials header, // which silently breaks anything relying on `credentials: include`. // Sessions are carried via a bearer access token (Authorization // header) plus a refresh token returned in the JSON response body // instead of a cookie, so no cross-site credentialed request is ever // needed — see controllers/auth.controller.js for the reasoning and // trade-offs of that approach in more detail. --- app.use( cors({ origin(origin, callback) { if (!origin || env.clientOrigins.includes(origin)) return callback(null, true); // Logged (not just thrown) so a mismatched frontend domain shows up // immediately in the server logs with the exact rejected value — // compare this directly against the CORS allow-list logged at // startup to spot typos, protocol mismatches, or a missing entry. logger.warn(`CORS rejected origin: "${origin}" (allow-list: ${JSON.stringify(env.clientOrigins)})`); return callback(new Error("Not allowed by CORS")); }, credentials: false, }) ); app.use(compression()); // Payoneer's webhook signature is computed over the exact raw request // body, so this route MUST be registered with a raw-body parser // BEFORE the global express.json() below — once express.json() has // run, the original bytes are gone and any signature check would // fail. See utils/payoneerCheckout.js -> verifyAndParseWebhook. app.use("/api/webhooks", express.raw({ type: "*/*", limit: "200kb" }), webhookRoutes); app.use(express.json({ limit: "100kb" })); // small limit: this API never needs large bodies app.use(express.urlencoded({ extended: true, limit: "100kb" })); // Strip any keys starting with "$" or containing "." from user input to // block MongoDB operator-injection attacks. app.use(mongoSanitize()); // Prevent HTTP parameter-pollution (e.g. ?role=user&role=admin). app.use(hpp()); app.use( morgan("combined", { stream: { write: (message) => logger.info(message.trim()) }, skip: () => env.nodeEnv === "test", }) ); app.use("/api/", apiLimiter); app.get("/api/health", (req, res) => res.json({ status: "ok", time: new Date().toISOString() })); app.use("/api/auth", authRoutes); app.use("/api/courses", courseRoutes); app.use("/api/orders", orderRoutes); app.use("/api/admin", adminRoutes); app.use("/api/vouchers", voucherRoutes); app.use(notFound); app.use(errorHandler); module.exports = app;