import { Injectable, NotFoundException } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; import { forTenant } from '../prisma/prisma-tenant.extension'; /** * Service managing the module registry and per-tenant activations. * * Modules are registered centrally; tenants activate/deactivate them * independently via TenantModuleActivation records (per D-07, D-08). */ @Injectable() export class ModuleRegistryService { constructor(private readonly prisma: PrismaService) {} /** * Returns all registered modules. * * Bewusst UNGEBUNDEN (260910-exd, Aufgabe 1, Befund E): "Module" traegt * heute keinen Zeilenschutz, eine Bindung waere heute wirkungslos, nicht * katastrophal. Katastrophal wuerde sie erst, WENN Etappe 3 dieser * Tabelle eine Regel gibt — dann verschwaende der gesamte Katalog fuer * jeden Mandanten. Diese Bedingung steht hier als Bedingung, nicht als * heute beobachtbare Tatsache. */ async findAll() { return this.prisma.module.findMany({ orderBy: { name: 'asc' }, }); } /** * Finds a module by its unique slug. * * Bewusst UNGEBUNDEN, dieselbe Begruendung wie `findAll` oben. Diese * Methode ist zusaetzlich die Stelle, die `ModuleGuard` bei JEDER * Modulanfrage aufruft — eine Bindung wuerde jede Modulanfrage mit einer * Meldung abweisen, die faelschlich von einer fehlenden Aktivierung * spricht. */ async findBySlug(slug: string) { return this.prisma.module.findUnique({ where: { slug }, }); } /** * Returns all active modules for a given tenant. */ async findActiveForTenant(tenantId: string) { const tenantPrisma = forTenant(this.prisma, tenantId); const activations = await tenantPrisma.tenantModuleActivation.findMany({ where: { tenantId, isActive: true, }, include: { module: true, }, }); return activations.map((a: { module: unknown }) => a.module); } /** * Activates a module for a tenant (upsert: creates or re-activates). * Per D-08: dynamic activation without restart. */ async activateForTenant(tenantId: string, moduleId: string) { // Verify module exists — bewusst UNGEBUNDEN, dieselbe Begruendung wie // `findAll` oben (Aufgabe 1, Befund E). Laeuft VOR jedem gebundenen // Schreibzugriff: eine unbekannte moduleId wirft, bevor der gebundene // Klient ueberhaupt erzeugt wird. const moduleExists = await this.prisma.module.findUnique({ where: { id: moduleId }, }); if (!moduleExists) { throw new NotFoundException(`Module with id '${moduleId}' not found`); } const tenantPrisma = forTenant(this.prisma, tenantId); return tenantPrisma.tenantModuleActivation.upsert({ where: { tenantId_moduleId: { tenantId, moduleId, }, }, update: { isActive: true, activatedAt: new Date(), }, create: { tenantId, moduleId, isActive: true, }, include: { module: true, }, }); } /** * Deactivates a module for a tenant (soft-delete: sets isActive=false). * Does not remove the activation record, preserving audit trail. */ async deactivateForTenant(tenantId: string, moduleId: string) { // Verify module exists — bewusst UNGEBUNDEN, dieselbe Begruendung wie // `findAll` oben. const moduleExists = await this.prisma.module.findUnique({ where: { id: moduleId }, }); if (!moduleExists) { throw new NotFoundException(`Module with id '${moduleId}' not found`); } // EIN gebundener Klient fuer beide Aktivierungszugriffe dieser Methode // (Lesen, Schreiben) — nicht ein Klient je Zugriff (260910-exd, // Aufgabe 3, dieselbe Konvention wie `module-access.service.ts`). const tenantPrisma = forTenant(this.prisma, tenantId); // Check if activation record exists const activation = await tenantPrisma.tenantModuleActivation.findUnique({ where: { tenantId_moduleId: { tenantId, moduleId, }, }, }); if (!activation) { throw new NotFoundException( `Module '${moduleId}' is not activated for this tenant`, ); } return tenantPrisma.tenantModuleActivation.update({ where: { tenantId_moduleId: { tenantId, moduleId, }, }, data: { isActive: false, }, include: { module: true, }, }); } /** * Checks whether a module (by slug) is active for a given tenant. * * Richtiggestellt (260910-exd, Aufgabe 1, Befund G): der vorherige * Kommentar behauptete, `ModuleGuard` benutze diese Methode — er tut es * NICHT. Gemessen (Aufgabe 1, TEIL 3, `grep -rn "isModuleActive" * apps/api/src apps/web/src packages`): genau EIN Treffer, die Definition * selbst, kein Aufrufer. Der Waechter nimmt stattdessen `findBySlug` plus * `ModuleAccessService.getAccessibleModuleIds`. Diese Methode bleibt * TROTZDEM umgestellt: heute toter, ungebunden gelassener Code ist die * Falle fuer den, der ihn morgen verdrahtet. */ async isModuleActive(tenantId: string, moduleSlug: string): Promise { const module = await this.prisma.module.findUnique({ where: { slug: moduleSlug }, }); if (!module) { return false; } const tenantPrisma = forTenant(this.prisma, tenantId); const activation = await tenantPrisma.tenantModuleActivation.findUnique({ where: { tenantId_moduleId: { tenantId, moduleId: module.id, }, }, }); return activation?.isActive === true; } /** * Registers or updates a module in the registry by slug (upsert). * Used during application startup to seed built-in modules. * * Bewusst UNGEBUNDEN, dieselbe Begruendung wie `findAll` oben — mit einem * zusaetzlichen Grund, den nur diese Methode hat: sie laeuft beim * Anwendungsstart aus vier Seed-Dateien, ohne Anfrage und ohne Mandanten * — ein gebundener Aufruf haette dort strukturell keinen Kontext. */ async seedModule(manifest: { slug: string; name: string; version: string; category: string; description: Record; icon?: string; isSystem?: boolean; }) { return this.prisma.module.upsert({ where: { slug: manifest.slug }, update: { name: manifest.name, version: manifest.version, category: manifest.category, description: manifest.description, icon: manifest.icon ?? null, isSystem: manifest.isSystem ?? false, }, create: { slug: manifest.slug, name: manifest.name, version: manifest.version, category: manifest.category, description: manifest.description, icon: manifest.icon ?? null, isSystem: manifest.isSystem ?? false, }, }); } }