From a0a55b660eb635cc277f71fbdf646ab9a18c4322 Mon Sep 17 00:00:00 2001 From: stae1102 Date: Mon, 26 May 2025 20:05:24 +0900 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20=EA=B5=AC=EA=B8=80=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20sdk=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 286 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 3 +- 2 files changed, 288 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 6d28c1f..b0c0616 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,6 +38,7 @@ "cross-env": "^7.0.3", "crypto-js": "^4.1.1", "form-data": "^4.0.0", + "google-auth-library": "^9.15.1", "groq-sdk": "^0.15.0", "ioredis": "^5.3.2", "joi": "^17.6.0", @@ -5148,6 +5149,14 @@ "tweetnacl": "^0.14.3" } }, + "node_modules/bignumber.js": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.0.tgz", + "integrity": "sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA==", + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -7002,6 +7011,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, "node_modules/external-editor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", @@ -7461,6 +7475,66 @@ "node": ">=10" } }, + "node_modules/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gaxios/node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "engines": { + "node": ">= 14" + } + }, + "node_modules/gaxios/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/gaxios/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/gcp-metadata": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", + "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", + "dependencies": { + "gaxios": "^6.1.1", + "google-logging-utils": "^0.0.2", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -7596,6 +7670,49 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/google-auth-library": { + "version": "9.15.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", + "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-auth-library/node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/google-auth-library/node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/google-logging-utils": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", + "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", + "engines": { + "node": ">=14" + } + }, "node_modules/graceful-fs": { "version": "4.2.10", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", @@ -7616,6 +7733,37 @@ "node-fetch": "^2.6.7" } }, + "node_modules/gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/gtoken/node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/gtoken/node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, "node_modules/has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -9074,6 +9222,14 @@ "node": ">=4" } }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -17259,6 +17415,11 @@ "tweetnacl": "^0.14.3" } }, + "bignumber.js": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.0.tgz", + "integrity": "sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA==" + }, "binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -18665,6 +18826,11 @@ } } }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, "external-editor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", @@ -19018,6 +19184,49 @@ "wide-align": "^1.1.2" } }, + "gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "requires": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "dependencies": { + "agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==" + }, + "https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "requires": { + "agent-base": "^7.1.2", + "debug": "4" + } + }, + "uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==" + } + } + }, + "gcp-metadata": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", + "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", + "requires": { + "gaxios": "^6.1.1", + "google-logging-utils": "^0.0.2", + "json-bigint": "^1.0.0" + } + }, "gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -19108,6 +19317,45 @@ "slash": "^3.0.0" } }, + "google-auth-library": { + "version": "9.15.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", + "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", + "requires": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "dependencies": { + "jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "requires": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "requires": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + } + } + }, + "google-logging-utils": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", + "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==" + }, "graceful-fs": { "version": "4.2.10", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", @@ -19128,6 +19376,36 @@ "node-fetch": "^2.6.7" } }, + "gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "requires": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "dependencies": { + "jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "requires": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "requires": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + } + } + }, "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -20215,6 +20493,14 @@ "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true }, + "json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "requires": { + "bignumber.js": "^9.0.0" + } + }, "json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", diff --git a/package.json b/package.json index d005428..f17fc6f 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "start:local": "npm run start:docker && TZ=Asia/Seoul NODE_ENV=local nest start --watch", "start:docker": "docker-compose -f ./docker-compose.yml up -d", "start:debug": "nest start --debug --watch", - "start:prod": "node dist/main", + "start:prod": "node dist/main ", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "test": "jest --detectOpenHandles --forceExit", "test:watch": "cross-env NODE_ENV=test jest --watch", @@ -56,6 +56,7 @@ "cross-env": "^7.0.3", "crypto-js": "^4.1.1", "form-data": "^4.0.0", + "google-auth-library": "^9.15.1", "groq-sdk": "^0.15.0", "ioredis": "^5.3.2", "joi": "^17.6.0", From 19098c70ab91d5df0e2fd42a444face98c493a21 Mon Sep 17 00:00:00 2001 From: stae1102 Date: Mon, 26 May 2025 20:06:58 +0900 Subject: [PATCH 2/6] =?UTF-8?q?feat:=20=EC=B9=B4=EC=B9=B4=EC=98=A4/?= =?UTF-8?q?=EA=B5=AC=EA=B8=80=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20v2=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 --- src/auth/oauth.v2.controller.ts | 75 +++++++++++++ src/auth/oauth.v2.service.ts | 192 ++++++++++++++++++++++++++++++++ src/common/logger.ts | 17 +-- 3 files changed, 268 insertions(+), 16 deletions(-) create mode 100644 src/auth/oauth.v2.controller.ts create mode 100644 src/auth/oauth.v2.service.ts diff --git a/src/auth/oauth.v2.controller.ts b/src/auth/oauth.v2.controller.ts new file mode 100644 index 0000000..400bff2 --- /dev/null +++ b/src/auth/oauth.v2.controller.ts @@ -0,0 +1,75 @@ +import { Body, Controller, Post } from '@nestjs/common'; +import { + ApiBadRequestResponse, + ApiOkResponse, + ApiOperation, + ApiTags, + ApiUnauthorizedResponse, +} from '@nestjs/swagger'; +import { LoginOutput } from './dtos/login.dto'; +import { ErrorOutput } from '../common/dtos/output.dto'; +import { OAuthLoginRequest } from './dtos/request/oauth-login.request.dto'; +import { OAuthV2Service } from './oauth.v2.service'; + +@Controller('oauth/v2') +@ApiTags('oauth v2') +export class OauthV2Controller { + constructor(private readonly oauthService: OAuthV2Service) {} + + @ApiOperation({ + summary: '카카오 로그인', + description: + '카카오 로그인 메서드. (회원가입이 안되어 있으면 회원가입 처리 후 로그인 처리)', + }) + @ApiOkResponse({ + description: '로그인 성공 여부와 함께 access, refresh token을 반환한다.', + type: LoginOutput, + }) + @ApiBadRequestResponse({ + description: + '카카오 로그인 요청 시 발생하는 에러를 알려준다.(ex : email 제공에 동의하지 않은 경우)', + type: ErrorOutput, + }) + @ApiUnauthorizedResponse({ + description: '카카오 로그인 실패 여부를 알려준다.', + type: ErrorOutput, + }) + @Post('kakao') + async kakaoOauth( + @Body() oauthRequest: OAuthLoginRequest, + ): Promise { + return this.oauthService.kakaoOauth(oauthRequest); + } + + @ApiOperation({ + summary: '구글 로그인 v2', + description: + 'id token을 사용하는 구글 로그인 메서드. (회원가입이 안되어 있으면 회원가입 처리 후 로그인 처리)', + }) + @ApiOkResponse({ + description: '로그인 성공 여부와 함께 access, refresh token을 반환한다.', + type: LoginOutput, + }) + @ApiBadRequestResponse({ + description: 'code가 잘못된 경우', + type: ErrorOutput, + }) + @Post('google') + async googleAuthRedirect( + @Body() oauthRequest: OAuthLoginRequest, + ): Promise { + return this.oauthService.googleOauth(oauthRequest); + } + + // @ApiOperation({ + // summary: '애플 로그인', + // description: + // '애플 로그인 메서드. (회원가입이 안되어 있으면 회원가입 처리 후 로그인 처리)', + // }) + // @Get('apple-login') + // async appleLogin( + // @Body() oauthRequest: OAuthLoginRequest, + // ): Promise { + // return this.oauthService.appleLogin(oauthRequest); + // } +} diff --git a/src/auth/oauth.v2.service.ts b/src/auth/oauth.v2.service.ts new file mode 100644 index 0000000..274562d --- /dev/null +++ b/src/auth/oauth.v2.service.ts @@ -0,0 +1,192 @@ +import { + BadRequestException, + Injectable, + InternalServerErrorException, + UnauthorizedException, +} from '@nestjs/common'; +import { LoginOutput } from './dtos/login.dto'; +import { PROVIDER } from '../users/constant/provider.constant'; +import { User } from '../users/entities/user.entity'; +import { OAuthUtil } from './util/oauth.util'; +import { UserRepository } from '../users/repository/user.repository'; +import { Payload } from './jwt/jwt.payload'; +import { REFRESH_TOKEN_KEY } from './constants'; +import { refreshTokenExpirationInCache } from './auth.module'; +import { customJwtService } from './jwt/jwt.service'; +import { RedisService } from '../infra/redis/redis.service'; +import * as CryptoJS from 'crypto-js'; +import { CategoryRepository } from '../categories/category.repository'; +import { OAuthLoginRequest } from './dtos/request/oauth-login.request.dto'; +import { OAuth2Client } from 'google-auth-library'; +import { ConfigService } from '@nestjs/config'; + +@Injectable() +export class OAuthV2Service { + constructor( + private readonly jwtService: customJwtService, + private readonly userRepository: UserRepository, + private readonly oauthUtil: OAuthUtil, + private readonly categoryRepository: CategoryRepository, + private readonly redisService: RedisService, + private readonly configService: ConfigService, + ) {} + + private readonly googleClient = new OAuth2Client( + this.configService.get('GOOGLE_SECRET'), + ); + + // OAuth Login + async oauthLogin(email: string, provider: PROVIDER): Promise { + try { + const user: User = await this.userRepository.findOneByOrFail({ + email, + provider, + }); + if (user) { + const payload: Payload = this.jwtService.createPayload( + user.email, + true, + user.id, + ); + const refreshToken = this.jwtService.generateRefreshToken(payload); + await this.redisService.set( + `${REFRESH_TOKEN_KEY}:${user.id}`, + refreshToken, + refreshTokenExpirationInCache, + ); + + return { + access_token: this.jwtService.sign(payload), + refresh_token: refreshToken, + }; + } else { + throw new UnauthorizedException('Error in OAuth login'); + } + } catch (e) { + throw e; + } + } + + /* + * Get user info from Kakao Auth Server then create account, + * login and return access token and refresh token + */ + async kakaoOauth({ + authorizationToken, + }: OAuthLoginRequest): Promise { + try { + const { userInfo } = await this.oauthUtil.getKakaoUserInfo( + authorizationToken, + ); + + const email = userInfo.kakao_account.email; + if (!email) { + throw new BadRequestException('Please Agree to share your email'); + } + + const user = await this.userRepository.findOneByEmailAndProvider( + email, + PROVIDER.KAKAO, + ); + if (user) { + return this.oauthLogin(user.email, PROVIDER.KAKAO); + } + + // 회원가입인 경우 기본 카테고리 생성 작업 진행 + const newUser = User.of({ + email, + name: userInfo.kakao_account.profile.nickname, + profileImage: userInfo.kakao_account.profile?.profile_image_url, + password: this.encodePasswordFromEmail(email, process.env.KAKAO_JS_KEY), + provider: PROVIDER.KAKAO, + }); + + await this.userRepository.createOne(newUser); + await this.categoryRepository.createDefaultCategories(newUser); + + return this.oauthLogin(newUser.email, PROVIDER.KAKAO); + } catch (e) { + throw e; + } + } + + // Login with Google account info + async googleOauth({ + authorizationToken, + }: OAuthLoginRequest): Promise { + const ticket = await this.googleClient.verifyIdToken({ + idToken: authorizationToken, + }); + const payload = ticket.getPayload(); + + if (!payload || !payload.name || !payload.email) { + throw new BadRequestException('Invalid google payload'); + } + + const user = await this.userRepository.findOneByEmailAndProvider( + payload.email, + PROVIDER.GOOGLE, + ); + + if (user) { + return this.oauthLogin(user.email, PROVIDER.GOOGLE); + } + + // 회원가입인 경우 기본 카테고리 생성 작업 진행 + const newUser = User.of({ + email: payload.email, + name: payload.name, + profileImage: payload.picture, + password: this.encodePasswordFromEmail( + payload.email, + process.env.GOOGLE_CLIENT_ID, + ), + provider: PROVIDER.GOOGLE, + }); + + await this.userRepository.createOne(newUser); + await this.categoryRepository.createDefaultCategories(newUser); + + return this.oauthLogin(newUser.email, PROVIDER.GOOGLE); + } + + private encodePasswordFromEmail(email: string, key?: string): string { + return CryptoJS.SHA256(email + key).toString(); + } + + public async appleLogin(code: string) { + const data = await this.oauthUtil.getAppleToken(code); + + if (!data.id_token) { + throw new InternalServerErrorException( + `No token: ${JSON.stringify(data)}`, + ); + } + + const { sub: id, email } = this.jwtService.decode(data.id_token); + + const user = await this.userRepository.findOneByEmailAndProvider( + email, + PROVIDER.APPLE, + ); + + if (user) { + return this.oauthLogin(user.email, PROVIDER.APPLE); + } + + const newUser = User.of({ + email, + name: email.split('@')[0], + password: this.encodePasswordFromEmail( + email, + process.env.APPLE_CLIENT_ID, + ), + provider: PROVIDER.APPLE, + }); + + await this.userRepository.createOne(newUser); + await this.categoryRepository.createDefaultCategories(newUser); + + return this.oauthLogin(newUser.email, PROVIDER.APPLE); + } +} diff --git a/src/common/logger.ts b/src/common/logger.ts index 9549ea4..984ba80 100644 --- a/src/common/logger.ts +++ b/src/common/logger.ts @@ -1,5 +1,4 @@ import * as winston from 'winston'; -import * as DailyRotateFile from 'winston-daily-rotate-file'; const { combine, label, printf, colorize } = winston.format; const logFormat = printf(({ level, label, message }) => { @@ -27,21 +26,7 @@ export const logger = winston.createLogger({ level: 'info', levels: custom_level.levels, format: combine(colorize(), label({ label: 'Quickchive' }), logFormat), - transports: [ - new winston.transports.Console(), - new DailyRotateFile({ - filename: 'errors-%DATE%.log', - datePattern: 'YYYY-MM-DD', - maxSize: '1024', - level: 'error', - }), - new DailyRotateFile({ - filename: '%DATE%.log', - datePattern: 'YYYY-MM-DD', - maxSize: '1024', - level: 'info', - }), - ], + transports: [new winston.transports.Console()], }); export function getKoreaTime(): Date { From 067d6f1027d51bca2a0ff19aa57679c1820b2255 Mon Sep 17 00:00:00 2001 From: stae1102 Date: Mon, 26 May 2025 20:12:22 +0900 Subject: [PATCH 3/6] =?UTF-8?q?feat:=20oauth=20v2=20=EC=BB=A8=ED=8A=B8?= =?UTF-8?q?=EB=A1=A4=EB=9F=AC=20=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/auth/auth.module.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts index 2cfd8fb..4cba4aa 100644 --- a/src/auth/auth.module.ts +++ b/src/auth/auth.module.ts @@ -14,6 +14,8 @@ import { ContentsModule } from '../contents/contents.module'; import { OAuthService } from './oauth.service'; import { CategoryModule } from '../categories/category.module'; import { RedisModule } from '../infra/redis/redis.module'; +import { OauthV2Controller } from './oauth.v2.controller'; +import { OAuthV2Service } from './oauth.v2.service'; const accessTokenExpiration = TWOHOUR; export const refreshTokenExpirationInCache = 60 * 60 * 24 * 365; // 1 year @@ -34,7 +36,7 @@ export const verifyEmailExpiration = 60 * 5; CategoryModule, RedisModule, ], - controllers: [AuthController, OAuthController], + controllers: [AuthController, OAuthController, OauthV2Controller], providers: [ AuthService, JwtStrategy, @@ -42,6 +44,7 @@ export const verifyEmailExpiration = 60 * 5; OAuthUtil, GoogleStrategy, customJwtService, + OAuthV2Service, ], exports: [AuthService], }) From 42babd50ad0bc8ed53b024dd8efca60084cace7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=B1=ED=83=9C?= Date: Sun, 8 Jun 2025 12:26:39 +0900 Subject: [PATCH 4/6] =?UTF-8?q?fix:=20=EC=98=88=EC=99=B8=20=ED=95=84?= =?UTF-8?q?=ED=84=B0=20=ED=83=80=EC=9E=85=20=EC=97=90=EB=9F=AC=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/exceptions/http-exception.filter.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/common/exceptions/http-exception.filter.ts b/src/common/exceptions/http-exception.filter.ts index 333e7bb..207b5c4 100644 --- a/src/common/exceptions/http-exception.filter.ts +++ b/src/common/exceptions/http-exception.filter.ts @@ -1,8 +1,9 @@ import { - ExceptionFilter, - Catch, ArgumentsHost, + Catch, + ExceptionFilter, HttpException, + HttpStatus, } from '@nestjs/common'; import { Request, Response } from 'express'; import { logger } from '../logger'; @@ -37,7 +38,10 @@ export class HttpExceptionFilter implements ExceptionFilter { const ctx = host.switchToHttp(); const response = ctx.getResponse(); const request = ctx.getRequest(); - const status = exception.getStatus(); + const status = + exception instanceof HttpException + ? exception.getStatus() + : HttpStatus.INTERNAL_SERVER_ERROR; const { ip, method, url } = request; logger.error( `${method} - ${url} - ${ip.split(':').at(-1)} - ${JSON.stringify( From af5df60df91596b1a8a85063caf51f89ca2a65e2 Mon Sep 17 00:00:00 2001 From: stae1102 Date: Fri, 4 Jul 2025 14:30:57 +0900 Subject: [PATCH 5/6] =?UTF-8?q?fix:=20http=20exception=EA=B0=80=20?= =?UTF-8?q?=EC=95=84=EB=8B=8C=20=EA=B8=B0=ED=83=80=20=EC=97=90=EB=9F=AC?= =?UTF-8?q?=EB=A5=BC=20internal=20server=20exception=20=EC=9C=BC=EB=A1=9C?= =?UTF-8?q?=20=EA=B0=90=EC=8B=B8=EC=84=9C=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/exceptions/http-exception.filter.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/common/exceptions/http-exception.filter.ts b/src/common/exceptions/http-exception.filter.ts index 207b5c4..3b3cdb6 100644 --- a/src/common/exceptions/http-exception.filter.ts +++ b/src/common/exceptions/http-exception.filter.ts @@ -3,7 +3,7 @@ import { Catch, ExceptionFilter, HttpException, - HttpStatus, + HttpStatus, InternalServerErrorException, } from '@nestjs/common'; import { Request, Response } from 'express'; import { logger } from '../logger'; @@ -34,7 +34,7 @@ class ErrorResponse { @Catch() export class HttpExceptionFilter implements ExceptionFilter { - catch(exception: HttpException, host: ArgumentsHost) { + catch(exception: HttpException | Error, host: ArgumentsHost) { const ctx = host.switchToHttp(); const response = ctx.getResponse(); const request = ctx.getRequest(); @@ -49,7 +49,11 @@ export class HttpExceptionFilter implements ExceptionFilter { )}`, ); - const exceptionResponse = exception.getResponse() as { + if (!(exception instanceof HttpException)) { + exception = new InternalServerErrorException(exception.message); + } + + const exceptionResponse = (exception as HttpException).getResponse() as { error: string; message?: string | string[]; data?: any; From a0406032bf2434c9570bfd5c1c2ee4ac64cdf752 Mon Sep 17 00:00:00 2001 From: stae1102 Date: Fri, 4 Jul 2025 14:31:34 +0900 Subject: [PATCH 6/6] =?UTF-8?q?fix:=20logging=20ip=20split=20=EC=98=B5?= =?UTF-8?q?=EC=85=94=EB=84=90=20=EC=B2=B4=EC=9D=B4=EB=8B=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/exceptions/http-exception.filter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/exceptions/http-exception.filter.ts b/src/common/exceptions/http-exception.filter.ts index 3b3cdb6..6cfa13e 100644 --- a/src/common/exceptions/http-exception.filter.ts +++ b/src/common/exceptions/http-exception.filter.ts @@ -44,7 +44,7 @@ export class HttpExceptionFilter implements ExceptionFilter { : HttpStatus.INTERNAL_SERVER_ERROR; const { ip, method, url } = request; logger.error( - `${method} - ${url} - ${ip.split(':').at(-1)} - ${JSON.stringify( + `${method} - ${url} - ${ip?.split(':')?.at(-1)} - ${JSON.stringify( exception, )}`, );