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>
This commit is contained in:
Grovy311 2026-10-15 09:40:33 +02:00
parent 67476ea59b
commit a7a1595cbf
38 changed files with 5852 additions and 442 deletions

View file

@ -1,83 +1,14 @@
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 { 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 = express();
const app = createApp();
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);