feat(15-03): GET /modules/catalog — beide Statusflags in einer Antwort

- ModuleAccessService.getCatalogFlags(tenantId, userId, role) liefert je
  aktivem Modul isActiveForTenant + hasAccess in einer Auflösung
- ModuleRegistryController.findCatalog (GET /modules/catalog), erreichbar
  für jeden authentifizierten Benutzer wie GET /modules (D-08)
- ADMIN/SUPER_ADMIN: hasAccess immer wahr für aktive Module (D-03)
- 4 neue Tests für getCatalogFlags
This commit is contained in:
2026-08-04 18:41:23 +02:00
parent 072fb7f62f
commit 1c32543f58
3 changed files with 114 additions and 0 deletions
@@ -63,6 +63,37 @@ export class ModuleRegistryController {
return this.moduleAccessService.findAccessibleModules(tenantId, userId, role);
}
/**
* 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: Request) {
const tenantId = (req as any).tenantId ?? (req as any).user?.tenantId;
const userId = (req as any).user?.id;
const role = (req as any).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),
]);
return modules.map((module) => ({
...module,
isActiveForTenant: flags.get(module.id)?.isActiveForTenant ?? false,
hasAccess: flags.get(module.id)?.hasAccess ?? false,
}));
}
/**
* POST /modules/:moduleId/activate
* Activates a module for the current tenant.