简体   繁体   中英

Nestjs graphql field guard

I am trying to make role guard for graphql field. Something like this:

 import { Field, ObjectType } from 'type-graphql'; import { Column, Entity, JoinTable, ManyToMany, PrimaryGeneratedColumn } from 'typeorm'; import Role from '../role/role.entity'; @ObjectType() @Entity() class User { @Field() @PrimaryGeneratedColumn() readonly id: number; @Field() @Column() @Guard('USER_SEE_NAME') //this line name: string; @Field() @Column() surname: string; } export default User;

The goal is that if a user does not have a required role the field will be sent to the client with null value.

I have found out that I should use class-transformer but I haven't found any examples of nestjs. I have also looked into nestjs documentation but there are only examples of built-in decorators and they are not used in ObjectType .

I would use Authorized decorator but I need to access nestjs context to get userId and I haven't found a way to do it.

Do you now about some examples or a ways to do it?

So after a few days I found a solution. I wrote a custom Interceptor that looks like this:

import {
  Injectable,
  ExecutionContext,
  CallHandler,
  ClassSerializerInterceptor,
  Inject,
} from '@nestjs/common';
// eslint-disable-next-line import/no-extraneous-dependencies
import { Observable } from 'rxjs';
// eslint-disable-next-line import/no-extraneous-dependencies
import { map } from 'rxjs/operators';
import { GqlExecutionContext } from '@nestjs/graphql';
import { ClassTransformOptions } from '@nestjs/common/interfaces/external/class-transform-options.interface';
import { PlainLiteralObject } from '@nestjs/common/serializer/class-serializer.interceptor';
import { CLASS_SERIALIZER_OPTIONS } from '@nestjs/common/serializer/class-serializer.constants';
import { loadPackage } from '@nestjs/common/utils/load-package.util';
import AuthService from './auth.service';

const REFLECTOR = 'Reflector';

let classTransformer: any = {};

@Injectable()
class ResourceInterceptor extends ClassSerializerInterceptor {
  constructor(
    @Inject(AuthService) private authService: AuthService,
    @Inject(REFLECTOR) protected readonly reflector: any,
  ) {
    super(reflector);
    classTransformer = loadPackage('class-transformer', 'ClassSerializerInterceptor', () =>
      // eslint-disable-next-line global-require
      require('class-transformer'),
    );
    // eslint-disable-next-line global-require
    require('class-transformer');
  }

  serializeCustom(
    response: PlainLiteralObject | Array<PlainLiteralObject>,
    options: ClassTransformOptions,
    user: number,
  ): PlainLiteralObject | PlainLiteralObject[] {
    const isArray = Array.isArray(response);
    if (!(typeof response === 'object') && response !== null && !isArray) {
      return response;
    }
    return isArray
      ? (response as PlainLiteralObject[]).map(item => this.transformToClass(item, options))
      : this.transformToGuard(this.transformToClass(response, options), user);
  }

  transformToClass(plainOrClass: any, options: ClassTransformOptions): PlainLiteralObject {
    return plainOrClass && plainOrClass.constructor !== Object
      ? classTransformer.classToClass(plainOrClass, options)
      : plainOrClass;
  }

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const options = this.getContextOptionsCustom(context);
    const ctx = GqlExecutionContext.create(context);
    const { user } = ctx.getContext().req;
    return next.handle().pipe(
      map((res: PlainLiteralObject | Array<PlainLiteralObject>) => {
        return this.serializeCustom(res, options, user);
      }),
    );
  }

  private getContextOptionsCustom(context: ExecutionContext): ClassTransformOptions | undefined {
    return (
      this.reflectSerializeMetadataCustom(context.getHandler()) ||
      this.reflectSerializeMetadataCustom(context.getClass())
    );
  }

  private reflectSerializeMetadataCustom(
    obj: object | Function,
  ): ClassTransformOptions | undefined {
    return this.reflector.get(CLASS_SERIALIZER_OPTIONS, obj);
  }

  async transformToGuard(response, userId: number) {
    // eslint-disable-next-line no-restricted-syntax
    for (const key of Object.keys(response)) {
      const item = response[key];
      // eslint-disable-next-line no-underscore-dangle
      if (typeof item === 'object' && item !== null && item.__RESOURCE_GUARD__ === true) {
        // eslint-disable-next-line no-await-in-loop
        response[key] = (await this.authService.hasAccess(userId, item.resources))
          ? response[key].value
          : null;
      }
    }
    return response;
  }
}

export default ResourceInterceptor;

Usage:

@UseInterceptors(ResourceInterceptor)
async userGetLogged(@CurrentUser() userId: number) {
  return this.userService.findById(userId);
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM