forked from jappel/leistungsbilanz-ts
- 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>
91 lines
3.3 KiB
TypeScript
91 lines
3.3 KiB
TypeScript
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 } from "./composition/auth.js";
|
|
import { userRepository } from "./composition/application-repositories.js";
|
|
import { createLogger } from "../shared/logging/logger.js";
|
|
|
|
const logger = createLogger("api");
|
|
|
|
/**
|
|
* Builds the Express app without listening, so tests (for example the
|
|
* security baseline under tests/security) can start it on a free port.
|
|
* Process concerns (listen, signals, bootstrap admin) stay in index.ts.
|
|
*/
|
|
export function createApp(): express.Express {
|
|
const app = express();
|
|
|
|
app.disable("x-powered-by");
|
|
// The web container sets the same header for pages (next.config.mjs); the
|
|
// API sets it itself so it also holds when the API is reached directly.
|
|
app.use((_req, res, next) => {
|
|
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
next();
|
|
});
|
|
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);
|
|
|
|
return app;
|
|
}
|