feat(03-01): add ModuleRegistry NestJS module with CRUD and activation endpoints

- ModuleRegistryService with findAll, findBySlug, findActiveForTenant, activate/deactivate, seedModule
- ModuleRegistryController with GET /modules, GET /modules/active, POST activate/deactivate
- ModuleGuard + @UseModule() decorator for tenant-scoped module access control
- ActivateModuleDto with UUID validation
- Registered ModuleRegistryModule in AppModule imports
This commit is contained in:
2026-06-19 12:36:33 +02:00
parent 8c24c1e267
commit fa15d3527a
6 changed files with 393 additions and 0 deletions
@@ -0,0 +1,81 @@
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<boolean> {
// Get moduleSlug from metadata (set by @UseModule decorator)
const moduleSlug = this.reflector.getAllAndOverride<string>(
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;
// Public routes or routes without tenant context skip module check
if (!tenantId) {
return true;
}
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),
);
}