feat: stabilize API startup and lifecycle management
- Refactor server initialization to separate concerns and improve error handling. - Implement centralized environment validation using Zod. - Introduce database, Redis, and queue lifecycle management. - Add health check endpoints for liveness and readiness. - Enhance error handling middleware for better response structure. - Implement rate limiting for API endpoints. - Add request ID middleware for traceability. - Create Sequelize CLI configuration and baseline migration for schema management. - Establish CI workflow with Gitea for testing and syntax checks. - Document foundational changes and migration strategy in PHASE_0_FOUNDATION_STABILIZATION.md. - Add Docker Compose configuration for local development and testing. - Implement unit and integration tests for critical functionality.
This commit is contained in:
@@ -1,99 +1,69 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app.js
|
||||
|
||||
const express = require("express");
|
||||
const cors = require("cors");
|
||||
const helmet = require("helmet");
|
||||
const morgan = require("morgan");
|
||||
const routes = require("./app/routes");
|
||||
const cookieParser = require("cookie-parser");
|
||||
const { bullBoardRouter } = require("./app/config/bullBoard.config");
|
||||
const path = require("path");
|
||||
const startAllCrons = require("./cron");
|
||||
|
||||
const db = require("./app/models");
|
||||
|
||||
// Test DB connection and sync models
|
||||
(async () => {
|
||||
try {
|
||||
await db.sequelize.authenticate();
|
||||
console.log("Database connected.");
|
||||
|
||||
await db.sequelize.sync();
|
||||
console.log("Tables synced.");
|
||||
|
||||
// Start all cron jobs
|
||||
startAllCrons();
|
||||
|
||||
} catch (error) {
|
||||
console.error("DB error:", error);
|
||||
}
|
||||
})();
|
||||
const routes = require("./app/routes");
|
||||
const { healthRouter, live } = require("./app/routes/health.routes");
|
||||
const { bullBoardRouter } = require("./app/config/bullBoard.config");
|
||||
const { authenticate } = require("./app/middleware/auth.middleware");
|
||||
const { authorizedAccountType } = require("./app/middleware/permission.middleware");
|
||||
const requestId = require("./app/middleware/requestId.middleware");
|
||||
const { generalApiLimiter } = require("./app/middleware/rateLimit.middleware");
|
||||
const { notFound, errorHandler } = require("./app/middleware/error.middleware");
|
||||
|
||||
const app = express();
|
||||
const trustProxy = Number(process.env.TRUST_PROXY || 0);
|
||||
if (trustProxy > 0) app.set("trust proxy", trustProxy);
|
||||
|
||||
app.disable("x-powered-by");
|
||||
app.use(requestId);
|
||||
app.use(helmet({
|
||||
contentSecurityPolicy: false,
|
||||
hsts: process.env.NODE_ENV === "production" ? undefined : false,
|
||||
}));
|
||||
app.use(cookieParser());
|
||||
|
||||
// CORS configuration
|
||||
const corsOptions = {
|
||||
origin: process.env.FRONTEND_URL || "https://oceanic-demo.vercel.app",
|
||||
app.use(cors({
|
||||
origin: process.env.FRONTEND_URL,
|
||||
methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
|
||||
allowedHeaders: ["Content-Type", "Authorization"],
|
||||
allowedHeaders: ["Content-Type", "Authorization", "X-Request-ID"],
|
||||
exposedHeaders: ["X-Request-ID"],
|
||||
credentials: true,
|
||||
};
|
||||
app.use(cors(corsOptions));
|
||||
app.options(/.*/, cors(corsOptions));
|
||||
}));
|
||||
app.use(express.json({ limit: process.env.JSON_BODY_LIMIT || "1mb" }));
|
||||
app.use(express.urlencoded({ extended: false, limit: process.env.JSON_BODY_LIMIT || "1mb" }));
|
||||
|
||||
app.use(express.json());
|
||||
app.use(morgan("dev"));
|
||||
morgan.token("request-id", (req) => req.id);
|
||||
app.use(morgan(process.env.NODE_ENV === "production" ? ':remote-addr - :method :url :status :response-time ms req-id=:request-id' : "dev"));
|
||||
|
||||
app.get("/health", (req, res) => {
|
||||
let dbStatus = "N/A";
|
||||
let emailStatus = "N/A";
|
||||
let redisStatus = "N/A";
|
||||
app.get("/health", live);
|
||||
app.use("/health", healthRouter);
|
||||
|
||||
db.sequelize
|
||||
.authenticate()
|
||||
.then(() => {
|
||||
console.log("DB connection successful.");
|
||||
dbStatus = "OK";
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("DB connection error:", err);
|
||||
res.status(500).send("Internal Server Error");
|
||||
});
|
||||
// Keep the legacy path while all new clients migrate to the versioned path.
|
||||
app.use("/api", generalApiLimiter, routes);
|
||||
app.use("/api/v1", generalApiLimiter, routes);
|
||||
|
||||
emailStatus = "OK";
|
||||
|
||||
redisStatus = "OK";
|
||||
|
||||
res.send({
|
||||
status: "Online ✅",
|
||||
database: dbStatus,
|
||||
emailService: emailStatus,
|
||||
redis: redisStatus,
|
||||
});
|
||||
});
|
||||
|
||||
app.use("/api", routes);
|
||||
app.use(
|
||||
"/Documentation",
|
||||
(req, res, next) => {
|
||||
if (req.path.endsWith(".md")) {
|
||||
return res.status(403).send("Forbidden");
|
||||
}
|
||||
|
||||
next();
|
||||
},
|
||||
(req, res, next) => req.path.endsWith(".md") ? res.status(403).send("Forbidden") : next(),
|
||||
express.static(path.join(__dirname, "Documentation")),
|
||||
);
|
||||
app.use("/admin/queues", bullBoardRouter);
|
||||
|
||||
app.use(
|
||||
"/admin/queues",
|
||||
authenticate,
|
||||
authorizedAccountType(["admin", "superadmin"]),
|
||||
bullBoardRouter,
|
||||
);
|
||||
|
||||
app.use(notFound);
|
||||
app.use(errorHandler);
|
||||
|
||||
module.exports = app;
|
||||
|
||||
Reference in New Issue
Block a user