139 lines
4.3 KiB
JavaScript
139 lines
4.3 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 = async () => {
|
|
await redis.ensureGroup("speech:commands", `${config.DEVICE_ID}-cg`);
|
|
await redis.ensureGroup("speech:events", `${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 }; |