// Import dotenv file if not in production if (process.env.NODE_ENV !== "production") { require("dotenv").config(); } const express = require("express"); const cors = require("cors"); const helmet = require("helmet"); const rateLimit = require("express-rate-limit"); const morgan = require("morgan"); const models = require('./models'); const db = models.db; const redis = models.redis; const {sequelize, Sequelize} = db; const Role = db.role; const authConfig = require("./config/auth.config"); const config = require("./config"); const createApp = async () => { await redis.ensureGroup("commands", `${config.DEVICE_ID}-cg`); const app = express(); // Security headers app.use(helmet()); // CORS restrict in production (set ALLOWED_ORIGINS env to comma-separated list) const corsOptions = { origin: config.NODE_ENV === "production" ? (config.ALLOWED_ORIGINS || "").split(",").map(s => s.trim()) : true, optionsSuccessStatus: 200 }; app.use(cors(corsOptions)); // Logging verbose in dev app.use(morgan(config.NODE_ENV === "production" ? "combined" : "dev")); // Body parsing with size limits app.use(express.json({limit: "10kb"})); app.use(express.urlencoded({extended: false, limit: "10kb"})); // Apply rate limiting in production if (config.NODE_ENV === "production") { app.use(rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // limit per IP standardHeaders: true, legacyHeaders: false })); } // Health check app.get("/health", (req, res) => res.status(200).send({status: "ok"})); // Mount routes which should have an Express Router app.use("/auth", require("./routes/auth.routes")); app.use("/user", require("./routes/user.routes")); app.use("/bills", require("./routes/bill.routes")); app.use("/fallback/intent", require("./routes/intent.routes")); // 404 handler app.use((req, res) => res.status(404).send({message: "Not Found"})); // Error handler (MUST BE LAST) app.use((err, req, res, next) => { console.error(err); res.status(err.status || 500).send({message: err.message || "Internal Server Error"}); }); return app; }; // Initialize roles safely const initRoles = async () => { const roles = ["user", "admin"]; for (const name of roles) { await Role.findOrCreate({where: {name}, defaults: {name}}); } }; const start = async () => { try { // Validate important env vars if (!authConfig.JWT_SECRET) { console.warn("JWT_SECRET not set. Ensure secure secret in production."); } // Sync DB (not forced in production) const doForce = config.FORCE_DB_SYNC === "true" && config.NODE_ENV !== "production"; await sequelize.authenticate(); await sequelize.sync({force: doForce}); if (doForce) console.info("Database dropped and resynced (force=true)."); await initRoles(); const app = createApp(); // Set port and listen const PORT = parseInt(config.PORT, 10) || 5000; const server = app.listen(PORT, () => { console.log(`Server listening on port ${PORT}`); }); // Graceful shutdown const shutdown = async (signal) => { console.info(`Received signal: ${signal}. Shutting down server...`); server.close(async () => { try { await sequelize.close(); console.info("Database connection closed. Exiting."); process.exit(0); } catch (err) { console.error("Error during shutdown:", err); process.exit(1); } }); // Force exit after timeout setTimeout(() => { console.error("Could not close connections in time, forcing shut down"); process.exit(1); }, 30_000); }; process.on("SIGINT", () => shutdown("SIGINT")); process.on("SIGTERM", () => shutdown("SIGTERM")); } catch (err) { console.error("Failed to start server:", err); process.exit(1); } }; start(); // Export createApp for tests module.exports = { createApp };