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

@ -0,0 +1,55 @@
import { randomUUID } from "node:crypto";
interface Session {
id: string;
userId: string;
expiresAt: number;
}
/**
* In-memory session store. The API runs as a single container/process, so a
* restart clearing every session is an acceptable trade-off for the
* simplicity of not needing a persisted session table.
*/
export class SessionStore {
private readonly sessions = new Map<string, Session>();
constructor(private readonly ttlMs: number) {}
create(userId: string): Session {
const session: Session = {
id: randomUUID(),
userId,
expiresAt: Date.now() + this.ttlMs,
};
this.sessions.set(session.id, session);
return session;
}
/** Returns the session and slides its expiry forward, or undefined if gone/expired. */
get(sessionId: string | undefined, slideExpiry = true): Session | undefined {
if (!sessionId) return undefined;
const session = this.sessions.get(sessionId);
if (!session) return undefined;
if (session.expiresAt < Date.now()) {
this.sessions.delete(sessionId);
return undefined;
}
if (slideExpiry) session.expiresAt = Date.now() + this.ttlMs;
return session;
}
destroy(sessionId: string | undefined): void {
if (!sessionId) return;
this.sessions.delete(sessionId);
}
/** Ends every other session for a user, e.g. after a password change. */
destroyAllForUser(userId: string, exceptSessionId?: string): void {
for (const [id, session] of this.sessions) {
if (session.userId === userId && id !== exceptSessionId) {
this.sessions.delete(id);
}
}
}
}