leistungsbilanz-ts/src/server/index.ts
Grovy311 a7a1595cbf Add test and security baseline
- make quick/check/mutation from cj/security-baseline (Makefile unchanged,
  settings in baseline.mk), pre-commit, gitleaks, Semgrep rules, ESLint
  security rules, Forgejo workflow
- tests/security: generic baseline tests plus app config and role tests
  against createApp() with an in-memory database
- extract createApp() from src/server/index.ts; LB_DATABASE_PATH for tests
- fix gaps found by the tests: reject unknown fields on project create,
  answer malformed JSON with 400 instead of 500, set nosniff on the API
- npm audit fix (next 16.4.1 and others, no high/critical left)
- clear ESLint errors, safeFileName without control-character regex
- per-project ownership stays an open, documented gap (idorGap -> TODO)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 09:40:33 +02:00

75 lines
2.4 KiB
TypeScript

import { createApp } from "./app.js";
import { userService } from "./composition/auth.js";
import { createLogger, toErrorMeta } from "../shared/logging/logger.js";
import { closeDefaultDatabase } from "../db/client.js";
const logger = createLogger("api");
const app = createApp();
const port = Number(process.env.PORT || 3000);
const heartbeatIntervalMs = 5 * 60 * 1000;
const shutdownTimeoutMs = 15 * 1000;
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);
});