128 lines
4.0 KiB
JavaScript
128 lines
4.0 KiB
JavaScript
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 { sequelize, Sequelize } = require('./models');
|
|
const db = require("./models");
|
|
const Role = db.role;
|
|
|
|
const createApp = () => {
|
|
const app = express();
|
|
|
|
// Security headers
|
|
app.use(helmet());
|
|
|
|
// CORS restrict in production (set ALLOWED_ORIGINS env to comma-separated list)
|
|
const corsOptions = {
|
|
origin: process.env.NODE_ENV === "production"
|
|
? (process.env.ALLOWED_ORIGINS || "").split(",").map(s => s.trim())
|
|
: true,
|
|
optionsSuccessStatus: 200
|
|
};
|
|
app.use(cors(corsOptions));
|
|
|
|
// Logging verbose in dev
|
|
app.use(morgan(process.env.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 (process.env.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 (!process.env.JWT_SECRET) {
|
|
console.warn("JWT_SECRET not set. Ensure secure secret in production.");
|
|
}
|
|
|
|
// Sync DB (not forced in production)
|
|
const doForce = process.env.FORCE_DB_SYNC === "true" && process.env.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(process.env.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 }; |