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:
@@ -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),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user