# Authentifizierung und Berechtigungen ## Grundsatz **OAuth klärt, wer jemand ist. Der Bot klärt, was er darf.** Das Discord-Zugangstoken des Nutzers wird nach dem Callback verworfen und nie wieder benutzt. ## Login-Ablauf 1. Nutzer klickt „Mit Discord anmelden". Auth.js startet OAuth2 mit Scope `identify`, dazu `state` und PKCE. 2. Im Callback liest die App nur `id`, `username`, `global_name`, `avatar`. 3. Die App fragt **serverseitig mit dem Bot-Token** `GET /guilds/{GUILD_ID}/members/{userId}`. - 404 → Login abgelehnt, Seite „Du bist nicht auf dem AfiS-Discord". Kein Konto anlegen. - 200 → Rollen werden übernommen, `User` wird angelegt oder aktualisiert. 4. Eigene Session in der Datenbank, Cookie mit `HttpOnly`, `Secure`, `SameSite=Lax`, Laufzeit 30 Tage, gleitende Verlängerung. 5. In der Session steht nur `userId` und `sessionId`. Rechte werden pro Request aufgelöst. Im Developer Portal muss das **Server Members Intent** aktiviert sein, sonst liefert der Bot keine Rollen. ## Rechteauflösung ```ts type EffectivePermissions = { userId: string clearanceLevel: number // höchste Stufe aller Rollen permissions: Set // Vereinigung aller Rollen isAdmin: boolean // aus BOOTSTRAP_ADMIN_IDS oder Recht 'admin' } ``` Auflösung aus `UserRole` × `RoleMapping`. Ergebnis wird pro Request einmal berechnet und in einem prozesslokalen LRU-Cache (60 s TTL) gehalten. Der Cache wird bei `pg_notify` `member_changed` oder `role_mapping_changed` sofort verworfen. Rechte-Enum: ``` case.read case.create case.edit case.close case.reopen case.delete case.edit_closed # Änderungen an abgeschlossenen Fällen entry.create entry.correct entry.edit_own # eigene Einträge nachträglich ändern entry.edit_any # fremde Einträge ändern, für die Leitung entry.retract # Eintrag zurückziehen (kein Löschen) personnel.read personnel.edit template.manage roles.manage audit.read export.approve admin `entry.edit_own`, `entry.edit_any` und `entry.retract` sind im Auslieferungszustand **keiner Rolle zugeordnet**. Der Leiter schaltet sie bewusst frei, wenn er sie will. ``` ## Notfallzugang `BOOTSTRAP_ADMIN_IDS` in der `.env` enthält eine oder zwei Discord-IDs. Diese Nutzer haben immer Stufe 5 und alle Rechte, unabhängig vom Rollen-Mapping. Ohne das sperrt man sich beim ersten Fehler in der Rollenverwaltung selbst aus. ## Zugriffsprüfung auf Akten Reihenfolge, erste zutreffende Regel gewinnt: 1. Nutzer steht in `CaseExclusion` → **kein Zugriff**, egal welche Stufe. Deckt den Fall ab, dass gegen einen hochrangigen Mitarbeiter ermittelt wird. 2. Nutzer ist Admin → Zugriff, wird protokolliert. 3. Nutzer ist Beteiligter (`CaseParticipant`) → Zugriff. 4. `user.clearanceLevel >= case.classification` → Zugriff. 5. Sonst kein Zugriff. Kein Zugriff bedeutet: - Ist die Stufe als `hiddenBelow` markiert: **404**, die Akte existiert für den Nutzer nicht. - Sonst: Eintrag in Listen sichtbar, Titel und Inhalt gesperrt, Detailseite zeigt Sperrhinweis. ## Umsetzung in Prisma Die Prüfung wird nie in JavaScript nachgelagert. `lib/access/caseFilter.ts` liefert ein wiederverwendbares Where-Fragment: ```ts export function visibleCaseWhere(p: EffectivePermissions): Prisma.CaseWhereInput { if (p.isAdmin) return {} return { AND: [ { exclusions: { none: { userId: p.userId } } }, { OR: [ { participants: { some: { userId: p.userId } } }, { classification: { lte: p.clearanceLevel } }, ], }, ], } } ``` Dasselbe Muster für `CaseEntry`, `Attachment`, `PersonnelFile` und die Suche. Trefferzahlen und Paginierung laufen über dieselbe Abfrage, damit sich aus Lücken nichts ableiten lässt. Für Abschnitte und Schwärzungen gibt es eine zweite Ebene: `CaseSection.minClearance`. Felder oberhalb der Nutzerstufe werden serverseitig durch einen Platzhalter ersetzt, **bevor** die Daten den Server verlassen. Nie im Client ausblenden. ## Sessions beenden Der Worker meldet `guildMemberRemove` oder den Verlust einer AfiS-Rolle. Die App löscht dann alle Sessions des Nutzers und markiert `User.isActive = false`. Der Datensatz bleibt, damit Autorenangaben in alten Akten gültig bleiben. ## Tests, die es geben muss - Nutzer ohne Guild-Mitgliedschaft kann sich nicht anmelden - Nutzer mit Stufe 2 bekommt für eine Stufe-4-Akte 404, nicht 403 - Ausgeschlossener Nutzer mit Stufe 5 bekommt 404 - Suche mit Stufe 1 liefert für eine Stufe-3-Akte keinen Treffer und keine veränderte Trefferzahl - Rollenentzug in Discord beendet laufende Sessions innerhalb von 5 Sekunden - Nutzer ohne `entry.edit_any` kann einen fremden Eintrag nicht ändern, auch nicht über einen direkten Aufruf der Server Action - Nutzer ohne `entry.edit_own` kann nach Ablauf der Karenzzeit den eigenen Eintrag nicht ändern - Ein Eintrag, dessen `minClearance` erhöht wurde, ist auch in der Versionshistorie für niedrigere Stufen nicht lesbar - Ein `DELETE` auf `CaseEntry` verändert nichts, weder über Prisma noch über rohes SQL - Autor und `createdAt` bleiben nach einer Änderung unverändert