https://github.com/jaewonhimnae/nestjs-board-app
NestJS를 이용해서 프로젝트를 시작할 때 Nest CLI를 이용하면 간단히 프로젝트를 시작할 수 있다.
Nest CLI를 이용해서 아래의 명령어를 작성하면
새 프로젝트 디렉터리가 생성되고 초기 핵심 Nest 파일 및 지원 모듈로 디렉터리가 채워져 프로젝트의 기존 기본 구조가 생성된다.
npm i -g @nestjs/cli nest new project-name
원하는 폴더에서 가서
nest new ./
명령어를 입력하면 현재 위치에 프로젝트 기본 구조를 설정해준다.
모듈은 싱글톤으로 사용된다.
nest g module boards
nest g controller boards --no-spec
테스트 코드 없이 생성
프로바이더는 Nest의 기본 개념입니다. 대부분의 기본 Nest 클래스는 서비스, 리포지토리, 팩토리, 헬퍼등 프로바이더로 취급될 수 있습니다. 프로바이더의 주요 아이디어는 종속성으로 주입 할 수 있다는 것입니다. 즉, 객체는 서로 다양한 관계를 만들 수 있으며 객체의 인스턴스를 "연결"하는 기능은 대부분 Nest 런타임 시스템에 위임될 수 있습니다.
Service 란 ?
서비스는 소프트웨어 개발내의 공통 개념이며, NestJS, Javascript에서만 쓰이는 개념
이 아닙니다.
@Injectable 데코레이터로 감싸져서 모듈에 제공되며, 이 서비스 인스턴스는 애플리
케이션 전체에서 사용 될 수 있다. 서비스는 컨트롤러에서 데이터의 유효성 체크를 하거나 데이터베이스에 아이템을 생성하는 등의 작업을 하는 부분을 처리합니다.
nest g service boards --no-spec
import { Controller, Get } from '@nestjs/common';
import { BoardsService } from './boards.service';
@Controller('boards')
export class BoardsController {
constructor(private boardsService: BoardsService) {}
@Get()
getAllBoard() {
return this.boardsService.getAllBoard();
}
}
import { Module } from '@nestjs/common';
import { BoardsController } from './boards.controller';
import { BoardsService } from './boards.service';
@Module({
controllers: [BoardsController],
providers: [BoardsService]
})
export class BoardsModule {}
board.model.ts
export interface Board {
id: string;
title: string;
description: string;
status: BoardStatus
}
board-status.enum.ts
export enum BoardStatus {
PUBLIC = 'PUBLIC',
PRIAVATE = 'PRIVATE'
}
npm install uuid --save
board.service.ts
import { Injectable } from '@nestjs/common';
import { Board, BoardStatus } from './board.model';
import {v1 as uuid} from 'uuid';
@Injectable()
export class BoardsService {
private boards: Board[] = [];
getAllBoard(): Board[] {
return this.boards;
}
createBoard(title: string, description: string) {
const board: Board = {
id: uuid,
title,
description,
status: BoardStatus.PUBLIC
}
this.boards.push(board);
return board;
}
}
js의 경우 키와 밸류가 같으면 하나만 써도 된다.
controller
import { Body, Controller, Get, Post } from '@nestjs/common';
import { Board } from './board.model';
import { BoardsService } from './boards.service';
@Controller('boards')
export class BoardsController {
constructor(private boardsService: BoardsService) {}
@Get('/')
getAllBoard(): Board[] {
return this.boardsService.getAllBoard();
}
@Post()
createBoard(
@Body('title') title: string,
@Body('description') description: string
): Board {
return this.boardsService.createBoard(title, description);
}
}
create-board.dto.ts
export class CreateBoardDto {
title: string;
description: string;
}
board.service.ts
import { Injectable } from '@nestjs/common';
import {v1 as uuid} from 'uuid';
import { BoardStatus } from './board-status.enum';
import {Board} from './board.model';
import { CreateBoardDto } from './dto/create-board.dto';
@Injectable()
export class BoardsService {
private boards: Board[] = [];
getAllBoard(): Board[] {
return this.boards;
}
createBoard(createBoardDto: CreateBoardDto) {
const {title, description} = createBoardDto;
const board: Board = {
id: uuid,
title,
description,
status: BoardStatus.PUBLIC
}
this.boards.push(board);
return board;
}
getBoardById(id: string): Board {
return this.boards.find((board) => board.id === id);
}
}
board.controller.ts
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
import { Board } from './board.model';
import { BoardsService } from './boards.service';
import { CreateBoardDto } from './dto/create-board.dto';
@Controller('boards')
export class BoardsController {
constructor(private boardsService: BoardsService) {}
@Get('/')
getAllBoard(): Board[] {
return this.boardsService.getAllBoard();
}
@Post()
createBoard(@Body() createBoardDto: CreateBoardDto): Board {
return this.boardsService.createBoard(createBoardDto);
}
@Get('/:id')
getBoardById(@Param('id') id: string): Board {
return this.boardsService.getBoardById(id);
}
}
board.controller.ts
import { Body, Controller, Delete, Get, Param, Patch, Post } from '@nestjs/common';
import { BoardStatus } from './board-status.enum';
import { Board } from './board.model';
import { BoardsService } from './boards.service';
import { CreateBoardDto } from './dto/create-board.dto';
@Controller('boards')
export class BoardsController {
constructor(private boardsService: BoardsService) {}
@Get('/')
getAllBoard(): Board[] {
return this.boardsService.getAllBoard();
}
@Post()
createBoard(@Body() createBoardDto: CreateBoardDto): Board {
return this.boardsService.createBoard(createBoardDto);
}
@Get('/:id')
getBoardById(@Param('id') id: string): Board {
return this.boardsService.getBoardById(id);
}
@Delete('/:id')
deleteBoard(@Param('id') id: string): void {
this.boardsService.deleteBoard(id);
}
@Patch('/:id/status')
updateBoardStatus(
@Param('id') id: string,
@Body('status') status: BoardStatus
) {
return this.boardsService.updateBoardStatus(id, status);
}
}
board.service.ts
import { Injectable } from '@nestjs/common';
import {v1 as uuid} from 'uuid';
import { BoardStatus } from './board-status.enum';
import {Board} from './board.model';
import { CreateBoardDto } from './dto/create-board.dto';
@Injectable()
export class BoardsService {
private boards: Board[] = [];
getAllBoard(): Board[] {
return this.boards;
}
createBoard(createBoardDto: CreateBoardDto) {
const {title, description} = createBoardDto;
const board: Board = {
id: uuid,
title,
description,
status: BoardStatus.PUBLIC
}
this.boards.push(board);
return board;
}
getBoardById(id: string): Board {
return this.boards.find((board) => board.id === id);
}
deleteBoard(id: string): void {
this.boards = this.boards.filter((board) => board.id != id);
}
updateBoardStatus(id: string, status: BoardStatus): Board {
const board = this.getBoardById(id);
board.status = status;
return board;
}
}
npm install class-validator class-transformer --save
create-board.dto.ts
import { IsNotEmpty } from "class-validator";
export class CreateBoardDto {
@IsNotEmpty()
title: string;
@IsNotEmpty()
description: string;
}
board.controller.ts
import { Body, Controller, Delete, Get, Param, Patch, Post, UsePipes, ValidationPipe } from '@nestjs/common';
import { BoardStatus } from './board-status.enum';
import { Board } from './board.model';
import { BoardsService } from './boards.service';
import { CreateBoardDto } from './dto/create-board.dto';
@Controller('boards')
export class BoardsController {
constructor(private boardsService: BoardsService) {}
...
@Post()
@UsePipes(ValidationPipe)
createBoard(@Body() createBoardDto: CreateBoardDto): Board {
return this.boardsService.createBoard(createBoardDto);
}
...
}
미리 만들어진 핸들레 레벨의 파이프를 사용한 것이다.
board.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { NotFoundError } from 'rxjs';
import {v1 as uuid} from 'uuid';
import { BoardStatus } from './board-status.enum';
import {Board} from './board.model';
import { CreateBoardDto } from './dto/create-board.dto';
@Injectable()
export class BoardsService {
private boards: Board[] = [];
getAllBoard(): Board[] {
return this.boards;
}
createBoard(createBoardDto: CreateBoardDto) {
const {title, description} = createBoardDto;
const board: Board = {
id: uuid,
title,
description,
status: BoardStatus.PUBLIC
}
this.boards.push(board);
return board;
}
getBoardById(id: string): Board {
const found = this.boards.find((board) => board.id === id);
if(!found) {
throw new NotFoundException(`Can't find board with id ${id}`);
}
return found;
}
deleteBoard(id: string): void {
const found = this.getBoardById(id);
if(!found) {
throw new NotFoundException(`Can't delete board with id ${id}`);
}
this.boards = this.boards.filter((board) => board.id != id);
}
updateBoardStatus(id: string, status: BoardStatus): Board {
const board = this.getBoardById(id);
board.status = status;
return board;
}
}
board-status-validation.pipe.ts
import { ArgumentMetadata, PipeTransform } from "@nestjs/common";
export class BoardStatusValidationPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
console.log('value', value);
console.log('metadata', metadata);
return value;
}
}
board.controller.ts
@Controller('boards')
export class BoardsController {
...
@Patch('/:id/status')
updateBoardStatus(
@Param('id') id: string,
@Body('status', BoardStatusValidationPipe) status: BoardStatus
) {
return this.boardsService.updateBoardStatus(id, status);
}
}
value sdsdsds
metadata { metatype: [Function: String], type: 'body', data: 'status' }
board-status-validation.pipe.ts
import { ArgumentMetadata, BadRequestException, PipeTransform } from "@nestjs/common";
import { AnyAaaaRecord } from "dns";
import { BoardStatus } from "../board-status.enum";
export class BoardStatusValidationPipe implements PipeTransform {
readonly StatusOptions = [
BoardStatus.PRIVATE,
BoardStatus.PUBLIC
]
transform(value: any) {
value = value.toUpperCase();
if (!this.isStatusValid(value)) {
throw new BadRequestException(`${value} isn't in the status options`);
}
return value;
}
private isStatusValid(status: any) {
const index = this.StatusOptions.indexOf(status);
return index !== -1;
}
}
필요에 맞게 커스텀된 파이프다.
설치할 두가지
1. PostgresSQL
2. pgAdmin (데이터베이스를 보는 툴(Tool)입니
다.)
Window 에서 PostgresSQL 설치하기
이 사이트 가서 인스톨러 다운로드
https://www.postgresql.org/download/windows/
Mac 에서 PostgresSQL 설치하기 이 사이트 가서 인스톨러 다운로드
https://postgresapp.com/downloads.html
Window & Mac 에서 pgAdmin 설치하기 이 사이트 가서 인스톨러 다운로드
https://www.pgadmin.org/download/
board-app db 생성
npm install pg typeorm @nestjs/typeorm --save
typeorm.config.ts
import { TypeOrmModuleOptions } from "@nestjs/typeorm";
export const typeORMConfig: TypeOrmModuleOptions = {
type: 'postgres',
host: 'localhost',
port: 5432,
username: 'postgres',
password: 'postgres',
database: 'board-app',
entities: [__dirname + '/../**/*.entity.{js,ts'],
synchronize: true
}
app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { BoardsModule } from './boards/boards.module';
import { typeORMConfig } from './boards/configs/typeorm.config';
@Module({
imports: [TypeOrmModule.forRoot(typeORMConfig),
BoardsModule],
controllers: [],
providers: [],
})
export class AppModule {}
board.entity.ts
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from "typeorm";
import { BoardStatus } from "./boards/board-status.enum";
@Entity()
export class Board extends BaseEntity {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
@Column()
description: string;
@Column()
status: BoardStatus;
}
board.repository.ts
import { EntityRepository, Repository } from "typeorm";
import { Board } from "./board.entity";
@EntityRepository(Board)
export class BoardRepository extends Repository<Board> {
}
board.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { BoardRepository } from 'src/board.repository';
import { BoardsController } from './boards.controller';
import { BoardsService } from './boards.service';
@Module({
imports: [
TypeOrmModule.forFeature([BoardRepository])
],
...
})
export class BoardsModule {}
board.repository.ts
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): Promise<Board> {
const {title, description} = createBoardDto;
const board = this.create({
title,
description,
status: BoardStatus.PUBLIC
})
await this.save(board);
return board;
}
}
board.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { NotFoundError } from 'rxjs';
import { Board } from 'src/boards/board.entity';
import { BoardStatus } from './board-status.enum';
import { BoardRepository } from './board.repository';
import { CreateBoardDto } from './dto/create-board.dto';
@Injectable()
export class BoardsService {
constructor(
@InjectRepository(BoardRepository)
private boardRepository: BoardRepository
){}
async getAllBoards(): Promise <Board[]> {
return this.boardRepository.find();
}
createBoard(createBoardDto: CreateBoardDto): Promise<Board> {
return this.boardRepository.createBoard(createBoardDto);
}
async getBoardById(id: number): Promise<Board> {
const found = await this.boardRepository.findOne(id);
if (!found) {
throw new NotFoundException(`Can't find the board with id ${id}`);
}
return found;
}
async deleteBoard(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}`);
}
}
async updateBoardStatus(id: number, status: BoardStatus): Promise<Board> {
const board = await this.getBoardById(id);
board.status = status;
await this.boardRepository.save(board);
return board;
}
}
board.controller.ts
import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post, UsePipes, ValidationPipe } from '@nestjs/common';
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')
export class BoardsController {
constructor(private boardsService: BoardsService) {}
@Get('/')
getAllBoard(): Promise<Board[]> {
return this.boardsService.getAllBoards();
}
@Post()
@UsePipes(ValidationPipe)
createBoard(@Body() CreateBoardDto: CreateBoardDto): Promise<Board> {
return this.boardsService.createBoard(CreateBoardDto);
}
@Get('/:id')
getBoardById(@Param('id') id: number): Promise<Board> {
return this.boardsService.getBoardById(id);
}
@Delete('/:id')
deleteBoard(@Param('id', ParseIntPipe) id): Promise<void> {
return this.boardsService.deleteBoard(id);
}
@Patch('/:id/status')
updateBoardStatus(
@Param('id', ParseIntPipe) id: number,
@Body('status', BoardStatusValidationPipe) status: BoardStatus
) {
return this.boardsService.updateBoardStatus(id, status);
}
}
ParseIntPipe
는 숫자만 들어오도록 만들어진 파이프다.
모듈 구조가 중요하다. 첨에 실습했을 때 Board의 Metadata를 못찾는다는 에러가 나왔는데,
typeorm.config.ts
에 엔티티의 위치가 상대경로로 들어가있어서 그런듯하다.
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);
}
}
user.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는 이 모듈 안에 등록을 해줬다는 말이다.
user.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
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);
}
}
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을 담고 있다.
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;
}
}