import { db } from '@/lib/db' import { getEnv } from '@/lib/env' import { TtlCache } from '@/lib/auth/cache' import { PERMISSIONS, isPermission, type EffectivePermissions, type Permission, } from '@/lib/auth/permissions' /** Höchste Freigabestufe. */ const MAX_CLEARANCE = 5 /** * 60 Sekunden Verfallszeit als Sicherheitsnetz. Der reguläre Weg ist die * sofortige Verwerfung bei `member_changed` oder `role_mapping_changed`. */ const cache = new TtlCache(60_000, 200) /** Wird vom LISTEN-Client aufgerufen, siehe lib/realtime/listener.ts. */ export function invalidateUser(userId: string): void { cache.delete(userId) } /** Eine Änderung am Rollen-Mapping betrifft potenziell jeden. */ export function invalidateAll(): void { cache.clear() } function bootstrapAdmin(userId: string): EffectivePermissions { return { userId, clearanceLevel: MAX_CLEARANCE, permissions: new Set(PERMISSIONS), isAdmin: true, hasMembership: true, } } /** * Löst die Rechte eines Nutzers aus `UserRole` × `RoleMapping` auf: * höchste Stufe aller Rollen, Vereinigung aller Rechte. * * `hasMembership` ist eine Ergänzung gegenüber docs/03-auth-und-rechte.md und * bildet den Türsteher ab: auf dem AfiS-Discord sind auch Gäste, die keine * Akte sehen dürfen. Siehe docs/13-entscheidungen.md. * * Notfall-Admins aus `BOOTSTRAP_ADMIN_IDS` bekommen immer Stufe 5 und alle * Rechte, ohne Datenbankabfrage. Ohne das sperrt man sich beim ersten Fehler * in der Rollenverwaltung selbst aus. */ export async function resolvePermissions(userId: string): Promise { const cached = cache.get(userId) if (cached) return cached if (getEnv().BOOTSTRAP_ADMIN_IDS.includes(userId)) { const resolved = bootstrapAdmin(userId) cache.set(userId, resolved) return resolved } // Eine Abfrage, kein Nachfiltern in JavaScript. const rows = await db.userRole.findMany({ where: { userId }, select: { discordRoleId: true, }, }) const roleIds = rows.map((r) => r.discordRoleId) const mappings = roleIds.length === 0 ? [] : await db.roleMapping.findMany({ where: { discordRoleId: { in: roleIds } }, select: { clearanceLevel: true, permissions: true, grantsMembership: true }, }) let clearanceLevel = 0 let hasMembership = false const permissions = new Set() for (const mapping of mappings) { if (mapping.clearanceLevel > clearanceLevel) clearanceLevel = mapping.clearanceLevel if (mapping.grantsMembership) hasMembership = true for (const value of mapping.permissions) { if (isPermission(value)) permissions.add(value) } } const resolved: EffectivePermissions = { userId, clearanceLevel: Math.min(clearanceLevel, MAX_CLEARANCE), permissions, isAdmin: permissions.has('admin'), hasMembership, } cache.set(userId, resolved) return resolved }