feat(auth): Rollen, Benutzerverwaltung und Passkeys (#027, #028)

Drei Rollen: Administrator, Techniker, Kunde. Jede der 28 Aktionen nennt das
Recht, das sie verlangt; die Rechte stehen an einer Stelle. Kundenkonten
haengen an genau einem Kunden und sehen nur dessen Hosts und Rechnungen -
ohne Einkaufspreise und Marge. Die Einschraenkung sitzt in den Abfragen
selbst, damit keine Seite vergessen kann, sie weiterzugeben; sechs Tests
pruefen das.

Dabei gefunden: Neu ueber OIDC angelegte Konten haetten die Vorgabe des
Datenmodells geerbt - ADMIN. Jetzt gibt es dafuer eine Einstellung mit der
Vorgabe Techniker.

Passkeys (#028) als gleichwertiger, passwortloser Anmeldeweg neben Passwort
und OIDC. Mehrere je Konto, Aufgabe in einem kurzlebigen Cookie, rpID aus der
oeffentlichen Adresse. Ohne HTTPS sagt die Seite es, statt still zu scheitern.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-19 00:46:29 +02:00
co-authored by Claude Opus 5
parent 7d8e5c9172
commit 2f45e33e20
66 changed files with 2201 additions and 114 deletions
+6 -6
View File
@@ -49,7 +49,7 @@ async function ausfuehren(arbeit: () => Promise<string | void>): Promise<Aktions
}
export async function laufzeitBestaetigenAktion(formular: FormData) {
await sitzungErforderlich();
await sitzungErforderlich("fachlichSchreiben");
const id = formular.get("laufzeitId")?.toString();
if (!id) return;
await laufzeitBestaetigen(id);
@@ -57,7 +57,7 @@ export async function laufzeitBestaetigenAktion(formular: FormData) {
}
export async function laufzeitVerwerfenAktion(formular: FormData) {
await sitzungErforderlich();
await sitzungErforderlich("fachlichSchreiben");
const id = formular.get("laufzeitId")?.toString();
if (!id) return;
await laufzeitVerwerfen(id);
@@ -66,7 +66,7 @@ export async function laufzeitVerwerfenAktion(formular: FormData) {
/** Häkchen aus der Rechnungstabelle. */
export async function statusUmschalten(formular: FormData): Promise<void> {
await sitzungErforderlich();
await sitzungErforderlich("fachlichSchreiben");
const laufzeitId = formular.get("laufzeitId")?.toString();
const feld = formular.get("feld")?.toString();
const wert = formular.get("wert")?.toString() === "true";
@@ -88,7 +88,7 @@ export async function proxmoxRechnungSpeichern(
_vorher: AktionsErgebnis,
formular: FormData,
): Promise<AktionsErgebnis> {
await sitzungErforderlich();
await sitzungErforderlich("fachlichSchreiben");
const laufzeitId = formular.get("laufzeitId")?.toString();
if (!laufzeitId) return { fehler: "Laufzeit fehlt." };
@@ -126,7 +126,7 @@ export async function kundenrechnungSpeichern(
_vorher: AktionsErgebnis,
formular: FormData,
): Promise<AktionsErgebnis> {
await sitzungErforderlich();
await sitzungErforderlich("fachlichSchreiben");
const laufzeitId = formular.get("laufzeitId")?.toString();
if (!laufzeitId) return { fehler: "Laufzeit fehlt." };
@@ -157,7 +157,7 @@ export async function laufzeitAnlegenAktion(
_vorher: AktionsErgebnis,
formular: FormData,
): Promise<AktionsErgebnis> {
await sitzungErforderlich();
await sitzungErforderlich("fachlichSchreiben");
const hostId = formular.get("hostId")?.toString();
const key = formular.get("key")?.toString().trim();
const start = datum(formular.get("start"));