import "server-only"; import type { Role } from "@/generated/prisma/client"; import type { Actor } from "@/lib/auth/actor"; import { QrRequestError } from "@/lib/qr/type-module"; /** * Sämtliche Berechtigungsregeln der Anwendung — an einer Stelle. * * Jeder schreibende oder lesende Zugriff geht durch `can` bzw. `require`. * Die Oberfläche blendet Knöpfe zwar zusätzlich aus, aber das ist nur * Bequemlichkeit: verlassen wird sich ausschließlich auf diese Prüfungen. */ export type Action = /** Codes einer Gruppe sehen und Bilder herunterladen. */ | "codes:read" /** Dynamische Codes anlegen. */ | "codes:create" /** Ziel, Name oder Gestaltung ändern. */ | "codes:update" /** Codes löschen. */ | "codes:delete" /** * Den Kurz-Link selbst vergeben oder ändern. * * Eigene Aktion und nicht Teil von `codes:update`, weil sie mehr wiegt: Ein * Kurz-Link ist eine öffentliche Adresse. Wer ihn ändert, entwertet im * Zweifel gedruckte Codes; wer ihn vergibt, besetzt eine Adresse, die * jemand anderes gebraucht hätte. Deshalb standardmäßig nur für * Administratoren — freigeben lässt es sich in der Verwaltung. */ | "codes:slug" /** Mitglieder und Rollen einer Gruppe verwalten. */ | "group:manage" /** Gruppen, Benutzer und die QR-Typ-Freigaben verwalten. */ | "instance:manage"; export type Scope = { groupId?: string; /** * Die Einstellung aus der Verwaltung, ob Bearbeiter Kurz-Links vergeben * dürfen. Wird von der aufrufenden Stelle mitgegeben, damit die Regeln hier * ohne Datenbankzugriff auskommen und prüfbar bleiben. */ editorsMaySetSlug?: boolean; }; const ROLE_RANK: Record = { VIEWER: 0, EDITOR: 1, ADMIN: 2 }; export const ROLE_LABELS: Record = { ADMIN: "Administrator", EDITOR: "Bearbeiter", VIEWER: "Betrachter", }; export const ROLE_DESCRIPTIONS: Record = { ADMIN: "Darf alles in der Gruppe, einschließlich Mitglieder und Rollen.", EDITOR: "Darf Codes anlegen, ändern und löschen.", VIEWER: "Darf Codes ansehen und herunterladen, aber nichts ändern.", }; /** Rolle des Actors in einer bestimmten Gruppe, falls vorhanden. */ export function roleInGroup(actor: Actor, groupId: string): Role | null { if (actor.kind !== "user") return null; return ( actor.user.memberships.find((m) => m.groupId === groupId)?.role ?? null ); } /** Alle Gruppen, die der Actor sehen darf. `null` heißt: alle. */ export function visibleGroupIds(actor: Actor): string[] | null { if (actor.kind === "system") return null; if (actor.kind === "anonymous") return []; if (actor.user.isInstanceAdmin) return null; return actor.user.memberships.map((m) => m.groupId); } /** Gruppen, in denen der Actor Codes anlegen darf. */ export function writableGroupIds(actor: Actor): string[] | null { if (actor.kind === "system") return null; if (actor.kind === "anonymous") return []; if (actor.user.isInstanceAdmin) return null; return actor.user.memberships .filter((m) => ROLE_RANK[m.role] >= ROLE_RANK.EDITOR) .map((m) => m.groupId); } export function can(actor: Actor, action: Action, scope: Scope = {}): boolean { if (actor.kind === "system") return true; if (actor.kind === "anonymous") return false; // Der Instanz-Administrator darf alles — er verwaltet die Installation. if (actor.user.isInstanceAdmin) return true; if (action === "instance:manage") return false; // Alle übrigen Aktionen beziehen sich auf eine Gruppe. if (!scope.groupId) return false; const role = roleInGroup(actor, scope.groupId); if (!role) return false; switch (action) { case "codes:read": return true; // jede Rolle in der Gruppe darf lesen case "codes:create": case "codes:update": case "codes:delete": return ROLE_RANK[role] >= ROLE_RANK.EDITOR; case "codes:slug": // Administratoren der Gruppe immer, Bearbeiter nur nach Freigabe. if (role === "ADMIN") return true; return ( ROLE_RANK[role] >= ROLE_RANK.EDITOR && scope.editorsMaySetSlug === true ); case "group:manage": return role === "ADMIN"; default: return false; } } const DENIED_MESSAGES: Record = { "codes:read": "Du hast keinen Zugriff auf die Codes dieser Gruppe.", "codes:create": "Zum Anlegen von Codes brauchst du in dieser Gruppe mindestens die Rolle „Bearbeiter“.", "codes:update": "Zum Ändern von Codes brauchst du in dieser Gruppe mindestens die Rolle „Bearbeiter“.", "codes:delete": "Zum Löschen von Codes brauchst du in dieser Gruppe mindestens die Rolle „Bearbeiter“.", "codes:slug": "Den Kurz-Link darf hier nur ein Administrator vergeben oder ändern. Ein Administrator der Installation kann das unter Verwaltung → Einstellungen für Bearbeiter freigeben.", "group:manage": "Mitglieder und Rollen darf nur ein Administrator der Gruppe verwalten.", "instance:manage": "Diesen Bereich darf nur ein Administrator der Installation verwalten.", }; /** Wie `can`, wirft aber mit passendem Status und deutscher Meldung. */ export function requirePermission( actor: Actor, action: Action, scope: Scope = {}, ): void { if (can(actor, action, scope)) return; if (actor.kind === "anonymous") { throw new QrRequestError("Bitte melde dich an.", 401); } throw new QrRequestError(DENIED_MESSAGES[action], 403); }