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..9315f0c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,18 +11,26 @@ "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", "@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", "dotenv": "^16.4.7", + "ioredis": "^5.4.2", + "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,11 +43,15 @@ "@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", "eslint-plugin-prettier": "^5.0.0", "husky": "^9.1.7", "jest": "^29.5.0", @@ -1845,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", @@ -2614,6 +2632,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", @@ -2839,6 +2884,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", @@ -2949,6 +3004,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", @@ -3933,6 +4053,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", @@ -4688,9 +4864,97 @@ "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": { + "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/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", @@ -4844,6 +5108,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", @@ -5380,6 +5653,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", @@ -5429,7 +5711,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" @@ -5732,7 +6013,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" @@ -5744,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", @@ -5838,6 +6127,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", @@ -6085,6 +6383,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", @@ -6683,6 +7017,36 @@ "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", + "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", @@ -6757,7 +7121,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", @@ -6910,6 +7273,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", @@ -6964,6 +7336,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", @@ -6977,6 +7363,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", @@ -7104,6 +7503,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", @@ -7328,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", @@ -7337,6 +7776,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", @@ -7357,6 +7813,29 @@ "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-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", @@ -7402,6 +7881,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", @@ -7445,6 +7943,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", @@ -7458,6 +7975,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", @@ -8342,6 +8875,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", @@ -8567,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", @@ -9072,6 +9627,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", @@ -9290,6 +9851,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", @@ -9300,6 +9896,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", @@ -9498,6 +10114,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", @@ -9618,6 +10244,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", @@ -9660,6 +10292,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", @@ -9769,6 +10411,44 @@ "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/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", @@ -9859,6 +10539,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", @@ -10023,12 +10713,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", @@ -10360,6 +11075,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", @@ -10383,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", @@ -11202,6 +11930,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", @@ -11268,6 +12002,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", @@ -11493,6 +12259,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", @@ -11573,6 +12360,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 e1b5a51..b75f88d 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,18 +24,26 @@ "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", "@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", "dotenv": "^16.4.7", + "ioredis": "^5.4.2", + "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,11 +56,15 @@ "@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", "eslint-plugin-prettier": "^5.0.0", "husky": "^9.1.7", "jest": "^29.5.0", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index ee282c7..cc2ebdf 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -1,14 +1,307 @@ -// 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" + 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.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/.gitkeep b/src/common/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/common/constants/error-messages.ts b/src/common/constants/error-messages.ts new file mode 100644 index 0000000..a834cfa --- /dev/null +++ b/src/common/constants/error-messages.ts @@ -0,0 +1,42 @@ +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/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/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/.gitkeep b/src/common/filters/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/common/filters/http-exception.filter.ts b/src/common/filters/http-exception.filter.ts new file mode 100644 index 0000000..2c26892 --- /dev/null +++ b/src/common/filters/http-exception.filter.ts @@ -0,0 +1,39 @@ +import { + ExceptionFilter, + Catch, + ArgumentsHost, + HttpException, + HttpStatus, +} from '@nestjs/common'; +import { Request, Response } from 'express'; +import { ErrorResponse } from '@common/dto/response.dto'; + +@Catch() +export class HttpExceptionFilter implements ExceptionFilter { + catch(exception: any, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + const request = ctx.getRequest(); + + 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({ + ...errorResponse, + timestamp: new Date().toISOString(), + path: request.url, + }); + } +} 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/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 fb51794..c74f07b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,12 +1,17 @@ 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); - const port = process.env.PORT; + app.enableCors({ + origin: true, + credentials: true, + exposedHeaders: ['Authorization'], + }); + app.useGlobalFilters(new HttpExceptionFilter()); await app.listen(process.env.PORT); - console.log(`Application is running on: http://localhost:${port}`); } bootstrap(); 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..db5a133 --- /dev/null +++ b/src/modules/auth/auth.controller.ts @@ -0,0 +1,171 @@ +import { + Controller, + Get, + UseGuards, + Req, + Body, + Post, + Put, + HttpException, + Res, +} from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; +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, + private readonly jwtService: JwtService + ) {} + + @Get('google') + @UseGuards(AuthGuard('google')) + async googleLogin() { + // Google 로그인 요청 + } + + @Post('google/callback') + async googleCallback(@Body('code') code: string, @Res() res: Response) { + // Authorization Code 교환 및 사용자 정보 가져오기 + 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') + @UseGuards(AuthGuard('github')) + async githubLogin() { + // GitHub 로그인 요청 + } + + @Post('github/callback') + 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 + @Put('roleselect') + @UseGuards(JwtAuthGuard) + async selectRole( + @Body('role_id') roleId: number, + @Req() req: any, + @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: { + code: HttpStatusCodes.OK, + text: serviceResult.message, + }, + 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) { + 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); + } + + 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 new file mode 100644 index 0000000..74b3159 --- /dev/null +++ b/src/modules/auth/auth.module.ts @@ -0,0 +1,32 @@ +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'; +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'; +import { RedisModule } from '../redis/redis.module'; +@Module({ + imports: [ + PassportModule, + RedisModule, + JwtModule.register({ + secret: process.env.JWT_SECRET, // JWT 비밀키 설정 + signOptions: { expiresIn: '1h' }, // 기본 만료 시간 + }), + ], + controllers: [AuthController], + 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 new file mode 100644 index 0000000..89a4e79 --- /dev/null +++ b/src/modules/auth/auth.service.ts @@ -0,0 +1,269 @@ +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 redisService: RedisService + ) {} + + async handleGoogleCallback(code: string) { + try { + // 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 accessToken = this.generateAccessToken(user.id); + const refreshToken = this.generateRefreshToken(user.id); // 리프레시 토큰 생성 + + // Redis에 리프레시 토큰 저장 + await this.storeRefreshToken(user.id, refreshToken); + + const responseUser = this.filterUserFields(user); + + 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 { + // 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; + // 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 primaryEmail = emailResponse.data.find( + (e: any) => e.primary && e.verified + ); + email = primaryEmail?.email; + } + + 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.generateAccessToken(user.id); + const refreshToken = await this.generateRefreshToken(user.id); // 리프레시 토큰 생성 + + // Redis에 리프레시 토큰 저장 + await this.storeRefreshToken(user.id, refreshToken); + const responseUser = this.filterUserFields(user); + return { + user: responseUser, + accessToken: jwt, + refreshToken: refreshToken, // 리프레시 토큰 반환 + isExistingUser, + }; + } catch (error) { + throw new HttpException( + ErrorMessages.SERVER.INTERNAL_ERROR.text, + ErrorMessages.SERVER.INTERNAL_ERROR.code + ); + } + } + + private async checkUserExist(email: string): Promise { + const user = await this.prisma.user.findUnique({ + where: { email }, + }); + return !!user; + } + // 사용자 찾기 또는 생성 + async findOrCreateUser(profile: AuthUserDto) { + 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 } }, + }, + }); + } + + private filterUserFields(user: any) { + return { + user_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, + }; + } + + generateAccessToken(userId: number): string { + return this.jwtService.sign( + { userId }, + { expiresIn: '15m', secret: process.env.ACCESS_TOKEN_SECRET } + ); + } + + 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); + } + + // 리프레시 토큰 검증 + 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) { + const error = ErrorMessages.AUTH.USER_NOT_FOUND; + throw new HttpException(error.text, error.code); + } + + // 역할에 따른 메시지 생성 + const roleMessages = { + 1: '프로그래머로 변경되었습니다.', + 2: '아티스트로 변경되었습니다.', + 3: '디자이너로 변경되었습니다.', + }; + + if (!roleMessages[roleId]) { + throw new HttpException( + '유효하지 않은 역할 ID입니다.', + HttpStatusCodes.BAD_REQUEST + ); + } + + // 사용자 역할 업데이트 + const updatedUser = await this.prisma.user.update({ + where: { id: userId }, + data: { role_id: roleId }, + }); + + const result = { + user: { + 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/dto/.gitkeep b/src/modules/auth/dto/.gitkeep new file mode 100644 index 0000000..e69de29 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/entities/.gitkeep b/src/modules/auth/entities/.gitkeep new file mode 100644 index 0000000..e69de29 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..31cadc3 --- /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:5173/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..2b7a85b --- /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:5173/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..ac9ef55 --- /dev/null +++ b/src/modules/auth/strategies/jwt.strategy.ts @@ -0,0 +1,19 @@ +import { Injectable } from '@nestjs/common'; +import { PassportStrategy } from '@nestjs/passport'; +import { ExtractJwt, Strategy } from 'passport-jwt'; + +@Injectable() +export class JwtStrategy extends PassportStrategy(Strategy) { + constructor() { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + secretOrKey: process.env.ACCESS_TOKEN_SECRET, + }); + } + + async validate(payload: any) { + console.log('JWT_payload: ', payload); + // req.user에 설정될 사용자 정보 반환 + return { user_id: payload.userId, email: payload.email }; + } +} 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/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/dto/.gitkeep b/src/modules/user/dto/.gitkeep new file mode 100644 index 0000000..e69de29 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/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..4bbfcb3 --- /dev/null +++ b/src/modules/user/user.controller.ts @@ -0,0 +1,7 @@ +import { Controller } from '@nestjs/common'; +import { UserService } from './user.service'; + +@Controller('users') +export class UserController { + constructor(private readonly userService: UserService) {} +} diff --git a/src/modules/user/user.module.ts b/src/modules/user/user.module.ts new file mode 100644 index 0000000..aedd372 --- /dev/null +++ b/src/modules/user/user.module.ts @@ -0,0 +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({ + 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 new file mode 100644 index 0000000..757992d --- /dev/null +++ b/src/modules/user/user.service.ts @@ -0,0 +1,8 @@ +import { Injectable } from '@nestjs/common'; + +import { PrismaService } from '@prisma/prisma.service'; + +@Injectable() +export class UserService { + constructor(private readonly prisma: PrismaService) {} +} 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/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 업로드 중 오류 발생'); + } + } +} 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 } } }