Files
tessera-ctl/apps/api/src/module-registry/module-registry.controller.ts
T
schalli 7c9d7c1223 refactor(quick-260921-m34): Aufgabe 2b - getypte Anfrage in elf Controllern, zwei Befunde gemeldet
(req as any) und @Req() req: any durch AuthenticatedRequest ersetzt in
dashboard, favorites, calendar, groups, module-grants, module-registry,
tenders, dkv, ldap, settings; @CurrentUser() in user.controller auf AuthUser.
Die abwehrenden Pruefungen ("No tenant context", "No user context") bleiben
lebendig, weil user auf dem Anfragetyp wahlfrei ist - genau das beschreibt
den Zustand auf oeffentlichen Wegen.

Nebengewinn ohne neue Zusicherungen: req.tenantId as string | undefined
(dkv, settings), file.buffer as Buffer und file.mimetype as string
(dkv, user) sind weggefallen, weil der Typ sie jetzt traegt.

BEFUND 1 (D-03, gemeldet) dashboard.controller.ts:74 alt: der Handler las
req.user?.role NACH extractContext und gab sie an getWidgets(role: Role)
weiter, das eine Rolle zwingend verlangt. Die Annahme "hier gibt es immer
einen Aufrufer" stimmt - die Pruefung "No user context" erzwingt sie -, aber
sie stand in einer anderen Methode, wo der Compiler sie nicht sehen konnte.
extractContext gibt die Rolle jetzt mit zurueck: keine neue Pruefung, kein
erfundener Wert, gleiche Reihenfolge, gleiche Meldungen.

BEFUND 2 (D-03, gemeldet) tenders.controller.ts:142: resolveRequestingTenantId
erklaerte string | undefined, liest aber req.tenantId, das TenantGuard fuer
einen SUPER_ADMIN ohne Mandanten auf null setzt. Die Erklaerung war also nie
vollstaendig. Erweitert auf string | null | undefined, und buildTenderWhere
nimmt string | null - beides nur Erklaerung, kein Verhalten: die Funktion
entscheidet seit jeher ueber Wahrheitswert und faellt bei beiden zu
(nur global sichtbare Ausschreibungen).

Fixtures in user.controller.spec.ts ergaenzt (username, mustChangePassword,
originalname, size). Testzahlen unveraendert.

noExplicitAny in apps/api/src: 137 -> 66. type-check 4/4, lint 5/5,
apps/api 72/1143, apps/web 73/531, tenant.guard.ts unveraendert.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
2026-09-21 17:08:44 +02:00

136 lines
4.7 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 { ModuleAccessService } from './module-access.service';
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)
* - 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,
) {}
/**
* 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 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');
}
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: 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),
]);
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.
* 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);
}
}