import { isPermission, type Permission } from '@/lib/auth/permissions' /** * Ranggefüge: Rollen, die aufeinander aufbauen. * * Eine Leiter ist eine benannte, geordnete Folge von Rollen, in der jede * Sprosse alles kann, was jede Sprosse unter ihr kann. Der Grund ist die * Pflege: ohne das trägt jede höhere Rangrolle die Rechte aller niedrigeren * noch einmal als Kopie, und jede spätere Änderung ist an mehreren Stellen * nachzuziehen - oder eben nicht, und dann laufen sie auseinander. * * Geerbt wird **alles**, was eine Rolle gewährt, nicht nur die Kästchen: * Rechte, Freigabestufe und der Türsteher. Eine höhere Sprosse darf nie * weniger wert sein als eine niedrigere, sonst wäre eine Beförderung unter * Umständen eine Herabstufung - und genau das passiert, wenn beim Befördern in * Discord die alte Rangrolle abgenommen wird, was dort üblich ist. * * Die Vererbung ist bewusst eine **Ordnung** und kein Zeiger je Rolle: über * ganzen Zahlen gibt es keinen Zyklus, den man abfangen müsste. */ /** Das, was eine Rolle gewährt - so viel, wie für die Rechnung nötig ist. */ export type LadderRung = { discordRoleId: string clearanceLevel: number permissions: readonly string[] grantsMembership: boolean ladderId: string | null ladderRank: number | null } /** Dasselbe, nachdem die Sprossen darunter eingerechnet sind. */ export type ResolvedRung = { clearanceLevel: number permissions: Set grantsMembership: boolean /** Nur das Geerbte, für die Anzeige im Adminbereich. */ inherited: Set /** Die Stufe der eigenen Zeile, falls die geerbte höher liegt. */ ownClearanceLevel: number } function leer(): ResolvedRung { return { clearanceLevel: 0, permissions: new Set(), grantsMembership: false, inherited: new Set(), ownClearanceLevel: 0, } } /** * Was eine einzelne Rolle gewährt, einschliesslich aller Sprossen unter ihr. * * `alle` muss nur die Rollen enthalten, die für diese Rechnung infrage kommen - * die Leiter selbst genügt. Rollen ohne Leiter erben nichts und werden von * keiner Leiter berührt. */ export function withInherited(rolle: LadderRung, alle: readonly LadderRung[]): ResolvedRung { const ergebnis = leer() ergebnis.clearanceLevel = rolle.clearanceLevel ergebnis.ownClearanceLevel = rolle.clearanceLevel ergebnis.grantsMembership = rolle.grantsMembership for (const wert of rolle.permissions) { if (isPermission(wert)) ergebnis.permissions.add(wert) } if (rolle.ladderId === null || rolle.ladderRank === null) return ergebnis for (const andere of alle) { if (andere.ladderId !== rolle.ladderId) continue if (andere.ladderRank === null || andere.ladderRank > rolle.ladderRank) continue if (andere.discordRoleId === rolle.discordRoleId) continue if (andere.clearanceLevel > ergebnis.clearanceLevel) { ergebnis.clearanceLevel = andere.clearanceLevel } if (andere.grantsMembership) ergebnis.grantsMembership = true for (const wert of andere.permissions) { if (!isPermission(wert)) continue if (!ergebnis.permissions.has(wert)) ergebnis.inherited.add(wert) ergebnis.permissions.add(wert) } } return ergebnis } /** * Dasselbe für einen ganzen Satz Rollen auf einmal, als Tabelle nach * Rollen-ID. Für die Adminansicht, die jede Rolle mit ihrem tatsächlichen * Ergebnis zeigen muss - sonst steht in der Vorschau etwas anderes, als der * Nutzer später bekommt. */ export function resolveAll(alle: readonly LadderRung[]): Map { const tabelle = new Map() for (const rolle of alle) { tabelle.set(rolle.discordRoleId, withInherited(rolle, alle)) } return tabelle } /** * Die höchste Sprosse je Leiter, die jemand mit diesen Rollen trägt. * * Damit lässt sich das Geerbte in **einer** weiteren Abfrage nachladen, statt * die ganze Zuordnungstabelle zu holen und in JavaScript zu filtern. */ export function highestRungPerLadder( rollen: readonly Pick[], ): Map { const hoechste = new Map() for (const rolle of rollen) { if (rolle.ladderId === null || rolle.ladderRank === null) continue const bisher = hoechste.get(rolle.ladderId) if (bisher === undefined || rolle.ladderRank > bisher) { hoechste.set(rolle.ladderId, rolle.ladderRank) } } return hoechste }