Search code examples
javascriptnestjsgraphql-jstypegraphqlclass-transformer

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?


Solution

  • 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);
    }