Skip to content

6. Auth module


1. Auth 모듈 관련 파일 생성하기

다음과 같이 Auth 모듈 관련 파일들을 생성한다.


nest g mo auth
nest g co auth
nest g s auth


2. User에 대한 엔터티 생성하기

다음과 같이 작성하여 User에 대한 엔터티를 생성한다.


src/auth/user.entity.ts
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";

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

  @Column()
  username: string;

  @Column()
  password: string;
}


리포지터리가 엔터티를 관리할 수 있도록 auth.module.tsimports 배열에 넣어준다.


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

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


3. 서비스에 리포지터리 주입하기

리포지터리를 사용할 수 있도록 AuthServiceconstructor에 넣어준다.


src/auth/auth.service.ts
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { User } from "./user.entity";

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


4. 회원 가입 기능 구현하기

다음과 같이 AuthCredentialDto를 생성를 위해 먼저 src/auth 디렉터리에 dto 디렉터리를 생성한다.


mkdir src/auth/dto


AuthCredentialDto를 정의한다.


src/auth/auth-credential.dto.ts
export class AuthCredentialDto {
  username: string;
  password: string;
}


다음과 같이 AuthService에서 기초적인 회원 가입 기능을 구현해 보자.


import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { AuthCredentialDto } from "./dto/auth-credential.dto";
import { User } from "./user.entity";

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

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

    await this.userRepository.save(user);
  }
}
import { Body, Controller, Post } from "@nestjs/common";
import { AuthService } from "./auth.service";
import { AuthCredentialDto } from "./dto/auth-credential.dto";

@Controller("auth")
export class AuthController {
  constructor(private authService: AuthService) {}

  @Post("/signup")
  signUp(@Body() authCredentialDto: AuthCredentialDto): Promise<void> {
    return this.authService.signUp(authCredentialDto);
  }
}


5. 유저 데이터 유효성 검사하기

유저를 생성할 때 원하는 이름의 길이 및 비밀번호 길이 등의 유효성 검사를 할 수 있게 구현해 보자. 유효성 검사를 하기 위해서는 class-validator 모듈을 이용하면 된다.


src/auth/dto/auth-credential.dto.ts
import { IsString, Matches, MaxLength, MinLength } from "class-validator";

export class AuthCredentialDto {
  @IsString()
  @MinLength(4)
  @MaxLength(20)
  username: string;

  @IsString()
  @MinLength(4)
  @MaxLength(20)
  @Matches(/^[a-zA-Z0-9]*$/)
  password: string;
}


유효성 검사를 할 수 있도록 하려면 해당 라우트 핸들러에 ValidationPipe를 넣어준다.


src/auth/auth.controller.ts
import { Body, Controller, Post, ValidationPipe } from "@nestjs/common";
import { AuthService } from "./auth.service";
import { AuthCredentialDto } from "./dto/auth-credential.dto";

@Controller("auth")
export class AuthController {
  constructor(private authService: AuthService) {}

  @Post("/signup")
  signUp(
    @Body(ValidationPipe) authCredentialDto: AuthCredentialDto
  ): Promise<void> {
    return this.authService.signUp(authCredentialDto);
  }
}


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

username을 유니크하게 만들려면 User 엔터티에 @Unique 데코레이터를 달아주면 된다. 만약 동일한 username이 들어오는 경우 에러가 발생한다.


src/auth/user.entity.ts
import { Column, Entity, PrimaryGeneratedColumn, Unique } from "typeorm";

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

  @Column()
  username: string;

  @Column()
  password: string;
}


동일한 username에 대한 에러를 처리하기 위해 다음과 같이 try/catch 구문을 작성한다. 이 때 발생하는 error.code'23505'이기 때문에 조건에 맞춰 에러를 처리한다.


src/auth/auth.service.ts
import {
  ConflictException,
  Injectable,
  InternalServerErrorException,
} from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { AuthCredentialDto } from "./dto/auth-credential.dto";
import { User } from "./user.entity";

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

  async signUp(authCredentialDto: AuthCredentialDto): Promise<void> {
    const { username, password } = authCredentialDto;
    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();
      }
    }
  }
}


7. 비밀번호 암호화하기

지금까지 구현한 방식을 따라 유저를 생성하면, 유저의 비밀번호가 데이터베이스에 그대로 저장된다. 그래서 비밀번호를 암호화해서 저장을 할 수 있도록 변경해 보자.


먼저 다음과 같이 bcryptjs 모듈을 설치한다.


npm install --save bcryptjs


User 엔터티를 관리하는 리포지터리에서 암호화 코드를 추가한다.


src/auth/auth.service.ts
import {
  ConflictException,
  Injectable,
  InternalServerErrorException,
} from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { AuthCredentialDto } from "./dto/auth-credential.dto";
import { User } from "./user.entity";
import * as bcrypt from "bcryptjs";

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

  async signUp(authCredentialDto: AuthCredentialDto): Promise<void> {
    const { username, password } = authCredentialDto;

    const salt = await bcrypt.genSalt();
    const hashedPassword = await bcrypt.hash(password, salt);

    const user = this.userRepository.create({
      username,
      password: hashedPassword,
    });

    try {
      await this.userRepository.save(user);
    } catch (error) {
      if (error.code === "23505") {
        throw new ConflictException("Existing username");
      } else {
        throw new InternalServerErrorException();
      }
    }
  }
}


8. 로그인 기능 구현하기

회원 가입, 비밀번호 암호화 기능을 구현했으므로, 가입한 아이디로 로그인을 할 수 있도록 구현해 보자.


먼저 클라이언트에서 제공 받은 아이디를 이용해서 해당 아이디가 데이터베이스에 있는지 확인한다. 만약 존재하는 아이디라면 제공 받은 비밀번호와 데이터베이스의 비밀번호를 비교한다. 그 외의 경우 에러를 발생시킨다.


import {
  ConflictException,
  Injectable,
  InternalServerErrorException,
  UnauthorizedException,
} from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { AuthCredentialDto } from "./dto/auth-credential.dto";
import { User } from "./user.entity";
import * as bcrypt from "bcryptjs";

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

  async signUp(authCredentialDto: AuthCredentialDto): Promise<void> {
    const { username, password } = authCredentialDto;

    const salt = await bcrypt.genSalt();
    const hashedPassword = await bcrypt.hash(password, salt);

    const user = this.userRepository.create({
      username,
      password: hashedPassword,
    });

    try {
      await this.userRepository.save(user);
    } catch (error) {
      if (error.code === "23505") {
        throw new ConflictException("Existing username");
      } else {
        throw new InternalServerErrorException();
      }
    }
  }

  async signIn(authCredentialDto: AuthCredentialDto): Promise<string> {
    const { username, password } = authCredentialDto;
    const user = await this.userRepository.findOneBy({ username });

    if (user && (await bcrypt.compare(password, user.password))) {
      return "login success";
    } else {
      throw new UnauthorizedException("login failed");
    }
  }
}
import { Body, Controller, Post, ValidationPipe } from "@nestjs/common";
import { AuthService } from "./auth.service";
import { AuthCredentialDto } from "./dto/auth-credential.dto";

@Controller("auth")
export class AuthController {
  constructor(private authService: AuthService) {}

  @Post("/signup")
  signUp(
    @Body(ValidationPipe) authCredentialDto: AuthCredentialDto
  ): Promise<void> {
    return this.authService.signUp(authCredentialDto);
  }

  @Post("/signIn")
  signIn(
    @Body(ValidationPipe) authCredentialDto: AuthCredentialDto
  ): Promise<string> {
    return this.authService.signIn(authCredentialDto);
  }
}

References