feat(15-03): GET /modules/catalog — beide Statusflags in einer Antwort
- ModuleAccessService.getCatalogFlags(tenantId, userId, role) liefert je aktivem Modul isActiveForTenant + hasAccess in einer Auflösung - ModuleRegistryController.findCatalog (GET /modules/catalog), erreichbar für jeden authentifizierten Benutzer wie GET /modules (D-08) - ADMIN/SUPER_ADMIN: hasAccess immer wahr für aktive Module (D-03) - 4 neue Tests für getCatalogFlags
This commit is contained in:
@@ -215,3 +215,48 @@ describe('ModuleAccessService.findAccessibleModules — ordering (PERM-04)', ()
|
|||||||
expect(prisma.module.findMany).not.toHaveBeenCalled();
|
expect(prisma.module.findMany).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('ModuleAccessService.getCatalogFlags — Marketplace-Katalog (D-08)', () => {
|
||||||
|
it('aktives und freigegebenes Modul: isActiveForTenant und hasAccess beide wahr', async () => {
|
||||||
|
const prisma = makeFakePrisma({
|
||||||
|
activations: [{ moduleId: 'mod-1' }],
|
||||||
|
directGrants: [{ moduleId: 'mod-1' }],
|
||||||
|
});
|
||||||
|
const service = new ModuleAccessService(prisma as any);
|
||||||
|
|
||||||
|
const flags = await service.getCatalogFlags('t1', 'user-1', 'USER');
|
||||||
|
|
||||||
|
expect(flags.get('mod-1')).toEqual({ isActiveForTenant: true, hasAccess: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('aktives, aber nicht freigegebenes Modul: isActiveForTenant wahr, hasAccess falsch', async () => {
|
||||||
|
const prisma = makeFakePrisma({
|
||||||
|
activations: [{ moduleId: 'mod-1' }],
|
||||||
|
});
|
||||||
|
const service = new ModuleAccessService(prisma as any);
|
||||||
|
|
||||||
|
const flags = await service.getCatalogFlags('t1', 'user-1', 'USER');
|
||||||
|
|
||||||
|
expect(flags.get('mod-1')).toEqual({ isActiveForTenant: true, hasAccess: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('nicht aktiviertes Modul erscheint nicht in der Flag-Map (Controller mappt fehlenden Eintrag auf beide Flags falsch)', async () => {
|
||||||
|
const prisma = makeFakePrisma({ activations: [] });
|
||||||
|
const service = new ModuleAccessService(prisma as any);
|
||||||
|
|
||||||
|
const flags = await service.getCatalogFlags('t1', 'user-1', 'USER');
|
||||||
|
|
||||||
|
expect(flags.has('mod-1')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('D-03: als ADMIN ist jedes aktive Modul zugänglich, auch ohne Grant', async () => {
|
||||||
|
const prisma = makeFakePrisma({
|
||||||
|
activations: [{ moduleId: 'mod-1' }],
|
||||||
|
});
|
||||||
|
const service = new ModuleAccessService(prisma as any);
|
||||||
|
|
||||||
|
const flags = await service.getCatalogFlags('t1', 'admin-1', 'ADMIN');
|
||||||
|
|
||||||
|
expect(flags.get('mod-1')).toEqual({ isActiveForTenant: true, hasAccess: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -92,4 +92,42 @@ export class ModuleAccessService {
|
|||||||
orderBy: { name: 'asc' },
|
orderBy: { name: 'asc' },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liefert je mandantenweit aktivem Modul zwei Flags in einer Antwort:
|
||||||
|
* `isActiveForTenant` (immer `true` — die Map enthält nur aktive Module)
|
||||||
|
* und `hasAccess` aus derselben `getAccessibleModuleIds`-Auflösung wie
|
||||||
|
* Guard und Sidebar. Bedient `GET /modules/catalog` (D-08): der
|
||||||
|
* Marketplace zeigt auch nicht freigegebene Module weiter, gekennzeichnet
|
||||||
|
* statt ausgeblendet — beide Flags kommen bewusst in einer Antwort, damit
|
||||||
|
* keine Karte kurzzeitig ohne Sperrhinweis klickbar erscheint. Ein nicht
|
||||||
|
* aktiviertes Modul erscheint schlicht nicht in der zurückgegebenen Map;
|
||||||
|
* der Controller mappt einen fehlenden Eintrag auf beide Flags `false`.
|
||||||
|
*
|
||||||
|
* Für ADMIN/SUPER_ADMIN ist `hasAccess` bei jedem aktiven Modul wahr,
|
||||||
|
* weil `getAccessibleModuleIds` den Rollen-Kurzschluss anwendet (D-03) —
|
||||||
|
* das Sperr-Badge erscheint für sie nie.
|
||||||
|
*/
|
||||||
|
async getCatalogFlags(
|
||||||
|
tenantId: string,
|
||||||
|
userId: string,
|
||||||
|
role: Role,
|
||||||
|
): Promise<Map<string, { isActiveForTenant: boolean; hasAccess: boolean }>> {
|
||||||
|
const [activations, accessibleIds] = await Promise.all([
|
||||||
|
this.prisma.tenantModuleActivation.findMany({
|
||||||
|
where: { tenantId, isActive: true },
|
||||||
|
select: { moduleId: true },
|
||||||
|
}),
|
||||||
|
this.getAccessibleModuleIds(tenantId, userId, role),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const flags = new Map<string, { isActiveForTenant: boolean; hasAccess: boolean }>();
|
||||||
|
for (const activation of activations) {
|
||||||
|
flags.set(activation.moduleId, {
|
||||||
|
isActiveForTenant: true,
|
||||||
|
hasAccess: accessibleIds.has(activation.moduleId),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return flags;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,6 +63,37 @@ export class ModuleRegistryController {
|
|||||||
return this.moduleAccessService.findAccessibleModules(tenantId, userId, role);
|
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: Request) {
|
||||||
|
const tenantId = (req as any).tenantId ?? (req as any).user?.tenantId;
|
||||||
|
const userId = (req as any).user?.id;
|
||||||
|
const role = (req as any).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
|
* POST /modules/:moduleId/activate
|
||||||
* Activates a module for the current tenant.
|
* Activates a module for the current tenant.
|
||||||
|
|||||||
Reference in New Issue
Block a user