import { Rolle } from "@/generated/prisma/enums"; import { ROLLEN_RANG, ROLLEN_TEXT } from "@/lib/rollen-text"; /** * Wer darf was (#007). * * Die Rechte stehen bewusst als eine Tabelle an einer Stelle -- nicht als * `if (rolle === "ADMIN")` verstreut ueber Seiten, Aktionen und Services. Drei Gruende: * * 1. Eine Rechteaenderung hat genau einen Ort. Kommt spaeter eine Rolle hinzu oder darf ein * `REDAKTEUR` etwas mehr, wird eine Zeile hier geaendert und nicht ein Dutzend Abfragen * gesucht, von denen eine uebersehen wird. * 2. Die Tabelle ist pruefbar. `tests/rollen.test.ts` schreibt jede Rolle gegen jedes Recht aus; * eine versehentlich erweiterte Liste faellt sofort auf, statt erst im Betrieb. * 3. Wer eine neue Aktion baut, sucht sich hier das passende Recht und prueft es mit `darf()`. * Ein Recht kann so nie doppelt und unterschiedlich ausgelegt werden. * * Geprueft wird ausschliesslich serverseitig. Die Rolle kommt dabei pro Anfrage aus der * Datenbank, nicht aus dem Sitzungstoken -- ein Rechteentzug wirkt sonst erst nach Ablauf * des Cookies. */ export const RECHTE = { /** * Vorlagen, Laeufe, Geraete und Verzeichnis ansehen. * * **Nicht mehr „jede Rolle“** (#035): `MITARBEITER` fehlt hier, und das ist * der ganze Punkt dieser Rolle. Wer sich selbst auskunftsberechtigt anmeldet, * darf nicht die Laeufe des Hauses lesen — dafuer gibt es `selbstauskunft`. */ lesen: ["ADMIN", "REDAKTEUR", "TECHNIKER", "LESER"], /** * Die eigenen Geraete und die Laeufe dazu ansehen (#035). * * **Nur `MITARBEITER`.** Die anderen Rollen sehen ueber `lesen` ohnehin alles; * sie hier mit aufzunehmen waere ein zweiter Weg zu denselben Daten und damit * eine zweite Stelle, an der jemand die Einschraenkung auf die eigene Person * vergessen kann. Ein Techniker, der seine eigenen Geraete sehen will, nimmt * die gewoehnliche Geraeteliste. */ selbstauskunft: ["MITARBEITER"], /** Laeufe starten, abarbeiten, abschliessen und archivieren. */ laeufeBearbeiten: ["ADMIN", "REDAKTEUR", "TECHNIKER"], /** Geraete und deren Stammdaten pflegen, Tags registrieren. */ geraeteBearbeiten: ["ADMIN", "REDAKTEUR", "TECHNIKER"], /** * Mitarbeiter von Hand anlegen, bearbeiten und deaktivieren (#011). * * Dieselben Rollen wie bei den Geraeten, und aus demselben Grund: beim * Onboarding steht der Techniker vor der Person, bevor der naechste * Verzeichnisabgleich gelaufen ist. Die **Abteilungen** bleiben dagegen bei * `verwaltung` — an ihnen haengen Ziel-OU und Standardgruppen, also das, was * die spaeteren PowerShell-Befehle in ein Verzeichnis schreiben. */ mitarbeiterBearbeiten: ["ADMIN", "REDAKTEUR", "TECHNIKER"], /** Vorlagen anlegen, bearbeiten und veroeffentlichen. */ vorlagenBearbeiten: ["ADMIN", "REDAKTEUR"], /** * Einen archivierten Lauf zurueckholen. Nur `ADMIN`: das Archiv ist die Zusage, dass ein * abgeschlossener Lauf sich nicht mehr aendert -- diese Zusage darf nur eine Stelle aufheben. */ archivVerwalten: ["ADMIN"], /** App-Benutzer, Verzeichnis, Abteilungen, Einstellungen, Kunden. */ verwaltung: ["ADMIN"], } as const satisfies Record; export type Recht = keyof typeof RECHTE; export function darf(rolle: Rolle, recht: Recht): boolean { return (RECHTE[recht] as readonly Rolle[]).includes(rolle); } /** * Die staerkste der uebergebenen Rollen. * * Der OIDC-Login braucht das: ist jemand in mehreren abgebildeten Gruppen, gewinnt die * staerkere Rolle. Eine leere Liste ergibt `LESER` -- nie ein Fehler und nie `ADMIN`. * * **Das gilt weiter fuer eine eingerichtete Zuordnung ohne Treffer in dieser * Funktion.** Ob aus „keine Gruppe getroffen“ eine Selbstauskunft wird, entscheidet * seit #035 der Login ueber `rolleAusGruppenGenau` — hier bleibt der Standard * `LESER`, damit ein Aufrufer ohne diese Unterscheidung nie versehentlich mehr * bekommt als vorher. */ export function staerksteRolle(rollen: readonly Rolle[]): Rolle { /* * `undefined` als Startwert, nicht `LESER`. * * Mit `LESER` als Startwert war der Wert zugleich **Untergrenze**: eine * uebergebene Rolle unterhalb davon kam nie heraus. Solange alle Rollen * mindestens `LESER` waren, fiel das nicht auf — mit `MITARBEITER` (#035) gab * `staerksteRolle(["MITARBEITER"])` ploetzlich `LESER`, also **mehr** Rechte * als uebergeben. Der Test „gibt bei nur diese Rolle zurueck“ stand * seit #007 da und hat es sofort gezeigt. * * Der Rueckfall auf `LESER` gilt jetzt nur noch fuer die **leere** Liste, und * genau so steht es auch im Kommentar darueber. */ let staerkste: Rolle | undefined; for (const rolle of rollen) { if (staerkste === undefined || ROLLEN_RANG[rolle] > ROLLEN_RANG[staerkste]) { staerkste = rolle; } } return staerkste ?? Rolle.LESER; } /** * Wo diese Rolle nach der Anmeldung landet (#035). * * **Gefunden beim Durchklicken:** ein `MITARBEITER` meldete sich an und sah * „Dafuer fehlt dir die Berechtigung“ — das Standardziel ist `/`, und die * Uebersicht verlangt `lesen`. Genau die leere Seite, die wie ein Fehler * aussieht und die #035 vermeiden will. * * Eine reine Funktion und nicht ein `if` in der Seite: dieselbe Antwort braucht * die Uebersicht (zum Umleiten), `/kein-zugriff` (fuer seinen Knopf — der auf * `/` zeigte und damit im Kreis) und spaeter jede weitere Rolle mit eigenem * Einstieg. */ export function startseite(rolle: Rolle): string { if (darf(rolle, "lesen")) return "/"; if (darf(rolle, "selbstauskunft")) return "/meine-geraete"; /* * Kein Recht fuer irgendeinen Einstieg. Gibt es heute nicht — jede Rolle hat * entweder `lesen` oder `selbstauskunft` —, und `/` ist hier die ehrlichere * Antwort als ein Wurf: dort erklaert die Seite, was los ist. */ return "/"; } // Beschriftung und Rang liegen in src/lib, damit Client-Komponenten sie ohne // die Rechtetabelle bekommen. Hier weitergereicht fuer serverseitige Aufrufer. export { ROLLEN_RANG, ROLLEN_TEXT };