Production readiness hardening and ops tooling

This commit is contained in:
2026-05-29 10:10:12 +07:00
parent e0b8f9af9a
commit 648e77cee9
68 changed files with 12222 additions and 848 deletions

View File

@ -1,22 +1,45 @@
import express from "express";
import helmet from "helmet";
import morgan from "morgan";
import { env } from "./config/env";
import { requestContext } from "./shared/middleware/requestContext";
import { requestLogging } from "./shared/middleware/requestLogging";
import { rateLimit } from "./shared/middleware/rateLimit";
import { handleErrors, successResponse } from "./shared/middleware/errorMiddleware";
import { NextFunction, Request, Response } from "express";
import adminRoutes from "./routes/admin";
import integrationRoutes from "./routes/integrations";
import deviceRoutes from "./routes/device";
import merchantRoutes from "./routes/merchant";
import { startNotificationOrchestrator } from "./shared/orchestrators/notificationOrchestrator";
import { startDynamicQrExpiryScheduler } from "./shared/services/dynamicQrExpiryScheduler";
import { startExportJobWorker } from "./shared/services/exportJobWorker";
import { startMqttSubscriber } from "./shared/services/mqttSubscriber";
import { getDatabaseHealth } from "./shared/services/health";
import path from "node:path";
import fs from "node:fs";
const app = express();
if (env.TRUST_PROXY === "true") {
app.set("trust proxy", 1);
}
startNotificationOrchestrator();
startDynamicQrExpiryScheduler();
startExportJobWorker();
startMqttSubscriber();
app.use(
helmet({
hidePoweredBy: true,
referrerPolicy: {
policy: "no-referrer"
},
hsts:
process.env.NODE_ENV === "production"
? {
maxAge: 15552000,
includeSubDomains: true
}
: false,
crossOriginResourcePolicy: {
policy: "cross-origin"
},
@ -35,14 +58,40 @@ app.use(
}
})
);
app.use(express.json());
app.use(morgan("dev"));
app.use(express.json({ limit: env.JSON_BODY_LIMIT }));
app.use(requestContext);
app.use(requestLogging);
const loginLimiter = rateLimit({
name: "login",
windowMs: env.RATE_LIMIT_LOGIN_WINDOW_MS,
max: env.RATE_LIMIT_LOGIN_MAX
});
const deviceLimiter = rateLimit({
name: "device",
windowMs: env.RATE_LIMIT_DEVICE_WINDOW_MS,
max: env.RATE_LIMIT_DEVICE_MAX,
key: (req) => `${req.ip}:${req.header("x-device-id") || "legacy"}`
});
const adminWriteLimiter = rateLimit({
name: "admin_write",
windowMs: env.RATE_LIMIT_ADMIN_WRITE_WINDOW_MS,
max: env.RATE_LIMIT_ADMIN_WRITE_MAX,
key: (req) => `${req.ip}:${req.header("authorization") || "anonymous"}`
});
app.get("/", (_req, res) => {
res.json(successResponse(_req, { status: "ok" }));
});
app.get("/favicon.ico", (_req, res) => {
res
.type("image/svg+xml")
.send(
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="12" fill="#004ac6"/><path fill="#fff" d="M16 17h12v12H16V17Zm4 4v4h4v-4h-4Zm16-4h12v12H36V17Zm4 4v4h4v-4h-4ZM16 35h12v12H16V35Zm4 4v4h4v-4h-4Zm18-4h4v4h-4v-4Zm6 0h4v10h-4V35Zm-10 6h8v4h-8v-4Zm0 8h4v4h-4v-4Zm8 0h10v4H42v-4Z"/></svg>`
);
});
function resolveUiPageFile(slug: string) {
const workspaceRoot = process.cwd();
const candidates = [
@ -74,7 +123,22 @@ app.get("/ui/:page", (req, res, next) => {
res.sendFile(filePath);
});
app.use("/admin/login", loginLimiter);
app.use("/merchant/login", loginLimiter);
app.use("/admin", (req, res, next) => {
if (req.path === "/login") {
return next();
}
if (["POST", "PATCH", "PUT", "DELETE"].includes(req.method)) {
return adminWriteLimiter(req, res, next);
}
return next();
});
app.use("/device", deviceLimiter);
app.use("/integrations", rateLimit({ name: "integrations", windowMs: env.RATE_LIMIT_DEVICE_WINDOW_MS, max: env.RATE_LIMIT_DEVICE_MAX }));
app.use("/admin", adminRoutes);
app.use("/merchant", merchantRoutes);
app.use("/integrations", integrationRoutes);
app.use("/device", deviceRoutes);
@ -91,6 +155,20 @@ app.get("/health", (req, res) => {
);
});
app.get("/health/deep", async (req, res) => {
const database = await getDatabaseHealth();
const healthy = database.status === "ok";
res.status(healthy ? 200 : 503).json(
successResponse(req, {
status: healthy ? "healthy" : "degraded",
time: new Date().toISOString(),
checks: {
database
}
})
);
});
app.use((req, res) => {
res.status(404).json({
code: "NOT_FOUND",