mtf-backend/src/server.js
2026-06-30 03:59:41 +00:00

137 lines
4.2 KiB
JavaScript

// 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 = () => {
redis.initializeStreams();
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 };