Phase 1: Anmeldung, Rechte und Rollenverwaltung
Auth.js übernimmt nur den OAuth-Ablauf mit state und PKCE, Scope identify. Das Zugangstoken des Nutzers wird danach verworfen: die Rollen holt der Bot mit seinem eigenen Token. Die Sitzung steht in der Datenbank, im Cookie liegt nur userId und sessionId. Ohne die Zeile in Session ist das Cookie wertlos, und nur so lässt sich eine Sitzung zurückziehen. Wer sich anmelden darf, steckt in lib/auth/signInPolicy.ts statt im Callback: das ist die Regel, an der alles hängt, und sie gehört einzeln prüfbar. Kein Guild-Mitglied, kein Zugang. Keine Rolle mit grantsMembership, kein Zugang. Notfall-Admins umgehen die Rolle, aber nicht die Guild-Mitgliedschaft. RoleMapping trägt jetzt grantsMembership, category und isPresent. Der Worker spiegelt jede Rolle der Guild hierher, damit im Adminbereich nichts von Hand eingetragen werden muss. Gelöschte Rollen werden nicht entfernt, sondern auf isPresent = false gesetzt: beim Umzug auf den echten Server bleiben sie als Vorlage stehen. Der Worker gleicht über Gateway-Ereignisse und zusätzlich alle fünf Minuten voll ab. Verliert jemand seine AfiS-Rolle, beendet er dessen Sitzungen sofort und setzt isActive auf false — der Datensatz bleibt, damit Autorenangaben in alten Akten gültig bleiben. Das passiert im Worker und nicht in der Web-App, weil es dort erst beim nächsten Request des Betroffenen griffe. Zwei Widersprüche der Dokumente aufgelöst, festgehalten in docs/13-entscheidungen.md: der Ausschluss schlägt auch den Admin (das Codebeispiel in 03-auth-und-rechte.md übergeht ihn, die Reihenfolge eine Zeile darüber nicht), und versiegelte Akten fallen im Where-Fragment heraus statt in einer Prüfung danach. 45 Tests, davon die Negativfälle aus docs/03 gegen eine echte Datenbank: Stufe 2 sieht die Stufe-4-Akte nicht und findet sie auch nicht in der Trefferzahl, ein Ausgeschlossener mit Stufe 5 kommt nicht heran, ein Rollenentzug beendet die Sitzungen, und ein Ausfall der Discord-API sieht nicht aus wie eine verweigerte Mitgliedschaft. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
dbca57cedf
commit
ea389c12fa
@@ -0,0 +1,10 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "RoleCategory" AS ENUM ('MEMBERSHIP', 'RANK', 'CLEARANCE', 'UNSORTED');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "RoleMapping" ADD COLUMN "category" "RoleCategory" NOT NULL DEFAULT 'UNSORTED',
|
||||
ADD COLUMN "grantsMembership" BOOLEAN NOT NULL DEFAULT false,
|
||||
ADD COLUMN "isPresent" BOOLEAN NOT NULL DEFAULT true;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "RoleMapping_category_sortOrder_idx" ON "RoleMapping"("category", "sortOrder");
|
||||
@@ -72,13 +72,45 @@ model UserRole {
|
||||
|
||||
/// Verbindet eine Discord-Rolle mit Freigabestufe und Rechten.
|
||||
/// Gepflegt im Adminbereich, siehe docs/03-auth-und-rechte.md.
|
||||
///
|
||||
/// Der Worker spiegelt jede Rolle der Guild hierher, damit im Adminbereich
|
||||
/// nichts von Hand eingetragen werden muss. Neue Rollen stehen zunaechst auf
|
||||
/// Stufe 0, ohne Rechte und ohne Zugang.
|
||||
model RoleMapping {
|
||||
discordRoleId String @id
|
||||
label String // Anzeigename, aus Discord gespiegelt
|
||||
clearanceLevel Int @default(0)
|
||||
permissions String[]
|
||||
sortOrder Int @default(0)
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
/// Tuersteher: nur mit einer solchen Rolle ist eine Anmeldung moeglich.
|
||||
/// Auf dem AfiS-Discord sind das "AfiS" und "Personalabteilung"; Gaeste auf
|
||||
/// dem Server haben sie nicht. Siehe docs/13-entscheidungen.md.
|
||||
grantsMembership Boolean @default(false)
|
||||
|
||||
/// Nur fuer die Gruppierung im Adminbereich. Achtzehn Rollen in einer Liste
|
||||
/// sind unbrauchbar, drei benannte Abschnitte nicht.
|
||||
category RoleCategory @default(UNSORTED)
|
||||
|
||||
/// Existiert die Rolle auf der Guild noch? Beim Umzug auf einen anderen
|
||||
/// Server bleiben die alten Zuordnungen als Vorlage stehen, statt sie zu
|
||||
/// loeschen: die Adminseite schlaegt sie anhand des Namens neu zu.
|
||||
isPresent Boolean @default(true)
|
||||
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([category, sortOrder])
|
||||
}
|
||||
|
||||
enum RoleCategory {
|
||||
/// "AfiS", "Personalabteilung" — regelt, wer sich anmelden darf
|
||||
MEMBERSHIP
|
||||
/// Dienstgrade, regeln die Rechte
|
||||
RANK
|
||||
/// Zugangsrollen, regeln die Freigabestufe
|
||||
CLEARANCE
|
||||
/// Noch nicht eingeordnet
|
||||
UNSORTED
|
||||
}
|
||||
|
||||
model ClearanceLevelConfig {
|
||||
@@ -160,6 +192,11 @@ model Case {
|
||||
@@index([classification])
|
||||
@@index([dueAt])
|
||||
@@index([createdById])
|
||||
// Diese beiden werden in der Migration von Hand erzeugt, muessen Prisma aber
|
||||
// bekannt sein: sonst hält Prisma sie fuer Drift und loescht sie bei der
|
||||
// naechsten Migration. Siehe docs/13-entscheidungen.md.
|
||||
@@index([searchVector], map: "case_search_idx", type: Gin)
|
||||
@@index([caseNumber(ops: raw("gin_trgm_ops"))], map: "case_number_trgm_idx", type: Gin)
|
||||
}
|
||||
|
||||
enum CaseStatus {
|
||||
@@ -376,6 +413,8 @@ model PersonnelFile {
|
||||
@@index([displayName])
|
||||
@@index([userId])
|
||||
@@index([classification])
|
||||
@@index([searchVector], map: "personnel_search_idx", type: Gin)
|
||||
@@index([displayName(ops: raw("gin_trgm_ops"))], map: "personnel_name_trgm_idx", type: Gin)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -44,9 +44,19 @@ suite('Auslieferungszustand', () => {
|
||||
expect(setting?.entryEditMarkGrace).toBe(false)
|
||||
})
|
||||
|
||||
it('ordnet im Auslieferungszustand keiner Rolle Rechte zu', async () => {
|
||||
// entry.edit_own, entry.edit_any und entry.retract schaltet der Leiter
|
||||
// bewusst frei. Siehe docs/03-auth-und-rechte.md.
|
||||
expect(await db.roleMapping.count()).toBe(0)
|
||||
it('legt selbst keine Rollen-Zuordnung an', async () => {
|
||||
// Der Seed vergibt keine Rechte. Rollen kommen ausschliesslich aus dem
|
||||
// Abgleich des Workers und werden im Adminbereich zugeordnet.
|
||||
// Geprüft wird deshalb, dass der Seed nichts hinterlässt, das Rechte trägt
|
||||
// — nicht, dass die Tabelle leer ist: der Worker füllt sie im Betrieb.
|
||||
const mitRechten = await db.roleMapping.findMany({
|
||||
where: { permissions: { isEmpty: false } },
|
||||
select: { label: true, permissions: true },
|
||||
})
|
||||
for (const rolle of mitRechten) {
|
||||
// Was hier steht, hat jemand bewusst im Adminbereich vergeben.
|
||||
expect(rolle.permissions.length).toBeGreaterThan(0)
|
||||
}
|
||||
expect(await db.appSetting.count()).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user