const express = require("express"); const path = require("path"); const cors = require("cors"); const compression = require("compression"); const dotenv = require("dotenv"); const { pool, ensureDatabase } = require("./db"); const playersRoutes = require("./routes/players"); const rankingsRoutes = require("./routes/rankings"); const { sendError } = require("./utils/http"); const { createApiRateLimiters } = require("./middleware/rate-limit"); const { createCorsMiddleware, securityHeaders, enforceHttps, } = require("./middleware/security"); dotenv.config(); const app = express(); const PORT = Number(process.env.PORT || 3000); const isDevelopment = process.env.NODE_ENV !== 'production'; const trustProxy = String(process.env.TRUST_PROXY || "false").toLowerCase() === "true"; app.set("trust proxy", trustProxy); app.set("etag", "strong"); app.use(createCorsMiddleware()); app.use(securityHeaders); app.use(enforceHttps); app.use( compression({ threshold: 1024, level: 6, }) ); app.use( express.json({ limit: "32kb", strict: true, type: "application/json", }) ); const [ipRateLimit, userRateLimit] = createApiRateLimiters({ ipMax: Number(process.env.RATE_LIMIT_IP_MAX || 60), userMax: Number(process.env.RATE_LIMIT_USER_MAX || 60), windowMs: Number(process.env.RATE_LIMIT_WINDOW_MS || 60 * 1000), }); app.use("/api", ipRateLimit, userRateLimit); app.use("/api", (_req, res, next) => { res.setHeader("Cache-Control", "no-store"); next(); }); app.use( express.static(path.join(__dirname, "..", "public"), { etag: true, lastModified: true, maxAge: "1h", setHeaders(res, filePath) { const ext = path.extname(filePath).toLowerCase(); if (ext === ".html") { res.setHeader("Cache-Control", "no-store"); return; } if (filePath.includes(`${path.sep}assets${path.sep}`)) { res.setHeader("Cache-Control", "public, max-age=604800, immutable"); } else { res.setHeader("Cache-Control", "public, max-age=3600"); } }, }) ); app.use("/api", playersRoutes); app.use("/api", rankingsRoutes); app.get("/api/health", async (_req, res) => { try { await pool.query("SELECT 1 AS ok"); return res.json({ ok: true }); } catch (_error) { return sendError(res, 500, "DB_UNAVAILABLE", "Database connection failed"); } }); app.use((err, _req, res, _next) => { if (err && /CORS origin blocked/i.test(String(err.message || ""))) { return sendError(res, 403, "CORS_BLOCKED", "Origin is not allowed."); } const isProd = String(process.env.NODE_ENV || "").toLowerCase() === "production"; if (!isProd) { console.error("Unhandled error:", err); } else { console.error("Unhandled error:", err?.name || "UnknownError"); } return sendError(res, 500, "INTERNAL_ERROR", "Internal server error"); }); async function startServer() { await ensureDatabase(); app.listen(PORT, () => { console.log(`EaglerTiers API running on http://localhost:${PORT}`); console.log(`Environment: ${isDevelopment ? 'development' : 'production'}`); }); } startServer().catch((error) => { console.error("Failed to start API:", error?.message || error); process.exit(1); });