309 lines
6.6 KiB
JavaScript
309 lines
6.6 KiB
JavaScript
const express = require("express");
|
|
const cors = require("cors");
|
|
const helmet = require("helmet");
|
|
const morgan = require("morgan");
|
|
const swaggerUi = require("swagger-ui-express");
|
|
const swaggerJsdoc = require("swagger-jsdoc");
|
|
const routes = require("./app/routes/routes");
|
|
const db = require("./app/models");
|
|
const path = require("path");
|
|
const cookieParser = require("cookie-parser");
|
|
const sanitizeInput = require("./app/utils/sanitizeInput");
|
|
const verifySignature = require("./app/middleware/app.middleware");
|
|
const authController = require("./app/controllers/auth.controller");
|
|
const establishmentController = require("./app/controllers/establishment.controller");
|
|
require("dotenv").config();
|
|
|
|
const csrf = require("csurf");
|
|
|
|
const isProd = process.env.NODE_ENV === "production";
|
|
const isLocal = process.env.NODE_ENV === "development";
|
|
|
|
/**
|
|
* =========================
|
|
* CSRF CONFIG (CORRECT)
|
|
* =========================
|
|
*/
|
|
const csrfProtection = csrf({
|
|
cookie: {
|
|
key: "_csrf",
|
|
httpOnly: true,
|
|
secure: isProd, // HTTPS in UAT/PROD
|
|
sameSite: isProd ? "none" : "lax",
|
|
},
|
|
});
|
|
|
|
const app = express();
|
|
|
|
/**
|
|
* =========================
|
|
* VIEW ENGINE SETUP (EJS)
|
|
* =========================
|
|
*/
|
|
app.set('view engine', 'ejs');
|
|
app.set('views', path.join(__dirname, 'app/views'));
|
|
|
|
/**
|
|
* REQUIRED for HSTS when behind proxy
|
|
*/
|
|
app.set("trust proxy", 1);
|
|
|
|
/**
|
|
* =========================
|
|
* GLOBAL MIDDLEWARE
|
|
* =========================
|
|
*/
|
|
app.use(express.json());
|
|
app.use(cookieParser());
|
|
|
|
app.use(
|
|
"/assets",
|
|
express.static(path.join(__dirname, "app/assets"))
|
|
);
|
|
|
|
/**
|
|
* =========================
|
|
* HELMET + SECURITY HEADERS
|
|
* =========================
|
|
*/
|
|
app.use(
|
|
helmet({
|
|
contentSecurityPolicy: {
|
|
directives: {
|
|
defaultSrc: ["'none'"],
|
|
connectSrc: ["'self'"],
|
|
scriptSrc: ["'self'", "'unsafe-inline'"],
|
|
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
imgSrc: ["'self'", "data:"],
|
|
fontSrc: ["'self'", "data:"],
|
|
frameAncestors: ["'none'"],
|
|
baseUri: ["'none'"],
|
|
formAction: ["'self'"],
|
|
},
|
|
},
|
|
})
|
|
);
|
|
|
|
// Clickjacking protection
|
|
app.use(helmet.frameguard({ action: "deny" }));
|
|
|
|
// HSTS for UAT + PROD
|
|
if (!isLocal) {
|
|
app.use(
|
|
helmet.hsts({
|
|
maxAge: 31536000,
|
|
includeSubDomains: true,
|
|
preload: true,
|
|
})
|
|
);
|
|
}
|
|
|
|
app.use(morgan("dev"));
|
|
app.use(sanitizeInput);
|
|
|
|
/**
|
|
* =========================
|
|
* CORS
|
|
* =========================
|
|
*/
|
|
const allowedOrigins = process.env.ALLOWED_ORIGINS
|
|
? process.env.ALLOWED_ORIGINS.split(",").map(o => o.trim())
|
|
: [];
|
|
|
|
app.use(
|
|
cors({
|
|
origin: allowedOrigins,
|
|
credentials: true,
|
|
})
|
|
);
|
|
|
|
/**
|
|
* Manual headers (kept as-is, but fixed OPTIONS flow)
|
|
*/
|
|
app.use((req, res, next) => {
|
|
if (allowedOrigins.includes(req.headers.origin)) {
|
|
res.header("Access-Control-Allow-Origin", req.headers.origin);
|
|
}
|
|
|
|
res.header("Access-Control-Allow-Credentials", "true");
|
|
res.header(
|
|
"Access-Control-Allow-Headers",
|
|
"Content-Type, Authorization, APP_SIGNATURE, x-app-signature, X-CSRF-Token"
|
|
);
|
|
res.header(
|
|
"Access-Control-Allow-Methods",
|
|
"GET, POST, PUT, PATCH, DELETE, OPTIONS"
|
|
);
|
|
|
|
// ⚠️ IMPORTANT: DO NOT TERMINATE REQUEST HERE
|
|
if (req.method === "OPTIONS") {
|
|
return next();
|
|
}
|
|
|
|
next();
|
|
});
|
|
|
|
/**
|
|
* =========================
|
|
* SWAGGER
|
|
* =========================
|
|
*/
|
|
const swaggerOptions = {
|
|
definition: {
|
|
openapi: "3.0.0",
|
|
info: {
|
|
title: "FCSC IPI Survey",
|
|
version: "1.0.0",
|
|
description:
|
|
"Federal Competitiveness and Statistics Centre (FCSC) - Industrial Production Index (IPI)",
|
|
},
|
|
components: {
|
|
securitySchemes: {
|
|
bearerAuth: {
|
|
type: "http",
|
|
scheme: "bearer",
|
|
bearerFormat: "JWT",
|
|
},
|
|
appSignature: {
|
|
type: "apiKey",
|
|
in: "header",
|
|
name: "x-app-signature",
|
|
},
|
|
csrfToken: {
|
|
type: "apiKey",
|
|
in: "header",
|
|
name: "X-CSRF-Token",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
apis: ["./app/routes/*.js"],
|
|
};
|
|
|
|
const swaggerDocs = swaggerJsdoc(swaggerOptions);
|
|
|
|
app.use(
|
|
"/api-docs",
|
|
swaggerUi.serve,
|
|
swaggerUi.setup(swaggerDocs, {
|
|
swaggerOptions: {
|
|
withCredentials: true,
|
|
},
|
|
})
|
|
);
|
|
|
|
/**
|
|
* =========================
|
|
* AUTH (NO CSRF)
|
|
* =========================
|
|
*/
|
|
|
|
const logRoutes = require('./app/routes/logRoutes');
|
|
app.use('/logs', logRoutes);
|
|
|
|
app.post("/api/auth/login", [verifySignature], authController.login);
|
|
app.post(
|
|
"/api/forgot-password/request-otp",
|
|
[verifySignature],
|
|
establishmentController.forgotPasswordRequestOTP
|
|
);
|
|
app.post(
|
|
"/api/forgot-password/verify-otp",
|
|
[verifySignature],
|
|
establishmentController.forgotPasswordVerifyOTP
|
|
);
|
|
|
|
/**
|
|
* =========================
|
|
* CSRF TOKEN ENDPOINT
|
|
* =========================
|
|
*/
|
|
app.get("/api/csrf-token", csrfProtection, (req, res) => {
|
|
res.status(200).json({
|
|
csrfToken: req.csrfToken(),
|
|
});
|
|
});
|
|
|
|
/**
|
|
* =========================
|
|
* ENFORCE CSRF FOR STATE-CHANGING REQUESTS
|
|
* =========================
|
|
*/
|
|
app.use((req, res, next) => {
|
|
// Allow safe methods
|
|
if (["GET", "HEAD", "OPTIONS"].includes(req.method)) {
|
|
return next();
|
|
}
|
|
|
|
// Only protect API routes
|
|
if (!req.path.startsWith("/api")) {
|
|
return next();
|
|
}
|
|
|
|
// Skip auth & public endpoints
|
|
const csrfExcludedPaths = [
|
|
"/api/auth/login",
|
|
"/api/forgot-password/request-otp",
|
|
"/api/forgot-password/verify-otp",
|
|
"/api/csrf-token",
|
|
"/api/auth/request-otp",
|
|
"/api/auth/verify-otp"
|
|
];
|
|
|
|
if (csrfExcludedPaths.includes(req.path)) {
|
|
return next();
|
|
}
|
|
|
|
// 🔒 Enforce CSRF
|
|
return csrfProtection(req, res, next);
|
|
});
|
|
|
|
/**
|
|
* =========================
|
|
* PROTECTED ROUTES
|
|
* =========================
|
|
*/
|
|
app.use("/api", routes);
|
|
|
|
/**
|
|
* =========================
|
|
* CSRF ERROR HANDLER
|
|
* =========================
|
|
*/
|
|
app.use((err, req, res, next) => {
|
|
if (err.code === "EBADCSRFTOKEN") {
|
|
return res.status(403).json({
|
|
status: "failed",
|
|
message: "Invalid or missing CSRF token",
|
|
});
|
|
}
|
|
next(err);
|
|
});
|
|
|
|
/**
|
|
* =========================
|
|
* TEST ROUTE
|
|
* =========================
|
|
*/
|
|
app.get("/api/test", (req, res) => {
|
|
res.json({ status: "success", message: "Test API working fine 🚀" });
|
|
});
|
|
|
|
/**
|
|
* =========================
|
|
* DEPLOYMENT ROUTE
|
|
* =========================
|
|
*/
|
|
const deploymentController = require("./app/controllers/deployment.controller");
|
|
app.post("/deploy", deploymentController.deployment);
|
|
|
|
/**
|
|
* =========================
|
|
* START SERVER
|
|
* =========================
|
|
*/
|
|
const PORT = process.env.PORT || 5000;
|
|
app.listen(PORT, () => {
|
|
console.log(`🚀 Server running on port ${PORT}`);
|
|
});
|