feat(15-01): ModuleAccessService as single source of truth for module access (D-01)
- ModuleAccessService.getAccessibleModuleIds(tenantId, userId, role): ADMIN/SUPER_ADMIN bypass (D-03) via one query, otherwise a single Promise.all of direct + group ModuleGrant lookups intersected against active TenantModuleActivation (D-02) — no N+1 over the user's groups - findAccessibleModules() adds the name-asc sort for stable sidebar order - ModuleGuard now resolves userId/role from request.user (JWT-sourced, never body/params) and calls getAccessibleModuleIds instead of the tenant-only isModuleActive check; caches the result on request.moduleAccessIds for same-request reuse (D-09, no cross-request caching) - ModuleRegistryController.findActive delegates to ModuleAccessService.findAccessibleModules instead of findActiveForTenant, which stays untouched for Plan 15-03's tenant-wide marketplace catalog - ModuleRegistryModule exports ModuleAccessService for Plan 15-03/15-05 - module-access.service.spec.ts / module.guard.spec.ts cover every case in the plan's <behavior> list with a hand-rolled Prisma mock - End-to-end verified against the running local API: a USER without a grant gets 403 on a @UseModule-protected endpoint and an empty /modules/active list; the same USER with a direct grant gets 200 plus the slug in the list; an ADMIN without any grant also gets 200 (D-03)
This commit is contained in:
@@ -11,13 +11,16 @@ import { Role } from '@prisma/client';
|
||||
import { Request } from 'express';
|
||||
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 active modules for current tenant
|
||||
* - 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)
|
||||
*
|
||||
@@ -28,6 +31,7 @@ import { ModuleRegistryService } from './module-registry.service';
|
||||
export class ModuleRegistryController {
|
||||
constructor(
|
||||
private readonly moduleRegistryService: ModuleRegistryService,
|
||||
private readonly moduleAccessService: ModuleAccessService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -42,15 +46,21 @@ export class ModuleRegistryController {
|
||||
|
||||
/**
|
||||
* GET /modules/active
|
||||
* Returns modules that are active for the requesting tenant.
|
||||
* 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: Request) {
|
||||
const tenantId = (req as any).tenantId ?? (req as any).user?.tenantId;
|
||||
if (!tenantId) {
|
||||
throw new ForbiddenException('No tenant context');
|
||||
const userId = (req as any).user?.id;
|
||||
const role = (req as any).user?.role;
|
||||
if (!tenantId || !userId || !role) {
|
||||
throw new ForbiddenException('No user context');
|
||||
}
|
||||
return this.moduleRegistryService.findActiveForTenant(tenantId);
|
||||
return this.moduleAccessService.findAccessibleModules(tenantId, userId, role);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user