6. Auth module
1. Auth
모듈 관련 파일 생성하기
다음과 같이 Auth
모듈 관련 파일들을 생성한다.
2. User
에 대한 엔터티 생성하기
다음과 같이 작성하여 User
에 대한 엔터티를 생성한다.
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
username: string;
@Column()
password: string;
}
리포지터리가 엔터티를 관리할 수 있도록 auth.module.ts
의 imports
배열에 넣어준다.
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. 서비스에 리포지터리 주입하기
리포지터리를 사용할 수 있도록 AuthService
의 constructor
에 넣어준다.
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
디렉터리를 생성한다.
AuthCredentialDto
를 정의한다.
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
모듈을 이용하면 된다.
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
를 넣어준다.
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
이 들어오는 경우 에러가 발생한다.
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'
이기 때문에 조건에 맞춰 에러를 처리한다.
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
모듈을 설치한다.
User
엔터티를 관리하는 리포지터리에서 암호화 코드를 추가한다.
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);
}
}