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"); const AutomatedSchedulerService = require('./app/services/scheduler.service'); const ipiDemoController = require("./app/controllers/ipiDemo.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(express.urlencoded({ extended: true })); 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", "/api/products/uploadCSV", "/api/establishments/uploadCSV", "/api/unit_master/uploadCSV", ]; 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", }); } if (err.name === "MulterError") { return res.status(400).json({ status: "failed", message: err.message, hint: 'Upload the CSV using form field "file".', }); } 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); /** * ========================= * IPI DEMO UI * ========================= */ const requireIpiDemoAuth = (req, res, next) => { const demoKey = process.env.IPI_DEMO_KEY; if (!demoKey) return next(); if (req.cookies.ipi_demo_auth === "1") return next(); if (req.path.startsWith("/ipi-demo") && req.method === "GET" && req.headers.accept?.includes("text/html")) { return res.redirect("/ipi-demo-auth"); } return res.status(401).json({ success: false, message: "Unauthorized demo access", }); }; app.get("/ipi-demo-auth", (req, res) => { if (!process.env.IPI_DEMO_KEY) return res.redirect("/ipi-demo"); if (req.cookies.ipi_demo_auth === "1") return res.redirect("/ipi-demo"); return res.render("ipi-demo-auth", { title: "IPI Demo Access", error: null }); }); app.post("/ipi-demo-auth", (req, res) => { const expectedKey = process.env.IPI_DEMO_KEY; if (!expectedKey) return res.redirect("/ipi-demo"); const providedKey = String(req.body.key || "").trim(); if (providedKey !== expectedKey) { return res.status(401).render("ipi-demo-auth", { title: "IPI Demo Access", error: "Invalid demo key", }); } res.cookie("ipi_demo_auth", "1", { httpOnly: true, sameSite: "lax", secure: isProd, maxAge: 12 * 60 * 60 * 1000, }); return res.redirect("/ipi-demo"); }); app.post("/ipi-demo-logout", (req, res) => { res.clearCookie("ipi_demo_auth"); return res.redirect("/ipi-demo-auth"); }); app.get("/ipi-demo", requireIpiDemoAuth, ipiDemoController.renderPage); app.get("/ipi-demo/data", requireIpiDemoAuth, ipiDemoController.getData); app.get("/ipi-demo/table-data", requireIpiDemoAuth, ipiDemoController.getTableData); app.get("/ipi-demo/run-status", requireIpiDemoAuth, ipiDemoController.getRunStatus); app.post("/ipi-demo/run-month", requireIpiDemoAuth, ipiDemoController.runMonthCalculation); /** * ========================= * Auto Submission and iip calculation Automation * ========================= */ const scheduler = new AutomatedSchedulerService(); scheduler.start(); scheduler.startRemainderEmail(); // manual trigger endpoint app.post('/api/admin/trigger-scheduler', async (req, res) => { try { await scheduler.manualTrigger(); res.json({ success: true, message: 'Scheduler triggered successfully' }); } catch (error) { res.status(500).json({ success: false, message: 'Error triggering scheduler', error: error.message }); } }); /** * ========================= * START SERVER * ========================= */ const PORT = process.env.PORT || 5000; app.listen(PORT, () => { console.log(`🚀 Server running on port ${PORT}`); console.log(`Automated scheduler (Index Calculation) is active and will run daily at 2:00 AM`); console.log(`Automated scheduler (Reminder Emails) is active and will run daily at 6:00 AM`); });