import { applyDecorators, CanActivate, ExecutionContext, ForbiddenException, Injectable, SetMetadata, UseGuards, } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { ModuleAccessService } from './module-access.service'; import { ModuleRegistryService } from './module-registry.service'; /** * Metadata key for the module slug attached by @UseModule(). */ export const MODULE_SLUG_KEY = 'moduleSlug'; /** * Guard that checks whether the requesting user has access to the module * identified by its slug — Aktivierung UND (Rolle ODER Direkt-Grant ODER * Gruppen-Grant), D-01. * * Per T-03-04/T-15-10: tenantId, userId und role stammen ausschließlich * aus dem validierten JWT (via TenantMiddleware/JwtAuthGuard), nie aus * Body oder Params — verhindert Elevation of Privilege. * * 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). */ @Injectable() export class ModuleGuard implements CanActivate { constructor( private readonly reflector: Reflector, private readonly moduleRegistryService: ModuleRegistryService, private readonly moduleAccessService: ModuleAccessService, ) {} async canActivate(context: ExecutionContext): Promise { // Get moduleSlug from metadata (set by @UseModule decorator) const moduleSlug = this.reflector.getAllAndOverride( MODULE_SLUG_KEY, [context.getHandler(), context.getClass()], ); // If no module slug is set, allow (guard is not applicable) if (!moduleSlug) { return true; } const request = context.switchToHttp().getRequest(); const tenantId = request.tenantId ?? request.user?.tenantId; if (!tenantId) { throw new ForbiddenException('No tenant context'); } const userId = request.user?.id; const role = request.user?.role; if (!userId || !role) { throw new ForbiddenException('No user context'); } const module = await this.moduleRegistryService.findBySlug(moduleSlug); if (!module) { throw new ForbiddenException( `Module '${moduleSlug}' is not activated for this tenant`, ); } const accessibleModuleIds = await this.moduleAccessService.getAccessibleModuleIds( tenantId, userId, role, ); if (!accessibleModuleIds.has(module.id)) { 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; return true; } } /** * Decorator that protects a controller or route handler with the ModuleGuard. * Ensures the specified module is accessible for the requesting user. * * Usage: * @UseModule('domaincheck') * @Controller('domaincheck') * export class DomaincheckController { ... } */ export function UseModule(slug: string) { return applyDecorators( SetMetadata(MODULE_SLUG_KEY, slug), UseGuards(ModuleGuard), ); }