Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[김하린] 휴대폰 인증 API #6

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 0 additions & 11 deletions .env.example

This file was deleted.

7 changes: 7 additions & 0 deletions nest-cli.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"plugins": [{
"name": "@nestjs/swagger",
"options": {
"classValidatorShim": false,
"introspectComments": true
}
}],
"deleteOutDir": true
}
}
18 changes: 15 additions & 3 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule } from '@nestjs/config';
import { addTransactionalDataSource } from 'typeorm-transactional';
import { DataSource } from 'typeorm';
import { PhoneVerifyModule } from './domain/phone-verify/phone-verify.module';
import { PhoneVerifyController } from './apps/phone-verify/phone-verify.controller';
import { PhoneVerify } from './domain/phone-verify/phone-verify.entity';
import { APP_FILTER } from '@nestjs/core';
import { HttpExceptionFilter } from './common/error/http-exception.filter';

@Module({
imports: [
Expand All @@ -17,19 +22,26 @@ import { DataSource } from 'typeorm';
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
synchronize: process.env.DB_SYNC === 'true',
entities: [PhoneVerify],
timezone: 'Z',
};
},
async dataSourceFactory(options) {
if (!options) {
throw new Error('Invalid options passed');
}

return addTransactionalDataSource(new DataSource(options));
},
}),
TypeOrmModule.forFeature([PhoneVerify]),
PhoneVerifyModule,
],
controllers: [PhoneVerifyController],
providers: [
{
provide: APP_FILTER,
useClass: HttpExceptionFilter,
},
],
controllers: [],
providers: [],
})
export class AppModule {}
29 changes: 29 additions & 0 deletions src/apps/phone-verify/phone-verify.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Body, Controller, Patch, Post } from '@nestjs/common';
import { PhoneVerifyService } from '../../domain/phone-verify/phone-verify.service';
import { PhoneVerifyCodeRequestDto } from '../../domain/phone-verify/dto/request/phone-verify-code-request.dto';
import { PhoneVerifyCodeResponseDto } from '../../domain/phone-verify/dto/response/phone-verify-code-response.dto';
import { PhoneVerifyRequestDto } from '../../domain/phone-verify/dto/request/phone-verify-request.dto';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
@Controller('phone-verifications')
@ApiTags('phone-verifications')
export class PhoneVerifyController {
constructor(private readonly phoneVerifyService: PhoneVerifyService) {}

@ApiOperation({
summary: '휴대폰 인증번호 발송',
})
@Post('/phone-verifications')
async sendVerifyCode(
@Body() dto: PhoneVerifyCodeRequestDto,
): Promise<PhoneVerifyCodeResponseDto> {
return this.phoneVerifyService.sendVerifyCode(dto);
}

@ApiOperation({
summary: '휴대폰 인증',
})
@Patch('/phone-verifications')
verify(@Body() dto: PhoneVerifyRequestDto) {
return this.phoneVerifyService.verify(dto);
}
}
4 changes: 4 additions & 0 deletions src/common/constant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const Messages = {
// auth
ERROR_AUTH_FAIL: '인증에 실패했습니다.',
};
21 changes: 21 additions & 0 deletions src/common/error/error-response.ts
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 함수를 만들어서 json 형태로 반환하는 형태를 작성했었는데, 하린님처럼 ErrorResponse Class를 만들어서 하는 방법 좋은 것 같습니다👍 저도 다음에 적용해봐야겠어요~ 덕분에 알아갑니다😄

Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { HttpException } from '@nestjs/common';

export class ErrorResponse {
public status: number;
public code: string;
public message: string;

constructor(exception: HttpException) {
this.status = exception.getStatus();
this.code = exception.getResponse()['error'];
this.message = exception.getResponse()['message'];
}

toJson() {
return {
status: this.status,
code: this.code,
message: this.message,
};
}
}
18 changes: 18 additions & 0 deletions src/common/error/http-exception.filter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
} from '@nestjs/common';
import { ErrorResponse } from './error-response';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const errorResponse = new ErrorResponse(exception);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

위에 ErrorResponse Class를 만들어 놓으니 코드가 한결 깔끔하네요!👍


response.status(errorResponse.status).json(errorResponse.toJson());
}
}
8 changes: 8 additions & 0 deletions src/common/error/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { UnauthorizedException } from '@nestjs/common';

/* 401 Unauthorized */
export class AuthFailedException extends UnauthorizedException {
constructor(message?: string, code?: string) {
super(message ?? '인증에 실패하였습니다.', code ?? 'AUTH_FAILED');
}
}
11 changes: 11 additions & 0 deletions src/common/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export function generateNumericToken(
length: number = 6,
alphabet: string = '1234567890',
): string {
let id = '';
let i = length;
while (i--) {
id += alphabet[(Math.random() * alphabet.length) | 0];
}
return id;
}
4 changes: 4 additions & 0 deletions src/common/validator/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const validator = {
PHONE_NUMBER_REGEX: '^[0-9]{11}$',
VERIFY_CODE_REGEX: '^[0-9]{6}$',
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { IsString, Matches } from 'class-validator';
import { validator } from '../../../../common/validator';

export class PhoneVerifyCodeRequestDto {
@IsString()
@Matches(validator.PHONE_NUMBER_REGEX)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

상수화 처리 좋네요👍

phoneNumber: string;
}
12 changes: 12 additions & 0 deletions src/domain/phone-verify/dto/request/phone-verify-request.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { IsString, Matches } from 'class-validator';
import { validator } from '../../../../common/validator';

export class PhoneVerifyRequestDto {
@IsString()
@Matches(validator.PHONE_NUMBER_REGEX)
phoneNumber: string;

@IsString()
@Matches(validator.VERIFY_CODE_REGEX)
verifyCode: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export class PhoneVerifyCodeResponseDto {
code: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export class PhoneVerifyResponseDto {
result: boolean;
}
31 changes: 31 additions & 0 deletions src/domain/phone-verify/phone-verify.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';

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

@CreateDateColumn()
createdAt: Date;

@UpdateDateColumn()
updatedAt: Date;

@Column()
phoneNumber: string;

@Column()
verifyCode: string;

@Column({ default: false })
isVerified: boolean;

@Column()
expiredAt: Date;
}
11 changes: 11 additions & 0 deletions src/domain/phone-verify/phone-verify.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { PhoneVerifyService } from './phone-verify.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { PhoneVerify } from './phone-verify.entity';

@Module({
imports: [TypeOrmModule.forFeature([PhoneVerify])],
providers: [PhoneVerifyService],
exports: [PhoneVerifyService],
})
export class PhoneVerifyModule {}
62 changes: 62 additions & 0 deletions src/domain/phone-verify/phone-verify.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { PhoneVerify } from './phone-verify.entity';
import { Repository } from 'typeorm';
import { PhoneVerifyCodeRequestDto } from './dto/request/phone-verify-code-request.dto';
import { PhoneVerifyCodeResponseDto } from './dto/response/phone-verify-code-response.dto';
import { generateNumericToken } from '../../common/util';
import { PhoneVerifyRequestDto } from './dto/request/phone-verify-request.dto';
import { PhoneVerifyResponseDto } from './dto/response/phone-verify-response.dto';

const VERIFY_CODE_VALID_TIME = 5;

@Injectable()
export class PhoneVerifyService {
constructor(
@InjectRepository(PhoneVerify)
private readonly phoneVerifyRepository: Repository<PhoneVerify>,
) {}

async sendVerifyCode(
dto: PhoneVerifyCodeRequestDto,
): Promise<PhoneVerifyCodeResponseDto> {
const verifyCode = generateNumericToken();

const expiredDate = new Date();
expiredDate.setMinutes(expiredDate.getMinutes() + VERIFY_CODE_VALID_TIME);

const phoneVerify = new PhoneVerify();
phoneVerify.verifyCode = verifyCode;
phoneVerify.phoneNumber = dto.phoneNumber;
phoneVerify.expiredAt = expiredDate;

await this.phoneVerifyRepository.save(phoneVerify);

const response = new PhoneVerifyCodeResponseDto();
response.code = verifyCode;

return response;
}

async verify(dto: PhoneVerifyRequestDto) {
const phoneVerification = await this.phoneVerifyRepository
.createQueryBuilder('pv')
.where('pv.phoneNumber = :phoneNumber', { phoneNumber: dto.phoneNumber })
.andWhere('pv.verifyCode = :verifyCode', { verifyCode: dto.verifyCode })
.andWhere('pv.isVerified = false')
.andWhere('pv.expiredAt > NOW()')
.orderBy({ createdAt: 'DESC' })
.getOne();
Comment on lines +42 to +49
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 findOne() 함수를 통해서 가져왔는데, createQueryBuilder를 쓰면 어떤 이점이 있을까요?!


const response = new PhoneVerifyResponseDto();
if (!phoneVerification) {
response.result = false;
return response;
}

phoneVerification.isVerified = true;
await this.phoneVerifyRepository.save(phoneVerification);
response.result = true;
return response;
}
}
24 changes: 23 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,34 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { initializeTransactionalContext } from 'typeorm-transactional';
import { ValidationPipe } from '@nestjs/common';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { HttpExceptionFilter } from './common/error/http-exception.filter';

async function bootstrap() {
initializeTransactionalContext();

const app = await NestFactory.create(AppModule);
// TODO: 프로그램 구현

// ---------------------------- Swagger 설정 ----------------------------
const config = new DocumentBuilder()
.setTitle('NestJS API')
.setDescription('어쩌다 Nest 과제 API')
.setVersion('1.0')
.build();
const swaggerOptions = {
operationIdFactory: (controllerKey: string, methodKey: string) => methodKey,
};

const document = SwaggerModule.createDocument(app, config, swaggerOptions);
SwaggerModule.setup('api', app, document);

// ---------------------------- Global Pipe 설정 ----------------------------
app.useGlobalPipes(new ValidationPipe({ transform: true }));

// ---------------------------- Global Filter 설정 ----------------------------
app.useGlobalFilters(new HttpExceptionFilter());

await app.listen(process.env.PORT || 8000);

console.log(`Application is running on: ${await app.getUrl()}`);
Expand Down