feat(module-grants): Freigabestufe Verwalten – Datenbank, Zugriffsprüfung und Kantinen-Einstellungen
- Migration: ModuleGrant.level (USE/MANAGE), Bestand bleibt USE - ModuleAccessService.getModuleAccessLevels als einzige Auflösung, MANAGE gewinnt - @ModuleManage(slug) am ModuleGuard, GET /modules/active liefert canManage - Kantinenabrechnung: Einstellungen für Benutzer mit Verwalten, Web-Hook useCanManageModule Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { ModuleGrantLevel } from '@prisma/client';
|
||||
import { ModuleAccessService } from './module-access.service';
|
||||
import { ModuleRegistryService } from './module-registry.service';
|
||||
|
||||
@@ -16,6 +17,12 @@ import { ModuleRegistryService } from './module-registry.service';
|
||||
*/
|
||||
export const MODULE_SLUG_KEY = 'moduleSlug';
|
||||
|
||||
/**
|
||||
* Metadata key set by @ModuleManage(): the route needs the Freigabestufe
|
||||
* Verwalten (MANAGE) for the module, not just access (261002-icv).
|
||||
*/
|
||||
export const MODULE_MANAGE_KEY = 'moduleManage';
|
||||
|
||||
/**
|
||||
* Guard that checks whether the requesting user has access to the module
|
||||
* identified by its slug — Aktivierung UND (Rolle ODER Direkt-Grant ODER
|
||||
@@ -28,6 +35,13 @@ export const MODULE_SLUG_KEY = 'moduleSlug';
|
||||
* T-15-03: Ohne `@UseModule(slug)`-Metadaten gibt der Guard bewusst
|
||||
* `true` zurück (Durchsetzung hängt am Dekorator) — jeder neue
|
||||
* Modul-Controller MUSS `@UseModule` tragen (Projektregel seit Phase 3).
|
||||
*
|
||||
* 261002-icv: Trägt die Route zusätzlich `@ModuleManage(slug)`, genügt
|
||||
* Zugriff allein nicht — die wirksame Freigabestufe muss Verwalten sein
|
||||
* (Administratoren erfüllen das über den Kurzschluss in
|
||||
* `getModuleAccessLevels`). Die Stufe wird serverseitig aus den ModuleGrant-
|
||||
* Zeilen aufgelöst, nie aus Body/Query (T-icv-02), und nur für das Modul
|
||||
* der Route (T-icv-04).
|
||||
*/
|
||||
@Injectable()
|
||||
export class ModuleGuard implements CanActivate {
|
||||
@@ -71,22 +85,37 @@ export class ModuleGuard implements CanActivate {
|
||||
);
|
||||
}
|
||||
|
||||
const accessibleModuleIds = await this.moduleAccessService.getAccessibleModuleIds(
|
||||
tenantId,
|
||||
userId,
|
||||
role,
|
||||
);
|
||||
const requireManage =
|
||||
this.reflector.getAllAndOverride<boolean>(MODULE_MANAGE_KEY, [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]) === true;
|
||||
|
||||
if (!accessibleModuleIds.has(module.id)) {
|
||||
// Per-Request-Memoisierung (D-09): ein Klassen-@UseModule plus ein
|
||||
// Handler-@ModuleManage lassen diesen Guard zweimal pro Request laufen;
|
||||
// die Aufloesung bezahlt nur der erste Lauf. Ueber Request-Grenzen
|
||||
// hinweg wird nichts zwischengespeichert.
|
||||
const levels: Map<string, ModuleGrantLevel> =
|
||||
request.moduleAccessLevels instanceof Map
|
||||
? request.moduleAccessLevels
|
||||
: await this.moduleAccessService.getModuleAccessLevels(tenantId, userId, role);
|
||||
|
||||
const level = levels.get(module.id);
|
||||
|
||||
if (!level) {
|
||||
throw new ForbiddenException(
|
||||
`Module '${moduleSlug}' is not accessible for this user`,
|
||||
);
|
||||
}
|
||||
|
||||
// Per-Request-Memoisierung (D-09): ein nachfolgender Handler im
|
||||
// selben Request bezahlt die Auflösung nicht ein zweites Mal. Über
|
||||
// Request-Grenzen hinweg wird nichts zwischengespeichert.
|
||||
request.moduleAccessIds = accessibleModuleIds;
|
||||
if (requireManage && level !== ModuleGrantLevel.MANAGE) {
|
||||
throw new ForbiddenException(
|
||||
`Module '${moduleSlug}' requires manage permission`,
|
||||
);
|
||||
}
|
||||
|
||||
request.moduleAccessLevels = levels;
|
||||
request.moduleAccessIds = new Set(levels.keys());
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -107,3 +136,30 @@ export function UseModule(slug: string) {
|
||||
UseGuards(ModuleGuard),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decorator fuer modul-eigene Konfiguration: verlangt Zugriff auf das Modul
|
||||
* UND die Freigabestufe Verwalten (261002-icv). Ersetzt
|
||||
* `@Roles(ADMIN, SUPER_ADMIN)` fuer Handler, die nur dieses eine Modul
|
||||
* konfigurieren.
|
||||
*
|
||||
* Verwendbar auf einem Handler innerhalb eines `@UseModule`-Controllers oder
|
||||
* auf einem ganzen Controller. Administratoren bestehen ueber den
|
||||
* D-03-Kurzschluss (sie loesen auf allen aktiven Modulen zu MANAGE auf).
|
||||
*
|
||||
* Niemals zusammen mit `@Roles` am selben Handler: der globale RolesGuard
|
||||
* wuerde Verwalter trotzdem sperren. Mandant, Benutzer und Rolle stammen
|
||||
* ausschliesslich aus dem JWT (T-15-10).
|
||||
*
|
||||
* Usage:
|
||||
* @ModuleManage('kantine-datev')
|
||||
* @Put('settings')
|
||||
* saveSettings(...) { ... }
|
||||
*/
|
||||
export function ModuleManage(slug: string) {
|
||||
return applyDecorators(
|
||||
SetMetadata(MODULE_SLUG_KEY, slug),
|
||||
SetMetadata(MODULE_MANAGE_KEY, true),
|
||||
UseGuards(ModuleGuard),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user