From 9b9d5aee9b368495103f08e9ba3bc0ecc2b3c7ad Mon Sep 17 00:00:00 2001 From: CodeVac513 Date: Thu, 5 Mar 2026 15:53:35 +0900 Subject: [PATCH 1/5] =?UTF-8?q?=E2=9C=A8=20feat:=20error=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=8D=BC=ED=8B=B0=20=EC=B6=94=EA=B0=80,=20code=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=8D=BC=ED=8B=B0=20optional=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/user/dto/request/oAuthCallbackDto.ts | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/server/src/user/dto/request/oAuthCallbackDto.ts b/server/src/user/dto/request/oAuthCallbackDto.ts index 182c28c5f..5c7f7d87d 100644 --- a/server/src/user/dto/request/oAuthCallbackDto.ts +++ b/server/src/user/dto/request/oAuthCallbackDto.ts @@ -1,19 +1,20 @@ -import { ApiProperty } from '@nestjs/swagger'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsNotEmpty, IsString } from 'class-validator'; +import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; export class OAuthCallbackRequestDto { - @ApiProperty({ + @ApiPropertyOptional({ example: 'testCode', description: 'Access Token 갱신 토큰', }) - @IsNotEmpty({ - message: 'code는 필수 입력 값입니다.', - }) + @IsOptional() + // @IsNotEmpty({ + // message: 'code는 필수 입력 값입니다.', + // }) @IsString({ message: '문자열로 입력해주세요.', }) - code: string; + code?: string; @ApiProperty({ example: '{ provider: {플랫폼 종류}}', @@ -27,6 +28,16 @@ export class OAuthCallbackRequestDto { }) state: string; + @ApiPropertyOptional({ + example: 'error=access_denied', + description: 'OAuth 서버 로그인 중 실패 발생 시 반환되는 값', + }) + @IsOptional() + @IsString({ + message: '문자열로 입력해주세요.', + }) + error?: string; + constructor(partial: Partial) { Object.assign(this, partial); } From cf3097318c9b0fad0494f61c621ded48110547e4 Mon Sep 17 00:00:00 2001 From: CodeVac513 Date: Thu, 5 Mar 2026 15:55:53 +0900 Subject: [PATCH 2/5] =?UTF-8?q?=E2=9C=A8=20feat:=20error=EA=B0=80=20?= =?UTF-8?q?=EC=9E=88=EA=B1=B0=EB=82=98=20code=EA=B0=80=20=EC=97=86?= =?UTF-8?q?=EB=8A=94=20=EA=B2=BD=EC=9A=B0=EC=97=90=20=EB=8C=80=ED=95=9C=20?= =?UTF-8?q?=EB=A6=AC=EB=94=94=EB=A0=89=EC=85=98=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EB=B0=8F=20=EB=A6=AC=EB=94=94=EB=A0=89=EC=85=98=20=EC=9D=B4?= =?UTF-8?q?=EC=A4=91=ED=99=94=EC=97=90=20=EB=8C=80=ED=95=9C=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/user/controller/oAuth.controller.ts | 2 +- server/src/user/service/oAuth.service.ts | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/server/src/user/controller/oAuth.controller.ts b/server/src/user/controller/oAuth.controller.ts index 9711af765..4faf271e5 100644 --- a/server/src/user/controller/oAuth.controller.ts +++ b/server/src/user/controller/oAuth.controller.ts @@ -38,7 +38,7 @@ export class OAuthController { @Res() res: Response, ) { await this.oauthService.callback(callbackDto, res); - return res.redirect(`${OAUTH_URL_PATH.BASE_URL}/oauth-success`); + return res; } @Get('e2e/callback') diff --git a/server/src/user/service/oAuth.service.ts b/server/src/user/service/oAuth.service.ts index b62cfad47..d08788cab 100644 --- a/server/src/user/service/oAuth.service.ts +++ b/server/src/user/service/oAuth.service.ts @@ -12,7 +12,11 @@ import { cookieConfig } from '@common/cookie/cookie.config'; import { Payload } from '@common/guard/jwt.guard'; import { WinstonLoggerService } from '@common/logger/logger.service'; -import { OAuthType, StateData } from '@user/constant/oauth.constant'; +import { + OAUTH_URL_PATH, + OAuthType, + StateData, +} from '@user/constant/oauth.constant'; import { REFRESH_TOKEN_TTL } from '@user/constant/user.constants'; import { OAuthCallbackRequestDto } from '@user/dto/request/oAuthCallbackDto'; import { Provider } from '@user/entity/provider.entity'; @@ -43,6 +47,10 @@ export class OAuthService { const stateData = this.parseStateData(callbackDto.state); const { provider: providerType } = stateData; + if (callbackDto.error || !callbackDto.code) { + return res.redirect(`${OAUTH_URL_PATH.BASE_URL}/signin`); + } + const tokenData = await this.providers[providerType].getTokens( callbackDto.code, ); @@ -60,6 +68,8 @@ export class OAuthService { }, res, ); + + return res.redirect(`${OAUTH_URL_PATH.BASE_URL}/oauth-success`); } async e2eCallback(providerType: OAuthType, res: Response) { From 9a49b5fb50cf5ec0773fcbe38c2593bb4c5ddae3 Mon Sep 17 00:00:00 2001 From: CodeVac513 Date: Thu, 5 Mar 2026 15:56:10 +0900 Subject: [PATCH 3/5] =?UTF-8?q?=F0=9F=A7=BC=20clean:=20=EC=A3=BC=EC=84=9D?= =?UTF-8?q?=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/user/dto/request/oAuthCallbackDto.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/server/src/user/dto/request/oAuthCallbackDto.ts b/server/src/user/dto/request/oAuthCallbackDto.ts index 5c7f7d87d..3725e7f01 100644 --- a/server/src/user/dto/request/oAuthCallbackDto.ts +++ b/server/src/user/dto/request/oAuthCallbackDto.ts @@ -8,9 +8,6 @@ export class OAuthCallbackRequestDto { description: 'Access Token 갱신 토큰', }) @IsOptional() - // @IsNotEmpty({ - // message: 'code는 필수 입력 값입니다.', - // }) @IsString({ message: '문자열로 입력해주세요.', }) From d98ae956c14229d5828f0fd6a7b90a400c2c661c Mon Sep 17 00:00:00 2001 From: CodeVac513 Date: Thu, 5 Mar 2026 16:30:58 +0900 Subject: [PATCH 4/5] =?UTF-8?q?=E2=9C=A8=20feat:=20error=EC=99=80=20code?= =?UTF-8?q?=EA=B0=80=20=EB=B9=88=20=EB=AC=B8=EC=9E=90=EC=97=B4=EC=9D=84=20?= =?UTF-8?q?=EA=B0=80=EC=A7=80=EC=A7=80=20=EC=95=8A=EB=8F=84=EB=A1=9D=20not?= =?UTF-8?q?=20empty=20=EB=8D=B0=EC=BD=94=EB=A0=88=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/user/dto/request/oAuthCallbackDto.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/server/src/user/dto/request/oAuthCallbackDto.ts b/server/src/user/dto/request/oAuthCallbackDto.ts index 3725e7f01..8e672a4d0 100644 --- a/server/src/user/dto/request/oAuthCallbackDto.ts +++ b/server/src/user/dto/request/oAuthCallbackDto.ts @@ -8,6 +8,9 @@ export class OAuthCallbackRequestDto { description: 'Access Token 갱신 토큰', }) @IsOptional() + @IsNotEmpty({ + message: 'code는 빈 문자열일 수 없습니다.', + }) @IsString({ message: '문자열로 입력해주세요.', }) @@ -30,6 +33,9 @@ export class OAuthCallbackRequestDto { description: 'OAuth 서버 로그인 중 실패 발생 시 반환되는 값', }) @IsOptional() + @IsNotEmpty({ + message: 'error는 빈 문자열일 수 없습니다.', + }) @IsString({ message: '문자열로 입력해주세요.', }) From 83b6b45409be30bf299f2d4639fa2ef820349bf0 Mon Sep 17 00:00:00 2001 From: CodeVac513 Date: Thu, 5 Mar 2026 16:31:31 +0900 Subject: [PATCH 5/5] =?UTF-8?q?=E2=9C=85=20test:=20dto=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=EC=97=90=20=EB=94=B0=EB=9D=BC=20error=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BC=80=EC=9D=B4=EC=8A=A4=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20null=20=EC=B2=B4=ED=81=AC=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test/oauth/dto/oauth-callback.dto.spec.ts | 53 ++++++++++++++----- 1 file changed, 39 insertions(+), 14 deletions(-) diff --git a/server/test/oauth/dto/oauth-callback.dto.spec.ts b/server/test/oauth/dto/oauth-callback.dto.spec.ts index c59847fdb..874a1ff31 100644 --- a/server/test/oauth/dto/oauth-callback.dto.spec.ts +++ b/server/test/oauth/dto/oauth-callback.dto.spec.ts @@ -23,18 +23,6 @@ describe('OAuthCallbackRequestDto Test', () => { }); describe('code', () => { - it('code가 없을 경우 유효성 검사에 실패한다.', async () => { - // given - dto.code = null; - - // when - const errors = await validate(dto); - - // then - expect(errors).toHaveLength(1); - expect(errors[0].constraints).toHaveProperty('isNotEmpty'); - }); - it('code가 빈 문자열일 경우 유효성 검사에 실패한다.', async () => { // given dto.code = ''; @@ -89,10 +77,47 @@ describe('OAuthCallbackRequestDto Test', () => { // given dto.state = 1 as any; - // when + //when const errors = await validate(dto); - // then + //then + expect(errors).toHaveLength(1); + expect(errors[0].constraints).toHaveProperty('isString'); + }); + }); + + describe('error', () => { + it('error가 있을 경우 유효성 검사에 성공한다.', async () => { + //given + dto.error = 'access_denied'; + + //when + const errors = await validate(dto); + + //then + expect(errors).toHaveLength(0); + }); + + it('error가 빈 문자열일 경우 유효성 검사에 실패한다.', async () => { + //given + dto.error = ''; + + //when + const errors = await validate(dto); + + //then + expect(errors).toHaveLength(1); + expect(errors[0].constraints).toHaveProperty('isNotEmpty'); + }); + + it('error가 문자가 아닌 숫자일 경우 유효성 검사에 실패한다.', async () => { + //given + dto.error = 1 as any; + + //when + const errors = await validate(dto); + + //then expect(errors).toHaveLength(1); expect(errors[0].constraints).toHaveProperty('isString'); });