forked from wangziqi/gongxue-base
feat: add CASL authorization and AI configuration
This commit is contained in:
79
apps/server/src/authorization/guards/policies.guard.ts
Normal file
79
apps/server/src/authorization/guards/policies.guard.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
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<boolean>(IS_PUBLIC_KEY, [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
if (isPublic) return true;
|
||||
|
||||
const handlers = this.reflector.getAllAndMerge<PolicyHandler[]>(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<AuthorizationRequest>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user