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,94 @@
import {
Controller,
ForbiddenException,
Get,
Param,
Post,
Req,
UseGuards,
} from '@nestjs/common';
import { Role } from '@prisma/client';
import { Request } from 'express';
import { Roles } from '../auth/decorators/roles.decorator';
import { RolesGuard } from '../auth/guards/roles.guard';
import { ModuleRegistryService } from './module-registry.service';
/**
* REST controller for the module registry.
*
* - GET /modules — list all registered modules (any authenticated user)
* - GET /modules/active — list active modules for current tenant
* - POST /modules/:moduleId/activate — activate a module (ADMIN/SUPER_ADMIN)
* - POST /modules/:moduleId/deactivate — deactivate a module (ADMIN/SUPER_ADMIN)
*
* All routes require JWT auth (global JwtAuthGuard).
* Activation/deactivation require ADMIN or SUPER_ADMIN role (T-03-01).
*/
@Controller('modules')
export class ModuleRegistryController {
constructor(
private readonly moduleRegistryService: ModuleRegistryService,
) {}
/**
* GET /modules
* Returns all registered modules in the system.
* Available to any authenticated user (T-03-03: module catalog is non-sensitive).
*/
@Get()
async findAll() {
return this.moduleRegistryService.findAll();
}
/**
* GET /modules/active
* Returns modules that are active for the requesting tenant.
*/
@Get('active')
async findActive(@Req() req: Request) {
const tenantId = (req as any).tenantId;
if (!tenantId) {
throw new ForbiddenException('No tenant context');
}
return this.moduleRegistryService.findActiveForTenant(tenantId);
}
/**
* POST /modules/:moduleId/activate
* Activates a module for the current tenant.
* T-03-01: Only ADMIN or SUPER_ADMIN can activate modules.
* T-03-02: moduleId validated as UUID by Prisma parameterized queries.
*/
@Post(':moduleId/activate')
@UseGuards(RolesGuard)
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
async activate(
@Param('moduleId') moduleId: string,
@Req() req: Request,
) {
const tenantId = (req as any).tenantId;
if (!tenantId) {
throw new ForbiddenException('No tenant context');
}
return this.moduleRegistryService.activateForTenant(tenantId, moduleId);
}
/**
* POST /modules/:moduleId/deactivate
* Deactivates a module for the current tenant (soft-delete).
* T-03-01: Only ADMIN or SUPER_ADMIN can deactivate modules.
*/
@Post(':moduleId/deactivate')
@UseGuards(RolesGuard)
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
async deactivate(
@Param('moduleId') moduleId: string,
@Req() req: Request,
) {
const tenantId = (req as any).tenantId;
if (!tenantId) {
throw new ForbiddenException('No tenant context');
}
return this.moduleRegistryService.deactivateForTenant(tenantId, moduleId);
}
}