import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { ANY_AUTHENTICATED_USER_KEY } from '../decorators/any-authenticated-user.decorator'; import { IS_PUBLIC_KEY } from '../decorators/public.decorator'; import { REQUIRE_PERMISSION_KEY } from '../decorators/require-permission.decorator'; import { PermissionKey } from '../permissions/permission-catalog'; import { PermissionResolverService } from '../permissions/permission-resolver.service'; import { AuthenticatedUser } from '../types/authenticated-user'; interface RequestWithUser { user?: AuthenticatedUser; } /** * Fails closed by decorator absence: a route reaches this guard whether or * not it's @Public (JwtAuthGuard only skips authentication for those, it * doesn't remove this guard from the chain - NestJS APP_GUARD entries all * run, there's no early-exit between them), so @Public is checked here too, * the same way JwtAuthGuard checks it. A non-public route is denied unless * it carries either @RequirePermission (checked against the caller's live * permission set) or the explicit @AnyAuthenticatedUser opt-out for routes * meant to be reachable by any logged-in user regardless of role. A route * with none of the three is a bug - forgetting to decorate a new route now * fails shut at runtime instead of silently granting access to every * authenticated user and relying solely on * test/authorization-coverage.e2e-spec.ts to catch it before deploy. */ @Injectable() export class PermissionsGuard implements CanActivate { constructor( private readonly reflector: Reflector, private readonly permissionResolver: PermissionResolverService, ) {} async canActivate(context: ExecutionContext): Promise { const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [ context.getHandler(), context.getClass(), ]); if (isPublic) { return true; } const requiredPermissions = this.reflector.getAllAndOverride< PermissionKey[] >(REQUIRE_PERMISSION_KEY, [context.getHandler(), context.getClass()]); const request = context.switchToHttp().getRequest(); const user = request.user; if (requiredPermissions && requiredPermissions.length > 0) { if (!user) { return false; } return this.permissionResolver.hasAny(user.id, requiredPermissions); } const isAnyAuthenticatedUser = this.reflector.getAllAndOverride( ANY_AUTHENTICATED_USER_KEY, [context.getHandler(), context.getClass()], ); return Boolean(isAnyAuthenticatedUser && user); } }