Add login-required user management (admin/user roles, Config->Benutzer)

No self-signup: admins create every account in Config -> Benutzer with a
typed start password; a forced password change gates first use. Session-
cookie auth protects all /api/* routes; /api/users additionally requires
the admin role. See docs/authentication.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Grovy311 2026-09-28 19:51:47 +02:00
parent b50c00c0ac
commit c56cfb30ad
36 changed files with 4928 additions and 38 deletions

View file

@ -4,7 +4,12 @@ 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 } 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";
@ -50,6 +55,15 @@ 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", 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);
@ -77,40 +91,48 @@ setInterval(() => {
});
}, heartbeatIntervalMs).unref();
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");
userService
.ensureBootstrapAdmin()
.catch((error) => {
logger.error("bootstrap admin setup failed", toErrorMeta(error));
process.exit(1);
}, shutdownTimeoutMs);
forceExit.unref();
})
.then(() => {
const server = app.listen(port, () => {
logger.info("server started", { port });
});
server.close((error) => {
if (error) logger.error("http server close failed", toErrorMeta(error));
try {
closeDefaultDatabase();
} catch (closeError) {
logger.error("database close failed", toErrorMeta(closeError));
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();
}
logger.info("shutdown complete");
process.exit(error ? 1 : 0);
process.on("SIGTERM", shutdown);
process.on("SIGINT", shutdown);
});
// 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);