79210d7966
- Typen, Register, Route GET /modules/changelog/:slug, Waechter-Test - Seed liest die Version per latestVersion aus dem Changelog - Marktplatz-Detailseite zeigt den Abschnitt Aenderungen Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
164 lines
6.4 KiB
TypeScript
164 lines
6.4 KiB
TypeScript
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/<slug>` 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);
|
|
}
|
|
}
|