[Nest js] 시작하기

최영섭·2024년 3월 3일
0

Nest js cli로 Nest js시작하기

npm i -g @nestjs/cli
nest new project-name

module 생성

nest g module boards

nest: using nestcli

g: generate

module: schematic that I want to create

boards: name of the schematic

controller생성

nest g controller boards --no-spec

—no-spec: 테스트코드 x

contoller생성 과정

  1. cli는 먼저 board폴더 찾고
  2. board controller파일 생성
  3. board 폴더 안에 module파일 찾기
  4. module파일 안에 board Controller넣어주기

service생성

nest g service boards --no-spec 

boards service 를 boards controller에서 이용할 수 있게 해주기(Dependency injection)

@Controller('board')
export class BoardController {
	**constructor(private boardService: BoardService){}**
}

constructor(private boardService: BoardService){}

Nest JS에는 여러가지 미들웨어가 있습니다.

Pipes, Filters, Gaurds, Interceptors 등의 미들웨어로 취급되는 것들이 있는데 각각 다른 목적을 가지며 사용되고 있습니다.

Untitled

pies: 파이프는 요청 유효성 검사 및 페이로드 변환을 위한 만들어집니다. 데이터를 예상한 대로 직렬화합니다.

Filters: 필터는 오류 처리 미들웨어입니다. 특정 오류 처리기를 사용할 경로와 각 경로 주변의 복잡성을 관리하는 방법을 알 수 있습니다.

Guards: 가드는 인증 미들웨어입니다. 지정된 경로로 통과할 수 있는 사람과 허용되지 않는 사람을 서버에 알려줍니다.

Interceptors: 인터셉터는 응답 매핑 및 캐시 관리와 함께 요청 로깅과 같은 전후 미들웨어입니다. 각 요청 전후에 이를 실행하는 기능은 매우 강력하고 유용합니다.

각각의 미들웨어가 불러지는 (called)순서

middleware → guard → interceptor(before)→ pipe → controller → service → controller → interceptor(after) → filter(if application) → client

Nestjs Pipe

파이프란

파이프는 @injectable() 데코레이터로 주석이 달린 클래스이다.

파이프는 data transformation과 data validation을 위해서 사용 됩니다.

파이프는 컨트롤러 경로 처리기에 의해 처리되는 인수에 대해 작동합니다.

nest는 메소드가 호출되기 직전에 파이프를 삽입하고 파이프는 메소드로 향하는 인수를 수신하고 이에 대해 작동합니다.

Untitled

라우트 핸들러가 처리하는 인수에 대해서 작동합니다.

그리고 파이프는 메소드를 바로 직전에 작동해서 메소드를 향하는 인수에 대해서 변환할 것이 있으면 변환하고 유효성 체크를 위해서도 호출됩니다.

pipe사용하는 법(Binding Pipes)

파이프를 사용하는 방법(Bingding pipes)은 ㅅ가지로 나눠질 수 있습니다.

Handler-level Pipes, Parameter-levelPipes, Global-level Pipes입니다.

이름에서 말하는 것 그대로 핸들러 레벨, 파라미터 레벨, 글로벌 레벨로 파이프 사용할 수 있습니다.

Handler-level Pipes

@Post()
@UsePipes(pipe)
createBoard(
@Body('title') title,
@Body('decription') description
) {

}

Parameter-level Pipes

파라미터 레벨의 파이프 이기에 특정한 파라미터에게만 적용이 되는 파이프

아래와 같은 경우에는 title만 파라미터 파이프가 적용됩니다.

@Post()
createBoard(
@Body('title', ParameterPipe) title,
@Body('description') description
) {

}

Global Pipes

글로벌 파이프로서 애플리케이션 레벨의 파이프입니다.

클라이언트에서 들어오는 모든 요청에 적용이 됩니다.

가장 상단 영역이 main.ts에 넣어주시면 됩니다

async function boostrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(GlobalPipes);
await app.listen(3000);
}
bootstrap();

Built-in Pipes

Nest JS에 기본적으로 사용할 수 잇게 만들어놓은 6가지 파이프가 있습니다.

  • ValisdatiopnPipe
  • ParseInPipe
  • ParseBoolPipe
  • ParseArrayPipe
  • ParseUUIDPipe
  • DefaultValuePipe

ex) ParseInPipe

@Get(':id')
findOne(@Param('id', ParseIntPipe) id:number) {
	return;
}

파이프를 이용한 유효성 체크

필요한 모듈

class-validator, class-transformer

npm install class-validator class-transformer --save

Documentation 페이지

커스텀 파이프를 활용한 유효성 체크

커스텀 파이프 구현 방법

먼저 Pipe Transfrom이란 인터페이스를 새롭게 만들 커스텀 파이프에 구현해줘야 합니다 이 Pipe Transfrom 인터페이스는 모든 파이프에서 구현해줘야 하는 인터페이스입니다. 그리고 이것과 함께 모든 파이프는 transfrom()메소드를 필요합니다. 이 메소드는 NestJS가 인자(arguments)를 처리하기 위해서 사용됩니다.

transform()메소드

이 메소드느 두 개의 파라미터를 가집니다.

첫번째 파라미터는 처리가 된 인자의 값(value)이며 두 번째 파라미터는 인자에 대한 메타 데이터를 포함한 객체입니다.

transfrom()메소드에서 Return된 값은 Route핸들러로 전해집니다.만약 예외가 발생하면 클라이언트에 바로 전해집니다.

Untitled

Untitled

파라미터 수준의 파이프라인 추가

특정 게시물을 찾을 때 없는 경우 결과 값 처리

현재 특정 게시물을 ID로 가져올때 만약 없는 아이디의 계시물을 가져오려고 한다면 결과값으로 아무 내용 없이 돌아옵니다. 그래서 그 부분을 게시물이 없는 것이면 없다고 내용을 넣어서 클라이언트로 보내 주겠습니다

에러를 표출해주기 위해서는…

찾는 게시물이 없을 때는

예외 인스턴스를 생성해서 이용해주면 됩니다

Untitled

없는 게시물을 지우려 할 때 결과 값 처리

PostgresSQL설치하기

window에서 PostgresSQL설 치하기

https://www.postgressql.org/download/windows/

Mac에서 PostgresSQL설치하기

https://postgresapp.com/downloads.html

Window & Mac 에서 pgAdmin 설치하기

https://www.pgadmin.org/download

TypeORM애플리케이션에서 이용하기

TypeORM을 사용하기 위해서 설치해야하는모듈들

  1. @nestjs/typeorm(nest js 에서 typeorm사용할 수 있도록 연동시켜주는 모듈)
  2. typeorm
  3. pg
npm install pg typeorm @nestjs/typeorm --save

https://docs.nestjs.com/techniques/database

TypeORM 애플리케이션에 연결

  1. typeORM 설정 파일 생성

    Untitled

  2. typeORM설정파일 작성

    • synchronize true는 production모드에서는 false로, 그렇지 않을 시 데이터를 잃을 수도 있습니다.
    import { TypeOrmModuleOptions } from '@nestjs/typeorm';
    
    export const typeORMConfig: TypeOrmModuleOptions = {
      type: 'postgres',
      host: '',
      port: 5432,
      username: 'youngsup',
      password: 'postgres',
      database: 'board-app',
      entities: [__dirname + '/../**/*.entity.{js,ts}'],
      //배포시에는 false로 true값을 주면 애플리케이션을 다시 실행할때 엔티티 안에서 수정된 컬럼의 길이 타입 변경값 등을 해당 테이블을 Drop한 후 다시 생성합니다.
      synchronize: true,
    };
  3. 루트 Module에서 Import합니다.

    Untitled

    forRoot안에 넣어준 설정(configuration)은 모든 Sub-Module 부수적인 모듈들에 다 적용이 됩니다.

엔티티 생성

엔티티 생성 코드

@Entity()
export class Board extends BaseEntity {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  title: string;

  @Column()
  description: string;

  @Column()
  status: BoardStatus;
}

Untitled

Repository 생성하기

리포지토리는 엔티티 개체와 함께 작동하며 엔티티 찾기, 삽입, 업데이트, 삭제 등을 처리합니다.

공식 문서 주소

http://typeorm.delightful.studio/classes/_repository_repository_repository.html

  1. 리포지토리 파일 생성하기
  • board.repository.ts
  1. 생성한 파일에 리포지토리를 위한 클래스 생성하기
  • 생성 시 Repository 클래스를 Extends해줍니다. (Find, Insert, Delete등 엔티티를 컨트롤 해줄 수 있습니다.)
  • @EntityRepository() - 클래스를 사용자 정의저장소로 선언하는 데 사용됩니다. 사용자 지정 저장소는 일부 특정 엔티티를 관리하거나 일반 저장소 일 수 있습니다. → 현재는 legacy, 사라질 예정 아래와 같이 사용하는 것이 바람직
    import { Repository } from 'typeorm';
    import { Board } from 'src/boards/boards.entity';
    
    export class BoardRepository extends Repository<Board>.extend({
      // 커스텀 메세지 추가 가능
    }) {
    
    }
  1. 생성한 Repository를 다른 곳에서도 사용할 수 있도록 하기 위해서 (injectable)board.module에서 import해줍니다.
    • board.module.ts
      import { Module } from '@nestjs/common';
      import { BoardsController } from './boards.controller';
      import { BoardsService } from './boards.service';
      import { TypeOrmModule } from '@nestjs/typeorm';
      import { BoardRepository } from 'src/boards/board.repository';
      
      @Module({
        **imports: [TypeOrmModule.forFeature([BoardRepository])],**
        controllers: [BoardsController],
        providers: [BoardsService],
      })
      export class BoardsModule { }

Service에 Repository넣어주기(Repository Injection)

@Injectable()
export class BoardsService {
  constructor(
    @InjectRepository(BoardRepository)
    private boardRepository: BoardRepository,
  ) { }

@injectRepository

  • 이 데코레이터를 이용해서 이 서비스에서 BoardRepository를 이용한다고 이걸 boardRepository변수에 넣어줍니다.

Service에서 getBoardById메소드 생성하기

typeORM에서 제공하는 findOne메소드 사용

게시물 삭제하기

remove() vs delete()

  • remove: 무조건 존재하는 아이템을 remve메소드를 이용해서 지워야합니다 .그러지 않으면 에러가 발생
  • delete: 만약 아이템이 존재하면 지우고 존재하지 않으면 아무런 영향이 없다.

document

https://github.com/typeorm/typeorm/blob/master/docs/repository-api.md

board.service.ts

async deleteBoardById(id: number): Promise<void> {
    const result = await this.boardRepository.delete(id);
    if (result.affected === 0) {
      throw new NotFoundException(`Can't find board with id ${id}`);
    }
    console.log('result', result);
  }

인증 기능 구현을 위한 준비

Untitled

CLI를 이용한 모듈, 컨트롤러, 서비스 생성

nest g module auth
nest g controller auth --no-spec
nest g service auth --no-spec

User entity생성

  1. user.entity.ts파일 생성
  2. 파일 소스 코드 작성
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from "typeorm";

@Entity()
export class User extends BaseEntity {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  username: string;

  @Column()
  password: string;
}

UserReposityry생성

  1. user.repository.ts파일 생성
  2. 소스코드 작성
import { User } from 'src/auth/user.entity';
import { EntityRepository, Repository } from 'typeorm';

@EntityRepository(User)
export class UserRepository extends Repository<User> {
}

생성된 UserRepository를 다른 곳에 사용하기 위해

auth.module.ts

import { Module } from '@nestjs/common';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from 'src/auth/user.entity';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  controllers: [AuthController],
  providers: [AuthService],
})
export class AuthModule { }

Repository Injection

User Repository를 auth Service안에서 사용하기 위해 유저 리포지터리를 넣어주기

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { UserRepository } from 'src/auth/user.repository';

@Injectable()
export class AuthService {

  constructor(
    @InjectRepository(UserRepository)
    private userRepository: UserRepository) {

  }

}

회원가입 기능 구현

user.repository.ts

import { AuthCredentialsDto } from 'src/auth/dto/auth-credential.dto';
import { User } from 'src/auth/user.entity';
import { EntityRepository, Repository } from 'typeorm';

@EntityRepository(User)
export class UserRepository extends Repository<User> {
  async createUser(authCredentialsDto: AuthCredentialsDto): Promise<User> {
    const { username, password } = authCredentialsDto;
    const user = this.create({
      username,
      password,
    });

    await this.save(user);
    return user;
  }
}

auth/dto/auth-credential.dto.ts

export class AuthCredentialsDto {
  username: string;
  password: string;
}

user.service.ts

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { AuthCredentialsDto } from 'src/auth/dto/auth-credential.dto';
import { User } from 'src/auth/user.entity';
import { UserRepository } from 'src/auth/user.repository';
import { Repository } from 'typeorm';

@Injectable()
export class AuthService {
  constructor(
    @InjectRepository(User)
    private userRepository: Repository<User>,
  ) { }

  async signUp(authCredentialsDto: AuthCredentialsDto): Promise<void> {
    const { username, password } = authCredentialsDto;
    const user = this.userRepository.create({
      username,
      password,
    });

    await this.userRepository.save(user);
  }
}

user.controller.ts

import { Body, Controller, Post } from '@nestjs/common';
import { AuthService } from 'src/auth/auth.service';
import { AuthCredentialsDto } from 'src/auth/dto/auth-credential.dto';

@Controller('auth')
export class AuthController {
  constructor(private authService: AuthService) { }
  //localhost:3000/auth/signup
  @Post('/signup')
  signUp(@Body() authCredentialsDto: AuthCredentialsDto): Promise<void> {
    return this.authService.signUp(authCredentialsDto);
  }
}

유저 데이터 유효성 체크

Class-validator

https://github.com/typestack/class-validator
Dto파일에서 Request로 들어오는 갑승ㄹ 정의해주고 있기때문에 Dto파일에 값들 하나하나에 class-validator를 이용해서 유효성 조건을 넣어주겠습니다.

import { IsString, Matches, MaxLength, MinLength } from "class-validator";

export class AuthCredentialsDto {
  @IsString()
  @MinLength(4)
  @MaxLength(20)
  username: string;
  @IsString()
  @MinLength(4)
  @MaxLength(20)
  //영어랑 숫자만 가능한 유효성 체크
  @Matches(/^[a-zA-Z0-9]*$/, {
    message: 'password only accepts english and numbers',
  })
  password: string;
}

ValidationPipe

signup 핸들러가 실행이 되기 전에 password와 username조건에 맞는지 확인해줌

@Post('/signup')
  signUp(
    @Body(ValidationPipe)
    authCredentialsDto: AuthCredentialsDto
  ): Promise<void> {
    return this.authService.signUp(authCredentialsDto);
  }

유저 이름 유니크하도록 유지

두가지 방법

  1. repository에서 findeOne메소드를 이용해서 같은 유저 이름을 가진 아이디가 있는지 확인하고 없다면 데이터를 저장하는 방법입니다.

  2. 데이터베이스 레벨에서 만약 같은 이름을 가진 유저가 있다면 에러를 던져주는 방법

    user.entity.ts에서 원하는 유니크한 값을 원하는 필드 값을 정해주면 됨

    import { BaseEntity, Column, Entity, PrimaryGeneratedColumn, Unique } from "typeorm";
    
    @Entity()
    @Unique(['username'])
    export class User extends BaseEntity {
      @PrimaryGeneratedColumn()
      id: number;
    
      @Column()
      username: string;
    
      @Column()
      password: string;
    }

하지만 위와 같이만 구현을 해놓는다면 500에러가 나오게됨, try catch구문에서 잡지 않으면 controller레벨로 가서 500에러가 생겨버림

Try Catch

async signUp(authCredentialsDto: AuthCredentialsDto): Promise<void> {
    const { username, password } = authCredentialsDto;
    const user = this.userRepository.create({
      username,
      password,
    });
    try {
      await this.userRepository.save(user);

    } catch (error) {
      if (error.code === '23505') {
        throw new ConflictException('Existing username');
      } else {
        throw new InternalServerErrorException();
      }
    }
  }

비밀번호 암호화하기

bcryptjs

npm install bcryptjs --save
import * as bcrypt from 'bcryptjs'

user.repository.ts

const { username, password } = authCredentialsDto;
    const salt = await bcrypt.genSalt();
    const hashedPassword = await bcrypt.hash(password, salt);
    const user = this.userRepository.create({
      username,
      password: hashedPassword,
    });

로그인 기능 구현하기

user.service.ts

async signIn(authCredentialsDto: AuthCredentialsDto): Promise<string> {
    const { username, password } = authCredentialsDto;
    const user = await this.userRepository.findOne({ where: { username } });
    if (user && (await bcrypt.compare(password, user.password))) {
      return 'login success';
    } else {
      throw new UnauthorizedException('login failed')
    }
  }

JWT에 대하여

JWT의 구조

Header

토큰에 대한 메타 데이터를 포함하고 있습니다.(타입, 해싱알고리즘 SHA256, RSA…)

Payload

유저정보(isuser), 만료기간(expiration time), 주제(subject) 등등…

Verify Signature

JWT의 마지막 세그먼트는 토큰이 보낸 사람에 의해 서명되었으며 어떤 식으로든 변겨오디지 않았는지 확인하는 데 사용되는 서명입니다. 서명은 헤더 및 페이로드 세그먼트, 서명 알고리즘, 비밀 또는 공개 키를 사용하여 생성됩니다.

JWT사용 흐름

Untitled

Untitled

비교하는 과정

Untitled

JWT를 이용해서 토큰 생성하기

필요한 모듈 설치

@nestjs/jwt

@nestjs/passport

@types/passport-jwt 모듈

(passport-jwt모듈을 위한 타입 정의 모듈)

passport-jwt

npm install @nestjs/jwt @nestjs/passport passport passport-jwt --save
npm install  @types/passport-jwt --save

애플리케이션에 JWT모듈 등록하기

  1. auth모듈 imports에 넣어주기

    auth.module.ts

    import { Module } from '@nestjs/common';
    import { AuthController } from './auth.controller';
    import { AuthService } from './auth.service';
    import { TypeOrmModule } from '@nestjs/typeorm';
    import { User } from 'src/auth/user.entity';
    import { JwtModule } from '@nestjs/jwt';
    import { PassportModule } from '@nestjs/passport';
    
    @Module({
      imports: [
        PassportModule.register({ defaultStrategy: 'jwt' }),
        JwtModule.register({
          secret: 'Secret1234',
          signOptions: {
            expiresIn: 3600,
          },
        }),
        TypeOrmModule.forFeature([User]),
      ],
      controllers: [AuthController],
      providers: [AuthService],
    })
    export class AuthModule { }

로그인 성공시 JWT를 이용해서 토큰 생성해주기

if (user && (await bcrypt.compare(password, user.password))) {
      // 유저 토큰 생성 ( Secret + Payload )
      // 중요한 정보는 넣으면 안됨
      const payload = { username };
      const accessToken = await this.jwtService.sign(payload);
      return { accessToken };
    }

Simple flow of how to remember and prove who I am

Untitled

Untitled

위 과정을 구현하는 순서

npm install  @types/passport-jwt --save
  1. jwt.strategy.ts파일 생성

    import { Injectable, UnauthorizedException } from "@nestjs/common";
    import { PassportStrategy } from "@nestjs/passport";
    import { InjectRepository } from "@nestjs/typeorm";
    import { ExtractJwt, Strategy } from "passport-jwt";
    import { User } from "src/auth/user.entity";
    import { Repository } from "typeorm";
    
    //얘를 다른 곳에서도 주입해서 사용할 수 있도록 해당 데코레이터 추가
    @Injectable()
    export class JwtStrategy extends PassportStrategy(Strategy) {
      constructor(
        @InjectRepository(User)
        private userRepository: Repository<User>,
      ) {
        super({
          secretOrKey: 'Secret1234',
          jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
        });
      }
      async validate(payload) {
        const { username } = payload;
        const user: User = await this.userRepository.findOne({
          where: { username },
        });
        if (!user) {
          throw new UnauthorizedException();
        }
        return user;
      }
    }
  2. AuthModule Providers항목에 넣어주고 다른 곳에서도 사용해야하기 때문에 exports항목에도 넣어줌

    import { Module } from '@nestjs/common';
    import { AuthController } from './auth.controller';
    import { AuthService } from './auth.service';
    import { TypeOrmModule } from '@nestjs/typeorm';
    import { User } from 'src/auth/user.entity';
    import { JwtModule } from '@nestjs/jwt';
    import { PassportModule } from '@nestjs/passport';
    import { JwtStrategy } from 'src/auth/jwt.strategy';
    
    @Module({
      imports: [
        PassportModule.register({ defaultStrategy: 'jwt' }),
        JwtModule.register({
          secret: 'Secret1234',
          signOptions: {
            expiresIn: 3600,
          },
        }),
        TypeOrmModule.forFeature([User]),
      ],
      controllers: [AuthController],
      // authmodule에서 사용하기 위함
      providers: [AuthService, JwtStrategy],
      // 다른 모듈에서 이용하기 위함
      exports: [JwtStrategy, PassportModule],
    })
    export class AuthModule { }
  3. 요청 안에 유저 정보(유저객체)가 들어가게 하는 방법

    UseGuards안에 @nestjs/passport에서가져온 AuthGuard()를 이용하면 요청 안에 유저 정보를 넣어줄 수 있습니다.

    UserGuards

    @Post('/test')
      // 유저 정보를 불러오기도 하고 인증에 대한 미들웨어 처리를 함, 토큰이 없거나 잘못된거라면 reject
      @UseGuards(AuthGuard())
      test(@Req() req) {
        console.log(req.user);
      }
    }

커스텀 데코레이터 생성하기

요청 안에 유저가 바로 들어오도록 하기위해서

get-user.decorator.ts

import { ExecutionContext, createParamDecorator } from '@nestjs/common';
import { User } from 'src/auth/user.entity';

export const GetUser = createParamDecorator(
  (data, ctx: ExecutionContext): User => {
    const req = ctx.switchToHttp().getRequest();
    return req.user;
  },
);
  1. HTTP 관련 메서드
    • switchToHttp(): HTTP 특화된 컨텍스트 객체를 반환합니다.
    • getRequest(): 현재 HTTP 요청 객체를 반환합니다.
    • getResponse(): 현재 HTTP 응답 객체를 반환합니다.
  2. WebSocket 관련 메서드
    • switchToWs(): WebSocket 특화된 컨텍스트 객체를 반환합니다.
    • getClient(): WebSocket 클라이언트 연결을 반환합니다.
    • getData(): WebSocket 이벤트에서 전송된 데이터를 반환합니다.
  3. Microservices (RPC) 관련 메서드
    • switchToRpc(): Microservices 컨텍스트 객체를 반환합니다.
    • getContext(): Microservice 요청의 컨텍스트를 반환합니다. 예를 들어, gRPC 요청의 컨텍스트입니다.
    • getArgs(): RPC 요청의 인자들을 반환합니다.
  4. 기타 유틸리티 메서드
    • getClass(): 현재 실행 중인 컨트롤러 클래스를 반환합니다.
    • getHandler(): 현재 실행 중인 핸들러 함수(예: 컨트롤러의 메서드)를 반환합니다.
    • getType(): 현재 컨텍스트의 타입을 반환합니다. 예를 들어, 'http', 'ws', 'rpc' 중 하나입니다.

인증된 유저만 게시물 보고 쓸 수 있게 만들기

유저에게 게시물 접근 권한 주기

  1. 인증에 관한 모듈을 board모듈에서 쓸 수 있어야 하기에 board moduledptj 인증 모듈 imports해오기(이렇게 되면 AuthModule에서 export하는 어떠한 것이든 board Module에서 사용 가능하게 됩니다.)

    import { Module } from '@nestjs/common';
    import { BoardsController } from './boards.controller';
    import { BoardsService } from './boards.service';
    import { TypeOrmModule } from '@nestjs/typeorm';
    import { Board } from 'src/boards/boards.entity';
    import { AuthModule } from 'src/auth/auth.module';
    
    @Module({
      imports: [TypeOrmModule.forFeature([Board]), AuthModule],
      controllers: [BoardsController],
      providers: [BoardsService],
    })
    export class BoardsModule { }
  2. UseGuard(AuthGuard())를 이용해서 이 사람이 요청을 줄 때 올바른 토큰을 가지고 요청을 주는지 본 후에 게시물에 접근할 권한을 줍니다. 그리고 이 AuthGuard는 각각의 라우트 별로 줄 수도 있고 한번에 하나의 컨트롤러 안에 들어있는 모든 라우트에 줄 수도 있습니다. 현재는 board컨트롤러 안에 있는 모든 라우트에 AuthGuard를 적용해보겠습니다.

    boards.controller.ts

    @Controller('boards')
    // 모든 핸들러가 영향을 받음 
    @UseGuards(AuthGuard())
    export class BoardsController {
      constructor(private boardsService: BoardsService) { }

유저와 게시물의 관계 형성해주기

유저와 게시물 데이터의 관계 형성

user.entity.ts

import { Board } from "src/boards/boards.entity";
import { BaseEntity, Column, Entity, OneToMany, PrimaryGeneratedColumn, Unique } from "typeorm";

@Entity()
@Unique(['username'])
export class User extends BaseEntity {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  username: string;

  @Column()
  password: string;

  @OneToMany((type) => Board, (board) => board.user, { eager: true })
  boards: Board[];
}

(type) => Board : board의 타입,

board ⇒ board.user : board에서 유저에 접근하려면 어떻게 해야하는지 명시

{eager:true} : 이 유저정보를 가져올때 보드 정보도 같이 가져온다는 것

board.entity.ts

import { User } from 'src/auth/user.entity';
import { BoardStatus } from 'src/boards/board-stauts.enum';
import { BaseEntity, Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class Board extends BaseEntity {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  title: string;

  @Column()
  description: string;

  @Column()
  status: BoardStatus;

  @ManyToOne((type) => User, (user) => user.boards, { eager: false })
	@JoinColumn({ name: "userId", referencedColumnName: "id" })
  user: User;
}

게시물 생성할 때 유저 정보 넣어주기

board.controller.ts

@Post()
  @UsePipes(ValidationPipe)
  createBoard(
    @Body() createBoardDto: CreateBoardDto,
    @GetUser() user:User): Promise<Board> {
    return this.boardsService.createBoard(createBoardDto, user);
  }

board.service.ts

async createBoard(
    createBoardDto: CreateBoardDto,
    user: User): Promise<Board> {
    const { title, description } = createBoardDto;
    const board = this.boardRepository.create({
      title,
      description,
      status: BoardStatus.PUBLIC,
      user,
    });

    await this.boardRepository.save(board);
    return board;
  }

해당 유저의 게시물만 가져오기

boards.controller.ts

@Get('/')
  getAllBoards(@GetUser() user: User): Promise<Board[]> {
    return this.boardsService.getAllBoards(user);
  }

boards.service.ts

async getAllBoards(user: User): Promise<Board[]> {
    const query = this.boardRepository.createQueryBuilder('board');
    query.where('board.userId = :userId', { userId: user.id });
    const boards = await query.getMany();
    return boards;
  }

자신이 생성한 게시물 자신만 삭제하기

boards.controller.ts

@Delete('/:id')
  deleteBoard(
    @Param('id', ParseIntPipe) id: number,
    @GetUser() user: User,
  ): Promise<void> {
    return this.boardsService.deleteBoardById(id, user);
  }

boards.service.ts

async deleteBoardById(id: number, user: User): Promise<void> {
    const result = await this.boardRepository.delete({id, user});
    if (result.affected === 0) {
      throw new NotFoundException(`Can't find board with id ${id}`);
    }
    console.log('result', result);
  }

로그에 대해서

로그의 종류

Untitled

로그 레벨

원하는 대로 환경에 따라서 로그의 레벨을 정의해서 넣어줄 수 있다.

Untitled

실제로 애플리케이션에 로그 적용하기

main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { Logger } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
  const port = 3000;
  Logger.log(`Application running on port ${port}`);
}
bootstrap();

boards.controller.ts

@Controller('boards')
@UseGuards(AuthGuard())
export class BoardsController {
  private logger = new Logger('BoardsController');
  constructor(private boardsService: BoardsService) { }

  @Get('/')
  getAllBoards(@GetUser() user: User): Promise<Board[]> {
    this.logger.verbose(`User ${user.username} trying to get all boards`);
    return this.boardsService.getAllBoards(user);
  }
@Post()
  @UsePipes(ValidationPipe)
  createBoard(
    @Body() createBoardDto: CreateBoardDto,
    @GetUser() user:User): Promise<Board> {
    this.logger.verbose(`User ${user.username} creating a new board.
      Payload: ${JSON.stringify(createBoardDto)}`)
    return this.boardsService.createBoard(createBoardDto, user);
  }

설정(configuration)이란?

소스 코드 안에서 어떤 코드들은 개발 환경이나 운영 환경에 이러한 환경에 다르게 코드를넣어줘야할 때가 있으며, 남들에게 노출되지 않아야 하는 코드들도 있습니다. 이러한 코드들을 위해서 설정 파일을 따로 만들어서 보관해주겠습니다.

설정파일은

runtime도중에 바뀌는 것이 아닌 애플리케이션이 시작될때 로드가 되어 그 값들ㅇ릉 정의하여 줍니다. 그리고 설정 파링ㄹ은 여러가지 파일형식을 사용할 수 있습니다. 예로는 XML, JSON, YAML, Environmant Variables같이 많은 형식을 이용할 수 있습니다.

Cdoebase VS Enviroment Variables(환경 변수)

XML, JSON, YALM은 codebase에 해당하며 그리고 다른 방법은 환경변수로 할 수 있습니다. 주로 이 둘을 나눠서 하는 이유는 비밀번호와 API Key와 같은 남들에게 노출되면 안되는 정보들을 주로 환경 변수를 이용해서 처리해줍니다.

Untitled

설정하기 위해서 필요한 모듈

윈도우에서 win-node-env를 설치해야합니다.

(윈도우에서는 기본적으로환경변수를 지원하지 않음)

npm install -g win-node-env

그리고 윈도우와 맥 모두에서는 config라는 모듈을 설치받아야합니다.

npm install config --save

config모듈을 이용한 설정 파일 생성

  1. 루트 디렉토리에 config라는 폴더를 만든 후에 그 폴더 안에 JSON이나 YALM형식의 파일을 생성합니다. config/default.yaml
  2. config폴더 안에 default.yml, development.yml, production.yml파일을 생성

Untitled

default.yml

server:
  port: 3000

db:
  type: 'postgres'
  port: 5432
  database: 'board-app'

jwt:
  expiresIn: 3600

development.yml

db:
  host: 'localhost'
  username: 'postgres'
  password: 'postgres'
  synchronize: true

jwt:
  secret: 'Secret1234'

production.yml

db:
  synchronize: false

config폴더 안에 저장된 것들을 사용하는 방법

  1. 어느 파일에서든지 config모듈을 import해서 사용

    import * as config from 'config';
  2. config.get(’server’) ⇒ 이렇게 하면 {port: 3000}이렇게 나옴

main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { Logger } from '@nestjs/common';
import * as config from 'config';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  const serverConfig = config.get('server');
  const port = serverConfig.port
  await app.listen(port);
  Logger.log(`Application running on port ${port}`);
}
bootstrap();

설정과 환경변수 코드에 적용하기

typeorm.config.ts

import { TypeOrmModuleOptions } from '@nestjs/typeorm';
import * as config from 'config';
const dbConfig = config.get('db');

export const typeORMConfig: TypeOrmModuleOptions = {
  type: dbConfig.type,
  host: process.env.RDS_HOSTNAME || dbConfig.host,
  port: process.env.RDS_PORT || dbConfig.host,
  username: process.env.RDS_USERNAME || dbConfig.username,
  password: process.env.RDS_PASSWORD || dbConfig.password,
  database: process.env.RDS_DB_NAME || dbConfig.database,
  entities: [__dirname + '/../**/*.entity.{js,ts}'],
  //배포시에는 false로 true값을 주면 애플리케이션을 다시 실행할때 엔티티 안에서 수정된 컬럼의 길이 타입 변경값 등을 해당 테이블을 Drop한 후 다시 생성합니다.
  synchronize: dbConfig.synchronize,
};

auth.module.ts

import { Module } from '@nestjs/common';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from 'src/auth/user.entity';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { JwtStrategy } from 'src/auth/jwt.strategy';
import * as config from 'config';

const jwtConfig = config.get('jwt');
@Module({
  imports: [
    PassportModule.register({ defaultStrategy: 'jwt' }),
    JwtModule.register({
      secret: process.env.JWT_SECRET || jwtConfig.secret,
      signOptions: {
        expiresIn: jwtConfig.expiresIn,
      },
    }),
    TypeOrmModule.forFeature([User]),
  ],
  controllers: [AuthController],
  // authmodule에서 사용하기 위함
  providers: [AuthService, JwtStrategy],
  // 다른 모듈에서 이용하기 위함
  exports: [JwtStrategy, PassportModule],
})
export class AuthModule { }
profile
세상에 필요한 것을 고민하고 그것을 만드는 과정에서 문제를 해결하는 일이 즐겁습니다. 창업, 백엔드, RAG에 관심을 가지고있습니다.

0개의 댓글