import express from "express"; import { globalDeviceRouter } from "./routes/global-device.routes.js"; import { projectDeviceRouter } from "./routes/project-device.routes.js"; import { documentBrandingRouter } from "./routes/document-branding.routes.js"; import { projectRouter } from "./routes/project.routes.js"; import { cableSizingRouter } from "./routes/cable-sizing.routes.js"; import { authRouter } from "./routes/auth.routes.js"; import { userRouter } from "./routes/user.routes.js"; import { errorMiddleware } from "./middleware/error.middleware.js"; import { createAuthGuard, requirePasswordChanged, requireSameOriginForWrites, } from "./auth/auth.middleware.js"; import { authConfig, sessionStore, userService } from "./composition/auth.js"; import { userRepository } from "./composition/application-repositories.js"; import { createLogger, toErrorMeta } from "../shared/logging/logger.js"; import { closeDefaultDatabase } from "../db/client.js"; const logger = createLogger("api"); const app = express(); const port = Number(process.env.PORT || 3000); const heartbeatIntervalMs = 5 * 60 * 1000; const shutdownTimeoutMs = 15 * 1000; app.disable("x-powered-by"); app.use(express.json({ limit: "25mb" })); app.use((req, res, next) => { if (req.path === "/health") { next(); return; } const startedAt = Date.now(); logger.debug("request started", { method: req.method, path: req.originalUrl }); res.on("finish", () => { const meta = { method: req.method, path: req.originalUrl, status: res.statusCode, durationMs: Date.now() - startedAt, }; if (res.statusCode >= 500) logger.error("request completed", meta); else if (res.statusCode >= 400) logger.warn("request completed", meta); else logger.info("request completed", meta); }); res.on("close", () => { if (!res.writableEnded) { logger.warn("request aborted before response finished", { method: req.method, path: req.originalUrl, durationMs: Date.now() - startedAt, }); } }); next(); }); app.get("/health", (_req, res) => { res.json({ ok: true }); }); // Everything below /api requires a signed-in, active user, except // /auth/login, /auth/status and /auth/logout (see createAuthGuard's // openPaths). authRouter is mounted AFTER the guard (not before) so that // /auth/me and /auth/change-password still get req.authUser populated by it. app.use("/api", requireSameOriginForWrites()); app.use("/api", createAuthGuard(authConfig, sessionStore, userRepository)); app.use("/api", requirePasswordChanged()); app.use("/api/auth", authRouter); app.use("/api/users", userRouter); app.use("/api/cable-sizing", cableSizingRouter); app.use("/api/projects", projectRouter); app.use("/api/global-devices", globalDeviceRouter); app.use("/api/project-devices", projectDeviceRouter); app.use("/api/document-branding", documentBrandingRouter); app.use(errorMiddleware); process.on("uncaughtException", (error) => { logger.error("uncaught exception, exiting", toErrorMeta(error)); process.exit(1); }); process.on("unhandledRejection", (reason) => { logger.error("unhandled rejection, exiting", toErrorMeta(reason)); process.exit(1); }); setInterval(() => { const memory = process.memoryUsage(); logger.verbose("heartbeat", { uptimeSeconds: Math.round(process.uptime()), rssMb: Math.round(memory.rss / 1024 / 1024), heapUsedMb: Math.round(memory.heapUsed / 1024 / 1024), }); }, heartbeatIntervalMs).unref(); userService .ensureBootstrapAdmin() .catch((error) => { logger.error("bootstrap admin setup failed", toErrorMeta(error)); process.exit(1); }) .then(() => { const server = app.listen(port, () => { logger.info("server started", { port }); }); let shuttingDown = false; function shutdown(signal: NodeJS.Signals): void { if (shuttingDown) return; shuttingDown = true; logger.info("shutting down", { signal }); // Falls ein Request haengt oder ein Socket nicht freigegeben wird, soll der // Prozess trotzdem enden, statt auf das SIGKILL des Containers zu warten. const forceExit = setTimeout(() => { logger.error("shutdown timed out, forcing exit"); process.exit(1); }, shutdownTimeoutMs); forceExit.unref(); server.close((error) => { if (error) logger.error("http server close failed", toErrorMeta(error)); try { closeDefaultDatabase(); } catch (closeError) { logger.error("database close failed", toErrorMeta(closeError)); } logger.info("shutdown complete"); process.exit(error ? 1 : 0); }); // Ohne das warten offene Keep-Alive-Verbindungen (z. B. die des // Next.js-Servers) den Timeout oben aus; laufende Requests laufen weiter. server.closeIdleConnections(); } process.on("SIGTERM", shutdown); process.on("SIGINT", shutdown); });