feat(quick-261003-387): Kategorien-API - Anlegen, Sortieren, Zuordnen, Loeschen mit Verschieben, Ueberlagerung in allen Modullisten

- Verwaltungs-Endpunkte nur fuer Administratoren, statische Routen vor :key
- Loeschen verschiebt alle Eintraege inkl. persoenlicher eigener Module in einer Transaktion, ohne Ziel 409
- /modules, /modules/catalog und /module-grants/matrix liefern wirksame Kategorie und Reihenfolge
- eigene Module pruefen die Kategorie gegen die Organisation (400 bei unbekannter)
- Zugriffsinventar nachgezogen

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-10-03 02:39:36 +02:00
parent 8ec116c75a
commit af1878d5ee
15 changed files with 1152 additions and 37 deletions
@@ -0,0 +1,76 @@
import { describe, expect, it, vi } from 'vitest';
import { ModuleGrantsController } from '../groups/module-grants.controller';
import { ModuleRegistryController } from './module-registry.controller';
/**
* quick-261003-387 (E-07): alle Modullisten laufen ueber
* `ModuleCategoriesService.applyToModules`, damit Seitenleiste, Marktplatz,
* Kategorieseite und Freigaben-Matrix dieselbe wirksame Kategorie sehen.
*/
const overlay = vi.fn(async (_tenantId: string, modules: Array<{ id: string }>) =>
modules.map((m) => ({ ...m, category: 'ueberlagert', sortOrder: 7 })),
);
const categories = { applyToModules: overlay } as any;
const req = { tenantId: 't1', user: { id: 'u1', role: 'USER', tenantId: 't1' } } as any;
describe('ModuleRegistryController — Kategorie-Ueberlagerung', () => {
const registry = { findAll: vi.fn(async () => [{ id: 'a', name: 'A', category: 'fleet' }]) };
const access = {
findAccessibleModules: vi.fn(async () => [{ id: 'a', name: 'A', category: 'fleet' }]),
getCatalogFlags: vi.fn(
async () => new Map([['a', { isActiveForTenant: true, hasAccess: true }]]),
),
};
const controller = new ModuleRegistryController(registry as any, access as any, categories);
it('GET /modules liefert die wirksame Kategorie', async () => {
expect(await controller.findAll(req)).toEqual([
{ id: 'a', name: 'A', category: 'ueberlagert', sortOrder: 7 },
]);
expect(overlay).toHaveBeenCalledWith('t1', expect.any(Array));
});
it('GET /modules ohne Organisationskontext bleibt unveraendert', async () => {
overlay.mockClear();
expect(await controller.findAll({} as any)).toEqual([
{ id: 'a', name: 'A', category: 'fleet' },
]);
expect(overlay).not.toHaveBeenCalled();
});
it('GET /modules/active liefert die wirksame Kategorie', async () => {
expect(await controller.findActive(req)).toEqual([
{ id: 'a', name: 'A', category: 'ueberlagert', sortOrder: 7 },
]);
});
it('GET /modules/catalog liefert die wirksame Kategorie samt Flags', async () => {
expect(await controller.findCatalog(req)).toEqual([
{
id: 'a',
name: 'A',
category: 'ueberlagert',
sortOrder: 7,
isActiveForTenant: true,
hasAccess: true,
},
]);
});
});
describe('ModuleGrantsController.matrix — Kategorie-Ueberlagerung', () => {
it('ersetzt nur die Module, Gruppen und Freigaben bleiben', async () => {
const grants = {
getMatrix: vi.fn(async () => ({
modules: [{ id: 'a', name: 'A', category: 'fleet' }],
groups: [{ id: 'g' }],
grants: [{ moduleId: 'a', groupId: 'g', level: 'USE' }],
})),
};
const controller = new ModuleGrantsController(grants as any, categories);
const out = await controller.matrix(req);
expect(out.modules).toEqual([{ id: 'a', name: 'A', category: 'ueberlagert', sortOrder: 7 }]);
expect(out.groups).toEqual([{ id: 'g' }]);
expect(out.grants).toHaveLength(1);
});
});
@@ -42,8 +42,13 @@ export class ModuleRegistryController {
* Available to any authenticated user (T-03-03: module catalog is non-sensitive).
*/
@Get()
async findAll() {
return this.moduleRegistryService.findAll();
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);
}
/**
@@ -92,7 +97,9 @@ export class ModuleRegistryController {
this.moduleAccessService.getCatalogFlags(tenantId, userId, role),
]);
return modules.map((module) => ({
// 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,