122 lines
3.2 KiB
JavaScript
122 lines
3.2 KiB
JavaScript
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);
|
|
});
|