Spaces:
Running
Running
Upload 2 files
Browse files
app.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const express = require("express");
|
| 2 |
+
const helmet = require("helmet");
|
| 3 |
+
const cors = require("cors");
|
| 4 |
+
const cookieParser = require("cookie-parser");
|
| 5 |
+
const compression = require("compression");
|
| 6 |
+
const morgan = require("morgan");
|
| 7 |
+
const mongoSanitize = require("express-mongo-sanitize");
|
| 8 |
+
const hpp = require("hpp");
|
| 9 |
+
|
| 10 |
+
const { env } = require("./config/env");
|
| 11 |
+
const { apiLimiter } = require("./middleware/rateLimiters");
|
| 12 |
+
const { errorHandler, notFound } = require("./middleware/errorHandler");
|
| 13 |
+
const logger = require("./utils/logger");
|
| 14 |
+
|
| 15 |
+
const authRoutes = require("./routes/auth.routes");
|
| 16 |
+
const courseRoutes = require("./routes/course.routes");
|
| 17 |
+
const orderRoutes = require("./routes/order.routes");
|
| 18 |
+
const adminRoutes = require("./routes/admin.routes");
|
| 19 |
+
|
| 20 |
+
const app = express();
|
| 21 |
+
|
| 22 |
+
// Trust the first proxy hop (Render/Heroku/Nginx/etc.) so req.ip and the
|
| 23 |
+
// "secure" cookie/HSTS logic reflect the real client over HTTPS.
|
| 24 |
+
app.set("trust proxy", 1);
|
| 25 |
+
|
| 26 |
+
// --- Security headers -------------------------------------------------
|
| 27 |
+
app.use(
|
| 28 |
+
helmet({
|
| 29 |
+
contentSecurityPolicy: {
|
| 30 |
+
directives: {
|
| 31 |
+
defaultSrc: ["'self'"],
|
| 32 |
+
scriptSrc: ["'self'"],
|
| 33 |
+
styleSrc: ["'self'", "'unsafe-inline'"],
|
| 34 |
+
imgSrc: ["'self'", "data:", "https:"],
|
| 35 |
+
objectSrc: ["'none'"],
|
| 36 |
+
frameAncestors: ["'none'"],
|
| 37 |
+
},
|
| 38 |
+
},
|
| 39 |
+
crossOriginResourcePolicy: { policy: "same-site" },
|
| 40 |
+
hsts: { maxAge: 63072000, includeSubDomains: true, preload: true },
|
| 41 |
+
})
|
| 42 |
+
);
|
| 43 |
+
|
| 44 |
+
// --- CORS: only the configured frontend origins may call the API, and
|
| 45 |
+
// credentials (refresh-token cookie) are only sent to those origins. ---
|
| 46 |
+
app.use(
|
| 47 |
+
cors({
|
| 48 |
+
origin(origin, callback) {
|
| 49 |
+
if (!origin || env.clientOrigins.includes(origin)) return callback(null, true);
|
| 50 |
+
// Logged (not just thrown) so a mismatched frontend domain shows up
|
| 51 |
+
// immediately in the server logs with the exact rejected value —
|
| 52 |
+
// compare this directly against the CORS allow-list logged at
|
| 53 |
+
// startup to spot typos, protocol mismatches, or a missing entry.
|
| 54 |
+
logger.warn(`CORS rejected origin: "${origin}" (allow-list: ${JSON.stringify(env.clientOrigins)})`);
|
| 55 |
+
return callback(new Error("Not allowed by CORS"));
|
| 56 |
+
},
|
| 57 |
+
credentials: true,
|
| 58 |
+
})
|
| 59 |
+
);
|
| 60 |
+
|
| 61 |
+
app.use(compression());
|
| 62 |
+
app.use(express.json({ limit: "100kb" })); // small limit: this API never needs large bodies
|
| 63 |
+
app.use(express.urlencoded({ extended: true, limit: "100kb" }));
|
| 64 |
+
app.use(cookieParser(env.cookieSecret));
|
| 65 |
+
|
| 66 |
+
// Strip any keys starting with "$" or containing "." from user input to
|
| 67 |
+
// block MongoDB operator-injection attacks.
|
| 68 |
+
app.use(mongoSanitize());
|
| 69 |
+
// Prevent HTTP parameter-pollution (e.g. ?role=user&role=admin).
|
| 70 |
+
app.use(hpp());
|
| 71 |
+
|
| 72 |
+
app.use(
|
| 73 |
+
morgan("combined", {
|
| 74 |
+
stream: { write: (message) => logger.info(message.trim()) },
|
| 75 |
+
skip: () => env.nodeEnv === "test",
|
| 76 |
+
})
|
| 77 |
+
);
|
| 78 |
+
|
| 79 |
+
app.use("/api/", apiLimiter);
|
| 80 |
+
|
| 81 |
+
app.get("/api/health", (req, res) => res.json({ status: "ok", time: new Date().toISOString() }));
|
| 82 |
+
|
| 83 |
+
app.use("/api/auth", authRoutes);
|
| 84 |
+
app.use("/api/courses", courseRoutes);
|
| 85 |
+
app.use("/api/orders", orderRoutes);
|
| 86 |
+
app.use("/api/admin", adminRoutes);
|
| 87 |
+
|
| 88 |
+
app.use(notFound);
|
| 89 |
+
app.use(errorHandler);
|
| 90 |
+
|
| 91 |
+
module.exports = app;
|
server.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const { env, assertProductionSafety } = require("./config/env");
|
| 2 |
+
const { connectDB, disconnectDB } = require("./config/db");
|
| 3 |
+
const logger = require("./utils/logger");
|
| 4 |
+
const app = require("./app");
|
| 5 |
+
|
| 6 |
+
assertProductionSafety();
|
| 7 |
+
|
| 8 |
+
let server;
|
| 9 |
+
|
| 10 |
+
async function start() {
|
| 11 |
+
try {
|
| 12 |
+
await connectDB();
|
| 13 |
+
server = app.listen(env.port, () => {
|
| 14 |
+
logger.info(`Aynwerd API listening on port ${env.port} [${env.nodeEnv}]`);
|
| 15 |
+
// Logged deliberately at startup: this is the #1 source of CORS
|
| 16 |
+
// support confusion (a typo'd env var name, stray quotes, a
|
| 17 |
+
// trailing slash, or the value falling back to its localhost
|
| 18 |
+
// default because it was never actually set). Seeing the parsed
|
| 19 |
+
// array here removes all guesswork versus staring at the raw
|
| 20 |
+
// dashboard value.
|
| 21 |
+
logger.info(`CORS allow-list (CLIENT_ORIGINS parsed as): ${JSON.stringify(env.clientOrigins)}`);
|
| 22 |
+
});
|
| 23 |
+
} catch (err) {
|
| 24 |
+
logger.error("Failed to start server", { error: err.message });
|
| 25 |
+
process.exit(1);
|
| 26 |
+
}
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
async function shutdown(signal) {
|
| 30 |
+
logger.info(`Received ${signal}, shutting down gracefully...`);
|
| 31 |
+
if (server) {
|
| 32 |
+
server.close(async () => {
|
| 33 |
+
await disconnectDB();
|
| 34 |
+
process.exit(0);
|
| 35 |
+
});
|
| 36 |
+
// Force-exit if something hangs
|
| 37 |
+
setTimeout(() => process.exit(1), 10000).unref();
|
| 38 |
+
} else {
|
| 39 |
+
process.exit(0);
|
| 40 |
+
}
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
| 44 |
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
| 45 |
+
process.on("unhandledRejection", (reason) => {
|
| 46 |
+
logger.error("Unhandled promise rejection", { reason: reason?.message || reason });
|
| 47 |
+
});
|
| 48 |
+
process.on("uncaughtException", (err) => {
|
| 49 |
+
logger.error("Uncaught exception", { error: err.message, stack: err.stack });
|
| 50 |
+
process.exit(1);
|
| 51 |
+
});
|
| 52 |
+
|
| 53 |
+
start();
|