import { Controller, ForbiddenException, Get, Param, Post, Req, UseGuards, } from '@nestjs/common'; import { Role } from '@prisma/client'; import type { AuthenticatedRequest } from '../auth/types/auth-user'; import { Roles } from '../auth/decorators/roles.decorator'; import { RolesGuard } from '../auth/guards/roles.guard'; import { ModuleCategoriesService } from '../module-categories/module-categories.service'; import { ModuleAccessService } from './module-access.service'; import { getModuleChangelog } from './module-changelog.registry'; 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 modules accessible to the requesting user * (Aktivierung UND Grant/Rolle, via ModuleAccessService — D-01: dieselbe * Auflösung wie ModuleGuard) * - GET /modules/changelog/:slug — Modul-Changelog aus dem Code (neueste Version * zuerst; unbekannte Kennung → leere Liste). Das statische Segment `changelog` * steht vorn, weil die Modul-Controller unter `modules/` eigene * `:id`-Routen haben und `/modules/:slug/changelog` sonst verschattet würde. * - 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, private readonly moduleAccessService: ModuleAccessService, private readonly moduleCategoriesService: ModuleCategoriesService, ) {} /** * 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(@Req() req: AuthenticatedRequest) { const modules = await this.moduleRegistryService.findAll(); // quick-261003-387: wirksame Kategorie + Reihenfolge der Organisation; // ohne Organisationskontext bleibt die Liste unveraendert. const tenantId = req?.tenantId ?? req?.user?.tenantId; if (!tenantId) return modules; return this.moduleCategoriesService.applyToModules(tenantId, modules); } /** * GET /modules/active * Returns modules accessible to the requesting user (D-01): the same * ModuleAccessService.getAccessibleModuleIds resolution ModuleGuard * uses, not just tenant-wide activation. findActiveForTenant on * ModuleRegistryService stays unchanged for Plan 15-03's marketplace * catalog (mandantenweite Sicht, kein Benutzerfilter). */ @Get('active') async findActive(@Req() req: AuthenticatedRequest) { const tenantId = req.tenantId ?? req.user?.tenantId; const userId = req.user?.id; const role = req.user?.role; if (!tenantId || !userId || !role) { throw new ForbiddenException('No user context'); } const modules = await this.moduleAccessService.findAccessibleModules(tenantId, userId, role); // quick-261003-387: wirksame Kategorie + Reihenfolge der Organisation // (Zuordnung schlaegt Manifest-Kategorie), sortiert fuer die Seitenleiste. return this.moduleCategoriesService.applyToModules(tenantId, modules); } /** * GET /modules/catalog * Liefert je registriertem Modul den vollständigen Datensatz plus zwei * Flags: isActiveForTenant (TenantModuleActivation) und hasAccess * (ModuleAccessService.getAccessibleModuleIds) — beide in einer Antwort * (D-08), damit der Marketplace für einen USER zwischen "nicht * aktiviert" und "aktiviert, aber nicht freigegeben" unterscheiden kann, * ohne dass eine Karte kurzzeitig ohne Sperrhinweis klickbar erscheint. * Erreichbar für jeden authentifizierten Benutzer, wie GET /modules. */ @Get('catalog') async findCatalog(@Req() req: AuthenticatedRequest) { const tenantId = req.tenantId ?? req.user?.tenantId; const userId = req.user?.id; const role = req.user?.role; if (!tenantId || !userId || !role) { throw new ForbiddenException('No user context'); } const [modules, flags] = await Promise.all([ this.moduleRegistryService.findAll(), this.moduleAccessService.getCatalogFlags(tenantId, userId, role), ]); // quick-261003-387: wirksame Kategorie + Reihenfolge der Organisation. const withCategories = await this.moduleCategoriesService.applyToModules(tenantId, modules); return withCategories.map((module) => ({ ...module, isActiveForTenant: flags.get(module.id)?.isActiveForTenant ?? false, hasAccess: flags.get(module.id)?.hasAccess ?? false, })); } /** * GET /modules/changelog/:slug * Liefert den Changelog eines eingebauten Moduls (quick-261008-w5w). Statische * Produkttexte, daher wie der Katalog ohne @Roles für jeden angemeldeten * Benutzer (T-03-03). Unbekannte und eigene Module → leere Liste. */ @Get('changelog/:slug') getChangelog(@Param('slug') slug: string) { return getModuleChangelog(slug); } /** * 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: AuthenticatedRequest, ) { const tenantId = req.tenantId ?? req.user?.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: AuthenticatedRequest, ) { const tenantId = req.tenantId ?? req.user?.tenantId; if (!tenantId) { throw new ForbiddenException('No tenant context'); } return this.moduleRegistryService.deactivateForTenant(tenantId, moduleId); } }