b188946e31
- prisma-tenant.extension.ts: (prisma as any) und die Handannotation an
$allOperations in forTenant()/forSystem() entfernt; Kopfkommentar
unveraendert. .then((results: any[]) => ...) auf unknown[] umgestellt.
- 105 Aufrufstellen `const X = forTenant(...) as any` / `forSystem(...) as
any` von der Zusicherung befreit, Zuweisungsform woertlich erhalten
(rls-access-inventory.spec.ts bleibt scharf, 30/30 gruen einzeln
geprueft).
- withTenantTransaction(): Prisma.TransactionClient fuer tx probiert,
gemessen verworfen - bricht das Testdoppel in
prisma-tenant.extension.spec.ts (TS2322 auf einem absichtlich
unvollstaendigen Fake-Objekt). tx bleibt any, mit Begruendung am Typ.
- Gefolge des jetzt getypten Klienten entfernt: any[]-Annotationen und
.map((x: any) => ...) in groups.service.ts, module-grants.service.ts,
dkv.service.ts, ldap-config.service.ts, tenders.controller.ts:270.
- Befund (D-03): tender-matching.service.ts:159 trug eine Handannotation
(match: { tender: unknown }), die den Wert nur deshalb auf unknown
verengte, um TS7006 unter dem alten any-Klienten zu vermeiden - mit dem
getypten Klienten war das falsch. Annotation geloescht, kein Ersatz
durch Zusicherung.
- Zwei any bleiben gezielt in groups.service.ts (u/a in
ensureDefaultGroup(), gefolge von tx: any) - Begruendung am Code.
noExplicitAny apps/api/src: 288 -> 149 (Schranke 155). type-check 4/4,
lint 5/5 (0 error). apps/api 72/1143 gruen, apps/web 73/531 gruen,
rls-access-inventory.spec.ts 30/30 gruen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
228 lines
6.9 KiB
TypeScript
228 lines
6.9 KiB
TypeScript
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<boolean> {
|
|
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<string, string>;
|
|
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,
|
|
},
|
|
});
|
|
}
|
|
}
|