nestjs

[NestJS] 인프런 -따라하며 배우는 NestJS 3

goldjun 2022. 8. 11. 17:06

인증 기능 구현하기

인증 기능 구현을 위한 준비

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

회원가입 기능 구현

user.entity.ts

import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from "typeorm";

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

    @Column()
    username: string;

    @Column()
    password: string;
}

user.repository.ts

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

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

auth.module.ts

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

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

UserRepository 등록했다. forFeature는 이 모듈 안에 등록을 해줬다는 말이다.

auth.service.ts 

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { AuthCredentialsDto } from './dto/auth-credential.dto';
import { UserRepository } from './user.respository';

@Injectable()
export class AuthService {

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

    async signUp(authCredentialsDto: AuthCredentialsDto): Promise<void> {
        return this.userRepository.createUser(authCredentialsDto);
    }
}

UserRepository DI

auth-credential.dto.ts

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

auth-controller.ts

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

@Controller('auth')
export class AuthController {

    constructor(private authService: AuthService){}

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

유저 데이터 유효성 체크

auth-credential.dto.ts

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 number'}
    )
    password: string;
}

ValidationPipe
요청이 컨트롤러에 있는 핸들러로 들어왔을 때 Dto에 있는 유효 성 조건에 맞게 체크를 해주려면 ValidationPipe을 넣어주셔야 합 니다.

auth.controller.ts

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

유저 이름에 유니크한 값 주기

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

unique 데코레이터를 사용했다.

Try Catch

  • 이미 있는 유저를 다시 생성하려 하면 아래와 같이 에러가 나옵 니다. 하지만 그냥 500 에러를 던져버립니다.
  • 그 이유는 Nest JS에서 에러가 발생하고 그걸 try catch 구문인 catch에서 잡아주지 않는 다면 이 에러가 Controller 레벨로 가서 그냥 500 에러를 던져 버립니다.
  • 이러한 이유 때문에 try catch 구문으로 에러를 잡아줘야합니다.

user.repository.ts

import { ConflictException } from "@nestjs/common";
import { EntityRepository, Repository } from "typeorm";
import { AuthCredentialsDto } from "./dto/auth-credential.dto";
import { User } from "./user.entity";

@EntityRepository(User)
export class UserRepository extends Repository<User> {
    async createUser(authCredentialsDto: AuthCredentialsDto): Promise<void> {
        const {username, password} = authCredentialsDto;
        const user = this.create({username, password});
        
        try {
            await this.save(user);
        } catch (error) {
            if (error.code === '23305') {
                throw new ConflictException('Existing username');
            } else {
                console.log('error', error);
            }
        }
    }
}

비밀번호 암호화 하기

이번 시간에는 유저를 생성할 때 현재는 비밀번호가 그대로 데이 터베이스에 저장됩니다. 그래서 비밀번호를 암호화 해서 저장을 하는 부분을 구현해주겠습니다.

bcryptjs
이 기능을 구현하기 위해서 bcryptjs 라는 모듈을 사용하겠습니다.

npm install bcryptjs --save

레인보우 테이블이란 수 많은 암호화된 비밀번호를 저장해서 그것을 알아내는 방법이다.
그래서 솔트라는 유니크한 값을 통해 암호화는게 더 낫다.

비밀번호 암호화하기 (소스코드 구현)

user.repository.ts

import { ConflictException } from "@nestjs/common";
import { EntityRepository, Repository } from "typeorm";
import { AuthCredentialsDto } from "./dto/auth-credential.dto";
import { User } from "./user.entity";
import * as bcrypt from 'bcryptjs';

@EntityRepository(User)
export class UserRepository extends Repository<User> {
    async createUser(authCredentialsDto: AuthCredentialsDto): Promise<void> {
        const {username, password} = authCredentialsDto;
        
        const salt = await bcrypt.genSalt();
        const hasedPassword = await bcrypt.hash(password, salt);

        const user = this.create({username, password: hasedPassword});
        
        try {
            await this.save(user);
        } catch (error) {
            if (error.code === '23305') {
                throw new ConflictException('Existing username');
            } else {
                console.log('error', error);
            }
        }
    }
}

로그인 기능 구현하기

auth.service.ts

import { Injectable, UnauthorizedException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { AuthCredentialsDto } from './dto/auth-credential.dto';
import { UserRepository } from './user.respository';
import * as bcrypt from 'bcryptjs';

@Injectable()
export class AuthService {

	...
    async signIn(authCredentialsDto: AuthCredentialsDto): Promise<string> {
        const {username, password} = authCredentialsDto;
        const user = await this.userRepository.findOne({username});

        if (user && (await bcrypt.compare(password, user.password))) {
            return 'login success';
        } else {
            throw new UnauthorizedException('login failed');
        }
    }
}

auth.controller.ts

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

@Controller('auth')
export class AuthController {

	...

    @Post('/signin')
    signIn(@Body(ValidationPipe) authCredentialsDto: AuthCredentialsDto) {
        return this.authService.signIn(authCredentialsDto);
    }
}

JWT에 대해서

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

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

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

auth.module.ts

import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { UserRepository } from './user.respository';

@Module({
  imports: [
    PassportModule.register({defaultStrategy: 'jwt'}),
    JwtModule.register({
      secret: 'Secret1234',
      signOptions: {
        expiresIn: 60 * 60,
      }
    }),
    TypeOrmModule.forFeature([UserRepository])
  ],
  controllers: [AuthController],
  providers: [AuthService]
})
export class AuthModule {}

로그인 성공 시 JWT를 이용해서 토큰 생성해주기 !!!
1. Service 에서 SignIn 메소드에서 생성해주면 됩니다.
auth 모듈에 JWT를 등록해주었기 때문에 Service에서 JWT를 가져 올 수 있습니다.

auth.service.ts

import { Injectable, UnauthorizedException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { AuthCredentialsDto } from './dto/auth-credential.dto';
import { UserRepository } from './user.respository';
import * as bcrypt from 'bcryptjs';
import { JwtService } from '@nestjs/jwt';

@Injectable()
export class AuthService {

    constructor(
        @InjectRepository(UserRepository)
        private userRepository: UserRepository,
        private jwtService: JwtService
    ) {}
	...

    async signIn(authCredentialsDto: AuthCredentialsDto): Promise<{accessToken: string}> {
	...

        if (user && (await bcrypt.compare(password, user.password))) {
            // 유저 토큰 생성
            const paylaod = {username}
            const accessToken = await this.jwtService.sign(paylaod);

            return {accessToken};
        } else {
            throw new UnauthorizedException('login failed');
        }
    }
}

반환을 객체를 하고 있다. 그 객체는 accessToken을 담고 있다.

Passport, Jwt 이용해서 토큰 인증 후 유저 정보 가져오기

 npm install @types/passport-jwt --save

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 "./user.entity";
import { UserRepository } from "./user.respository";

@Injectable()
// Nest.js can inject it anywhere this service is needed
// via its Dependency Injection system.
export class JwtStrategy extends PassportStrategy(Strategy) {
    // The class extends the PassportStrategy class defined by @nestjs/passport package
    // you're passing the JWT Strategy defined by the passport-jwt Node.js package.
    constructor(
        @InjectRepository(UserRepository)
        private userRepository: UserRepository
    ) {
        // passes two important options
        super({
            secretOrKey: 'Secret1234',
            // The counfigures the secret key that JWT Strategy will use
            // to decrypt the JWT toekn in order to validate it
            // and access its payload
            jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken()
            // This configures the Strategy (imported from passport-jwt package)
            // to look for the JWT in the Authorization Header of the current Request
            // passed over as a Bearer token.
        })
    }

    // 위에서 토큰이 유효햔지 체크가 되면 validate 메서드에서 payload에 있는 유저 이름이 데이터베이스에서
    // 있는 유저인지 확인 후 있다면 유저 객체를 return 값으로 던져준다.
    // return 값은 @UseGuards(AuthGuard())를 이용한 모든 요청의 Request Object에 들어간다.
    async validate(payload) {
        const {username} = payload;
        const user: User = await this.userRepository.findOne({username});

        if(!user) {
            throw new UnauthorizedException();
        }
      
        return user;
    }
}

auth.module.ts

import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { JwtStrategy } from './jwt.strategy';
import { UserRepository } from './user.respository';

@Module({
	...
  providers: [AuthService, JwtStrategy],
  exports: [JwtStrategy, PassportModule]
})
export class AuthModule {}

유저 정보가 잘 반환되었다.

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

get-user.decorator.ts

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

export const GetUser = createParamDecorator((data, ctx: ExecutionContext):User => {
    const req = ctx.switchToHttp().getRequest();
    return req.user;
})

auth.controller.ts

import { Body, Controller, Param, Post, Req, UseGuards, ValidationPipe } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { AuthCredentialsDto } from './dto/auth-credential.dto';
import { GetUser } from './get-user.decorator';
import { User } from './user.entity';

@Controller('auth')
export class AuthController {

	...

    @Post('/test')
    @UseGuards(AuthGuard())
    test(@GetUser() user: User) {
        console.log('req', user);
    }
}

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

boards.controller.ts

import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post, UseGuards, UsePipes, ValidationPipe } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

import { BoardStatus } from './board-status.enum';
import { Board } from './board.entity';
import { BoardsService } from './boards.service';
import { CreateBoardDto } from './dto/create-board.dto';
import { BoardStatusValidationPipe } from './pipes/board-status-validation.pipes';

@Controller('boards')
@UseGuards(AuthGuard())
export class BoardsController {
    constructor(private boardsService: BoardsService) {}

	...
}

@UseGuards 추가

게시물에 접근하는 권한 처리

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

사람과 게시글의 관계는 oneToMany이다

user.entity.ts

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

@Entity()
@Unique(['username'])
export class User extends BaseEntity {
	...

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

}

type => Board는 Board 타입을 가진다는 의미고 board => board.user는 보드에서 유저를 접근하려면 어떻게 해야하는지 정의해주는 것이고, {eager: true}는 user정보를 가져올 때 board 정보도 가져온다는 설정이다.

board.entity.ts

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


@Entity()
export class Board extends BaseEntity {
	...

    @ManyToOne(type => User, user => user.boards, {eager: false})
    user: User;

}

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

board.controller.ts

@Controller('boards')
@UseGuards(AuthGuard())
export class BoardsController {
	...

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

@GetUser() user: user 로 user를 받아서 파라미터로 넘겨주고 있다.

board.service.ts

@Injectable()
export class BoardsService {
	...

    createBoard(createBoardDto: CreateBoardDto, user: User): Promise<Board> {
        return this.boardRepository.createBoard(createBoardDto, user);
    }
}

board.repository.ts

import { User } from "src/auth/user.entity";
import { EntityRepository, Repository } from "typeorm";
import { BoardStatus } from "./board-status.enum";
import { Board } from "./board.entity";
import { CreateBoardDto } from "./dto/create-board.dto";

@EntityRepository(Board)
export class BoardRepository extends Repository<Board> {
    
    async createBoard(createBoardDto: CreateBoardDto, user: User): Promise<Board> {
        const {title, description} =  createBoardDto;
        
        const board = this.create({
            title,
            description,
            status: BoardStatus.PUBLIC,
            user
        })

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

토큰 값까지 잘 넣어서 포스트하면 유저 정보까지 들어간 것을 볼 수 있다.

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

board.controller.ts

@Controller('boards')
@UseGuards(AuthGuard())
export class BoardsController {
    constructor(private boardsService: BoardsService) {}

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

@GetUser로 user정보가 담겨있다.

board.service.ts

@Injectable()
export class BoardsService {
	...

    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 this.boardRepository.find();
    }
  ...
}

여기에는 QueryBuilder를 사용했다. 받아온 user정보를 이용해 조건을 맞는 게시물만 가져온다.

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

board.controller.ts

@Controller('boards')
@UseGuards(AuthGuard())
export class BoardsController {
    constructor(private boardsService: BoardsService) {}

    ...

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

board.service.ts

@Injectable()
export class BoardsService {
    ...

    async deleteBoard(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}`);
        }
    }

로그 남기기

로그에 대해서

원래는 개발을 할때 기능을 하나 구현하고 거기에 대해 로그를 달아주고 다른 기능을 개발하고 이런식이다.

board.controller.ts

@Controller('boards')
@UseGuards(AuthGuard())
export class BoardsController {
    private logger = new Logger('BoardsController');
    ...

    @Get('/')
    getAllBoard(
        @GetUser() user: User
    ): Promise<Board[]> {
        this.logger.verbose(`User ${user.username} trying to get all boards`);
        return this.boardsService.getAllBoards(user);
    }
  ...
}

Logger를 생성할 때 인자로 준 값은 로그에서 [ ] 안에 표시된다. 주로 어디서 발생했는지를 나타내기 위해 적어준다.

설정 및 마무리

yml과 yaml은 같은 것이다.

npm install config --save

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

main.ts

import { Logger } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
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();

설정 적용 & 강의 마무리

|| 문법을 사용한 이유는 앞에 것이 없다면 뒤에것을 사용한다는 것이다. aws를 사용하면 이미 그쪽에 정보를 넘겨주기 때문에 그것을 사용하게된다.

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.port,
    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}'],
    synchronize: dbConfig.synchronize
}

auth.module.ts

import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { JwtStrategy } from './jwt.strategy';
import { UserRepository } from './user.respository';
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([UserRepository])
  ],
  controllers: [AuthController],
  providers: [AuthService, JwtStrategy],
  exports: [JwtStrategy, PassportModule]
})
export class AuthModule {}

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 "./user.entity";
import { UserRepository } from "./user.respository";
import * as config from 'config';

@Injectable()
// Nest.js can inject it anywhere this service is needed
// via its Dependency Injection system.
export class JwtStrategy extends PassportStrategy(Strategy) {
    // The class extends the PassportStrategy class defined by @nestjs/passport package
    // you're passing the JWT Strategy defined by the passport-jwt Node.js package.
    constructor(
        @InjectRepository(UserRepository)
        private userRepository: UserRepository
    ) {
        // passes two important options
        super({
            secretOrKey: config.env.JWT_SECRET || config.get('jwt.secret'),
            // The counfigures the secret key that JWT Strategy will use
            // to decrypt the JWT toekn in order to validate it
            // and access its payload
            jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken()
            // This configures the Strategy (imported from passport-jwt package)
            // to look for the JWT in the Authorization Header of the current Request
            // passed over as a Bearer token.
        })
    }

    // 위에서 토큰이 유효햔지 체크가 되면 validate 메서드에서 payload에 있는 유저 이름이 데이터베이스에서
    // 있는 유저인지 확인 후 있다면 유저 객체를 return 값으로 던져준다.
    // return 값은 @UseGuards(AuthGuard())를 이용한 모든 요청의 Request Object에 들어간다.
    async validate(payload) {
        const {username} = payload;
        const user: User = await this.userRepository.findOne({username});

        if(!user) {
            throw new UnauthorizedException();
        }

        return user;
    }
}