avatar
Published on

NestJS 프레임워크 - Decorator

Authors
  • avatar
    Name
    Haneul
    Twitter

NestJS 프레임워크 - Decorator

NestJS는 ES7(혹은 타입 스크립트)에 새로 추가된 @ 데코레이터(Decorator)를 주로 이용한다.


@Role 데코레이터

SetMetadata는 localstorage 처럼 metadata를 key/value로 저장 해줍니다.
'roles'라는 key에 roles라는 변수를 값으로 metadata에 저장을 해 놓아서 어디서든 꺼내볼 수 있습니다.
물론 우리는 AuthGuard에서 꺼내봅니다.

RoleType은 keyof typeof로 지정 해 놓을 수 있습니다.
keyof 연산자는 피연산자의 키타입에 해당하는 타입을 리턴해줍니다.
이를테면, let person: Person { name: 'Jarid', age: 35} 라는 오브젝트에서 let personProp: keyof Person;으로 정의하면 personProp이 가질 수 있는 값은 Person의 key 값이 ”name”|”age”가 됩니다.

먼저 Union of literal types이라는 개념을 이해하면 좋은데, type Greeting = 'Hello';
에서 Greeting 타입은 'Hello' 하나만 가능하지만,

type Greeting = 'Hello'|'Hi'|'Welcome';
에서 Greeting 타입은 'Hello', 'Hi', 'Welcome' 이 세가지 값을 가질 수 있습니다.

enum에서도 union of literal types를 이용할 수 있습니다.
enum을 union of literal types로 만들기 위해 사용하는 것이 keyof typeof 입니다.
아래에서는 추가적으로 'Any'타입까지 추가시켰으므로, keyof typeof UserRole | 'Any'로 사용한 것입니다.

import { UserRole } from 'src/users/entities/user.entity'
import { SetMetadata } from '@nestjs/common'

export type AllowedRoles = keyof typeof UserRole | 'Any'

export const Role = (roles: AllowedRoles[]) => SetMetadata('roles', roles)

APP_GUARD

import { Module } from '@nestjs/common'
import { APP_GUARD } from '@nestjs/core'
import { AuthGuard } from './auth.guard'

@Module({
  providers: [
    {
      provide: APP_GUARD,
      useClass: AuthGuard,
    },
  ],
})
export class AuthModule {}

@Role 데코레이터와 @UseGuards (AuthGuard) 를 같이 사용하는것은 좋지 않다.
그래서 nestjs 에서 제공해주는 APP_GUARD 상수를 이용해서 UseGuards 사용을 줄일 수 있습니다.
위와 같이 auth 모듈 설정을 해주면, 더이상 UseGuard는 사용하지 않으셔도 됩니다
대신에 Auth module을 App 모듈에서 꼭 import 해주셔야 합니다.

AuthGuard

SetMetadata로 넘겨준 'roles'라는 키를 가진 metadata를 AuthGuard가 작동할 때 값을 가져와서 role이 어떤지 확인 해야 합니다.
constructor에 보면 어김없이 나오는 parameter properties가 보입니다.

reflector라는 property가 Reflector 클래스 타입으로 설정되었습니다.
this.reflector.get<AllowedRoles> 에서 SetMetadata로 넘겨준 'roles' 키에 해당하는 값을 얻어올 수 있습니다.

Role을 설정 안해주면 public resolver로 취급할 수 있다는 의미입니다.
https://developer.mozilla.org/ko/docs/Glossary/Falsy

Role 값들을 얻어 오려면 메타데이터를 설정을 해줘야 하는데, 앞에 코드처럼 우리는 @Role 데코레이터를 이용하여 설정하기로 했습니다.
사용을 할 때 원하는 resolver 위에 사용해주면 됩니다.

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'
import { GqlExecutionContext } from '@nestjs/graphql'
import { Reflector } from '@nestjs/core'
import { AllowedRoles } from './role.decorator'
import { User } from 'src/users/entities/user.entity'

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private readonly reflector: Reflector) {}

  canActivate(context: ExecutionContext) {
    const roles = this.reflector.get<AllowedRoles>('roles', context.getHandler())
    if (!roles) {
      return true
    }
    const gqlContext = GqlExecutionContext.create(context).getContext()
    const user: User = gqlContext['user']
    if (!user) {
      return false
    }
    if (roles.includes('Any')) {
      return true
    }
    return roles.includes(user.role)
  }
}

Resolver 에서 @Role 데코레이터 사용

getAllPodcasts에는 @Role 이 없는데, createPodcast에는 @Role 이 있습니다.
전자는 Role이 없으므로, 우리의 AuthGuard 코드에 보면, true를 리턴하게 되므로 public한 resolver로 볼 수 있습니다.
로그인하지 않아도 팟캐스트들을 검색할 수 있습니다.

반면에 createPodcast를 보면 Role이 있는데 'Host'여야 한다는 의미로 AuthGuard에서 처리 해줄 수 있습니다.
로그인 하지 않거나 Listener인 유저에게 Forbidden Resource 에러가 발생하게 됩니다.

 @Query((returns) => GetAllPodcastsOutput)
  getAllPodcasts(): Promise<GetAllPodcastsOutput> {
    return this.podcastsService.getAllPodcasts();
  }

  @Mutation((returns) => CreatePodcastOutput)
  @Role(["Host"])
  createPodcast(
    @Args("input") createPodcastInput: CreatePodcastInput
  ): Promise<CreatePodcastOutput> {
    return this.podcastsService.createPodcast(createPodcastInput);
  }

참고 : https://docs.nestjs.com/guards