import { applyDecorators, CanActivate, ExecutionContext, ForbiddenException, Injectable, SetMetadata, UseGuards, } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; 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 tenant has an active * module activation for the module identified by its slug. * * Per T-03-04: tenantId is sourced from JWT (via TenantMiddleware), * not from user-supplied input, preventing elevation of privilege. */ @Injectable() export class ModuleGuard implements CanActivate { constructor( private readonly reflector: Reflector, private readonly moduleRegistryService: ModuleRegistryService, ) {} 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 isActive = await this.moduleRegistryService.isModuleActive( tenantId, moduleSlug, ); if (!isActive) { throw new ForbiddenException( `Module '${moduleSlug}' is not activated for this tenant`, ); } return true; } } /** * Decorator that protects a controller or route handler with the ModuleGuard. * Ensures the specified module is activated for the requesting tenant. * * Usage: * @UseModule('domaincheck') * @Controller('domaincheck') * export class DomaincheckController { ... } */ export function UseModule(slug: string) { return applyDecorators( SetMetadata(MODULE_SLUG_KEY, slug), UseGuards(ModuleGuard), ); }