Production readiness hardening and ops tooling
This commit is contained in:
84
src/app.ts
84
src/app.ts
@ -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",
|
||||
|
||||
Reference in New Issue
Block a user