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'); 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); /** * ========================= * 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`); });