import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { CHECK_POLICIES_KEY } from '../decorators/check-policies.decorator'; import { IS_PUBLIC_KEY } from '../../auth/decorators/public.decorator'; import { CaslAbilityFactory } from '../casl-ability.factory'; import { AppAbility, AuthorizationRequest, PolicyHandler } from '../interfaces'; /** * Evaluates CASL policies declared via @CheckPolicies(). * * Registered as a global APP_GUARD — runs after JwtAuthGuard and * PermissionGuard in the guard chain. Only activates when a route * carries @CheckPolicies metadata. * * ## Interaction with other guards * * - @Public() → PoliciesGuard skips (same as other guards). * - @CheckPolicies alone (no @RequirePermission) → PermissionGuard * passes through if user is authenticated; PoliciesGuard evaluates. * - @CheckPolicies + @RequirePermission → both guards run independently; * both must pass. * * ## Handler types * * Two forms are supported: * * ```ts * // Callback form * @CheckPolicies((ability) => ability.can('read', 'Student')) * * // Class-based form (testable, NestJS official pattern) * class ReadStudentPolicyHandler implements IPolicyHandler { * handle(ability: AppAbility) { return ability.can('read', 'Student'); } * } * @CheckPolicies(new ReadStudentPolicyHandler()) * ``` */ @Injectable() export class PoliciesGuard implements CanActivate { constructor( private reflector: Reflector, private abilityFactory: CaslAbilityFactory, ) {} canActivate(context: ExecutionContext): boolean { // @Public() routes skip all authorization const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [ context.getHandler(), context.getClass(), ]); if (isPublic) return true; const handlers = this.reflector.getAllAndMerge(CHECK_POLICIES_KEY, [ context.getHandler(), context.getClass(), ]); // No @CheckPolicies → let other guards decide if (!handlers || handlers.length === 0) return true; const request = context.switchToHttp().getRequest(); const user = request.user; if (!user) return false; const ability = this.abilityFactory.createForUser(user); return handlers.every((handler) => this.execHandler(handler, ability)); } /** * Execute a policy handler — supports both callback and class-based forms. */ private execHandler(handler: PolicyHandler, ability: AppAbility): boolean { if (typeof handler === 'function') { return handler(ability); } return handler.handle(ability); } }