-
Notifications
You must be signed in to change notification settings - Fork 20
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 #4
base: main
Are you sure you want to change the base?
Changes from all commits
920f1b1
771fdf1
ef84638
75e33f7
e2db653
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { Test, TestingModule } from '@nestjs/testing'; | ||
import { AuthController } from './auth.controller'; | ||
|
||
describe('AuthController', () => { | ||
let controller: AuthController; | ||
|
||
beforeEach(async () => { | ||
const module: TestingModule = await Test.createTestingModule({ | ||
controllers: [AuthController], | ||
}).compile(); | ||
|
||
controller = module.get<AuthController>(AuthController); | ||
}); | ||
|
||
it('should be defined', () => { | ||
expect(controller).toBeDefined(); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import { | ||
Body, | ||
Controller, | ||
HttpCode, | ||
HttpStatus, | ||
Param, | ||
Post, | ||
} from '@nestjs/common'; | ||
import { AuthService } from './auth.service'; | ||
import { RequestSigninCodeDto } from './dto/request-signin-code.dto'; | ||
import { SmsCodeDto } from './dto/sms-code.dto'; | ||
import { VerifySinginCodeDto } from './dto/verify-singin-code.dto'; | ||
|
||
@Controller('auth') | ||
export class AuthController { | ||
constructor(private readonly authService: AuthService) {} | ||
|
||
/** SMS 인증번호 요청 */ | ||
@Post('signin') | ||
@HttpCode(HttpStatus.OK) | ||
async requestSigninToken(@Body() body: RequestSigninCodeDto) { | ||
return this.authService.requestSigninCode(body); | ||
} | ||
|
||
/** SMS 인증 */ | ||
@Post('signin/:code') | ||
@HttpCode(HttpStatus.OK) | ||
async verifySigninToken( | ||
@Param() param: SmsCodeDto, | ||
@Body() body: VerifySinginCodeDto, | ||
) { | ||
return await this.authService.verifySigninCode(param, body); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import { Module } from '@nestjs/common'; | ||
import { AuthService } from './auth.service'; | ||
import { AuthController } from './auth.controller'; | ||
import { TypeOrmModule } from '@nestjs/typeorm'; | ||
import { Auth } from './entity/auth.entity'; | ||
|
||
@Module({ | ||
imports: [TypeOrmModule.forFeature([Auth])], | ||
providers: [AuthService], | ||
controllers: [AuthController], | ||
}) | ||
export class AuthModule {} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { Test, TestingModule } from '@nestjs/testing'; | ||
import { AuthService } from './auth.service'; | ||
|
||
describe('AuthService', () => { | ||
let service: AuthService; | ||
|
||
beforeEach(async () => { | ||
const module: TestingModule = await Test.createTestingModule({ | ||
providers: [AuthService], | ||
}).compile(); | ||
|
||
service = module.get<AuthService>(AuthService); | ||
}); | ||
|
||
it('should be defined', () => { | ||
expect(service).toBeDefined(); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
import { Injectable, UnauthorizedException } from '@nestjs/common'; | ||
import { RequestSigninCodeDto } from './dto/request-signin-code.dto'; | ||
import { SinginCodeResponseDto } from './dto/singin-code-response.dto'; | ||
import { SmsCodeDto } from './dto/sms-code.dto'; | ||
import { VerifySinginResponseDto } from './dto/verify-singin-response.dto'; | ||
import { TTL } from '../util/consts'; | ||
import { InjectRepository } from '@nestjs/typeorm'; | ||
import { Auth } from './entity/auth.entity'; | ||
import { Repository } from 'typeorm'; | ||
import { VerifySinginCodeDto } from './dto/verify-singin-code.dto'; | ||
|
||
@Injectable() | ||
export class AuthService { | ||
constructor( | ||
@InjectRepository(Auth) | ||
private readonly authRepository: Repository<Auth>, | ||
) {} | ||
|
||
private generateCode(): string { | ||
return Math.floor(100000 + Math.random() * 900000).toString(); | ||
} | ||
|
||
/** | ||
* SMS 인증번호 요청 : | ||
* 실제 휴대전화 번호로 전송되는 것이 아니라, 휴대전화 번호를 입력하면 인증번호가 전송된다고 가정한다. | ||
* 인증번호은 인증 요청시간으로부터 5분간 유효하다고 가정한다. | ||
* 인증번호 전송시에는 API 응답으로 인증번호를 리턴한다 | ||
*/ | ||
async requestSigninCode( | ||
body: RequestSigninCodeDto, | ||
): Promise<SinginCodeResponseDto> { | ||
const { phone } = body; | ||
const code = this.generateCode(); | ||
const expires = Date.now() + TTL.VALIDITY_DURATION * 1000; | ||
|
||
const smsCode = this.authRepository.create({ | ||
phone, | ||
code, | ||
expires, | ||
}); | ||
await this.authRepository.save(smsCode); | ||
|
||
return { code }; | ||
} | ||
|
||
/** | ||
* SMS 인증 | ||
* 인증번호의 유효시간이 5분이 지나면 인증번호가 만료되었다고 반환한다. | ||
* 인증번호가 일치하는지 확인한다. | ||
*/ | ||
async verifySigninCode( | ||
param: SmsCodeDto, | ||
body: VerifySinginCodeDto, | ||
): Promise<VerifySinginResponseDto> { | ||
const { code } = param; | ||
const { phone } = body; | ||
|
||
const smsCode = await this.authRepository.findOne({ | ||
where: { phone }, | ||
}); | ||
|
||
if (!smsCode || smsCode.expires < Date.now()) { | ||
throw new UnauthorizedException( | ||
'인증 시간이 만료되었습니다. 다시 시도해 주세요.', | ||
); | ||
} | ||
Comment on lines
+58
to
+66
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. smsCode 변수를 보고 인증 번호를 가져오는 줄 알았어요! 변수명이 명시적이면 좋을 것 같아요😄
|
||
|
||
if (smsCode.code !== code) { | ||
throw new UnauthorizedException( | ||
'인증번호가 일치하지 않습니다. 다시 시도해 주세요.', | ||
); | ||
} | ||
|
||
return { result: true }; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { IsNotEmpty, IsString, Matches } from 'class-validator'; | ||
import { ApiProperty } from '@nestjs/swagger'; | ||
|
||
export class RequestSigninCodeDto { | ||
@ApiProperty({ description: 'phone number', example: '010-1234-5678' }) | ||
@IsString() | ||
@IsNotEmpty() | ||
@Matches(/^01[0-9]-[0-9]{3,4}-[0-9]{4}$/, { | ||
message: | ||
'phoneNumber must be a valid Korean phone number format (e.g., 010-1234-5678)', | ||
}) | ||
phone: string; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
export class SinginCodeResponseDto { | ||
/** 인증번호 */ | ||
code: string; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import { IsNumberString, Length } from 'class-validator'; | ||
import { ApiProperty } from '@nestjs/swagger'; | ||
|
||
export class SmsCodeDto { | ||
@ApiProperty({ description: 'sms code', example: '123456' }) | ||
@Length(6, 6) | ||
@IsNumberString() | ||
code: string; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { IsNotEmpty, IsString, Matches } from 'class-validator'; | ||
import { ApiProperty } from '@nestjs/swagger'; | ||
|
||
export class VerifySinginCodeDto { | ||
@ApiProperty({ description: 'phone number', example: '010-1234-5678' }) | ||
@IsString() | ||
@IsNotEmpty() | ||
@Matches(/^01[0-9]-[0-9]{3,4}-[0-9]{4}$/, { | ||
message: | ||
'phoneNumber must be a valid Korean phone number format (e.g., 010-1234-5678)', | ||
}) | ||
phone: string; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export class VerifySinginResponseDto { | ||
result: boolean; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; | ||
|
||
@Entity() | ||
export class Auth { | ||
@PrimaryGeneratedColumn() | ||
id: number; | ||
|
||
@Column({ length: 50 }) | ||
phone: string; | ||
|
||
@Column() | ||
code: string; | ||
|
||
// 밀리초 단위의 타임스탬프 데이터를 정확하고 안정적으로 저장하기 위함 | ||
@Column('bigint') | ||
expires: number; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
상태 코드 반환을 HttpStatus 객체를 통해 반환할 수 있군요👍
저는 200, 400 이렇게 써서 보냈었는데.. 훨씬 명시적이라 좋습니다! 알아갑니다😄