From 9164e623f77cb43aaeef131d99c708759663afe0 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Sun, 5 Jan 2025 22:17:06 +0900 Subject: [PATCH 01/14] =?UTF-8?q?[Test]=20develop=20=EB=B8=8C=EB=9E=9C?= =?UTF-8?q?=EC=B9=98=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.ts b/src/main.ts index fb51794..3ba7e8e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -7,6 +7,6 @@ async function bootstrap() { const app = await NestFactory.create(AppModule); const port = process.env.PORT; await app.listen(process.env.PORT); - console.log(`Application is running on: http://localhost:${port}`); + console.log(`Application is run on: http://localhost:${port}`); } bootstrap(); From f421d7c8427b9982ea5df16ea6f05ba67614fde1 Mon Sep 17 00:00:00 2001 From: ssomae <80831228+Ss0Mae@users.noreply.github.com> Date: Sun, 5 Jan 2025 22:21:20 +0900 Subject: [PATCH 02/14] #1 [Feature /1 directory setting] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Chore] 1feature/1-directory_setting 디렉토리 셋팅 * [Chore] Prettier 설정 수정 * [Fix] import alias 설정 수정 * [Add] 프로젝트 패키지 추가 --- .eslintrc.js | 7 ++ .prettierrc | 9 ++- package-lock.json | 87 +++++++++++++++++++++++ package.json | 1 + src/common/.gitkeep | 0 src/common/decorators/.gitkeep | 0 src/common/dto/.gitkeep | 0 src/common/filters/.gitkeep | 0 src/common/pipes/.gitkeep | 0 src/common/utils/.gitkeep | 0 src/config/.gitkeep | 0 src/modules/.gitkeep | 0 src/modules/auth/auth.controller.ts | 4 ++ src/modules/auth/auth.module.ts | 9 +++ src/modules/auth/auth.service.ts | 4 ++ src/modules/auth/dto/.gitkeep | 0 src/modules/auth/entities/.gitkeep | 0 src/modules/feed/dto/.gitkeep | 0 src/modules/feed/entities/.gitkeep | 0 src/modules/feed/feed.controller.ts | 4 ++ src/modules/feed/feed.module.ts | 9 +++ src/modules/feed/feed.service.ts | 4 ++ src/modules/project/dto/.gitkeep | 0 src/modules/project/entities/.gitkeep | 0 src/modules/project/project.controller.ts | 4 ++ src/modules/project/project.module.ts | 9 +++ src/modules/project/project.service.ts | 4 ++ src/modules/user/dto/.gitkeep | 0 src/modules/user/entities/.gitkeep | 0 src/modules/user/user.controller.ts | 4 ++ src/modules/user/user.module.ts | 4 ++ src/modules/user/user.service.ts | 4 ++ {prisma => src/prisma}/schema.prisma | 0 tsconfig.json | 8 ++- 34 files changed, 171 insertions(+), 4 deletions(-) create mode 100644 src/common/.gitkeep create mode 100644 src/common/decorators/.gitkeep create mode 100644 src/common/dto/.gitkeep create mode 100644 src/common/filters/.gitkeep create mode 100644 src/common/pipes/.gitkeep create mode 100644 src/common/utils/.gitkeep create mode 100644 src/config/.gitkeep create mode 100644 src/modules/.gitkeep create mode 100644 src/modules/auth/auth.controller.ts create mode 100644 src/modules/auth/auth.module.ts create mode 100644 src/modules/auth/auth.service.ts create mode 100644 src/modules/auth/dto/.gitkeep create mode 100644 src/modules/auth/entities/.gitkeep create mode 100644 src/modules/feed/dto/.gitkeep create mode 100644 src/modules/feed/entities/.gitkeep create mode 100644 src/modules/feed/feed.controller.ts create mode 100644 src/modules/feed/feed.module.ts create mode 100644 src/modules/feed/feed.service.ts create mode 100644 src/modules/project/dto/.gitkeep create mode 100644 src/modules/project/entities/.gitkeep create mode 100644 src/modules/project/project.controller.ts create mode 100644 src/modules/project/project.module.ts create mode 100644 src/modules/project/project.service.ts create mode 100644 src/modules/user/dto/.gitkeep create mode 100644 src/modules/user/entities/.gitkeep create mode 100644 src/modules/user/user.controller.ts create mode 100644 src/modules/user/user.module.ts create mode 100644 src/modules/user/user.service.ts rename {prisma => src/prisma}/schema.prisma (100%) diff --git a/.eslintrc.js b/.eslintrc.js index 259de13..e7e8667 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -22,4 +22,11 @@ module.exports = { '@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/no-explicit-any': 'off', }, + settings: { + 'import/resolver': { + typescript: { + project: './tsconfig.json', // tsconfig.json 파일 경로 + }, + } + } }; diff --git a/.prettierrc b/.prettierrc index dcb7279..813f04d 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,4 +1,9 @@ { "singleQuote": true, - "trailingComma": "all" -} \ No newline at end of file + "semi": true, + "tabWidth": 2, + "useTabs": false, + "trailingComma": "es5", + "printWidth": 80, + "arrowParens": "avoid" +} diff --git a/package-lock.json b/package-lock.json index e1d1494..2ba8253 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,6 +40,7 @@ "@typescript-eslint/parser": "^8.0.0", "eslint": "^8.0.0", "eslint-config-prettier": "^9.0.0", + "eslint-import-resolver-typescript": "^3.7.0", "eslint-plugin-prettier": "^5.0.0", "husky": "^9.1.7", "jest": "^29.5.0", @@ -2839,6 +2840,16 @@ "node": ">= 8" } }, + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.4.0" + } + }, "node_modules/@nuxtjs/opencollective": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/@nuxtjs/opencollective/-/opencollective-0.3.2.tgz", @@ -6085,6 +6096,42 @@ "eslint": ">=7.0.0" } }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.7.0.tgz", + "integrity": "sha512-Vrwyi8HHxY97K5ebydMtffsWAn1SCR9eol49eCd5fJS4O1WV7PaAjbcjmbfJJSMz/t4Mal212Uz/fQZrOB8mow==", + "dev": true, + "license": "ISC", + "dependencies": { + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.3.7", + "enhanced-resolve": "^5.15.0", + "fast-glob": "^3.3.2", + "get-tsconfig": "^4.7.5", + "is-bun-module": "^1.0.2", + "is-glob": "^4.0.3", + "stable-hash": "^0.0.4" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts/projects/eslint-import-resolver-ts" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, "node_modules/eslint-plugin-prettier": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", @@ -6977,6 +7024,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-tsconfig": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz", + "integrity": "sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -7357,6 +7417,16 @@ "node": ">=8" } }, + "node_modules/is-bun-module": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-1.3.0.tgz", + "integrity": "sha512-DgXeu5UWI0IsMQundYb5UAOzm6G2eVnarJ0byP6Tm55iZNKceD59LNPA2L4VvsScTtHcw0yEkVwSf7PC+QoLSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.6.3" + } + }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -9859,6 +9929,16 @@ "node": ">=4" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/resolve.exports": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", @@ -10360,6 +10440,13 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/stable-hash": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.4.tgz", + "integrity": "sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==", + "dev": true, + "license": "MIT" + }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", diff --git a/package.json b/package.json index e1b5a51..80eef47 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "@typescript-eslint/parser": "^8.0.0", "eslint": "^8.0.0", "eslint-config-prettier": "^9.0.0", + "eslint-import-resolver-typescript": "^3.7.0", "eslint-plugin-prettier": "^5.0.0", "husky": "^9.1.7", "jest": "^29.5.0", diff --git a/src/common/.gitkeep b/src/common/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/common/decorators/.gitkeep b/src/common/decorators/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/common/dto/.gitkeep b/src/common/dto/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/common/filters/.gitkeep b/src/common/filters/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/common/pipes/.gitkeep b/src/common/pipes/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/common/utils/.gitkeep b/src/common/utils/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/config/.gitkeep b/src/config/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/.gitkeep b/src/modules/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts new file mode 100644 index 0000000..268eeb2 --- /dev/null +++ b/src/modules/auth/auth.controller.ts @@ -0,0 +1,4 @@ +import { Controller } from '@nestjs/common'; + +@Controller('auth') +export class AuthController {} diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts new file mode 100644 index 0000000..a7d9fbc --- /dev/null +++ b/src/modules/auth/auth.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { AuthController } from './auth.controller'; +import { AuthService } from './auth.service'; + +@Module({ + controllers: [AuthController], + providers: [AuthService], +}) +export class AuthModule {} diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts new file mode 100644 index 0000000..a41c649 --- /dev/null +++ b/src/modules/auth/auth.service.ts @@ -0,0 +1,4 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class AuthService {} diff --git a/src/modules/auth/dto/.gitkeep b/src/modules/auth/dto/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/auth/entities/.gitkeep b/src/modules/auth/entities/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/feed/dto/.gitkeep b/src/modules/feed/dto/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/feed/entities/.gitkeep b/src/modules/feed/entities/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/feed/feed.controller.ts b/src/modules/feed/feed.controller.ts new file mode 100644 index 0000000..791329a --- /dev/null +++ b/src/modules/feed/feed.controller.ts @@ -0,0 +1,4 @@ +import { Controller } from '@nestjs/common'; + +@Controller('feed') +export class FeedController {} diff --git a/src/modules/feed/feed.module.ts b/src/modules/feed/feed.module.ts new file mode 100644 index 0000000..b5d6096 --- /dev/null +++ b/src/modules/feed/feed.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { FeedService } from './feed.service'; +import { FeedController } from './feed.controller'; + +@Module({ + providers: [FeedService], + controllers: [FeedController], +}) +export class FeedModule {} diff --git a/src/modules/feed/feed.service.ts b/src/modules/feed/feed.service.ts new file mode 100644 index 0000000..6aa77a2 --- /dev/null +++ b/src/modules/feed/feed.service.ts @@ -0,0 +1,4 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class FeedService {} diff --git a/src/modules/project/dto/.gitkeep b/src/modules/project/dto/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/project/entities/.gitkeep b/src/modules/project/entities/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/project/project.controller.ts b/src/modules/project/project.controller.ts new file mode 100644 index 0000000..66980ae --- /dev/null +++ b/src/modules/project/project.controller.ts @@ -0,0 +1,4 @@ +import { Controller } from '@nestjs/common'; + +@Controller('project') +export class ProjectController {} diff --git a/src/modules/project/project.module.ts b/src/modules/project/project.module.ts new file mode 100644 index 0000000..4e9329e --- /dev/null +++ b/src/modules/project/project.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { ProjectController } from './project.controller'; +import { ProjectService } from './project.service'; + +@Module({ + controllers: [ProjectController], + providers: [ProjectService], +}) +export class ProjectModule {} diff --git a/src/modules/project/project.service.ts b/src/modules/project/project.service.ts new file mode 100644 index 0000000..3274dd0 --- /dev/null +++ b/src/modules/project/project.service.ts @@ -0,0 +1,4 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class ProjectService {} diff --git a/src/modules/user/dto/.gitkeep b/src/modules/user/dto/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/user/entities/.gitkeep b/src/modules/user/entities/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/user/user.controller.ts b/src/modules/user/user.controller.ts new file mode 100644 index 0000000..ad8c2a6 --- /dev/null +++ b/src/modules/user/user.controller.ts @@ -0,0 +1,4 @@ +import { Controller } from '@nestjs/common'; + +@Controller('user') +export class UserController {} diff --git a/src/modules/user/user.module.ts b/src/modules/user/user.module.ts new file mode 100644 index 0000000..309e84a --- /dev/null +++ b/src/modules/user/user.module.ts @@ -0,0 +1,4 @@ +import { Module } from '@nestjs/common'; + +@Module({}) +export class UserModule {} diff --git a/src/modules/user/user.service.ts b/src/modules/user/user.service.ts new file mode 100644 index 0000000..668a7d6 --- /dev/null +++ b/src/modules/user/user.service.ts @@ -0,0 +1,4 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class UserService {} diff --git a/prisma/schema.prisma b/src/prisma/schema.prisma similarity index 100% rename from prisma/schema.prisma rename to src/prisma/schema.prisma diff --git a/tsconfig.json b/tsconfig.json index b1952c5..ba89119 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,8 +17,12 @@ "strictBindCallApply": false, "forceConsistentCasingInFileNames": false, "noFallthroughCasesInSwitch": false, - "paths": { // 하단부에 이 부분만 추가하시면 됩니다 - "@src/*": ["./src/*"] + "paths": { + "@src/*": ["src/*"], // alias 설정 + "@modules/*": ["src/modules/*"], // modules alias + "@common/*": ["src/common/*"], // common alias + "@config/*": ["src/config/*"], // config alias + "@prisma/*": ["src/prisma/*"] // prisma alias } } } From 586db3c822d4ca4abaf19d4831a8e931173b61c4 Mon Sep 17 00:00:00 2001 From: ssomae <80831228+Ss0Mae@users.noreply.github.com> Date: Mon, 6 Jan 2025 21:39:25 +0900 Subject: [PATCH 03/14] =?UTF-8?q?#2=20=EA=B5=AC=EA=B8=80=20=EC=86=8C?= =?UTF-8?q?=EC=85=9C=EB=A1=9C=EA=B7=B8=EC=9D=B8,=20=EA=B9=83=ED=97=88?= =?UTF-8?q?=EB=B8=8C=20=EC=86=8C=EC=85=9C=EB=A1=9C=EA=B7=B8=EC=9D=B8,=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=EB=B2=A0=EC=9D=B4=EC=8A=A4=20?= =?UTF-8?q?=EC=8A=A4=ED=82=A4=EB=A7=88=EC=9E=91=EC=84=B1,=20=20(#5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Feat] main Module에 auth Module 연결 구현 * [Add] 패키지 추가 * [Feat] Prisma 사용을 위한 모듈 및 서비스 구현 * [Feat] S3 사용을 위한 모듈 및 서비스 구현 * [Feat] Prisma Scheme 생성 구현 * [Feat] Prisma Scheme 초기 기본 데이터 생성 * [Feat] 유저 가입시 데이터 전달을 위한 DTO 타입 구현 * [Feat] github 소셜 로그인을 위한 Strategy 구현 * [Feat] google 소셜 로그인을 위한 Strategy 구현 * [Feat] Common Error Filter 구현 * [Feat] Constant 에러 메세지 구현 * [Feat] 깃허브, 구글 소셜로그인 컨트롤러 * [Feat] AuthModule 구현 * [Feat] Auth Service 로직 구현 * [Feat] Error Message 추가 * [Feat] UserModule 연결 * [Feat] JWT 공통 인증 가드 추가 * [Feat] User Select-role API 1차적으로 구현 --- package-lock.json | 611 ++++++++++++++++++ package.json | 10 + prisma/schema.prisma | 307 +++++++++ src/app.controller.ts | 11 +- src/app.module.ts | 16 +- src/common/constants/error-messages.ts | 10 + src/common/filters/http-exception.filter.ts | 36 ++ src/main.ts | 4 + src/modules/auth/auth.controller.ts | 35 +- src/modules/auth/auth.module.ts | 26 +- src/modules/auth/auth.service.ts | 48 +- src/modules/auth/dto/auth-user.dto.ts | 7 + src/modules/auth/guards/jwt-auth.guard.ts | 5 + .../auth/strategies/github.strategy.ts | 36 ++ .../auth/strategies/google.strategy.ts | 37 ++ src/modules/auth/strategies/jwt.strategy.ts | 18 + src/modules/user/dto/set-role.dto.ts | 3 + src/modules/user/user.controller.ts | 17 +- src/modules/user/user.module.ts | 11 +- src/modules/user/user.service.ts | 27 +- src/prisma/prisma.module.ts | 9 + src/prisma/prisma.service.ts | 18 + src/prisma/schema.prisma | 14 - src/prisma/seed.ts | 39 ++ src/s3/s3.module.ts | 8 + src/s3/s3.service.ts | 37 ++ 26 files changed, 1368 insertions(+), 32 deletions(-) create mode 100644 prisma/schema.prisma create mode 100644 src/common/constants/error-messages.ts create mode 100644 src/common/filters/http-exception.filter.ts create mode 100644 src/modules/auth/dto/auth-user.dto.ts create mode 100644 src/modules/auth/guards/jwt-auth.guard.ts create mode 100644 src/modules/auth/strategies/github.strategy.ts create mode 100644 src/modules/auth/strategies/google.strategy.ts create mode 100644 src/modules/auth/strategies/jwt.strategy.ts create mode 100644 src/modules/user/dto/set-role.dto.ts create mode 100644 src/prisma/prisma.module.ts create mode 100644 src/prisma/prisma.service.ts delete mode 100644 src/prisma/schema.prisma create mode 100644 src/prisma/seed.ts create mode 100644 src/s3/s3.module.ts create mode 100644 src/s3/s3.service.ts diff --git a/package-lock.json b/package-lock.json index 2ba8253..07d95a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@aws-sdk/client-s3": "^3.717.0", "@nestjs/common": "^10.0.0", + "@nestjs/config": "^3.3.0", "@nestjs/core": "^10.0.0", "@nestjs/jwt": "^10.2.0", "@nestjs/passport": "^10.0.3", @@ -21,8 +22,13 @@ "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "dotenv": "^16.4.7", + "jsonwebtoken": "^9.0.2", "passport": "^0.7.0", + "passport-github": "^1.1.0", + "passport-github2": "^0.1.12", + "passport-google-oauth20": "^2.0.0", "passport-jwt": "^4.0.1", + "redis": "^4.7.0", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", "swagger-ui-express": "^5.0.1" @@ -35,9 +41,12 @@ "@types/express": "^5.0.0", "@types/jest": "^29.5.2", "@types/node": "^20.3.1", + "@types/passport-github2": "^1.2.9", + "@types/passport-google-oauth20": "^2.0.16", "@types/supertest": "^6.0.0", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", + "aws-sdk": "^2.1692.0", "eslint": "^8.0.0", "eslint-config-prettier": "^9.0.0", "eslint-import-resolver-typescript": "^3.7.0", @@ -2615,6 +2624,33 @@ } } }, + "node_modules/@nestjs/config": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-3.3.0.tgz", + "integrity": "sha512-pdGTp8m9d0ZCrjTpjkUbZx6gyf2IKf+7zlkrPNMsJzYZ4bFRRTpXrnj+556/5uiI6AfL5mMrJc2u7dB6bvM+VA==", + "license": "MIT", + "dependencies": { + "dotenv": "16.4.5", + "dotenv-expand": "10.0.0", + "lodash": "4.17.21" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "rxjs": "^7.1.0" + } + }, + "node_modules/@nestjs/config/node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/@nestjs/core": { "version": "10.4.15", "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.4.15.tgz", @@ -2960,6 +2996,71 @@ "@prisma/debug": "6.1.0" } }, + "node_modules/@redis/bloom": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz", + "integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/client": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.0.tgz", + "integrity": "sha512-aR0uffYI700OEEH4gYnitAnv3vzVGXCFvYfdpu/CJKvk4pHfLPEy/JSZyrpQ+15WhXe1yJRXLtfQ84s4mEXnPg==", + "license": "MIT", + "dependencies": { + "cluster-key-slot": "1.1.2", + "generic-pool": "3.9.0", + "yallist": "4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@redis/client/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/@redis/graph": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.1.tgz", + "integrity": "sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/json": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.7.tgz", + "integrity": "sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/search": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.2.0.tgz", + "integrity": "sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/time-series": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.1.0.tgz", + "integrity": "sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, "node_modules/@scarf/scarf": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", @@ -3944,6 +4045,62 @@ "undici-types": "~6.19.2" } }, + "node_modules/@types/oauth": { + "version": "0.9.6", + "resolved": "https://registry.npmjs.org/@types/oauth/-/oauth-0.9.6.tgz", + "integrity": "sha512-H9TRCVKBNOhZZmyHLqFt9drPM9l+ShWiqqJijU1B8P3DX3ub84NjxDuy+Hjrz+fEca5Kwip3qPMKNyiLgNJtIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/passport": { + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz", + "integrity": "sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/passport-github2": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@types/passport-github2/-/passport-github2-1.2.9.tgz", + "integrity": "sha512-/nMfiPK2E6GKttwBzwj0Wjaot8eHrM57hnWxu52o6becr5/kXlH/4yE2v2rh234WGvSgEEzIII02Nc5oC5xEHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/passport": "*", + "@types/passport-oauth2": "*" + } + }, + "node_modules/@types/passport-google-oauth20": { + "version": "2.0.16", + "resolved": "https://registry.npmjs.org/@types/passport-google-oauth20/-/passport-google-oauth20-2.0.16.tgz", + "integrity": "sha512-ayXK2CJ7uVieqhYOc6k/pIr5pcQxOLB6kBev+QUGS7oEZeTgIs1odDobXRqgfBPvXzl0wXCQHftV5220czZCPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/passport": "*", + "@types/passport-oauth2": "*" + } + }, + "node_modules/@types/passport-oauth2": { + "version": "1.4.17", + "resolved": "https://registry.npmjs.org/@types/passport-oauth2/-/passport-oauth2-1.4.17.tgz", + "integrity": "sha512-ODiAHvso6JcWJ6ZkHHroVp05EHGhqQN533PtFNBkg8Fy5mERDqsr030AX81M0D69ZcaMvhF92SRckEk2B0HYYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/oauth": "*", + "@types/passport": "*" + } + }, "node_modules/@types/qs": { "version": "6.9.17", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz", @@ -4702,6 +4859,84 @@ "dev": true, "license": "MIT" }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/aws-sdk": { + "version": "2.1692.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1692.0.tgz", + "integrity": "sha512-x511uiJ/57FIsbgUe5csJ13k3uzu25uWQE+XqfBis/sB0SFoiElJWXRkgEAUh0U6n40eT3ay5Ue4oPkRMu1LYw==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "buffer": "4.9.2", + "events": "1.1.1", + "ieee754": "1.1.13", + "jmespath": "0.16.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "util": "^0.12.4", + "uuid": "8.0.0", + "xml2js": "0.6.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/aws-sdk/node_modules/buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "node_modules/aws-sdk/node_modules/events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/aws-sdk/node_modules/ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/aws-sdk/node_modules/uuid": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", + "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -4855,6 +5090,15 @@ ], "license": "MIT" }, + "node_modules/base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/bcrypt": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", @@ -5391,6 +5635,15 @@ "node": ">=0.8" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -5849,6 +6102,15 @@ "url": "https://dotenvx.com" } }, + "node_modules/dotenv-expand": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz", + "integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -6730,6 +6992,16 @@ "dev": true, "license": "ISC" }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.1.3" + } + }, "node_modules/foreground-child": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", @@ -6957,6 +7229,15 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "license": "ISC" }, + "node_modules/generic-pool": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", + "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -7011,6 +7292,20 @@ "node": ">=8.0.0" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -7164,6 +7459,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", @@ -7397,6 +7708,23 @@ "node": ">= 0.10" } }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -7427,6 +7755,19 @@ "semver": "^7.6.3" } }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -7472,6 +7813,25 @@ "node": ">=6" } }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -7515,6 +7875,25 @@ "node": ">=8" } }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -7528,6 +7907,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -8412,6 +8807,16 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/jmespath": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", + "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -9142,6 +9547,12 @@ "set-blocking": "^2.0.0" } }, + "node_modules/oauth": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.0.tgz", + "integrity": "sha512-1orQ9MT1vHFGQxhuy7E/0gECD3fd2fCC+PIX+/jgmU/gI3EpRocXtmtvxCO5x3WZ443FLTLFWNDjl5MPJf9u+Q==", + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -9360,6 +9771,41 @@ "url": "https://github.com/sponsors/jaredhanson" } }, + "node_modules/passport-github": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/passport-github/-/passport-github-1.1.0.tgz", + "integrity": "sha512-XARXJycE6fFh/dxF+Uut8OjlwbFEXgbPVj/+V+K7cvriRK7VcAOm+NgBmbiLM9Qv3SSxEAV+V6fIk89nYHXa8A==", + "license": "MIT", + "dependencies": { + "passport-oauth2": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/passport-github2": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/passport-github2/-/passport-github2-0.1.12.tgz", + "integrity": "sha512-3nPUCc7ttF/3HSP/k9sAXjz3SkGv5Nki84I05kSQPo01Jqq1NzJACgMblCK0fGcv9pKCG/KXU3AJRDGLqHLoIw==", + "dependencies": { + "passport-oauth2": "1.x.x" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/passport-google-oauth20": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz", + "integrity": "sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ==", + "license": "MIT", + "dependencies": { + "passport-oauth2": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/passport-jwt": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz", @@ -9370,6 +9816,26 @@ "passport-strategy": "^1.0.0" } }, + "node_modules/passport-oauth2": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz", + "integrity": "sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==", + "license": "MIT", + "dependencies": { + "base64url": "3.x.x", + "oauth": "0.10.x", + "passport-strategy": "1.x.x", + "uid2": "0.0.x", + "utils-merge": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, "node_modules/passport-strategy": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", @@ -9568,6 +10034,16 @@ "node": ">=4" } }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -9730,6 +10206,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "dev": true, + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -9839,6 +10325,23 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/redis": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/redis/-/redis-4.7.0.tgz", + "integrity": "sha512-zvmkHEAdGMn+hMRXuMBtu4Vo5P6rHQjLoHftu+lBqq8ZTA3RCVC/WzD790bkKKiNFp7d5/9PcSD19fJyyRvOdQ==", + "license": "MIT", + "workspaces": [ + "./packages/*" + ], + "dependencies": { + "@redis/bloom": "1.2.0", + "@redis/client": "1.6.0", + "@redis/graph": "1.1.1", + "@redis/json": "1.0.7", + "@redis/search": "1.2.0", + "@redis/time-series": "1.1.0" + } + }, "node_modules/reflect-metadata": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", @@ -10103,12 +10606,37 @@ ], "license": "MIT" }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==", + "dev": true, + "license": "ISC" + }, "node_modules/schema-utils": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", @@ -11289,6 +11817,12 @@ "node": ">=8" } }, + "node_modules/uid2": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz", + "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==", + "license": "MIT" + }, "node_modules/undici-types": { "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", @@ -11355,6 +11889,38 @@ "punycode": "^2.1.0" } }, + "node_modules/url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "node_modules/url/node_modules/punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -11580,6 +12146,27 @@ "node": ">= 8" } }, + "node_modules/which-typed-array": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.18.tgz", + "integrity": "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/wide-align": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", @@ -11660,6 +12247,30 @@ "dev": true, "license": "ISC" }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "dev": true, + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 80eef47..ffa2593 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "scripts": { "build": "nest build", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", + "prisma:seed": "ts-node src/prisma/seed.ts", "start": "nest start", "start:dev": "nest start --watch", "start:debug": "nest start --debug --watch", @@ -23,6 +24,7 @@ "dependencies": { "@aws-sdk/client-s3": "^3.717.0", "@nestjs/common": "^10.0.0", + "@nestjs/config": "^3.3.0", "@nestjs/core": "^10.0.0", "@nestjs/jwt": "^10.2.0", "@nestjs/passport": "^10.0.3", @@ -33,8 +35,13 @@ "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "dotenv": "^16.4.7", + "jsonwebtoken": "^9.0.2", "passport": "^0.7.0", + "passport-github": "^1.1.0", + "passport-github2": "^0.1.12", + "passport-google-oauth20": "^2.0.0", "passport-jwt": "^4.0.1", + "redis": "^4.7.0", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", "swagger-ui-express": "^5.0.1" @@ -47,9 +54,12 @@ "@types/express": "^5.0.0", "@types/jest": "^29.5.2", "@types/node": "^20.3.1", + "@types/passport-github2": "^1.2.9", + "@types/passport-google-oauth20": "^2.0.16", "@types/supertest": "^6.0.0", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", + "aws-sdk": "^2.1692.0", "eslint": "^8.0.0", "eslint-config-prettier": "^9.0.0", "eslint-import-resolver-typescript": "^3.7.0", diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..cc2ebdf --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,307 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "mysql" + url = env("DATABASE_URL") +} + +model User { + id Int @id @default(autoincrement()) + email String @unique + name String + nickname String + auth_provider String + profile_url String? + role_id Int + introduce String? + status_id Int + apply_count Int? @default(0) + post_count Int? @default(0) + push_alert Boolean @default(false) + following_alert Boolean @default(false) + project_alert Boolean @default(false) + created_at DateTime @default(now()) + updated_at DateTime @updatedAt + ArtistData ArtistData? + FeedComments FeedComment[] + FeedLikes FeedLike[] + FeedPosts FeedPost[] + Followed Follows[] @relation("FollowedUsers") + Follows Follows[] @relation("UserFollows") + ProgrammerData ProgrammerData? + ProjectPosts Project? + ProjectPost ProjectPost[] + ProjectSaves ProjectSave[] + Resume Resume[] + role Role @relation(fields: [role_id], references: [id]) + status Status @relation(fields: [status_id], references: [id]) + UserApplyProject UserApplyProject[] + UserLinks UserLink[] + UserSkills UserSkill[] + + + @@index([role_id], map: "User_role_id_fkey") + @@index([status_id], map: "User_status_id_fkey") +} + +model Role { + id Int @id @default(autoincrement()) + name String @unique + Users User[] +} + +model Project { + id Int @id @default(autoincrement()) + user_id Int @unique + title String + description String + created_at DateTime @default(now()) + updated_at DateTime @updatedAt + user User @relation(fields: [user_id], references: [id]) + ProjectLinks ProjectLink[] +} + +model ProjectLink { + id Int @id @default(autoincrement()) + project_id Int + type_id Int + url String + project Project @relation(fields: [project_id], references: [id]) + type LinkType @relation(fields: [type_id], references: [id]) + + @@index([project_id], map: "ProjectLink_project_id_fkey") + @@index([type_id], map: "ProjectLink_type_id_fkey") +} + +model LinkType { + id Int @id @default(autoincrement()) + name String @unique + Links ProjectLink[] +} + +model ProgrammerData { + id Int @id @default(autoincrement()) + user_id Int @unique + github_username String + github_url String + commit_count Int + contribution_data String + user User @relation(fields: [user_id], references: [id]) +} + +model ArtistData { + id Int @id @default(autoincrement()) + user_id Int @unique + soundcloud_url String + portfolio_url String + music_data String + user User @relation(fields: [user_id], references: [id]) +} + +model Status { + id Int @id @default(autoincrement()) + name String + Users User[] +} + +model Resume { + id Int @id @default(autoincrement()) + user_id Int + title String + introduce String + user User @relation(fields: [user_id], references: [id]) + + @@index([user_id], map: "Resume_user_id_fkey") +} + +model Skill { + id Int @id @default(autoincrement()) + name String @unique + UserSkills UserSkill[] +} + +model UserSkill { + id Int @id @default(autoincrement()) + user_id Int + skill_id Int + skill Skill @relation(fields: [skill_id], references: [id]) + user User @relation(fields: [user_id], references: [id]) + + @@unique([user_id, skill_id]) + @@index([skill_id], map: "UserSkill_skill_id_fkey") +} + +model UserLink { + id Int @id @default(autoincrement()) + user_id Int + platform String + link String + user User @relation(fields: [user_id], references: [id]) + + @@index([user_id], map: "UserLink_user_id_fkey") +} + +model Follows { + id Int @id @default(autoincrement()) + following_user_id Int + followed_user_id Int + created_at DateTime @default(now()) + followed_user User @relation("FollowedUsers", fields: [followed_user_id], references: [id]) + following_user User @relation("UserFollows", fields: [following_user_id], references: [id]) + + @@unique([following_user_id, followed_user_id]) + @@index([followed_user_id], map: "Follows_followed_user_id_fkey") +} + +model FeedPost { + id Int @id @default(autoincrement()) + user_id Int + title String + content String + thumbnail_url String + created_at DateTime @default(now()) + updated_at DateTime @updatedAt + view Int + comment_count Int + like_count Int + Comments FeedComment[] + Likes FeedLike[] + user User @relation(fields: [user_id], references: [id]) + Tags FeedPostTag[] + + @@index([user_id], map: "FeedPost_user_id_fkey") +} + +model FeedTag { + id Int @id @default(autoincrement()) + name String + Posts FeedPostTag[] +} + +model FeedPostTag { + id Int @id @default(autoincrement()) + post_id Int + tag_id Int + post FeedPost @relation(fields: [post_id], references: [id]) + tag FeedTag @relation(fields: [tag_id], references: [id]) + + @@unique([post_id, tag_id]) + @@index([tag_id], map: "FeedPostTag_tag_id_fkey") +} + +model FeedComment { + id Int @id @default(autoincrement()) + user_id Int + post_id Int + content String + image_url String? + created_at DateTime @default(now()) + updated_at DateTime @updatedAt + post FeedPost @relation(fields: [post_id], references: [id]) + user User @relation(fields: [user_id], references: [id]) + + @@index([post_id], map: "FeedComment_post_id_fkey") + @@index([user_id], map: "FeedComment_user_id_fkey") +} + +model FeedLike { + id Int @id @default(autoincrement()) + user_id Int + post_id Int + post FeedPost @relation(fields: [post_id], references: [id]) + user User @relation(fields: [user_id], references: [id]) + + @@index([post_id], map: "FeedLike_post_id_fkey") + @@index([user_id], map: "FeedLike_user_id_fkey") +} + +model ProjectPost { + id Int @id @default(autoincrement()) + user_id Int + title String + content String + thumbnail_url String + role Int + unit String + start_date DateTime + end_date DateTime + work_type_id Int + recruiting Boolean + applicant_count Int + view Int + saved_count Int + Details ProjectDetailRole[] + user User @relation(fields: [user_id], references: [id]) + work_type WorkType @relation(fields: [work_type_id], references: [id]) + Tags ProjectPostTag[] + Saves ProjectSave[] + Applications UserApplyProject[] + + @@index([user_id], map: "ProjectPost_user_id_fkey") + @@index([work_type_id], map: "ProjectPost_work_type_id_fkey") +} + +model WorkType { + id Int @id @default(autoincrement()) + name String + ProjectPosts ProjectPost[] +} + +model ProjectSave { + id Int @id @default(autoincrement()) + user_id Int + post_id Int + post ProjectPost @relation(fields: [post_id], references: [id]) + user User @relation(fields: [user_id], references: [id]) + + @@index([post_id], map: "ProjectSave_post_id_fkey") + @@index([user_id], map: "ProjectSave_user_id_fkey") +} + +model ProjectDetailRole { + id Int @id @default(autoincrement()) + post_id Int + detail_role_id Int + detail_role DetailRole @relation(fields: [detail_role_id], references: [id]) + post ProjectPost @relation(fields: [post_id], references: [id]) + + @@index([detail_role_id], map: "ProjectDetailRole_detail_role_id_fkey") + @@index([post_id], map: "ProjectDetailRole_post_id_fkey") +} + +model DetailRole { + id Int @id @default(autoincrement()) + role_id Int + name String + Details ProjectDetailRole[] +} + +model ProjectTag { + id Int @id @default(autoincrement()) + name String + Tags ProjectPostTag[] +} + +model ProjectPostTag { + id Int @id @default(autoincrement()) + post_id Int + tag_id Int + post ProjectPost @relation(fields: [post_id], references: [id]) + tag ProjectTag @relation(fields: [tag_id], references: [id]) + + @@unique([post_id, tag_id]) + @@index([tag_id], map: "ProjectPostTag_tag_id_fkey") +} + +model UserApplyProject { + id Int @id @default(autoincrement()) + user_id Int + post_id Int + post ProjectPost @relation(fields: [post_id], references: [id]) + user User @relation(fields: [user_id], references: [id]) + + @@index([post_id], map: "UserApplyProject_post_id_fkey") + @@index([user_id], map: "UserApplyProject_user_id_fkey") +} diff --git a/src/app.controller.ts b/src/app.controller.ts index b0f585e..fa2ccf6 100644 --- a/src/app.controller.ts +++ b/src/app.controller.ts @@ -1,5 +1,6 @@ -import { Controller, Get } from '@nestjs/common'; +import { Controller, Get, HttpStatus, HttpException } from '@nestjs/common'; import { AppService } from '@src/app.service'; +import { ERROR_MESSAGES } from '@common/constants/error-messages'; @Controller() export class AppController { @@ -9,4 +10,12 @@ export class AppController { getHello(): string { return this.appService.getHello(); } + + @Get('test-error') + testError() { + throw new HttpException( + ERROR_MESSAGES.INTERNAL_SERVER_ERROR, + HttpStatus.INTERNAL_SERVER_ERROR + ); + } } diff --git a/src/app.module.ts b/src/app.module.ts index 346bcd3..5c900ea 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,10 +1,14 @@ import { Module } from '@nestjs/common'; -import { AppController } from '@src/app.controller'; -import { AppService } from '@src/app.service'; - +import { ConfigModule } from '@nestjs/config'; +import { AuthModule } from '@modules/auth/auth.module'; +import { UserModule } from '@modules/user/user.module'; @Module({ - imports: [], - controllers: [AppController], - providers: [AppService], + imports: [ + ConfigModule.forRoot({ + isGlobal: true, // 환경 변수를 글로벌하게 사용할 수 있도록 설정 + }), + AuthModule, // Auth 모듈 추가 + UserModule, + ], }) export class AppModule {} diff --git a/src/common/constants/error-messages.ts b/src/common/constants/error-messages.ts new file mode 100644 index 0000000..3dbce0f --- /dev/null +++ b/src/common/constants/error-messages.ts @@ -0,0 +1,10 @@ +export const ERROR_MESSAGES = { + INVALID_ROLE_ID: '유효하지 않은 역할 ID입니다. 허용된 값은 1, 2, 3입니다.', + USER_NOT_FOUND: '해당 ID를 가진 사용자를 찾을 수 없습니다.', + UNAUTHORIZED: '인증이 필요합니다. 다시 로그인해주세요.', + FORBIDDEN: '이 리소스에 접근할 권한이 없습니다.', + JWT_EXPIRED: '세션이 만료되었습니다. 다시 로그인해주세요.', + INTERNAL_SERVER_ERROR: + '예기치 않은 오류가 발생했습니다. 잠시 후 다시 시도해주세요.', + BAD_REQUEST: '요청 데이터가 올바르지 않습니다. 입력값을 확인해주세요.', +}; diff --git a/src/common/filters/http-exception.filter.ts b/src/common/filters/http-exception.filter.ts new file mode 100644 index 0000000..94de81b --- /dev/null +++ b/src/common/filters/http-exception.filter.ts @@ -0,0 +1,36 @@ +import { + ExceptionFilter, + Catch, + ArgumentsHost, + HttpException, + HttpStatus, +} from '@nestjs/common'; +import { Response } from 'express'; + +@Catch() // 모든 예외를 잡도록 설정 +export class HttpExceptionFilter implements ExceptionFilter { + catch(exception: unknown, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + const request = ctx.getRequest(); + + // HTTP 예외인지 확인 + const status = + exception instanceof HttpException + ? exception.getStatus() + : HttpStatus.INTERNAL_SERVER_ERROR; + + // 에러 메시지 추출 + const message = + exception instanceof HttpException + ? exception.getResponse() + : '서버 내부의 에러입니다'; + + response.status(status).json({ + statusCode: status, + message: typeof message === 'string' ? message : (message as any).message, + timestamp: new Date().toISOString(), + path: request.url, + }); + } +} diff --git a/src/main.ts b/src/main.ts index 3ba7e8e..c4cc14d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,10 +1,14 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; +import { HttpExceptionFilter } from '@common/filters/http-exception.filter'; import { config } from 'dotenv'; config(); async function bootstrap() { const app = await NestFactory.create(AppModule); + app.enableCors(); + app.useGlobalFilters(new HttpExceptionFilter()); + const port = process.env.PORT; await app.listen(process.env.PORT); console.log(`Application is run on: http://localhost:${port}`); diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index 268eeb2..a82afeb 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -1,4 +1,35 @@ -import { Controller } from '@nestjs/common'; +import { Controller, Get, UseGuards, Req } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; +import { AuthService } from './auth.service'; +import { AuthUserDto } from './dto/auth-user.dto'; @Controller('auth') -export class AuthController {} +export class AuthController { + constructor(private readonly authService: AuthService) {} + + @Get('google') + @UseGuards(AuthGuard('google')) + async googleLogin() { + // Google 로그인 요청 + } + + @Get('google/callback') + @UseGuards(AuthGuard('google')) + async googleCallback(@Req() req: { user: AuthUserDto }) { + const { user, accessToken } = await this.authService.socialLogin(req.user); + return { user, accessToken }; + } + + @Get('github') + @UseGuards(AuthGuard('github')) + async githubLogin() { + // GitHub 로그인 요청 + } + + @Get('github/callback') + @UseGuards(AuthGuard('github')) + async githubCallback(@Req() req: { user: AuthUserDto }) { + const { user, accessToken } = await this.authService.socialLogin(req.user); + return { user, accessToken }; + } +} diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts index a7d9fbc..46153c1 100644 --- a/src/modules/auth/auth.module.ts +++ b/src/modules/auth/auth.module.ts @@ -1,9 +1,31 @@ import { Module } from '@nestjs/common'; -import { AuthController } from './auth.controller'; +import { JwtModule } from '@nestjs/jwt'; +import { PassportModule } from '@nestjs/passport'; import { AuthService } from './auth.service'; +import { AuthController } from './auth.controller'; +import { JwtStrategy } from './strategies/jwt.strategy'; +import { GitHubStrategy } from './strategies/github.strategy'; +import { GoogleStrategy } from './strategies/google.strategy'; +import { PrismaService } from '@src/prisma/prisma.service'; +import { JwtAuthGuard } from '@modules/auth/guards/jwt-auth.guard'; @Module({ + imports: [ + PassportModule, + JwtModule.register({ + secret: process.env.JWT_SECRET, // JWT 비밀키 설정 + signOptions: { expiresIn: '1h' }, // 기본 만료 시간 + }), + ], controllers: [AuthController], - providers: [AuthService], + providers: [ + AuthService, + PrismaService, // Prisma 사용 + JwtStrategy, // JWT 검증 전략 + GitHubStrategy, // GitHub OAuth 전략 + GoogleStrategy, // Google OAuth 전략 + JwtAuthGuard, + ], + exports: [JwtAuthGuard], // 다른 모듈에서 AuthService를 사용할 수 있도록 내보냄 }) export class AuthModule {} diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index a41c649..6287de5 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -1,4 +1,50 @@ import { Injectable } from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; +import { PrismaService } from '@src/prisma/prisma.service'; +import { AuthUserDto } from './dto/auth-user.dto'; @Injectable() -export class AuthService {} +export class AuthService { + constructor( + private readonly jwtService: JwtService, + private readonly prisma: PrismaService + ) {} + + // 사용자 찾기 또는 생성 + async findOrCreateUser(profile: AuthUserDto) { + const user = await this.prisma.user.findUnique({ + where: { email: profile.email }, + }); + + if (!user) { + return this.prisma.user.create({ + data: { + email: profile.email, + name: profile.name, + nickname: profile.nickname, + profile_url: profile.profile_url, + auth_provider: profile.auth_provider, + push_alert: false, + following_alert: false, + project_alert: false, + role: { connect: { id: 1 } }, + status: { connect: { id: 1 } }, + }, + }); + } + return user; + } + + // JWT Access Token 생성 + async generateAccessToken(user: any) { + const payload = { userId: user.id, email: user.email }; + return this.jwtService.sign(payload, { expiresIn: '1h' }); + } + + // 소셜 로그인 프로세스 + async socialLogin(profile: AuthUserDto) { + const user = await this.findOrCreateUser(profile); + const accessToken = await this.generateAccessToken(user); + return { user, accessToken }; + } +} diff --git a/src/modules/auth/dto/auth-user.dto.ts b/src/modules/auth/dto/auth-user.dto.ts new file mode 100644 index 0000000..04c14d7 --- /dev/null +++ b/src/modules/auth/dto/auth-user.dto.ts @@ -0,0 +1,7 @@ +export class AuthUserDto { + email: string; + name: string; + nickname: string; + profile_url?: string; + auth_provider: string; +} diff --git a/src/modules/auth/guards/jwt-auth.guard.ts b/src/modules/auth/guards/jwt-auth.guard.ts new file mode 100644 index 0000000..2155290 --- /dev/null +++ b/src/modules/auth/guards/jwt-auth.guard.ts @@ -0,0 +1,5 @@ +import { Injectable } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +@Injectable() +export class JwtAuthGuard extends AuthGuard('jwt') {} diff --git a/src/modules/auth/strategies/github.strategy.ts b/src/modules/auth/strategies/github.strategy.ts new file mode 100644 index 0000000..0608257 --- /dev/null +++ b/src/modules/auth/strategies/github.strategy.ts @@ -0,0 +1,36 @@ +import { Injectable } from '@nestjs/common'; +import { PassportStrategy } from '@nestjs/passport'; +import { Strategy } from 'passport-github2'; +import { AuthUserDto } from '../dto/auth-user.dto'; + +@Injectable() +export class GitHubStrategy extends PassportStrategy(Strategy, 'github') { + constructor() { + super({ + clientID: process.env.GITHUB_CLIENT_ID, + clientSecret: process.env.GITHUB_CLIENT_SECRET, + callbackURL: 'http://localhost:8080/auth/github/callback', + scope: ['user:email'], + }); + } + + async validate( + accessToken: string, + refreshToken: string, + profile: any + ): Promise { + const { id, emails, displayName, username, photos } = profile; + + if (!emails || emails.length === 0) { + throw new Error('No email associated with this GitHub account'); + } + + return { + email: emails[0].value, + name: displayName || username, + nickname: username, + profile_url: photos[0]?.value || null, + auth_provider: 'github', + }; + } +} diff --git a/src/modules/auth/strategies/google.strategy.ts b/src/modules/auth/strategies/google.strategy.ts new file mode 100644 index 0000000..2721cab --- /dev/null +++ b/src/modules/auth/strategies/google.strategy.ts @@ -0,0 +1,37 @@ +import { Injectable } from '@nestjs/common'; +import { PassportStrategy } from '@nestjs/passport'; +import { Strategy, VerifyCallback } from 'passport-google-oauth20'; +import { AuthUserDto } from '../dto/auth-user.dto'; + +@Injectable() +export class GoogleStrategy extends PassportStrategy(Strategy, 'google') { + constructor() { + super({ + clientID: process.env.GOOGLE_CLIENT_ID, + clientSecret: process.env.GOOGLE_CLIENT_SECRET, + callbackURL: 'http://localhost:8080/auth/google/callback', + scope: ['email', 'profile'], + }); + } + + async validate( + accessToken: string, + refreshToken: string, + profile: any, + done: VerifyCallback + ): Promise { + const { id, emails, displayName, photos } = profile; + + if (!emails || emails.length === 0) { + throw new Error('No email associated with this Google account'); + } + + return { + email: emails[0].value, + name: displayName, + nickname: displayName.split(' ')[0], // 예시로 첫 단어를 닉네임으로 설정 + profile_url: photos[0]?.value || null, + auth_provider: 'google', + }; + } +} diff --git a/src/modules/auth/strategies/jwt.strategy.ts b/src/modules/auth/strategies/jwt.strategy.ts new file mode 100644 index 0000000..dfedf53 --- /dev/null +++ b/src/modules/auth/strategies/jwt.strategy.ts @@ -0,0 +1,18 @@ +import { Injectable } from '@nestjs/common'; +import { PassportStrategy } from '@nestjs/passport'; +import { ExtractJwt, Strategy } from 'passport-jwt'; + +@Injectable() +export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { + constructor() { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + ignoreExpiration: false, // 만료된 토큰은 거부 + secretOrKey: process.env.JWT_SECRET, // 환경 변수에서 비밀키 가져오기 + }); + } + + async validate(payload: any) { + return { userId: payload.userId, email: payload.email }; + } +} diff --git a/src/modules/user/dto/set-role.dto.ts b/src/modules/user/dto/set-role.dto.ts new file mode 100644 index 0000000..72b4e13 --- /dev/null +++ b/src/modules/user/dto/set-role.dto.ts @@ -0,0 +1,3 @@ +export class SetRoleDto { + role_id: number; +} diff --git a/src/modules/user/user.controller.ts b/src/modules/user/user.controller.ts index ad8c2a6..896c6a4 100644 --- a/src/modules/user/user.controller.ts +++ b/src/modules/user/user.controller.ts @@ -1,4 +1,15 @@ -import { Controller } from '@nestjs/common'; +import { Controller, Patch, Body, UseGuards, Req } from '@nestjs/common'; +import { UserService } from './user.service'; +import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; -@Controller('user') -export class UserController {} +@Controller('users') +export class UserController { + constructor(private readonly userService: UserService) {} + + @UseGuards(JwtAuthGuard) + @Patch('select-role') + async selectRole(@Req() req, @Body('roleId') roleId: number) { + const userId = req.user.id; // JWT로부터 가져온 사용자 ID + return this.userService.updateUserRole(userId, roleId); + } +} diff --git a/src/modules/user/user.module.ts b/src/modules/user/user.module.ts index 309e84a..aedd372 100644 --- a/src/modules/user/user.module.ts +++ b/src/modules/user/user.module.ts @@ -1,4 +1,13 @@ import { Module } from '@nestjs/common'; +import { AuthModule } from '@modules/auth/auth.module'; +import { UserService } from './user.service'; +import { UserController } from './user.controller'; +import { PrismaService } from '@prisma/prisma.service'; -@Module({}) +@Module({ + imports: [AuthModule], // AuthModule을 가져옴 + controllers: [UserController], + providers: [UserService, PrismaService], + exports: [UserService], +}) export class UserModule {} diff --git a/src/modules/user/user.service.ts b/src/modules/user/user.service.ts index 668a7d6..37036d0 100644 --- a/src/modules/user/user.service.ts +++ b/src/modules/user/user.service.ts @@ -1,4 +1,27 @@ -import { Injectable } from '@nestjs/common'; +import { + BadRequestException, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { ERROR_MESSAGES } from '@common/constants/error-messages'; +import { PrismaService } from '@prisma/prisma.service'; @Injectable() -export class UserService {} +export class UserService { + constructor(private readonly prisma: PrismaService) {} + async updateUserRole(userId: number, roleId: number) { + if (![1, 2, 3].includes(roleId)) { + throw new BadRequestException(ERROR_MESSAGES.INVALID_ROLE_ID); + } + + const user = await this.prisma.user.findUnique({ where: { id: userId } }); + if (!user) { + throw new NotFoundException(ERROR_MESSAGES.USER_NOT_FOUND); + } + console.log(userId); + return this.prisma.user.update({ + where: { id: userId }, + data: { role_id: roleId }, + }); + } +} diff --git a/src/prisma/prisma.module.ts b/src/prisma/prisma.module.ts new file mode 100644 index 0000000..67e656f --- /dev/null +++ b/src/prisma/prisma.module.ts @@ -0,0 +1,9 @@ +import { Global, Module } from '@nestjs/common'; +import { PrismaService } from './prisma.service'; + +@Global() // PrismaService를 전역으로 사용 +@Module({ + providers: [PrismaService], + exports: [PrismaService], // 다른 모듈에서 PrismaService를 사용할 수 있도록 내보내기 +}) +export class PrismaModule {} diff --git a/src/prisma/prisma.service.ts b/src/prisma/prisma.service.ts new file mode 100644 index 0000000..3c974f8 --- /dev/null +++ b/src/prisma/prisma.service.ts @@ -0,0 +1,18 @@ +import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; +import { PrismaClient } from '@prisma/client'; + +@Injectable() +export class PrismaService + extends PrismaClient + implements OnModuleInit, OnModuleDestroy +{ + async onModuleInit() { + await this.$connect(); + console.log('Prisma connected to the database'); + } + + async onModuleDestroy() { + await this.$disconnect(); + console.log('Prisma disconnected from the database'); + } +} diff --git a/src/prisma/schema.prisma b/src/prisma/schema.prisma deleted file mode 100644 index ee282c7..0000000 --- a/src/prisma/schema.prisma +++ /dev/null @@ -1,14 +0,0 @@ -// This is your Prisma schema file, -// learn more about it in the docs: https://pris.ly/d/prisma-schema - -// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? -// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init - -generator client { - provider = "prisma-client-js" -} - -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} diff --git a/src/prisma/seed.ts b/src/prisma/seed.ts new file mode 100644 index 0000000..1939717 --- /dev/null +++ b/src/prisma/seed.ts @@ -0,0 +1,39 @@ +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + +async function main() { + // Role 데이터 생성 + await prisma.role.createMany({ + data: [ + { id: 1, name: '프로그래머' }, + { id: 2, name: '아티스트' }, + { id: 3, name: '디자이너' }, + ], + skipDuplicates: true, // 중복 생성 방지 + }); + // Status 데이터 업데이트 + const statuses = [ + { id: 1, name: '둘러보는 중' }, + { id: 2, name: '외주/프로젝트 구하는 중' }, + { id: 3, name: '구인하는 중' }, + { id: 4, name: '작업 중' }, + ]; + + for (const status of statuses) { + await prisma.status.upsert({ + where: { id: status.id }, // ID 기준으로 존재 여부 확인 + update: { name: status.name }, // 이미 존재하면 업데이트 + create: { id: status.id, name: status.name }, // 존재하지 않으면 생성 + }); + } +} + +main() + .catch(e => { + console.error(e); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/src/s3/s3.module.ts b/src/s3/s3.module.ts new file mode 100644 index 0000000..ab620fc --- /dev/null +++ b/src/s3/s3.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { S3Service } from './s3.service'; + +@Module({ + providers: [S3Service], + exports: [S3Service], // 다른 모듈에서도 사용 가능하도록 내보냄 +}) +export class S3Module {} diff --git a/src/s3/s3.service.ts b/src/s3/s3.service.ts new file mode 100644 index 0000000..1b0775f --- /dev/null +++ b/src/s3/s3.service.ts @@ -0,0 +1,37 @@ +import { Injectable, InternalServerErrorException } from '@nestjs/common'; +import { S3 } from 'aws-sdk'; +import * as crypto from 'crypto'; + +@Injectable() +export class S3Service { + private s3 = new S3({ + accessKeyId: process.env.AWS_ACCESS_KEY_ID, + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, + region: process.env.AWS_REGION, + }); + + private bucketName = process.env.AWS_S3_BUCKET_NAME; + + async uploadImage( + userId: number, + fileBuffer: Buffer, + fileType: string + ): Promise { + try { + const fileName = `mypli_users/profile_${crypto.randomUUID()}.${fileType}`; + const uploadResult = await this.s3 + .upload({ + Bucket: this.bucketName, + Key: fileName, + Body: fileBuffer, + ContentType: `image/${fileType}`, + }) + .promise(); + + return uploadResult.Location; + } catch (error) { + console.error('S3 Upload Error:', error); // 에러 로깅 + throw new InternalServerErrorException('S3 업로드 중 오류 발생'); + } + } +} From 2d6584fde3b74ab89a08ea0a48383c18c72f3a9e Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Tue, 7 Jan 2025 01:04:05 +0900 Subject: [PATCH 04/14] [Add] Project Package --- package-lock.json | 42 ++++++++++++++++++++++++++++++++++++++---- package.json | 1 + 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 07d95a6..1985150 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@nestjs/platform-express": "^10.0.0", "@nestjs/swagger": "^8.1.0", "@prisma/client": "^6.1.0", + "axios": "^1.7.9", "bcrypt": "^5.1.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", @@ -4856,7 +4857,6 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, "license": "MIT" }, "node_modules/available-typed-arrays": { @@ -4937,6 +4937,17 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/axios": { + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", + "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -5693,7 +5704,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" @@ -5996,7 +6006,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -6992,6 +7001,26 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -7076,7 +7105,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", - "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -10164,6 +10192,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", diff --git a/package.json b/package.json index ffa2593..14ead8d 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@nestjs/platform-express": "^10.0.0", "@nestjs/swagger": "^8.1.0", "@prisma/client": "^6.1.0", + "axios": "^1.7.9", "bcrypt": "^5.1.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", From 95d00288bffa0fa7081630de312ec56077bcc951 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Tue, 7 Jan 2025 01:04:46 +0900 Subject: [PATCH 05/14] =?UTF-8?q?[Feat]=20=EC=86=8C=EC=85=9C=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EB=A1=9C=EC=A7=81=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/auth/auth.controller.ts | 16 +++++++++++----- src/modules/auth/auth.service.ts | 27 +++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index a82afeb..44bf877 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, UseGuards, Req } from '@nestjs/common'; +import { Controller, Get, UseGuards, Req, Body, Post } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { AuthService } from './auth.service'; import { AuthUserDto } from './dto/auth-user.dto'; @@ -13,10 +13,16 @@ export class AuthController { // Google 로그인 요청 } - @Get('google/callback') - @UseGuards(AuthGuard('google')) - async googleCallback(@Req() req: { user: AuthUserDto }) { - const { user, accessToken } = await this.authService.socialLogin(req.user); + // @Get('google/callback') + // @UseGuards(AuthGuard('google')) + // async googleCallback(@Req() req: { user: AuthUserDto }) { + // const { user, accessToken } = await this.authService.socialLogin(req.user); + // return { user, accessToken }; + // } + @Post('google/callback') + async googleCallback(@Body('code') code: string) { + // Authorization Code 교환 및 사용자 정보 가져오기 + const { user, accessToken } = await this.authService.handleGoogleCallback(code); return { user, accessToken }; } diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index 6287de5..14244a6 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { PrismaService } from '@src/prisma/prisma.service'; import { AuthUserDto } from './dto/auth-user.dto'; +import axios from 'axios'; @Injectable() export class AuthService { @@ -10,6 +11,28 @@ export class AuthService { private readonly prisma: PrismaService ) {} + async handleGoogleCallback(code: string) { + // Google 토큰 요청 + const tokenResponse = await axios.post('https://oauth2.googleapis.com/token', { + code, + client_id: '1030869508870-7svsfcscu3la43lpprj6kui580cp0uhf.apps.googleusercontent.com', + client_secret: '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', + redirect_uri: 'http://localhost:5173/auth/google/callback', + grant_type: 'authorization_code', + }); + const { access_token } = tokenResponse.data; + // Google 사용자 정보 요청 + const userInfoResponse = await axios.get('https://www.googleapis.com/oauth2/v2/userinfo', { + headers: { Authorization: `Bearer ${access_token}` }, + }); + + const userData = userInfoResponse.data; + + // JWT 생성 + const jwt = this.generateJwt(userData); + return { user: userData, accessToken: jwt }; + } + // 사용자 찾기 또는 생성 async findOrCreateUser(profile: AuthUserDto) { const user = await this.prisma.user.findUnique({ @@ -41,6 +64,10 @@ export class AuthService { return this.jwtService.sign(payload, { expiresIn: '1h' }); } + private generateJwt(user: any) { + const payload = { email: user.email, id: user.id }; + return this.jwtService.sign(payload, { expiresIn: '1h' }); + } // 소셜 로그인 프로세스 async socialLogin(profile: AuthUserDto) { const user = await this.findOrCreateUser(profile); From 82be11662e05b5acf3963e116974eebe42c36a30 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Tue, 7 Jan 2025 01:05:15 +0900 Subject: [PATCH 06/14] =?UTF-8?q?[Feat]=20google=20=EC=86=8C=EC=85=9C=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EB=A6=AC=EB=8B=A4=EC=9D=B4?= =?UTF-8?q?=EB=A0=89=EC=85=98=20=EC=A3=BC=EC=86=8C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/auth/strategies/google.strategy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/auth/strategies/google.strategy.ts b/src/modules/auth/strategies/google.strategy.ts index 2721cab..2b7a85b 100644 --- a/src/modules/auth/strategies/google.strategy.ts +++ b/src/modules/auth/strategies/google.strategy.ts @@ -9,7 +9,7 @@ export class GoogleStrategy extends PassportStrategy(Strategy, 'google') { super({ clientID: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET, - callbackURL: 'http://localhost:8080/auth/google/callback', + callbackURL: 'http://localhost:5173/auth/google/callback', scope: ['email', 'profile'], }); } From 04722b48e0d81d4042d3ab226c50ba976595c3e5 Mon Sep 17 00:00:00 2001 From: ssomae <80831228+Ss0Mae@users.noreply.github.com> Date: Tue, 7 Jan 2025 17:16:34 +0900 Subject: [PATCH 07/14] =?UTF-8?q?[Feature]=20=EC=86=8C=EC=85=9C=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EB=A1=9C=EC=A7=81=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD,=20=EC=97=AD=ED=95=A0=20=EC=84=A0=ED=83=9D=20API=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Feat] JWT Strategy 리턴값 변경 * [Feat] CallBack URI 변경 * [Feat] Role 선택 API 구현 * [Feat] 소셜 로그인 로직 변경 * [Feat] CORS 설정 --- src/main.ts | 9 +- src/modules/auth/auth.controller.ts | 48 +++-- src/modules/auth/auth.service.ts | 201 +++++++++++++++--- .../auth/strategies/github.strategy.ts | 2 +- src/modules/auth/strategies/jwt.strategy.ts | 8 +- 5 files changed, 217 insertions(+), 51 deletions(-) diff --git a/src/main.ts b/src/main.ts index c4cc14d..717935d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,15 +2,18 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { HttpExceptionFilter } from '@common/filters/http-exception.filter'; import { config } from 'dotenv'; +import * as express from 'express'; config(); async function bootstrap() { const app = await NestFactory.create(AppModule); - app.enableCors(); - app.useGlobalFilters(new HttpExceptionFilter()); + app.enableCors({ + origin: 'http://localhost:5173' | 'https://'; + credentials:true, + }); + //app.useGlobalFilters(new HttpExceptionFilter()); const port = process.env.PORT; await app.listen(process.env.PORT); - console.log(`Application is run on: http://localhost:${port}`); } bootstrap(); diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index 44bf877..daba169 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -1,8 +1,7 @@ -import { Controller, Get, UseGuards, Req, Body, Post } from '@nestjs/common'; +import { Controller, Get, UseGuards, Req, Body, Post, Put, BadRequestException } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { AuthService } from './auth.service'; -import { AuthUserDto } from './dto/auth-user.dto'; - +import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; @Controller('auth') export class AuthController { constructor(private readonly authService: AuthService) {} @@ -13,17 +12,11 @@ export class AuthController { // Google 로그인 요청 } - // @Get('google/callback') - // @UseGuards(AuthGuard('google')) - // async googleCallback(@Req() req: { user: AuthUserDto }) { - // const { user, accessToken } = await this.authService.socialLogin(req.user); - // return { user, accessToken }; - // } @Post('google/callback') async googleCallback(@Body('code') code: string) { // Authorization Code 교환 및 사용자 정보 가져오기 - const { user, accessToken } = await this.authService.handleGoogleCallback(code); - return { user, accessToken }; + const { user, accessToken, isExistingUser } = await this.authService.handleGoogleCallback(code); + return { user, accessToken, isExistingUser }; } @Get('github') @@ -32,10 +25,33 @@ export class AuthController { // GitHub 로그인 요청 } - @Get('github/callback') - @UseGuards(AuthGuard('github')) - async githubCallback(@Req() req: { user: AuthUserDto }) { - const { user, accessToken } = await this.authService.socialLogin(req.user); - return { user, accessToken }; + @Post('github/callback') + async githubCallback(@Body('code') code: string) { + const { user, accessToken, isExistingUser } = await this.authService.handleGithubCallback(code); + return { user, accessToken, isExistingUser }; } + + // Role 선택 API + @Put('roleselect') + @UseGuards(JwtAuthGuard) + async selectRole( + @Body('role_id') roleId: number, + @Req() req: any, // JWT에서 사용자 정보 추출 + ) { + const validRoles = [1, 2, 3]; // 1: Programmer, 2: Artist, 3: Designer + + // 유효한 role_id인지 확인 + if (!validRoles.includes(roleId)) { + throw new BadRequestException('유효하지 않은 역할 ID입니다.'); + } + + const userId = req.user?.id; // JWT에서 추출된 userId 확인 + + if (!userId) { + throw new BadRequestException('사용자 ID가 누락되었습니다.'); + } + + return await this.authService.updateUserRole(userId, roleId); + } + } diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index 14244a6..b340dda 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { PrismaService } from '@src/prisma/prisma.service'; import { AuthUserDto } from './dto/auth-user.dto'; @@ -12,27 +12,123 @@ export class AuthService { ) {} async handleGoogleCallback(code: string) { - // Google 토큰 요청 - const tokenResponse = await axios.post('https://oauth2.googleapis.com/token', { - code, - client_id: '1030869508870-7svsfcscu3la43lpprj6kui580cp0uhf.apps.googleusercontent.com', - client_secret: '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', - redirect_uri: 'http://localhost:5173/auth/google/callback', - grant_type: 'authorization_code', - }); - const { access_token } = tokenResponse.data; - // Google 사용자 정보 요청 - const userInfoResponse = await axios.get('https://www.googleapis.com/oauth2/v2/userinfo', { - headers: { Authorization: `Bearer ${access_token}` }, - }); + try { + console.log('Received Authorization Code:', code); + + // Google 토큰 요청 + const tokenResponse = await axios.post('https://oauth2.googleapis.com/token', { + code, + client_id: process.env.GOOGLE_CLIENT_ID, + client_secret: process.env.GOOGLE_CLIENT_SECRET, + redirect_uri: process.env.GOOGLE_CALLBACK_DEVELOP_URL, + grant_type: 'authorization_code', + }); + + const { access_token } = tokenResponse.data; + + // Google 사용자 정보 요청 + const userInfoResponse = await axios.get('https://www.googleapis.com/oauth2/v2/userinfo', { + headers: { Authorization: `Bearer ${access_token}` }, + }); + + const userData = userInfoResponse.data; + + const isExistingUser = await this.checkUserExist(userData.email); + const user = await this.findOrCreateUser({ + email: userData.email, + name: userData.name, + nickname: userData.given_name, + profile_url: userData.picture, + auth_provider: 'google', + }); + + const jwt = this.generateJwt(user); + const responseUser = this.filterUserFields(user); + + return { user: responseUser, accessToken: jwt, isExistingUser }; + } catch (error) { + console.error('Google OAuth Error:', error.response?.data || error.message); + + if (error.response?.data?.error === 'invalid_grant') { + throw new Error('Authorization Code가 이미 사용되었거나 만료되었습니다.'); + } + + throw new Error('Google OAuth 인증 실패'); + } + } + + async handleGithubCallback(code: string) { + try { + console.log('Received GitHub Authorization Code:', code); + + // GitHub 토큰 요청 + const tokenResponse = await axios.post( + 'https://github.com/login/oauth/access_token', + { + code, + client_id: process.env.GITHUB_CLIENT_ID, + client_secret: process.env.GITHUB_CLIENT_SECRET, + redirect_uri: process.env.GITHUB_CALLBACK_URL, + }, + { + headers: { Accept: 'application/json' }, + }, + ); + + const { access_token } = tokenResponse.data; + console.log(access_token); + // GitHub 사용자 정보 요청 + const userInfoResponse = await axios.get('https://api.github.com/user', { + headers: { Authorization: `Bearer ${access_token}` }, + }); + + const userData = userInfoResponse.data; + + // GitHub 사용자 이메일 요청 (필요 시) + let email = userData.email; + if (!email) { + const emailResponse = await axios.get('https://api.github.com/user/emails', { + headers: { Authorization: `Bearer ${access_token}` }, + }); - const userData = userInfoResponse.data; + const primaryEmail = emailResponse.data.find((e: any) => e.primary && e.verified); + email = primaryEmail?.email; + } - // JWT 생성 - const jwt = this.generateJwt(userData); - return { user: userData, accessToken: jwt }; + if (!email) { + throw new Error('GitHub 사용자 이메일을 확인할 수 없습니다.'); + } + + const isExistingUser = await this.checkUserExist(email); + const user = await this.findOrCreateUser({ + email, + name: userData.name || userData.login, + nickname: userData.login, + profile_url: userData.avatar_url, + auth_provider: 'github', + }); + + const jwt = this.generateJwt(user); + const responseUser = this.filterUserFields(user); + + return { user: responseUser, accessToken: jwt, isExistingUser }; + } catch (error) { + console.error('GitHub OAuth Error:', error.response?.data || error.message); + + if (error.response?.data?.error === 'bad_verification_code') { + throw new Error('Authorization Code가 잘못되었거나 만료되었습니다.'); + } + + throw new Error('GitHub OAuth 인증 실패'); + } } + private async checkUserExist(email: string): Promise{ + const user = await this.prisma.user.findUnique({ + where: { email }, + }); + return !!user; + } // 사용자 찾기 또는 생성 async findOrCreateUser(profile: AuthUserDto) { const user = await this.prisma.user.findUnique({ @@ -58,20 +154,71 @@ export class AuthService { return user; } - // JWT Access Token 생성 - async generateAccessToken(user: any) { - const payload = { userId: user.id, email: user.email }; - return this.jwtService.sign(payload, { expiresIn: '1h' }); + private filterUserFields(user: any) { + return { + id: user.id, + email: user.email, + name: user.name, + nickname: user.nickname, + profile_url: user.profile_url, + auth_provider: user.auth_provider, + role_id: user.role_id, + }; } private generateJwt(user: any) { const payload = { email: user.email, id: user.id }; return this.jwtService.sign(payload, { expiresIn: '1h' }); } - // 소셜 로그인 프로세스 - async socialLogin(profile: AuthUserDto) { - const user = await this.findOrCreateUser(profile); - const accessToken = await this.generateAccessToken(user); - return { user, accessToken }; + + // 사용자 Role 업데이트 + async updateUserRole(userId: number, roleId: number) { + if (!userId) { + throw new BadRequestException('유효하지 않은 사용자 ID입니다.'); + } + + // 사용자 확인 + const user = await this.prisma.user.findUnique({ + where: { id: userId }, // userId가 반드시 존재해야 함 + }); + + if (!user) { + throw new NotFoundException('사용자를 찾을 수 없습니다.'); + } + + // Role 메시지 설정 + let roleMessage = ''; + switch (roleId) { + case 1: + roleMessage = '프로그래머로 변경되었습니다.'; + break; + case 2: + roleMessage = '아티스트로 변경되었습니다.'; + break; + case 3: + roleMessage = '디자이너로 변경되었습니다.'; + break; + default: + throw new BadRequestException('유효하지 않은 역할 ID입니다.'); + } + // 사용자 Role 업데이트 + const updatedUser = await this.prisma.user.update({ + where: { id: userId }, + data: { role_id: roleId }, + }); + + return { + message: { + code : 200, + text: `${roleMessage}` + }, + user: { + id: updatedUser.id, + email: updatedUser.email, + name: updatedUser.name, + nickname: updatedUser.nickname, + role_id: updatedUser.role_id, + }, + }; } } diff --git a/src/modules/auth/strategies/github.strategy.ts b/src/modules/auth/strategies/github.strategy.ts index 0608257..31cadc3 100644 --- a/src/modules/auth/strategies/github.strategy.ts +++ b/src/modules/auth/strategies/github.strategy.ts @@ -9,7 +9,7 @@ export class GitHubStrategy extends PassportStrategy(Strategy, 'github') { super({ clientID: process.env.GITHUB_CLIENT_ID, clientSecret: process.env.GITHUB_CLIENT_SECRET, - callbackURL: 'http://localhost:8080/auth/github/callback', + callbackURL: 'http://localhost:5173/auth/github/callback', scope: ['user:email'], }); } diff --git a/src/modules/auth/strategies/jwt.strategy.ts b/src/modules/auth/strategies/jwt.strategy.ts index dfedf53..988c9ef 100644 --- a/src/modules/auth/strategies/jwt.strategy.ts +++ b/src/modules/auth/strategies/jwt.strategy.ts @@ -3,16 +3,16 @@ import { PassportStrategy } from '@nestjs/passport'; import { ExtractJwt, Strategy } from 'passport-jwt'; @Injectable() -export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { +export class JwtStrategy extends PassportStrategy(Strategy) { constructor() { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), - ignoreExpiration: false, // 만료된 토큰은 거부 - secretOrKey: process.env.JWT_SECRET, // 환경 변수에서 비밀키 가져오기 + secretOrKey: process.env.JWT_SECRET, }); } async validate(payload: any) { - return { userId: payload.userId, email: payload.email }; + // req.user에 설정될 사용자 정보 반환 + return { id: payload.id, email: payload.email }; } } From eaa561379418f29e76fbad58a3338878dc812c36 Mon Sep 17 00:00:00 2001 From: ssomae <80831228+Ss0Mae@users.noreply.github.com> Date: Thu, 9 Jan 2025 17:52:38 +0900 Subject: [PATCH 08/14] =?UTF-8?q?[Feature]=209=20-Refresh=20token=20?= =?UTF-8?q?=EB=B0=8F=20=EC=BD=94=EB=93=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81=20(#13)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Add] ioredis 패키지 추가 * [Feat] Redis 관련 설정 파일 구현 * [Feat] payload 변경 * [Feat] Redis를 이용한 리프레쉬 토큰 구현 * [Feat] http status code 타입화 * [Feat] 응답 관련 Interface DTO 구현 * [Feat] Redis 연결 config 구현 * [Feat] Redis 사용을 위한 module, service 로직 구현 * [Feat] 에러 메세지 관련 타입화 구현 * [Feat] http-exception filter 고도화 * [Refactor] 사용하지 않는 코드 정리 * [Refactor] 사용하지 않는 코드 정리 * [Refactor] auth 관련 코드들 응답처리, 에러처리 리펙토링 * [Feat] 에러메세지 추가 구현 * [Feat] 에러메세지 추가 구현 * [Feat] HttpExceptionFilter Global 설정 * [Feat] 응답 데이터에 대한 DTO 구현 * [Refactor] 필요없는 코드 제거 * [Feat] 리프레쉬 토큰 쿠키에 전달하도록 로직 변경 구현 * [Feat] payload 응답 값 변경 * [Feat] Response dto 변경 구현 * [Refactor] auth 관련 컨트롤러, 서비스 코드 리팩토링 * [Fix] accessToken 시간 변경 --- package-lock.json | 79 +++++++ package.json | 1 + src/app.controller.ts | 11 +- src/common/constants/error-messages.ts | 50 +++- src/common/constants/http-status-code.ts | 9 + src/common/dto/response.dto.ts | 33 +++ src/common/filters/http-exception.filter.ts | 19 +- src/config/redis.config.ts | 8 + src/main.ts | 10 +- src/modules/auth/auth.controller.ts | 154 +++++++++++-- src/modules/auth/auth.module.ts | 3 +- src/modules/auth/auth.service.ts | 241 ++++++++++++-------- src/modules/auth/strategies/jwt.strategy.ts | 5 +- src/modules/redis/redis.module.ts | 23 ++ src/modules/redis/redis.service.ts | 19 ++ src/modules/user/user.controller.ts | 10 +- src/modules/user/user.service.ts | 23 +- 17 files changed, 514 insertions(+), 184 deletions(-) create mode 100644 src/common/constants/http-status-code.ts create mode 100644 src/common/dto/response.dto.ts create mode 100644 src/config/redis.config.ts create mode 100644 src/modules/redis/redis.module.ts create mode 100644 src/modules/redis/redis.service.ts diff --git a/package-lock.json b/package-lock.json index 1985150..9315f0c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "dotenv": "^16.4.7", + "ioredis": "^5.4.2", "jsonwebtoken": "^9.0.2", "passport": "^0.7.0", "passport-github": "^1.1.0", @@ -1856,6 +1857,12 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@ioredis/commands": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", + "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==", + "license": "MIT" + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -6017,6 +6024,15 @@ "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", "license": "MIT" }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -7727,6 +7743,30 @@ "node": ">=12.0.0" } }, + "node_modules/ioredis": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.4.2.tgz", + "integrity": "sha512-0SZXGNGZ+WzISQ67QDyZ2x0+wVxjjUndtD8oSeik/4ajifeiRufed8fCb8QW8VMyi4MXcS+UO1k/0NGhvq1PAg==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "^1.1.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -9070,12 +9110,24 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", "license": "MIT" }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "license": "MIT" + }, "node_modules/lodash.isboolean": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", @@ -10376,6 +10428,27 @@ "@redis/time-series": "1.1.0" } }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/reflect-metadata": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", @@ -11032,6 +11105,12 @@ "node": ">=8" } }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", diff --git a/package.json b/package.json index 14ead8d..b75f88d 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "dotenv": "^16.4.7", + "ioredis": "^5.4.2", "jsonwebtoken": "^9.0.2", "passport": "^0.7.0", "passport-github": "^1.1.0", diff --git a/src/app.controller.ts b/src/app.controller.ts index fa2ccf6..b0f585e 100644 --- a/src/app.controller.ts +++ b/src/app.controller.ts @@ -1,6 +1,5 @@ -import { Controller, Get, HttpStatus, HttpException } from '@nestjs/common'; +import { Controller, Get } from '@nestjs/common'; import { AppService } from '@src/app.service'; -import { ERROR_MESSAGES } from '@common/constants/error-messages'; @Controller() export class AppController { @@ -10,12 +9,4 @@ export class AppController { getHello(): string { return this.appService.getHello(); } - - @Get('test-error') - testError() { - throw new HttpException( - ERROR_MESSAGES.INTERNAL_SERVER_ERROR, - HttpStatus.INTERNAL_SERVER_ERROR - ); - } } diff --git a/src/common/constants/error-messages.ts b/src/common/constants/error-messages.ts index 3dbce0f..a834cfa 100644 --- a/src/common/constants/error-messages.ts +++ b/src/common/constants/error-messages.ts @@ -1,10 +1,42 @@ -export const ERROR_MESSAGES = { - INVALID_ROLE_ID: '유효하지 않은 역할 ID입니다. 허용된 값은 1, 2, 3입니다.', - USER_NOT_FOUND: '해당 ID를 가진 사용자를 찾을 수 없습니다.', - UNAUTHORIZED: '인증이 필요합니다. 다시 로그인해주세요.', - FORBIDDEN: '이 리소스에 접근할 권한이 없습니다.', - JWT_EXPIRED: '세션이 만료되었습니다. 다시 로그인해주세요.', - INTERNAL_SERVER_ERROR: - '예기치 않은 오류가 발생했습니다. 잠시 후 다시 시도해주세요.', - BAD_REQUEST: '요청 데이터가 올바르지 않습니다. 입력값을 확인해주세요.', +import { HttpStatusCodes } from '@common/constants/http-status-code'; + +export const ErrorMessages = { + AUTH: { + INVALID_REFRESH_TOKEN: { + code: HttpStatusCodes.UNAUTHORIZED, + text: '유효하지 않은 리프레시 토큰입니다.', + }, + USER_NOT_FOUND: { + code: HttpStatusCodes.NOT_FOUND, + text: '사용자를 찾을 수 없습니다.', + }, + TOKEN_EXPIRED: { + code: HttpStatusCodes.UNAUTHORIZED, + text: '토큰이 만료되었습니다.', + }, + }, + VALIDATION: { + MISSING_REQUIRED_FIELDS: { + code: HttpStatusCodes.BAD_REQUEST, + text: '필수 입력 필드가 누락되었습니다.', + }, + INVALID_EMAIL: { + code: HttpStatusCodes.BAD_REQUEST, + text: '유효하지 않은 이메일 형식입니다.', + }, + INVALID_ROLE_ID: { + code: HttpStatusCodes.BAD_REQUEST, + text: '유효하지 않은 역할 ID입니다.', + }, + }, + SERVER: { + DATABASE_ERROR: { + code: HttpStatusCodes.INTERNAL_SERVER_ERROR, + text: '데이터베이스 작업 중 오류가 발생했습니다.', + }, + INTERNAL_ERROR: { + code: HttpStatusCodes.INTERNAL_SERVER_ERROR, + text: '서버에서 오류가 발생했습니다.', + }, + }, }; diff --git a/src/common/constants/http-status-code.ts b/src/common/constants/http-status-code.ts new file mode 100644 index 0000000..bc71485 --- /dev/null +++ b/src/common/constants/http-status-code.ts @@ -0,0 +1,9 @@ +export const HttpStatusCodes = { + OK: 200, + CREATED: 201, + BAD_REQUEST: 400, + UNAUTHORIZED: 401, + FORBIDDEN: 403, + NOT_FOUND: 404, + INTERNAL_SERVER_ERROR: 500, +}; diff --git a/src/common/dto/response.dto.ts b/src/common/dto/response.dto.ts new file mode 100644 index 0000000..ed37552 --- /dev/null +++ b/src/common/dto/response.dto.ts @@ -0,0 +1,33 @@ +export class ApiResponse { + message: { + code: number; // HTTP 상태 코드 + text: string; // 메시지 + }; + + [key: string]: any; // 동적으로 다른 필드를 추가 가능 + + constructor(statusCode: number, message: string, additionalFields?: T) { + this.message = { + code: statusCode, + text: message, + }; + + // 추가 데이터를 최상위 레벨에 병합 + if (additionalFields) { + Object.assign(this, additionalFields); // `data` 키 없이 병합 + } + } +} +export class ErrorResponse { + message: { + code: number; // HTTP 상태 코드 + text: string; // 에러 메시지 + }; + + constructor(statusCode: number, message: string) { + this.message = { + code: statusCode, + text: message, + }; + } +} diff --git a/src/common/filters/http-exception.filter.ts b/src/common/filters/http-exception.filter.ts index 94de81b..2c26892 100644 --- a/src/common/filters/http-exception.filter.ts +++ b/src/common/filters/http-exception.filter.ts @@ -5,30 +5,33 @@ import { HttpException, HttpStatus, } from '@nestjs/common'; -import { Response } from 'express'; +import { Request, Response } from 'express'; +import { ErrorResponse } from '@common/dto/response.dto'; -@Catch() // 모든 예외를 잡도록 설정 +@Catch() export class HttpExceptionFilter implements ExceptionFilter { - catch(exception: unknown, host: ArgumentsHost) { + catch(exception: any, host: ArgumentsHost) { const ctx = host.switchToHttp(); const response = ctx.getResponse(); const request = ctx.getRequest(); - // HTTP 예외인지 확인 const status = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR; - // 에러 메시지 추출 const message = exception instanceof HttpException ? exception.getResponse() - : '서버 내부의 에러입니다'; + : '서버에서 오류가 발생했습니다.'; + + const errorResponse = new ErrorResponse( + status, + typeof message === 'string' ? message : (message as any).message + ); response.status(status).json({ - statusCode: status, - message: typeof message === 'string' ? message : (message as any).message, + ...errorResponse, timestamp: new Date().toISOString(), path: request.url, }); diff --git a/src/config/redis.config.ts b/src/config/redis.config.ts new file mode 100644 index 0000000..35615b9 --- /dev/null +++ b/src/config/redis.config.ts @@ -0,0 +1,8 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class RedisConfig { + public static readonly host = process.env.REDIS_HOST || 'localhost'; + public static readonly port = parseInt(process.env.REDIS_PORT, 10) || 6379; + public static readonly password = process.env.REDIS_PASSWORD || undefined; +} diff --git a/src/main.ts b/src/main.ts index 717935d..c74f07b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,18 +2,16 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { HttpExceptionFilter } from '@common/filters/http-exception.filter'; import { config } from 'dotenv'; -import * as express from 'express'; config(); async function bootstrap() { const app = await NestFactory.create(AppModule); app.enableCors({ - origin: 'http://localhost:5173' | 'https://'; - credentials:true, + origin: true, + credentials: true, + exposedHeaders: ['Authorization'], }); - //app.useGlobalFilters(new HttpExceptionFilter()); - - const port = process.env.PORT; + app.useGlobalFilters(new HttpExceptionFilter()); await app.listen(process.env.PORT); } bootstrap(); diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index daba169..db5a133 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -1,10 +1,28 @@ -import { Controller, Get, UseGuards, Req, Body, Post, Put, BadRequestException } from '@nestjs/common'; +import { + Controller, + Get, + UseGuards, + Req, + Body, + Post, + Put, + HttpException, + Res, +} from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; -import { AuthService } from './auth.service'; -import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; +import { AuthService } from '@src/modules/auth/auth.service'; +import { JwtAuthGuard } from '@src/modules/auth/guards/jwt-auth.guard'; +import { JwtService } from '@nestjs/jwt'; +import { ApiResponse } from '@common/dto/response.dto'; +import { ErrorMessages } from '@common/constants/error-messages'; +import { HttpStatusCodes } from '@common/constants/http-status-code'; +import { Response } from 'express'; @Controller('auth') export class AuthController { - constructor(private readonly authService: AuthService) {} + constructor( + private readonly authService: AuthService, + private readonly jwtService: JwtService + ) {} @Get('google') @UseGuards(AuthGuard('google')) @@ -13,10 +31,33 @@ export class AuthController { } @Post('google/callback') - async googleCallback(@Body('code') code: string) { + async googleCallback(@Body('code') code: string, @Res() res: Response) { // Authorization Code 교환 및 사용자 정보 가져오기 - const { user, accessToken, isExistingUser } = await this.authService.handleGoogleCallback(code); - return { user, accessToken, isExistingUser }; + const { user, accessToken, refreshToken, isExistingUser } = + await this.authService.handleGoogleCallback(code); + + console.log(refreshToken); + // 리프레시 토큰을 HTTP-Only 쿠키로 설정 + res.cookie('refreshToken', refreshToken, { + httpOnly: true, + secure: false, + sameSite: 'none', // CSRF 방지 + maxAge: 7 * 24 * 60 * 60 * 1000, // 7일 + }); + + return res.status(HttpStatusCodes.OK).json( + new ApiResponse(HttpStatusCodes.OK, 'Google 로그인 성공', { + user, + accessToken, + refreshToken, + isExistingUser, + }) + ); + // return new ApiResponse(HttpStatusCodes.OK, 'Google 로그인 성공', { + // user, + // accessToken, + // isExistingUser, + // }); } @Get('github') @@ -26,9 +67,24 @@ export class AuthController { } @Post('github/callback') - async githubCallback(@Body('code') code: string) { - const { user, accessToken, isExistingUser } = await this.authService.handleGithubCallback(code); - return { user, accessToken, isExistingUser }; + async githubCallback(@Body('code') code: string, @Res() res: Response) { + // Authorization Code 교환 및 사용자 정보 가져오기 + const { user, accessToken, refreshToken, isExistingUser } = + await this.authService.handleGithubCallback(code); + + // 리프레시 토큰을 HTTP-Only 쿠키로 설정 + res.cookie('refreshToken', refreshToken, { + httpOnly: true, + secure: true, + sameSite: 'strict', // CSRF 방지 + maxAge: 7 * 24 * 60 * 60 * 1000, // 7일 + }); + + return new ApiResponse(HttpStatusCodes.OK, 'Google 로그인 성공', { + user, + accessToken, + isExistingUser, + }); } // Role 선택 API @@ -36,22 +92,80 @@ export class AuthController { @UseGuards(JwtAuthGuard) async selectRole( @Body('role_id') roleId: number, - @Req() req: any, // JWT에서 사용자 정보 추출 + @Req() req: any, + @Res() res: Response ) { - const validRoles = [1, 2, 3]; // 1: Programmer, 2: Artist, 3: Designer + const userId = req.user?.user_id; + console.log(userId); + const { user, message } = await this.authService.updateUserRole( + userId, + roleId + ); - // 유효한 role_id인지 확인 - if (!validRoles.includes(roleId)) { - throw new BadRequestException('유효하지 않은 역할 ID입니다.'); - } + const serviceResult = await this.authService.updateUserRole(userId, roleId); + + console.log('Service Result in Controller:', serviceResult); - const userId = req.user?.id; // JWT에서 추출된 userId 확인 + // 응답 객체 생성 + const responseBody = { + message: { + code: HttpStatusCodes.OK, + text: serviceResult.message, + }, + user: serviceResult.user, + }; - if (!userId) { - throw new BadRequestException('사용자 ID가 누락되었습니다.'); + console.log('Response Body:', responseBody); + + // 응답 반환 + return res.status(HttpStatusCodes.OK).json(responseBody); + } + + @Post('refresh') + async refreshAccessToken(@Req() req: any, @Res() res: Response) { + const refreshToken = req.cookies['refreshToken']; + + if (!refreshToken) { + throw new HttpException( + ErrorMessages.AUTH.INVALID_REFRESH_TOKEN.text, + ErrorMessages.AUTH.INVALID_REFRESH_TOKEN.code + ); + } + //const userId = req.user?.user_id; + const userId = this.authService.getUserIdFromRefreshToken(refreshToken); + const isValid = await this.authService.validateRefreshToken( + userId, + refreshToken + ); + if (!isValid) { + const error = ErrorMessages.AUTH.INVALID_REFRESH_TOKEN; + throw new HttpException(error.text, error.code); } - return await this.authService.updateUserRole(userId, roleId); + const newAccessToken = await this.authService.generateAccessToken(userId); + const newRefreshToken = await this.authService.generateRefreshToken(userId); + res.cookie('refreshToken', newRefreshToken, { + httpOnly: true, + secure: true, + sameSite: 'strict', + maxAge: 7 * 24 * 60 * 60 * 1000, + }); + return res.status(HttpStatusCodes.OK).json( + new ApiResponse( + HttpStatusCodes.OK, + '액세스 토큰이 성공적으로 갱신되었습니다.', + { + accessToken: newAccessToken, + } + ) + ); } + @Post('logout') + async logout(@Req() req: any, @Res() res: Response) { + res.clearCookie('refreshToken'); // HTTP-Only 쿠키 삭제 + return res + .status(HttpStatusCodes.OK) + .json(new ApiResponse(HttpStatusCodes.OK, '로그아웃 성공')); + } } diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts index 46153c1..74b3159 100644 --- a/src/modules/auth/auth.module.ts +++ b/src/modules/auth/auth.module.ts @@ -8,10 +8,11 @@ import { GitHubStrategy } from './strategies/github.strategy'; import { GoogleStrategy } from './strategies/google.strategy'; import { PrismaService } from '@src/prisma/prisma.service'; import { JwtAuthGuard } from '@modules/auth/guards/jwt-auth.guard'; - +import { RedisModule } from '../redis/redis.module'; @Module({ imports: [ PassportModule, + RedisModule, JwtModule.register({ secret: process.env.JWT_SECRET, // JWT 비밀키 설정 signOptions: { expiresIn: '1h' }, // 기본 만료 시간 diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index b340dda..89a4e79 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -1,35 +1,41 @@ -import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; +import { Injectable, HttpException } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; +import { RedisService } from '@modules/redis/redis.service'; import { PrismaService } from '@src/prisma/prisma.service'; import { AuthUserDto } from './dto/auth-user.dto'; import axios from 'axios'; +import { ErrorMessages } from '@common/constants/error-messages'; +import { HttpStatusCodes } from '@common/constants/http-status-code'; @Injectable() export class AuthService { constructor( private readonly jwtService: JwtService, - private readonly prisma: PrismaService + private readonly prisma: PrismaService, + private readonly redisService: RedisService ) {} async handleGoogleCallback(code: string) { try { - console.log('Received Authorization Code:', code); - // Google 토큰 요청 - const tokenResponse = await axios.post('https://oauth2.googleapis.com/token', { - code, - client_id: process.env.GOOGLE_CLIENT_ID, - client_secret: process.env.GOOGLE_CLIENT_SECRET, - redirect_uri: process.env.GOOGLE_CALLBACK_DEVELOP_URL, - grant_type: 'authorization_code', - }); - + const tokenResponse = await axios.post( + 'https://oauth2.googleapis.com/token', + { + code, + client_id: process.env.GOOGLE_CLIENT_ID, + client_secret: process.env.GOOGLE_CLIENT_SECRET, + redirect_uri: process.env.GOOGLE_CALLBACK_DEVELOP_URL, + grant_type: 'authorization_code', + } + ); const { access_token } = tokenResponse.data; - // Google 사용자 정보 요청 - const userInfoResponse = await axios.get('https://www.googleapis.com/oauth2/v2/userinfo', { - headers: { Authorization: `Bearer ${access_token}` }, - }); + const userInfoResponse = await axios.get( + 'https://www.googleapis.com/oauth2/v2/userinfo', + { + headers: { Authorization: `Bearer ${access_token}` }, + } + ); const userData = userInfoResponse.data; @@ -42,25 +48,30 @@ export class AuthService { auth_provider: 'google', }); - const jwt = this.generateJwt(user); - const responseUser = this.filterUserFields(user); + const accessToken = this.generateAccessToken(user.id); + const refreshToken = this.generateRefreshToken(user.id); // 리프레시 토큰 생성 - return { user: responseUser, accessToken: jwt, isExistingUser }; - } catch (error) { - console.error('Google OAuth Error:', error.response?.data || error.message); + // Redis에 리프레시 토큰 저장 + await this.storeRefreshToken(user.id, refreshToken); - if (error.response?.data?.error === 'invalid_grant') { - throw new Error('Authorization Code가 이미 사용되었거나 만료되었습니다.'); - } + const responseUser = this.filterUserFields(user); - throw new Error('Google OAuth 인증 실패'); + return { + user: responseUser, + accessToken, + refreshToken, + isExistingUser, + }; + } catch (error) { + throw new HttpException( + ErrorMessages.SERVER.INTERNAL_ERROR.text, + ErrorMessages.SERVER.INTERNAL_ERROR.code + ); } } async handleGithubCallback(code: string) { try { - console.log('Received GitHub Authorization Code:', code); - // GitHub 토큰 요청 const tokenResponse = await axios.post( 'https://github.com/login/oauth/access_token', @@ -72,11 +83,10 @@ export class AuthService { }, { headers: { Accept: 'application/json' }, - }, + } ); - + const { access_token } = tokenResponse.data; - console.log(access_token); // GitHub 사용자 정보 요청 const userInfoResponse = await axios.get('https://api.github.com/user', { headers: { Authorization: `Bearer ${access_token}` }, @@ -87,11 +97,16 @@ export class AuthService { // GitHub 사용자 이메일 요청 (필요 시) let email = userData.email; if (!email) { - const emailResponse = await axios.get('https://api.github.com/user/emails', { - headers: { Authorization: `Bearer ${access_token}` }, - }); + const emailResponse = await axios.get( + 'https://api.github.com/user/emails', + { + headers: { Authorization: `Bearer ${access_token}` }, + } + ); - const primaryEmail = emailResponse.data.find((e: any) => e.primary && e.verified); + const primaryEmail = emailResponse.data.find( + (e: any) => e.primary && e.verified + ); email = primaryEmail?.email; } @@ -108,22 +123,27 @@ export class AuthService { auth_provider: 'github', }); - const jwt = this.generateJwt(user); - const responseUser = this.filterUserFields(user); + const jwt = this.generateAccessToken(user.id); + const refreshToken = await this.generateRefreshToken(user.id); // 리프레시 토큰 생성 - return { user: responseUser, accessToken: jwt, isExistingUser }; + // Redis에 리프레시 토큰 저장 + await this.storeRefreshToken(user.id, refreshToken); + const responseUser = this.filterUserFields(user); + return { + user: responseUser, + accessToken: jwt, + refreshToken: refreshToken, // 리프레시 토큰 반환 + isExistingUser, + }; } catch (error) { - console.error('GitHub OAuth Error:', error.response?.data || error.message); - - if (error.response?.data?.error === 'bad_verification_code') { - throw new Error('Authorization Code가 잘못되었거나 만료되었습니다.'); - } - - throw new Error('GitHub OAuth 인증 실패'); + throw new HttpException( + ErrorMessages.SERVER.INTERNAL_ERROR.text, + ErrorMessages.SERVER.INTERNAL_ERROR.code + ); } } - private async checkUserExist(email: string): Promise{ + private async checkUserExist(email: string): Promise { const user = await this.prisma.user.findUnique({ where: { email }, }); @@ -131,32 +151,27 @@ export class AuthService { } // 사용자 찾기 또는 생성 async findOrCreateUser(profile: AuthUserDto) { - const user = await this.prisma.user.findUnique({ + return this.prisma.user.upsert({ where: { email: profile.email }, + update: {}, // 이미 존재하면 아무것도 업데이트하지 않음 + create: { + email: profile.email, + name: profile.name, + nickname: profile.nickname, + profile_url: profile.profile_url, + auth_provider: profile.auth_provider, + push_alert: false, + following_alert: false, + project_alert: false, + role: { connect: { id: 1 } }, + status: { connect: { id: 1 } }, + }, }); - - if (!user) { - return this.prisma.user.create({ - data: { - email: profile.email, - name: profile.name, - nickname: profile.nickname, - profile_url: profile.profile_url, - auth_provider: profile.auth_provider, - push_alert: false, - following_alert: false, - project_alert: false, - role: { connect: { id: 1 } }, - status: { connect: { id: 1 } }, - }, - }); - } - return user; } private filterUserFields(user: any) { return { - id: user.id, + user_id: user.id, email: user.email, name: user.name, nickname: user.nickname, @@ -166,59 +181,89 @@ export class AuthService { }; } - private generateJwt(user: any) { - const payload = { email: user.email, id: user.id }; - return this.jwtService.sign(payload, { expiresIn: '1h' }); + generateAccessToken(userId: number): string { + return this.jwtService.sign( + { userId }, + { expiresIn: '15m', secret: process.env.ACCESS_TOKEN_SECRET } + ); } - // 사용자 Role 업데이트 - async updateUserRole(userId: number, roleId: number) { - if (!userId) { - throw new BadRequestException('유효하지 않은 사용자 ID입니다.'); + generateRefreshToken(userId: number): string { + return this.jwtService.sign( + { userId }, + { expiresIn: '7d', secret: process.env.REFRESH_TOKEN_SECRET } + ); + } + + getUserIdFromRefreshToken(refreshToken: string): number | null { + try { + const payload = this.jwtService.verify(refreshToken, { + secret: process.env.REFRESH_TOKEN_SECRET, + }); + return payload.userId; + } catch (error) { + return null; } + } + // 리프레시 토큰 저장 + async storeRefreshToken(userId: number, refreshToken: string): Promise { + const key = `refresh_token:${userId}`; + const ttl = 7 * 24 * 60 * 60; // 7일 + await this.redisService.set(key, refreshToken, ttl); + } - // 사용자 확인 - const user = await this.prisma.user.findUnique({ - where: { id: userId }, // userId가 반드시 존재해야 함 - }); + // 리프레시 토큰 검증 + async validateRefreshToken(userId: number, token: string): Promise { + const key = `refresh_token:${userId}`; + const storedToken = await this.redisService.get(key); + return storedToken === token; + } + // 리프레시 토큰 삭제 + async deleteRefreshToken(userId: number): Promise { + const key = `refresh_token:${userId}`; + await this.redisService.del(key); + } + // 사용자 Role 업데이트 + async updateUserRole(userId: number, roleId: number) { + const user = await this.prisma.user.findUnique({ where: { id: userId } }); if (!user) { - throw new NotFoundException('사용자를 찾을 수 없습니다.'); + const error = ErrorMessages.AUTH.USER_NOT_FOUND; + throw new HttpException(error.text, error.code); } - // Role 메시지 설정 - let roleMessage = ''; - switch (roleId) { - case 1: - roleMessage = '프로그래머로 변경되었습니다.'; - break; - case 2: - roleMessage = '아티스트로 변경되었습니다.'; - break; - case 3: - roleMessage = '디자이너로 변경되었습니다.'; - break; - default: - throw new BadRequestException('유효하지 않은 역할 ID입니다.'); + // 역할에 따른 메시지 생성 + const roleMessages = { + 1: '프로그래머로 변경되었습니다.', + 2: '아티스트로 변경되었습니다.', + 3: '디자이너로 변경되었습니다.', + }; + + if (!roleMessages[roleId]) { + throw new HttpException( + '유효하지 않은 역할 ID입니다.', + HttpStatusCodes.BAD_REQUEST + ); } - // 사용자 Role 업데이트 + + // 사용자 역할 업데이트 const updatedUser = await this.prisma.user.update({ where: { id: userId }, data: { role_id: roleId }, }); - return { - message: { - code : 200, - text: `${roleMessage}` - }, + const result = { user: { - id: updatedUser.id, + user_id: updatedUser.id, email: updatedUser.email, name: updatedUser.name, nickname: updatedUser.nickname, role_id: updatedUser.role_id, }, + message: roleMessages[roleId], }; + + console.log('Service Result:', result); // 디버깅 + return result; } } diff --git a/src/modules/auth/strategies/jwt.strategy.ts b/src/modules/auth/strategies/jwt.strategy.ts index 988c9ef..ac9ef55 100644 --- a/src/modules/auth/strategies/jwt.strategy.ts +++ b/src/modules/auth/strategies/jwt.strategy.ts @@ -7,12 +7,13 @@ export class JwtStrategy extends PassportStrategy(Strategy) { constructor() { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), - secretOrKey: process.env.JWT_SECRET, + secretOrKey: process.env.ACCESS_TOKEN_SECRET, }); } async validate(payload: any) { + console.log('JWT_payload: ', payload); // req.user에 설정될 사용자 정보 반환 - return { id: payload.id, email: payload.email }; + return { user_id: payload.userId, email: payload.email }; } } diff --git a/src/modules/redis/redis.module.ts b/src/modules/redis/redis.module.ts new file mode 100644 index 0000000..a61e7b9 --- /dev/null +++ b/src/modules/redis/redis.module.ts @@ -0,0 +1,23 @@ +import { Module, Global } from '@nestjs/common'; +import Redis from 'ioredis'; +import { RedisService } from './redis.service'; +import { RedisConfig } from '@config/redis.config'; + +@Global() +@Module({ + providers: [ + { + provide: 'REDIS_CLIENT', + useFactory: () => { + return new Redis({ + host: RedisConfig.host, + port: RedisConfig.port, + password: RedisConfig.password, + }); + }, + }, + RedisService, + ], + exports: [RedisService], +}) +export class RedisModule {} diff --git a/src/modules/redis/redis.service.ts b/src/modules/redis/redis.service.ts new file mode 100644 index 0000000..308a778 --- /dev/null +++ b/src/modules/redis/redis.service.ts @@ -0,0 +1,19 @@ +import { Injectable, Inject } from '@nestjs/common'; +import { Redis } from 'ioredis'; + +@Injectable() +export class RedisService { + constructor(@Inject('REDIS_CLIENT') private readonly redis: Redis) {} + + async set(key: string, value: string, ttl: number): Promise { + await this.redis.set(key, value, 'EX', ttl); // TTL 설정 + } + + async get(key: string): Promise { + return this.redis.get(key); + } + + async del(key: string): Promise { + await this.redis.del(key); + } +} diff --git a/src/modules/user/user.controller.ts b/src/modules/user/user.controller.ts index 896c6a4..4bbfcb3 100644 --- a/src/modules/user/user.controller.ts +++ b/src/modules/user/user.controller.ts @@ -1,15 +1,7 @@ -import { Controller, Patch, Body, UseGuards, Req } from '@nestjs/common'; +import { Controller } from '@nestjs/common'; import { UserService } from './user.service'; -import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; @Controller('users') export class UserController { constructor(private readonly userService: UserService) {} - - @UseGuards(JwtAuthGuard) - @Patch('select-role') - async selectRole(@Req() req, @Body('roleId') roleId: number) { - const userId = req.user.id; // JWT로부터 가져온 사용자 ID - return this.userService.updateUserRole(userId, roleId); - } } diff --git a/src/modules/user/user.service.ts b/src/modules/user/user.service.ts index 37036d0..757992d 100644 --- a/src/modules/user/user.service.ts +++ b/src/modules/user/user.service.ts @@ -1,27 +1,8 @@ -import { - BadRequestException, - Injectable, - NotFoundException, -} from '@nestjs/common'; -import { ERROR_MESSAGES } from '@common/constants/error-messages'; +import { Injectable } from '@nestjs/common'; + import { PrismaService } from '@prisma/prisma.service'; @Injectable() export class UserService { constructor(private readonly prisma: PrismaService) {} - async updateUserRole(userId: number, roleId: number) { - if (![1, 2, 3].includes(roleId)) { - throw new BadRequestException(ERROR_MESSAGES.INVALID_ROLE_ID); - } - - const user = await this.prisma.user.findUnique({ where: { id: userId } }); - if (!user) { - throw new NotFoundException(ERROR_MESSAGES.USER_NOT_FOUND); - } - console.log(userId); - return this.prisma.user.update({ - where: { id: userId }, - data: { role_id: roleId }, - }); - } } From aefcc6d1381c28c575034c7da63b71fc128a7ddb Mon Sep 17 00:00:00 2001 From: Griffin <142657661+peppertown@users.noreply.github.com> Date: Thu, 9 Jan 2025 21:13:33 +0900 Subject: [PATCH 09/14] =?UTF-8?q?=EC=B1=84=ED=8C=85=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=9A=A9=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EA=B5=AC=ED=98=84=20(#17)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Chore] install socket.io * [Feat] 게이트웨이 생성 * [Feature] 소셜 로그인 로직 변경, 역할 선택 API 구현 (#8) * [Feat] JWT Strategy 리턴값 변경 * [Feat] CallBack URI 변경 * [Feat] Role 선택 API 구현 * [Feat] 소셜 로그인 로직 변경 * [Feat] CORS 설정 * [Feat] Prisma 채팅 관련 테이블 추가 마이그레이션 * [Feat] 게이트웨이 로직 분리를 위해 Chat 모듈 생성 * [Feat] Prisma 모듈 임포트 * [Feat] 채팅방 생성 구현 * [Refactor] 채팅방 생성 로직 수정 * [Feat] 채팅방 멤버 저장 구현 * [Feat] Prisma 마이그레이션 채팅 관련 테이블 이름 및 속성 변경 * [Feat] 채팅방 존재 유무 확인 로직 구현 * [Feat] 게이트웨이 생성 * [Feat] 채팅방 생성 구현 * [Feat] Jwt 모듈 임포트 * [Feat] JWT 토큰 디코드 로직 구현 * [Feat] 채팅방 멤버 저장 로직 구현 * [Feat] 채팅방 생성관련 로직 오류 수정 * [Feat] 채팅방 존재 유무 확인 로직 수정 * [Feature] 9 -Refresh token 및 코드 리팩토링 (#13) * [Add] ioredis 패키지 추가 * [Feat] Redis 관련 설정 파일 구현 * [Feat] payload 변경 * [Feat] Redis를 이용한 리프레쉬 토큰 구현 * [Feat] http status code 타입화 * [Feat] 응답 관련 Interface DTO 구현 * [Feat] Redis 연결 config 구현 * [Feat] Redis 사용을 위한 module, service 로직 구현 * [Feat] 에러 메세지 관련 타입화 구현 * [Feat] http-exception filter 고도화 * [Refactor] 사용하지 않는 코드 정리 * [Refactor] 사용하지 않는 코드 정리 * [Refactor] auth 관련 코드들 응답처리, 에러처리 리펙토링 * [Feat] 에러메세지 추가 구현 * [Feat] 에러메세지 추가 구현 * [Feat] HttpExceptionFilter Global 설정 * [Feat] 응답 데이터에 대한 DTO 구현 * [Refactor] 필요없는 코드 제거 * [Feat] 리프레쉬 토큰 쿠키에 전달하도록 로직 변경 구현 * [Feat] payload 응답 값 변경 * [Feat] Response dto 변경 구현 * [Refactor] auth 관련 컨트롤러, 서비스 코드 리팩토링 * [Fix] accessToken 시간 변경 * [Feat] Prisma 마이그레이션 Message 테이블 컬럼 수정 * [Feat] 메세지 저장 로직 구현 * [Feat] 메세지 보낸 유저 추가 정보 조회 로직 구현 * [Feat] 소켓 통신 설정 * [Feat] 채팅방 참여 시 소켓룸에 join * [Feat] 게이트웨이 namespace 추가 --------- Co-authored-by: ssomae <80831228+Ss0Mae@users.noreply.github.com> --- package-lock.json | 254 ++++++++++ package.json | 2 + .../migration.sql | 457 ++++++++++++++++++ .../migration.sql | 59 +++ .../migration.sql | 14 + prisma/migrations/migration_lock.toml | 3 + prisma/schema.prisma | 51 +- src/app.module.ts | 4 + src/chat/chat.gateway.spec.ts | 18 + src/chat/chat.gateway.ts | 78 +++ src/chat/chat.module.ts | 10 + src/chat/chat.service.spec.ts | 18 + src/chat/chat.service.ts | 92 ++++ 13 files changed, 1055 insertions(+), 5 deletions(-) create mode 100644 prisma/migrations/20250107090813_creat_chat_tables/migration.sql create mode 100644 prisma/migrations/20250109063122_fix_chat_table/migration.sql create mode 100644 prisma/migrations/20250109085641_fix_message_table/migration.sql create mode 100644 prisma/migrations/migration_lock.toml create mode 100644 src/chat/chat.gateway.spec.ts create mode 100644 src/chat/chat.gateway.ts create mode 100644 src/chat/chat.module.ts create mode 100644 src/chat/chat.service.spec.ts create mode 100644 src/chat/chat.service.ts diff --git a/package-lock.json b/package-lock.json index 9315f0c..48df7ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@nestjs/jwt": "^10.2.0", "@nestjs/passport": "^10.0.3", "@nestjs/platform-express": "^10.0.0", + "@nestjs/platform-socket.io": "^10.4.15", "@nestjs/swagger": "^8.1.0", "@prisma/client": "^6.1.0", "axios": "^1.7.9", @@ -33,6 +34,7 @@ "redis": "^4.7.0", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", + "socket.io": "^4.8.1", "swagger-ui-express": "^5.0.1" }, "devDependencies": { @@ -2761,6 +2763,25 @@ "@nestjs/core": "^10.0.0" } }, + "node_modules/@nestjs/platform-socket.io": { + "version": "10.4.15", + "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.4.15.tgz", + "integrity": "sha512-KZAxNEADPwoORixh3NJgGYWMVGORVPKeTqjD7hbF8TPDLKWWxru9yasBQwEz2/wXH/WgpkQbbaYwx4nUjCIVpw==", + "license": "MIT", + "dependencies": { + "socket.io": "4.8.1", + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/websockets": "^10.0.0", + "rxjs": "^7.1.0" + } + }, "node_modules/@nestjs/schematics": { "version": "10.2.3", "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.2.3.tgz", @@ -2846,6 +2867,30 @@ } } }, + "node_modules/@nestjs/websockets": { + "version": "10.4.15", + "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.4.15.tgz", + "integrity": "sha512-OmCUJwvtagzXfMVko595O98UI3M9zg+URL+/HV7vd3QPMCZ3uGCKSq15YYJ99LHJn9NyK4e4Szm2KnHtUg2QzA==", + "license": "MIT", + "peer": true, + "dependencies": { + "iterare": "1.2.1", + "object-hash": "3.0.0", + "tslib": "2.8.1" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0", + "@nestjs/platform-socket.io": "^10.0.0", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "@nestjs/platform-socket.io": { + "optional": true + } + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -3793,6 +3838,12 @@ "node": ">=16.0.0" } }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", @@ -3897,6 +3948,12 @@ "@types/node": "*" } }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", + "license": "MIT" + }, "node_modules/@types/cookiejar": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", @@ -3904,6 +3961,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/eslint": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", @@ -5108,6 +5174,15 @@ ], "license": "MIT" }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, "node_modules/base64url": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", @@ -6223,6 +6298,62 @@ "node": ">= 0.8" } }, + "node_modules/engine.io": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz", + "integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==", + "license": "MIT", + "dependencies": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/enhanced-resolve": { "version": "5.18.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz", @@ -9642,6 +9773,16 @@ "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.13.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", @@ -11037,6 +11178,98 @@ "node": ">=8" } }, + "node_modules/socket.io": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "license": "MIT", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/source-map": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", @@ -12360,6 +12593,27 @@ "dev": true, "license": "ISC" }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xml2js": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", diff --git a/package.json b/package.json index b75f88d..a511ab4 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@nestjs/jwt": "^10.2.0", "@nestjs/passport": "^10.0.3", "@nestjs/platform-express": "^10.0.0", + "@nestjs/platform-socket.io": "^10.4.15", "@nestjs/swagger": "^8.1.0", "@prisma/client": "^6.1.0", "axios": "^1.7.9", @@ -46,6 +47,7 @@ "redis": "^4.7.0", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", + "socket.io": "^4.8.1", "swagger-ui-express": "^5.0.1" }, "devDependencies": { diff --git a/prisma/migrations/20250107090813_creat_chat_tables/migration.sql b/prisma/migrations/20250107090813_creat_chat_tables/migration.sql new file mode 100644 index 0000000..420f7b8 --- /dev/null +++ b/prisma/migrations/20250107090813_creat_chat_tables/migration.sql @@ -0,0 +1,457 @@ +-- CreateTable +CREATE TABLE `User` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `email` VARCHAR(191) NOT NULL, + `name` VARCHAR(191) NOT NULL, + `nickname` VARCHAR(191) NOT NULL, + `auth_provider` VARCHAR(191) NOT NULL, + `profile_url` VARCHAR(191) NULL, + `role_id` INTEGER NOT NULL, + `introduce` VARCHAR(191) NULL, + `status_id` INTEGER NOT NULL, + `apply_count` INTEGER NULL DEFAULT 0, + `post_count` INTEGER NULL DEFAULT 0, + `push_alert` BOOLEAN NOT NULL DEFAULT false, + `following_alert` BOOLEAN NOT NULL DEFAULT false, + `project_alert` BOOLEAN NOT NULL DEFAULT false, + `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` DATETIME(3) NOT NULL, + + UNIQUE INDEX `User_email_key`(`email`), + INDEX `User_role_id_fkey`(`role_id`), + INDEX `User_status_id_fkey`(`status_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `Role` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `name` VARCHAR(191) NOT NULL, + + UNIQUE INDEX `Role_name_key`(`name`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `Project` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `user_id` INTEGER NOT NULL, + `title` VARCHAR(191) NOT NULL, + `description` VARCHAR(191) NOT NULL, + `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` DATETIME(3) NOT NULL, + + UNIQUE INDEX `Project_user_id_key`(`user_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `ProjectLink` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `project_id` INTEGER NOT NULL, + `type_id` INTEGER NOT NULL, + `url` VARCHAR(191) NOT NULL, + + INDEX `ProjectLink_project_id_fkey`(`project_id`), + INDEX `ProjectLink_type_id_fkey`(`type_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `LinkType` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `name` VARCHAR(191) NOT NULL, + + UNIQUE INDEX `LinkType_name_key`(`name`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `ProgrammerData` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `user_id` INTEGER NOT NULL, + `github_username` VARCHAR(191) NOT NULL, + `github_url` VARCHAR(191) NOT NULL, + `commit_count` INTEGER NOT NULL, + `contribution_data` VARCHAR(191) NOT NULL, + + UNIQUE INDEX `ProgrammerData_user_id_key`(`user_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `ArtistData` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `user_id` INTEGER NOT NULL, + `soundcloud_url` VARCHAR(191) NOT NULL, + `portfolio_url` VARCHAR(191) NOT NULL, + `music_data` VARCHAR(191) NOT NULL, + + UNIQUE INDEX `ArtistData_user_id_key`(`user_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `Status` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `name` VARCHAR(191) NOT NULL, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `Resume` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `user_id` INTEGER NOT NULL, + `title` VARCHAR(191) NOT NULL, + `introduce` VARCHAR(191) NOT NULL, + + INDEX `Resume_user_id_fkey`(`user_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `Skill` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `name` VARCHAR(191) NOT NULL, + + UNIQUE INDEX `Skill_name_key`(`name`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `UserSkill` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `user_id` INTEGER NOT NULL, + `skill_id` INTEGER NOT NULL, + + INDEX `UserSkill_skill_id_fkey`(`skill_id`), + UNIQUE INDEX `UserSkill_user_id_skill_id_key`(`user_id`, `skill_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `UserLink` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `user_id` INTEGER NOT NULL, + `platform` VARCHAR(191) NOT NULL, + `link` VARCHAR(191) NOT NULL, + + INDEX `UserLink_user_id_fkey`(`user_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `Follows` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `following_user_id` INTEGER NOT NULL, + `followed_user_id` INTEGER NOT NULL, + `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + + INDEX `Follows_followed_user_id_fkey`(`followed_user_id`), + UNIQUE INDEX `Follows_following_user_id_followed_user_id_key`(`following_user_id`, `followed_user_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `FeedPost` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `user_id` INTEGER NOT NULL, + `title` VARCHAR(191) NOT NULL, + `content` VARCHAR(191) NOT NULL, + `thumbnail_url` VARCHAR(191) NOT NULL, + `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` DATETIME(3) NOT NULL, + `view` INTEGER NOT NULL, + `comment_count` INTEGER NOT NULL, + `like_count` INTEGER NOT NULL, + + INDEX `FeedPost_user_id_fkey`(`user_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `FeedTag` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `name` VARCHAR(191) NOT NULL, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `FeedPostTag` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `post_id` INTEGER NOT NULL, + `tag_id` INTEGER NOT NULL, + + INDEX `FeedPostTag_tag_id_fkey`(`tag_id`), + UNIQUE INDEX `FeedPostTag_post_id_tag_id_key`(`post_id`, `tag_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `FeedComment` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `user_id` INTEGER NOT NULL, + `post_id` INTEGER NOT NULL, + `content` VARCHAR(191) NOT NULL, + `image_url` VARCHAR(191) NULL, + `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` DATETIME(3) NOT NULL, + + INDEX `FeedComment_post_id_fkey`(`post_id`), + INDEX `FeedComment_user_id_fkey`(`user_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `FeedLike` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `user_id` INTEGER NOT NULL, + `post_id` INTEGER NOT NULL, + + INDEX `FeedLike_post_id_fkey`(`post_id`), + INDEX `FeedLike_user_id_fkey`(`user_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `ProjectPost` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `user_id` INTEGER NOT NULL, + `title` VARCHAR(191) NOT NULL, + `content` VARCHAR(191) NOT NULL, + `thumbnail_url` VARCHAR(191) NOT NULL, + `role` INTEGER NOT NULL, + `unit` VARCHAR(191) NOT NULL, + `start_date` DATETIME(3) NOT NULL, + `end_date` DATETIME(3) NOT NULL, + `work_type_id` INTEGER NOT NULL, + `recruiting` BOOLEAN NOT NULL, + `applicant_count` INTEGER NOT NULL, + `view` INTEGER NOT NULL, + `saved_count` INTEGER NOT NULL, + + INDEX `ProjectPost_user_id_fkey`(`user_id`), + INDEX `ProjectPost_work_type_id_fkey`(`work_type_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `WorkType` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `name` VARCHAR(191) NOT NULL, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `ProjectSave` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `user_id` INTEGER NOT NULL, + `post_id` INTEGER NOT NULL, + + INDEX `ProjectSave_post_id_fkey`(`post_id`), + INDEX `ProjectSave_user_id_fkey`(`user_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `ProjectDetailRole` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `post_id` INTEGER NOT NULL, + `detail_role_id` INTEGER NOT NULL, + + INDEX `ProjectDetailRole_detail_role_id_fkey`(`detail_role_id`), + INDEX `ProjectDetailRole_post_id_fkey`(`post_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `DetailRole` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `role_id` INTEGER NOT NULL, + `name` VARCHAR(191) NOT NULL, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `ProjectTag` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `name` VARCHAR(191) NOT NULL, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `ProjectPostTag` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `post_id` INTEGER NOT NULL, + `tag_id` INTEGER NOT NULL, + + INDEX `ProjectPostTag_tag_id_fkey`(`tag_id`), + UNIQUE INDEX `ProjectPostTag_post_id_tag_id_key`(`post_id`, `tag_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `UserApplyProject` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `user_id` INTEGER NOT NULL, + `post_id` INTEGER NOT NULL, + + INDEX `UserApplyProject_post_id_fkey`(`post_id`), + INDEX `UserApplyProject_user_id_fkey`(`user_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `Room` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `name` VARCHAR(191) NOT NULL, + `active` BOOLEAN NOT NULL DEFAULT true, + `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `Room_users` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `room_id` INTEGER NOT NULL, + `user_id` INTEGER NOT NULL, + `joined_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `Message` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `room_id` INTEGER NOT NULL, + `user_id` INTEGER NOT NULL, + `photo_url` VARCHAR(191) NULL, + `message` VARCHAR(191) NOT NULL, + `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `Message_status` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `message_id` INTEGER NOT NULL, + `user_id` INTEGER NOT NULL, + `is_read` BOOLEAN NOT NULL DEFAULT false, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- AddForeignKey +ALTER TABLE `User` ADD CONSTRAINT `User_role_id_fkey` FOREIGN KEY (`role_id`) REFERENCES `Role`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `User` ADD CONSTRAINT `User_status_id_fkey` FOREIGN KEY (`status_id`) REFERENCES `Status`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `Project` ADD CONSTRAINT `Project_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `ProjectLink` ADD CONSTRAINT `ProjectLink_project_id_fkey` FOREIGN KEY (`project_id`) REFERENCES `Project`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `ProjectLink` ADD CONSTRAINT `ProjectLink_type_id_fkey` FOREIGN KEY (`type_id`) REFERENCES `LinkType`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `ProgrammerData` ADD CONSTRAINT `ProgrammerData_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `ArtistData` ADD CONSTRAINT `ArtistData_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `Resume` ADD CONSTRAINT `Resume_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `UserSkill` ADD CONSTRAINT `UserSkill_skill_id_fkey` FOREIGN KEY (`skill_id`) REFERENCES `Skill`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `UserSkill` ADD CONSTRAINT `UserSkill_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `UserLink` ADD CONSTRAINT `UserLink_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `Follows` ADD CONSTRAINT `Follows_followed_user_id_fkey` FOREIGN KEY (`followed_user_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `Follows` ADD CONSTRAINT `Follows_following_user_id_fkey` FOREIGN KEY (`following_user_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `FeedPost` ADD CONSTRAINT `FeedPost_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `FeedPostTag` ADD CONSTRAINT `FeedPostTag_post_id_fkey` FOREIGN KEY (`post_id`) REFERENCES `FeedPost`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `FeedPostTag` ADD CONSTRAINT `FeedPostTag_tag_id_fkey` FOREIGN KEY (`tag_id`) REFERENCES `FeedTag`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `FeedComment` ADD CONSTRAINT `FeedComment_post_id_fkey` FOREIGN KEY (`post_id`) REFERENCES `FeedPost`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `FeedComment` ADD CONSTRAINT `FeedComment_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `FeedLike` ADD CONSTRAINT `FeedLike_post_id_fkey` FOREIGN KEY (`post_id`) REFERENCES `FeedPost`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `FeedLike` ADD CONSTRAINT `FeedLike_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `ProjectPost` ADD CONSTRAINT `ProjectPost_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `ProjectPost` ADD CONSTRAINT `ProjectPost_work_type_id_fkey` FOREIGN KEY (`work_type_id`) REFERENCES `WorkType`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `ProjectSave` ADD CONSTRAINT `ProjectSave_post_id_fkey` FOREIGN KEY (`post_id`) REFERENCES `ProjectPost`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `ProjectSave` ADD CONSTRAINT `ProjectSave_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `ProjectDetailRole` ADD CONSTRAINT `ProjectDetailRole_detail_role_id_fkey` FOREIGN KEY (`detail_role_id`) REFERENCES `DetailRole`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `ProjectDetailRole` ADD CONSTRAINT `ProjectDetailRole_post_id_fkey` FOREIGN KEY (`post_id`) REFERENCES `ProjectPost`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `ProjectPostTag` ADD CONSTRAINT `ProjectPostTag_post_id_fkey` FOREIGN KEY (`post_id`) REFERENCES `ProjectPost`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `ProjectPostTag` ADD CONSTRAINT `ProjectPostTag_tag_id_fkey` FOREIGN KEY (`tag_id`) REFERENCES `ProjectTag`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `UserApplyProject` ADD CONSTRAINT `UserApplyProject_post_id_fkey` FOREIGN KEY (`post_id`) REFERENCES `ProjectPost`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `UserApplyProject` ADD CONSTRAINT `UserApplyProject_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `Room_users` ADD CONSTRAINT `Room_users_room_id_fkey` FOREIGN KEY (`room_id`) REFERENCES `Room`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `Room_users` ADD CONSTRAINT `Room_users_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `Message` ADD CONSTRAINT `Message_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `Message` ADD CONSTRAINT `Message_room_id_fkey` FOREIGN KEY (`room_id`) REFERENCES `Room`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `Message_status` ADD CONSTRAINT `Message_status_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `Message_status` ADD CONSTRAINT `Message_status_message_id_fkey` FOREIGN KEY (`message_id`) REFERENCES `Message`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20250109063122_fix_chat_table/migration.sql b/prisma/migrations/20250109063122_fix_chat_table/migration.sql new file mode 100644 index 0000000..072405a --- /dev/null +++ b/prisma/migrations/20250109063122_fix_chat_table/migration.sql @@ -0,0 +1,59 @@ +/* + Warnings: + + - You are about to drop the column `room_id` on the `Message` table. All the data in the column will be lost. + - You are about to drop the `Room` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `Room_users` table. If the table is not empty, all the data it contains will be lost. + - Added the required column `channel_id` to the `Message` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropForeignKey +ALTER TABLE `Message` DROP FOREIGN KEY `Message_room_id_fkey`; + +-- DropForeignKey +ALTER TABLE `Room_users` DROP FOREIGN KEY `Room_users_room_id_fkey`; + +-- DropForeignKey +ALTER TABLE `Room_users` DROP FOREIGN KEY `Room_users_user_id_fkey`; + +-- DropIndex +DROP INDEX `Message_room_id_fkey` ON `Message`; + +-- AlterTable +ALTER TABLE `Message` DROP COLUMN `room_id`, + ADD COLUMN `channel_id` VARCHAR(191) NOT NULL; + +-- DropTable +DROP TABLE `Room`; + +-- DropTable +DROP TABLE `Room_users`; + +-- CreateTable +CREATE TABLE `Channel` ( + `id` VARCHAR(191) NOT NULL, + `name` VARCHAR(191) NOT NULL DEFAULT 'default_channel_name', + `active` BOOLEAN NOT NULL DEFAULT true, + `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `Channel_users` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `channel_id` VARCHAR(191) NOT NULL, + `user_id` INTEGER NOT NULL, + `joined_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- AddForeignKey +ALTER TABLE `Channel_users` ADD CONSTRAINT `Channel_users_channel_id_fkey` FOREIGN KEY (`channel_id`) REFERENCES `Channel`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `Channel_users` ADD CONSTRAINT `Channel_users_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `Message` ADD CONSTRAINT `Message_channel_id_fkey` FOREIGN KEY (`channel_id`) REFERENCES `Channel`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20250109085641_fix_message_table/migration.sql b/prisma/migrations/20250109085641_fix_message_table/migration.sql new file mode 100644 index 0000000..6fdbbfb --- /dev/null +++ b/prisma/migrations/20250109085641_fix_message_table/migration.sql @@ -0,0 +1,14 @@ +/* + Warnings: + + - You are about to drop the column `message` on the `Message` table. All the data in the column will be lost. + - You are about to drop the column `photo_url` on the `Message` table. All the data in the column will be lost. + - Added the required column `content` to the `Message` table without a default value. This is not possible if the table is not empty. + - Added the required column `type` to the `Message` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE `Message` DROP COLUMN `message`, + DROP COLUMN `photo_url`, + ADD COLUMN `content` VARCHAR(191) NOT NULL, + ADD COLUMN `type` VARCHAR(191) NOT NULL; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..8a21669 --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "mysql" \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index cc2ebdf..e63e746 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -40,7 +40,9 @@ model User { UserApplyProject UserApplyProject[] UserLinks UserLink[] UserSkills UserSkill[] - + Channel_users Channel_users[] + Message Message[] + Message_status Message_status[] @@index([role_id], map: "User_role_id_fkey") @@index([status_id], map: "User_status_id_fkey") @@ -144,12 +146,12 @@ model UserLink { } model Follows { - id Int @id @default(autoincrement()) + id Int @id @default(autoincrement()) following_user_id Int followed_user_id Int - created_at DateTime @default(now()) - followed_user User @relation("FollowedUsers", fields: [followed_user_id], references: [id]) - following_user User @relation("UserFollows", fields: [following_user_id], references: [id]) + created_at DateTime @default(now()) + followed_user User @relation("FollowedUsers", fields: [followed_user_id], references: [id]) + following_user User @relation("UserFollows", fields: [following_user_id], references: [id]) @@unique([following_user_id, followed_user_id]) @@index([followed_user_id], map: "Follows_followed_user_id_fkey") @@ -305,3 +307,42 @@ model UserApplyProject { @@index([post_id], map: "UserApplyProject_post_id_fkey") @@index([user_id], map: "UserApplyProject_user_id_fkey") } + +model Channel { + id String @id + name String @default("default_channel_name") + active Boolean @default(true) + created_at DateTime @default(now()) + Channel_users Channel_users[] + Message Message[] +} + +model Channel_users { + id Int @id @default(autoincrement()) + channel_id String + user_id Int + joined_at DateTime @default(now()) + channel Channel @relation(fields: [channel_id], references: [id]) + user User @relation(fields: [user_id], references: [id]) +} + +model Message { + id Int @id @default(autoincrement()) + channel_id String + user_id Int + type String + content String + created_at DateTime @default(now()) + user User @relation(fields: [user_id], references: [id]) + channel Channel @relation(fields: [channel_id], references: [id]) + message_status Message_status[] +} + +model Message_status { + id Int @id @default(autoincrement()) + message_id Int + user_id Int + is_read Boolean @default(false) + user User @relation(fields: [user_id], references: [id]) + message Message @relation(fields: [message_id], references: [id]) +} diff --git a/src/app.module.ts b/src/app.module.ts index 5c900ea..29b5416 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -2,6 +2,8 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { AuthModule } from '@modules/auth/auth.module'; import { UserModule } from '@modules/user/user.module'; +import { ChatGateway } from './chat/chat.gateway'; +import { ChatModule } from './chat/chat.module'; @Module({ imports: [ ConfigModule.forRoot({ @@ -9,6 +11,8 @@ import { UserModule } from '@modules/user/user.module'; }), AuthModule, // Auth 모듈 추가 UserModule, + ChatModule, ], + providers: [ChatGateway], }) export class AppModule {} diff --git a/src/chat/chat.gateway.spec.ts b/src/chat/chat.gateway.spec.ts new file mode 100644 index 0000000..34daca9 --- /dev/null +++ b/src/chat/chat.gateway.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ChatGateway } from './chat.gateway'; + +describe('ChatGateway', () => { + let gateway: ChatGateway; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ChatGateway], + }).compile(); + + gateway = module.get(ChatGateway); + }); + + it('should be defined', () => { + expect(gateway).toBeDefined(); + }); +}); diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts new file mode 100644 index 0000000..28962cb --- /dev/null +++ b/src/chat/chat.gateway.ts @@ -0,0 +1,78 @@ +import { + SubscribeMessage, + WebSocketGateway, + MessageBody, + WebSocketServer, + ConnectedSocket, +} from '@nestjs/websockets'; +import { ChatService } from './chat.service'; +import { JwtService } from '@nestjs/jwt'; +import { Server, Socket } from 'socket.io'; + +@WebSocketGateway({ namespace: 'chat', cors: { origin: '*' } }) +export class ChatGateway { + constructor( + private readonly chatService: ChatService, + private readonly jwtService: JwtService + ) {} + @WebSocketServer() server: Server; + // 채팅방 참여 + @SubscribeMessage('joinChannel') + async handleJoinChannel( + @MessageBody() data: { channelId: string; userId: string }, + @ConnectedSocket() client: Socket + ) { + const { channelId, userId } = data; + + // userId JWT 토큰 값이라 디코딩 해야함 + const user = this.jwtService.decode(userId); + + // 존재하는 채팅방인지 확인 + const existData = await this.chatService.channelExist(channelId, user.id); + if (existData) { + // 채팅방 참여 + client.join(channelId); + return existData; + } + + // 채팅방 생성 + await this.chatService.createChannel(channelId); + + // 채팅방 참여 + client.join(channelId); + + // 채팅방 멤버 저장 + await this.chatService.joinChannel(channelId, user.id); + } + + // 메세지 송수신 + @SubscribeMessage('sendMessage') + async handleSendMessage( + @MessageBody() + data: { + type: string; + content: string; + user: any; + channelId: string; + } + ) { + // user 디코딩 + const user = this.jwtService.decode(data.user); + + // 메세지 데이터 저장 + await this.chatService.createMessage( + data.type, + data.channelId, + user.id, + data.content + ); + + // 유저 정보 추가 + const userData = await this.chatService.getSenderProfile(user.id); + data.user = userData; + const createdAt = new Date(); + const sendData = { ...data, createdAt }; + + this.server.to(data.channelId).emit('message', sendData); + } +} diff --git a/src/chat/chat.module.ts b/src/chat/chat.module.ts new file mode 100644 index 0000000..1e54921 --- /dev/null +++ b/src/chat/chat.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { ChatService } from './chat.service'; +import { PrismaModule } from '@src/prisma/prisma.module'; +import { JwtModule } from '@nestjs/jwt'; + +@Module({ + imports: [PrismaModule, JwtModule], + providers: [ChatService], +}) +export class ChatModule {} diff --git a/src/chat/chat.service.spec.ts b/src/chat/chat.service.spec.ts new file mode 100644 index 0000000..110cd7d --- /dev/null +++ b/src/chat/chat.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ChatService } from './chat.service'; + +describe('ChatService', () => { + let service: ChatService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ChatService], + }).compile(); + + service = module.get(ChatService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts new file mode 100644 index 0000000..41476a9 --- /dev/null +++ b/src/chat/chat.service.ts @@ -0,0 +1,92 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '@src/prisma/prisma.service'; + +@Injectable() +export class ChatService { + constructor(private readonly prisma: PrismaService) {} + + // 이미 존재하는 채팅방인지 확인 + // 없다면 생성, 있다면 매핑 테이블 확인 + // 매핑 테이블 유저 데이터 있다면 대화 내용 불러오기, 없다면 매핑 테이블에 유저 데이터 생성 + async channelExist(channelId, userId) { + const exist = await this.prisma.channel.count({ + where: { id: channelId }, + }); + + // 존재하지 않으면 함수 종료 후 컨트롤러에서 채팅방 생성 로직 재개 + if (!exist) return false; + + // 매핑 테이블 channel_users에 유저 데이터 있는지 확인 + const userExist = await this.prisma.channel_users.findFirst({ + where: { + channel_id: channelId, + user_id: userId, + }, + }); + + // 있다면 대화내용 불러오기 + if (userExist) { + const message = await this.prisma.message.findMany({ + where: { + channel_id: channelId, + }, + }); + return message; + } else { + // 없다면 매핑 테이블에 유저 데이터 추가 + await this.prisma.channel_users.create({ + data: { + channel_id: channelId, + user_id: userId, + }, + }); + const notice = `${userId}님이 채팅방에 참가했습니다`; + return { notice }; + } + } + + // 채팅방 생성 + async createChannel(id) { + await this.prisma.channel.create({ data: { id } }); + } + + // 채팅방 멤버 저장 + async joinChannel(channelId, userId) { + await this.prisma.channel_users.create({ + data: { + channel_id: channelId, + user_id: userId, + }, + }); + } + // 메세지 저장 + async createMessage(type, channelId, userId, content) { + await this.prisma.message.create({ + data: { + type, + content, + channel_id: channelId, + user_id: userId, + }, + }); + } + + // 유저 정보 확인 + async getSenderProfile(userId) { + const data = await this.prisma.user.findUnique({ + where: { + id: userId, + }, + select: { + id: true, + email: true, + name: true, + nickname: true, + role_id: true, + profile_url: true, + }, + }); + return data; + } + // 메세지 상태 업데이트 +} From cffd1e2709264f7541d42dbbbe672f10ebc9abb0 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Fri, 10 Jan 2025 22:30:42 +0900 Subject: [PATCH 10/14] =?UTF-8?q?[Feat]=20Redis=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/redis/redis.service.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/modules/redis/redis.service.ts b/src/modules/redis/redis.service.ts index 308a778..657049b 100644 --- a/src/modules/redis/redis.service.ts +++ b/src/modules/redis/redis.service.ts @@ -16,4 +16,9 @@ export class RedisService { async del(key: string): Promise { await this.redis.del(key); } + + async exists(key: string): Promise { + const result = await this.redis.exists(key); + return result === 1; + } } From 2d39ffdac9b86d98faf0a9f92c78b431aa6b8877 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Fri, 10 Jan 2025 22:31:06 +0900 Subject: [PATCH 11/14] =?UTF-8?q?[Add]=20Cookie=20Parser=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 34 ++++++++++++++++++++++++++++++++++ package.json | 2 ++ 2 files changed, 36 insertions(+) diff --git a/package-lock.json b/package-lock.json index 9315f0c..61d01b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "bcrypt": "^5.1.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", + "cookie-parser": "^1.4.7", "dotenv": "^16.4.7", "ioredis": "^5.4.2", "jsonwebtoken": "^9.0.2", @@ -40,6 +41,7 @@ "@nestjs/schematics": "^10.0.0", "@nestjs/testing": "^10.0.0", "@types/bcrypt": "^5.0.2", + "@types/cookie-parser": "^1.4.8", "@types/express": "^5.0.0", "@types/jest": "^29.5.2", "@types/node": "^20.3.1", @@ -3897,6 +3899,16 @@ "@types/node": "*" } }, + "node_modules/@types/cookie-parser": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.8.tgz", + "integrity": "sha512-l37JqFrOJ9yQfRQkljb41l0xVphc7kg5JTjjr+pLRZ0IyZ49V4BQ8vbF4Ut2C2e+WH4al3xD3ZwYwIUfnbT4NQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/express": "*" + } + }, "node_modules/@types/cookiejar": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", @@ -5826,6 +5838,28 @@ "node": ">= 0.6" } }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", diff --git a/package.json b/package.json index b75f88d..44bbb38 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "bcrypt": "^5.1.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", + "cookie-parser": "^1.4.7", "dotenv": "^16.4.7", "ioredis": "^5.4.2", "jsonwebtoken": "^9.0.2", @@ -53,6 +54,7 @@ "@nestjs/schematics": "^10.0.0", "@nestjs/testing": "^10.0.0", "@types/bcrypt": "^5.0.2", + "@types/cookie-parser": "^1.4.8", "@types/express": "^5.0.0", "@types/jest": "^29.5.2", "@types/node": "^20.3.1", From 60b606c63d433adaffd7591f95f6a39c479332f2 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Fri, 10 Jan 2025 22:31:24 +0900 Subject: [PATCH 12/14] =?UTF-8?q?[Feat]=20Cookie=20MiddleWare=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main.ts b/src/main.ts index c74f07b..0f27b22 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,12 +2,14 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { HttpExceptionFilter } from '@common/filters/http-exception.filter'; import { config } from 'dotenv'; +import * as cookieParser from 'cookie-parser'; config(); async function bootstrap() { const app = await NestFactory.create(AppModule); + app.use(cookieParser()); app.enableCors({ - origin: true, + origin: ['http://localhost:5173', 'http://localhost:8080'], credentials: true, exposedHeaders: ['Authorization'], }); From 325cc18f6fb5d2056ac678bf71fddbc3089f02d6 Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Fri, 10 Jan 2025 22:31:55 +0900 Subject: [PATCH 13/14] =?UTF-8?q?[Feat]=20auth=20Module=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/auth/auth.module.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts index 74b3159..9b6663a 100644 --- a/src/modules/auth/auth.module.ts +++ b/src/modules/auth/auth.module.ts @@ -1,5 +1,4 @@ import { Module } from '@nestjs/common'; -import { JwtModule } from '@nestjs/jwt'; import { PassportModule } from '@nestjs/passport'; import { AuthService } from './auth.service'; import { AuthController } from './auth.controller'; @@ -9,13 +8,14 @@ import { GoogleStrategy } from './strategies/google.strategy'; import { PrismaService } from '@src/prisma/prisma.service'; import { JwtAuthGuard } from '@modules/auth/guards/jwt-auth.guard'; import { RedisModule } from '../redis/redis.module'; +import { JwtModule } from '@nestjs/jwt'; @Module({ imports: [ PassportModule, RedisModule, JwtModule.register({ - secret: process.env.JWT_SECRET, // JWT 비밀키 설정 - signOptions: { expiresIn: '1h' }, // 기본 만료 시간 + secret: process.env.ACCESS_TOKEN_SECRET, // 비밀키 설정 + signOptions: { expiresIn: '1m' }, // 기본 만료 시간 }), ], controllers: [AuthController], From 937eb9f6e3390b5f73c11acdd7d44e94bc82d54b Mon Sep 17 00:00:00 2001 From: Ss0Mae Date: Fri, 10 Jan 2025 22:32:52 +0900 Subject: [PATCH 14/14] =?UTF-8?q?[Test]=20=EB=94=94=EB=B2=84=EA=B9=85?= =?UTF-8?q?=EC=9A=A9=20=EC=BD=98=EC=86=94=20=EB=A1=9C=EA=B7=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/auth/auth.controller.ts | 164 ++++++++++++++------ src/modules/auth/auth.service.ts | 22 ++- src/modules/auth/strategies/jwt.strategy.ts | 2 - 3 files changed, 131 insertions(+), 57 deletions(-) diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index db5a133..f16ac84 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -35,14 +35,15 @@ export class AuthController { // Authorization Code 교환 및 사용자 정보 가져오기 const { user, accessToken, refreshToken, isExistingUser } = await this.authService.handleGoogleCallback(code); - - console.log(refreshToken); + console.log('accessToken: ' + accessToken); + console.log('refreshToken: ' + refreshToken); // 리프레시 토큰을 HTTP-Only 쿠키로 설정 res.cookie('refreshToken', refreshToken, { - httpOnly: true, + httpOnly: false, secure: false, - sameSite: 'none', // CSRF 방지 - maxAge: 7 * 24 * 60 * 60 * 1000, // 7일 + sameSite: 'none', + path: '/', + maxAge: 7 * 24 * 60 * 60 * 1000, }); return res.status(HttpStatusCodes.OK).json( @@ -53,11 +54,6 @@ export class AuthController { isExistingUser, }) ); - // return new ApiResponse(HttpStatusCodes.OK, 'Google 로그인 성공', { - // user, - // accessToken, - // isExistingUser, - // }); } @Get('github') @@ -96,16 +92,7 @@ export class AuthController { @Res() res: Response ) { const userId = req.user?.user_id; - console.log(userId); - const { user, message } = await this.authService.updateUserRole( - userId, - roleId - ); - const serviceResult = await this.authService.updateUserRole(userId, roleId); - - console.log('Service Result in Controller:', serviceResult); - // 응답 객체 생성 const responseBody = { message: { @@ -115,50 +102,129 @@ export class AuthController { user: serviceResult.user, }; - console.log('Response Body:', responseBody); - // 응답 반환 return res.status(HttpStatusCodes.OK).json(responseBody); } + // @Post('refresh') + // async refreshAccessToken(@Req() req: any, @Res() res: Response) { + // console.log('Refresh Token API 호출'); + // const refreshToken = req.cookies['refreshToken']; + // + // if (!refreshToken) { + // throw new HttpException( + // ErrorMessages.AUTH.INVALID_REFRESH_TOKEN.text, + // ErrorMessages.AUTH.INVALID_REFRESH_TOKEN.code + // ); + // } + // //const userId = req.user?.user_id; + // const userId = this.authService.getUserIdFromRefreshToken(refreshToken); + // console.log('user_id: ' + userId); + // const isValid = await this.authService.validateRefreshToken( + // userId, + // refreshToken + // ); + // if (!isValid) { + // const error = ErrorMessages.AUTH.INVALID_REFRESH_TOKEN; + // throw new HttpException(error.text, error.code); + // } + // + // const newAccessToken = this.authService.generateAccessToken(userId); + // const newRefreshToken = this.authService.generateRefreshToken(userId); + // res.cookie('refreshToken', newRefreshToken, { + // httpOnly: true, + // secure: false, + // sameSite: 'none', + // maxAge: 7 * 24 * 60 * 60 * 1000, + // }); + // return res.status(HttpStatusCodes.OK).json( + // new ApiResponse( + // HttpStatusCodes.OK, + // '액세스 토큰이 성공적으로 갱신되었습니다.', + // { + // accessToken: newAccessToken, + // } + // ) + // ); + // } + @Post('refresh') async refreshAccessToken(@Req() req: any, @Res() res: Response) { + console.log('Refresh Token API 호출'); const refreshToken = req.cookies['refreshToken']; - + console.log('refreshToken from Cookie: ' + refreshToken); + // 1. Refresh Token이 없는 경우 예외 처리 if (!refreshToken) { + console.error('리프레쉬 토큰 없음'); throw new HttpException( ErrorMessages.AUTH.INVALID_REFRESH_TOKEN.text, ErrorMessages.AUTH.INVALID_REFRESH_TOKEN.code ); } - //const userId = req.user?.user_id; - const userId = this.authService.getUserIdFromRefreshToken(refreshToken); - const isValid = await this.authService.validateRefreshToken( - userId, - refreshToken - ); - if (!isValid) { - const error = ErrorMessages.AUTH.INVALID_REFRESH_TOKEN; - throw new HttpException(error.text, error.code); - } - const newAccessToken = await this.authService.generateAccessToken(userId); - const newRefreshToken = await this.authService.generateRefreshToken(userId); - res.cookie('refreshToken', newRefreshToken, { - httpOnly: true, - secure: true, - sameSite: 'strict', - maxAge: 7 * 24 * 60 * 60 * 1000, - }); - return res.status(HttpStatusCodes.OK).json( - new ApiResponse( - HttpStatusCodes.OK, - '액세스 토큰이 성공적으로 갱신되었습니다.', - { - accessToken: newAccessToken, - } - ) - ); + console.log('요청된 Refresh Token:', refreshToken); + + try { + // 2. Refresh Token에서 User ID 추출 + const userId = this.authService.getUserIdFromRefreshToken(refreshToken); + if (!userId) { + console.error('Refresh Token으로부터 User ID 추출 실패'); + throw new HttpException( + ErrorMessages.AUTH.INVALID_REFRESH_TOKEN.text, + ErrorMessages.AUTH.INVALID_REFRESH_TOKEN.code + ); + } + console.log('추출된 User ID:', userId); + + // 3. Refresh Token 유효성 검증 + const isValid = await this.authService.validateRefreshToken( + userId, + refreshToken + ); + console.log('Refresh Token 유효성 검증 결과:', isValid); + + if (!isValid) { + console.error('Refresh Token이 Redis와 일치하지 않음'); + throw new HttpException( + ErrorMessages.AUTH.INVALID_REFRESH_TOKEN.text, + ErrorMessages.AUTH.INVALID_REFRESH_TOKEN.code + ); + } + + // 4. 새로운 Access Token 및 Refresh Token 생성 + const newAccessToken = this.authService.generateAccessToken(userId); + const newRefreshToken = this.authService.generateRefreshToken(userId); + + console.log('새로운 Access Token:', newAccessToken); + console.log('새로운 Refresh Token:', newRefreshToken); + + // 5. Redis에 새로운 Refresh Token 저장 + await this.authService.storeRefreshToken(userId, newRefreshToken); + + // 6. Refresh Token을 쿠키에 저장 + res.cookie('refreshToken', refreshToken, { + httpOnly: false, + secure: false, + sameSite: 'none', + path: '/', + maxAge: 7 * 24 * 60 * 60 * 1000, + }); + + return res.status(HttpStatusCodes.OK).json( + new ApiResponse( + HttpStatusCodes.OK, + '액세스 토큰이 성공적으로 갱신되었습니다.', + { + accessToken: newAccessToken, + } + ) + ); + } catch (error) { + console.error('Refresh Token 갱신 중 오류 발생:', error.message); + return res.status(HttpStatusCodes.INTERNAL_SERVER_ERROR).json({ + message: 'Internal Server Error', + }); + } } @Post('logout') diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index 89a4e79..51f81cc 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -63,10 +63,11 @@ export class AuthService { isExistingUser, }; } catch (error) { - throw new HttpException( - ErrorMessages.SERVER.INTERNAL_ERROR.text, - ErrorMessages.SERVER.INTERNAL_ERROR.code - ); + console.error(error); + // throw new HttpException( + // ErrorMessages.SERVER.INTERNAL_ERROR.text, + // ErrorMessages.SERVER.INTERNAL_ERROR.code + // ); } } @@ -182,13 +183,15 @@ export class AuthService { } generateAccessToken(userId: number): string { + console.log(`Access Token 생성: userId=${userId}`); return this.jwtService.sign( { userId }, - { expiresIn: '15m', secret: process.env.ACCESS_TOKEN_SECRET } + { expiresIn: '1m', secret: process.env.ACCESS_TOKEN_SECRET } ); } generateRefreshToken(userId: number): string { + console.log(`Refresh Token 생성: userId=${userId}`); return this.jwtService.sign( { userId }, { expiresIn: '7d', secret: process.env.REFRESH_TOKEN_SECRET } @@ -197,11 +200,14 @@ export class AuthService { getUserIdFromRefreshToken(refreshToken: string): number | null { try { + console.log('Refresh Token 디코딩 중...'); const payload = this.jwtService.verify(refreshToken, { secret: process.env.REFRESH_TOKEN_SECRET, }); + console.log('Refresh Token 디코딩 성공:', payload); return payload.userId; } catch (error) { + console.error('Refresh Token 디코딩 실패:', error.message); return null; } } @@ -209,13 +215,18 @@ export class AuthService { async storeRefreshToken(userId: number, refreshToken: string): Promise { const key = `refresh_token:${userId}`; const ttl = 7 * 24 * 60 * 60; // 7일 + console.log( + `Redis에 Refresh Token 저장: key=${key}, token=${refreshToken}` + ); await this.redisService.set(key, refreshToken, ttl); } // 리프레시 토큰 검증 async validateRefreshToken(userId: number, token: string): Promise { const key = `refresh_token:${userId}`; + console.log(`Redis에서 Refresh Token 조회: key=${key}`); const storedToken = await this.redisService.get(key); + console.log('Redis에서 조회된 Refresh Token:', storedToken); return storedToken === token; } @@ -263,7 +274,6 @@ export class AuthService { message: roleMessages[roleId], }; - console.log('Service Result:', result); // 디버깅 return result; } } diff --git a/src/modules/auth/strategies/jwt.strategy.ts b/src/modules/auth/strategies/jwt.strategy.ts index ac9ef55..8ff45f6 100644 --- a/src/modules/auth/strategies/jwt.strategy.ts +++ b/src/modules/auth/strategies/jwt.strategy.ts @@ -12,8 +12,6 @@ export class JwtStrategy extends PassportStrategy(Strategy) { } async validate(payload: any) { - console.log('JWT_payload: ', payload); - // req.user에 설정될 사용자 정보 반환 return { user_id: payload.userId, email: payload.email }; } }