diff --git a/backend/package-lock.json b/backend/package-lock.json index 42fbf276..0f2da728 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -10,10 +10,15 @@ "license": "UNLICENSED", "dependencies": { "@nestjs/common": "^10.0.0", + "@nestjs/config": "^3.1.1", "@nestjs/core": "^10.0.0", + "@nestjs/jwt": "^10.2.0", + "@nestjs/passport": "^10.0.3", "@nestjs/platform-express": "^10.0.0", "@nestjs/swagger": "^7.1.17", "@prisma/client": "^5.8.1", + "passport-github": "^1.1.0", + "passport-jwt": "^4.0.1", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1" }, @@ -1750,6 +1755,21 @@ } } }, + "node_modules/@nestjs/config": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-3.1.1.tgz", + "integrity": "sha512-qu5QlNiJdqQtOsnB6lx4JCXPQ96jkKUsOGd+JXfXwqJqZcOSAq6heNFg0opW4pq4J/VZoNwoo87TNnx9wthnqQ==", + "dependencies": { + "dotenv": "16.3.1", + "dotenv-expand": "10.0.0", + "lodash": "4.17.21", + "uuid": "9.0.0" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "reflect-metadata": "^0.1.13" + } + }, "node_modules/@nestjs/core": { "version": "10.3.0", "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.3.0.tgz", @@ -1787,6 +1807,18 @@ } } }, + "node_modules/@nestjs/jwt": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-10.2.0.tgz", + "integrity": "sha512-x8cG90SURkEiLOehNaN2aRlotxT0KZESUliOPKKnjWiyJOcWurkF3w345WOX0P4MgFzUjGoZ1Sy0aZnxeihT0g==", + "dependencies": { + "@types/jsonwebtoken": "9.0.5", + "jsonwebtoken": "9.0.2" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0" + } + }, "node_modules/@nestjs/mapped-types": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.4.tgz", @@ -1806,6 +1838,15 @@ } } }, + "node_modules/@nestjs/passport": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-10.0.3.tgz", + "integrity": "sha512-znJ9Y4S8ZDVY+j4doWAJ8EuuVO7SkQN3yOBmzxbGaXbvcSwFDAdGJ+OMCg52NdzIO4tQoN4pYKx8W6M0ArfFRQ==", + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "passport": "^0.4.0 || ^0.5.0 || ^0.6.0 || ^0.7.0" + } + }, "node_modules/@nestjs/platform-express": { "version": "10.3.0", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.3.0.tgz", @@ -2255,6 +2296,14 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.5.tgz", + "integrity": "sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/methods": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", @@ -2271,7 +2320,6 @@ "version": "20.11.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.1.tgz", "integrity": "sha512-DsXojJUES2M+FE8CpptJTKpg+r54moV9ZEncPstni1WHFmTcCzeFLnMFfyhCVS8XNOy/OQG+8lVxRLRrVHmV5A==", - "dev": true, "dependencies": { "undici-types": "~5.26.4" } @@ -3069,6 +3117,14 @@ } ] }, + "node_modules/base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -3237,6 +3293,11 @@ "ieee754": "^1.1.13" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -3876,12 +3937,39 @@ "node": ">=6.0.0" } }, + "node_modules/dotenv": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", + "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/motdotla/dotenv?sponsor=1" + } + }, + "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==", + "engines": { + "node": ">=12" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -6178,6 +6266,46 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -6253,6 +6381,36 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "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==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -6265,6 +6423,11 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -6490,8 +6653,7 @@ "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/multer": { "version": "1.4.4-lts.1", @@ -6603,6 +6765,11 @@ "node": ">=8" } }, + "node_modules/oauth": { + "version": "0.9.15", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", + "integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -6780,6 +6947,71 @@ "node": ">= 0.8" } }, + "node_modules/passport": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", + "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "peer": true, + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "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==", + "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", + "integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==", + "dependencies": { + "jsonwebtoken": "^9.0.0", + "passport-strategy": "^1.0.0" + } + }, + "node_modules/passport-oauth2": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.7.0.tgz", + "integrity": "sha512-j2gf34szdTF2Onw3+76alNnaAExlUmHvkc7cL+cmaS5NzHzDP/BvFHJruueQ9XAeNOdpI+CH+PWid8RA7KCwAQ==", + "dependencies": { + "base64url": "3.x.x", + "oauth": "0.9.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", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -6852,6 +7084,12 @@ "node": ">=8" } }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==", + "peer": true + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -7505,7 +7743,6 @@ "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, "dependencies": { "lru-cache": "^6.0.0" }, @@ -7520,7 +7757,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -7531,8 +7767,7 @@ "node_modules/semver/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/send": { "version": "0.18.0", @@ -8477,11 +8712,15 @@ "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==" + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, "node_modules/universalify": { "version": "2.0.1", @@ -8552,6 +8791,14 @@ "node": ">= 0.4.0" } }, + "node_modules/uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", diff --git a/backend/package.json b/backend/package.json index 50bbdfd5..8a7bf36e 100644 --- a/backend/package.json +++ b/backend/package.json @@ -22,10 +22,15 @@ }, "dependencies": { "@nestjs/common": "^10.0.0", + "@nestjs/config": "^3.1.1", "@nestjs/core": "^10.0.0", + "@nestjs/jwt": "^10.2.0", + "@nestjs/passport": "^10.0.3", "@nestjs/platform-express": "^10.0.0", "@nestjs/swagger": "^7.1.17", "@prisma/client": "^5.8.1", + "passport-github": "^1.1.0", + "passport-jwt": "^4.0.1", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1" }, diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index da763915..9233e913 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -1,13 +1,23 @@ import { Module } from "@nestjs/common"; import { AppController } from "./app.controller"; import { AppService } from "./app.service"; -import { AuthController } from "./user/auth.controller"; -import { AuthService } from "./user/auth.service"; import { PrismaService } from "./db/prisma.service"; +import { UsersModule } from "./users/users.module"; +import { AuthModule } from "./auth/auth.module"; +import { ConfigModule } from "@nestjs/config"; +import { APP_GUARD } from "@nestjs/core/constants"; +import { JwtAuthGuard } from "./auth/jwt.guard"; @Module({ - imports: [], - controllers: [AppController, AuthController], - providers: [AppService, PrismaService, AuthService], + imports: [ConfigModule.forRoot({ isGlobal: true }), UsersModule, AuthModule], + controllers: [AppController], + providers: [ + AppService, + PrismaService, + { + provide: APP_GUARD, + useClass: JwtAuthGuard, + }, + ], }) export class AppModule {} diff --git a/backend/src/auth/auth.controller.spec.ts b/backend/src/auth/auth.controller.spec.ts new file mode 100644 index 00000000..8de821fb --- /dev/null +++ b/backend/src/auth/auth.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from "@nestjs/testing"; +import { AuthController } from "./auth.controller"; + +describe("AuthController", () => { + let controller: AuthController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [AuthController], + }).compile(); + + controller = module.get(AuthController); + }); + + it("should be defined", () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/backend/src/auth/auth.controller.ts b/backend/src/auth/auth.controller.ts new file mode 100644 index 00000000..b869de77 --- /dev/null +++ b/backend/src/auth/auth.controller.ts @@ -0,0 +1,31 @@ +import { Controller, Get, Req, UseGuards } from "@nestjs/common"; +import { AuthGuard } from "@nestjs/passport"; +import { LoginRequest } from "./interfaces/LoginRequest"; +import { JwtService } from "@nestjs/jwt"; +import { LoginResponse } from "./interfaces/LoginResponse"; +import { UsersService } from "src/users/users.service"; +import { Public } from "src/utils/decorators/auth.decorator"; + +@Controller("auth") +export class AuthController { + constructor( + private jwtService: JwtService, + private usersService: UsersService + ) {} + + @Public() + @Get("login/github") + @Get("callback/github") + @UseGuards(AuthGuard("github")) + async login(@Req() req: LoginRequest): Promise { + const user = await this.usersService.findOrCreate( + req.user.socialProvider, + req.user.socialUid, + req.user.nickname + ); + + const accessToken = this.jwtService.sign({ sub: user.id, nickname: user.nickname }); + + return { accessToken }; + } +} diff --git a/backend/src/auth/auth.module.ts b/backend/src/auth/auth.module.ts new file mode 100644 index 00000000..3c485ca8 --- /dev/null +++ b/backend/src/auth/auth.module.ts @@ -0,0 +1,26 @@ +import { Module } from "@nestjs/common"; +import { AuthService } from "./auth.service"; +import { UsersModule } from "src/users/users.module"; +import { AuthController } from "./auth.controller"; +import { GithubStrategy } from "./github.strategy"; +import { ConfigService } from "@nestjs/config"; +import { JwtModule } from "@nestjs/jwt"; +import { JwtStrategy } from "./jwt.strategy"; + +@Module({ + imports: [ + UsersModule, + JwtModule.registerAsync({ + useFactory: async (configService: ConfigService) => { + return { + signOptions: { expiresIn: "24h" }, + secret: configService.get("JWT_SECRET"), + }; + }, + inject: [ConfigService], + }), + ], + providers: [AuthService, GithubStrategy, JwtStrategy], + controllers: [AuthController], +}) +export class AuthModule {} diff --git a/backend/src/auth/auth.service.spec.ts b/backend/src/auth/auth.service.spec.ts new file mode 100644 index 00000000..5430748f --- /dev/null +++ b/backend/src/auth/auth.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from "@nestjs/testing"; +import { AuthService } from "./auth.service"; + +describe("AuthService", () => { + let service: AuthService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [AuthService], + }).compile(); + + service = module.get(AuthService); + }); + + it("should be defined", () => { + expect(service).toBeDefined(); + }); +}); diff --git a/backend/src/auth/auth.service.ts b/backend/src/auth/auth.service.ts new file mode 100644 index 00000000..3bff1d09 --- /dev/null +++ b/backend/src/auth/auth.service.ts @@ -0,0 +1,20 @@ +import { Injectable } from "@nestjs/common"; +import { UsersService } from "src/users/users.service"; + +@Injectable() +export class AuthService { + constructor(private usersService: UsersService) {} + + async issueJwtToken(socialProvider: string, socialUid: string, nickname: string) { + const user = await this.usersService.findOrCreate(socialProvider, socialUid, nickname); + + if (user) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { socialProvider: _socaialProvider, socialUid: _social, ...result } = user; + + return result; + } + + return null; + } +} diff --git a/backend/src/auth/github.strategy.ts b/backend/src/auth/github.strategy.ts new file mode 100644 index 00000000..32102f96 --- /dev/null +++ b/backend/src/auth/github.strategy.ts @@ -0,0 +1,29 @@ +import { Injectable } from "@nestjs/common"; +import { ConfigService } from "@nestjs/config"; +import { PassportStrategy } from "@nestjs/passport"; +import { Profile, Strategy } from "passport-github"; +import { LoginUserInfo } from "./interfaces/LoginRequest"; + +@Injectable() +export class GithubStrategy extends PassportStrategy(Strategy, "github") { + constructor(configService: ConfigService) { + super({ + clientID: configService.get("GITHUB_CLIENT_ID"), + clientSecret: configService.get("GITHUB_CLIENT_SECRET"), + callbackURL: configService.get("GITHUB_CALLBACK_URL"), + scope: ["public_profile"], + }); + } + + async validate( + _accessToken: string, + _refreshToken: string, + profile: Profile + ): Promise { + return { + socialProvider: "github", + socialUid: profile.id, + nickname: profile.username, + }; + } +} diff --git a/backend/src/auth/interfaces/LoginRequest.ts b/backend/src/auth/interfaces/LoginRequest.ts new file mode 100644 index 00000000..1a6dd07c --- /dev/null +++ b/backend/src/auth/interfaces/LoginRequest.ts @@ -0,0 +1,9 @@ +import { SocialProvider } from "src/utils/types/auth.type"; + +export interface LoginUserInfo { + socialProvider: SocialProvider; + socialUid: string; + nickname: string; +} + +export type LoginRequest = Request & { user: LoginUserInfo }; diff --git a/backend/src/auth/interfaces/LoginResponse.ts b/backend/src/auth/interfaces/LoginResponse.ts new file mode 100644 index 00000000..80a52ff4 --- /dev/null +++ b/backend/src/auth/interfaces/LoginResponse.ts @@ -0,0 +1,3 @@ +export interface LoginResponse { + accessToken: string; +} diff --git a/backend/src/auth/jwt.guard.ts b/backend/src/auth/jwt.guard.ts new file mode 100644 index 00000000..c33f292d --- /dev/null +++ b/backend/src/auth/jwt.guard.ts @@ -0,0 +1,22 @@ +import { ExecutionContext, Injectable } from "@nestjs/common"; +import { Reflector } from "@nestjs/core"; +import { AuthGuard } from "@nestjs/passport"; +import { IS_PUBLIC_PATH } from "src/utils/decorators/auth.decorator"; + +@Injectable() +export class JwtAuthGuard extends AuthGuard("jwt") { + constructor(private reflector: Reflector) { + super(); + } + + canActivate(context: ExecutionContext) { + const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_PATH, [ + context.getHandler(), + context.getClass(), + ]); + if (isPublic) { + return true; + } + return super.canActivate(context); + } +} diff --git a/backend/src/auth/jwt.strategy.ts b/backend/src/auth/jwt.strategy.ts new file mode 100644 index 00000000..af88cda8 --- /dev/null +++ b/backend/src/auth/jwt.strategy.ts @@ -0,0 +1,20 @@ +import { ExtractJwt, Strategy as PassportJwtStrategy } from "passport-jwt"; +import { ConfigService } from "@nestjs/config"; +import { Injectable } from "@nestjs/common"; +import { PassportStrategy } from "@nestjs/passport"; +import { JwtPayload } from "src/utils/types/jwt.type"; + +@Injectable() +export class JwtStrategy extends PassportStrategy(PassportJwtStrategy) { + constructor(configService: ConfigService) { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + ignoreExpiration: false, + secretOrKey: configService.get("JWT_SECRET"), + }); + } + + async validate(payload: JwtPayload) { + return { id: payload.sub, nickname: payload.nickname }; + } +} diff --git a/backend/src/user/auth.controller.ts b/backend/src/user/auth.controller.ts deleted file mode 100644 index 9bad0170..00000000 --- a/backend/src/user/auth.controller.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Controller, Post, Body } from "@nestjs/common"; -import { AuthService } from "./auth.service"; -import { LoginDto } from "./dto/login.dto"; - -@Controller("") -export class AuthController { - constructor(private authService: AuthService) {} - - @Post("/login") - async login(@Body() loginDto: LoginDto) { - return this.authService.findOrCreateUser(loginDto.idToken, loginDto.nickname); - } -} diff --git a/backend/src/user/dto/login.dto.ts b/backend/src/user/dto/login.dto.ts deleted file mode 100644 index fb7d1387..00000000 --- a/backend/src/user/dto/login.dto.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { ApiProperty } from "@nestjs/swagger"; - -export class LoginDto { - @ApiProperty({ description: "idToken from Firebase" }) - idToken: string; - @ApiProperty({ description: "nickname of user to create" }) - nickname: string; -} diff --git a/backend/src/users/users.module.ts b/backend/src/users/users.module.ts new file mode 100644 index 00000000..af6aa1bf --- /dev/null +++ b/backend/src/users/users.module.ts @@ -0,0 +1,9 @@ +import { Module } from "@nestjs/common"; +import { UsersService } from "./users.service"; +import { PrismaService } from "src/db/prisma.service"; + +@Module({ + providers: [UsersService, PrismaService], + exports: [UsersService], +}) +export class UsersModule {} diff --git a/backend/src/users/users.service.spec.ts b/backend/src/users/users.service.spec.ts new file mode 100644 index 00000000..f6c683ac --- /dev/null +++ b/backend/src/users/users.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from "@nestjs/testing"; +import { UsersService } from "./users.service"; + +describe("UsersService", () => { + let service: UsersService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [UsersService], + }).compile(); + + service = module.get(UsersService); + }); + + it("should be defined", () => { + expect(service).toBeDefined(); + }); +}); diff --git a/backend/src/user/auth.service.ts b/backend/src/users/users.service.ts similarity index 56% rename from backend/src/user/auth.service.ts rename to backend/src/users/users.service.ts index a9b95c92..8d645fd9 100644 --- a/backend/src/user/auth.service.ts +++ b/backend/src/users/users.service.ts @@ -1,19 +1,24 @@ import { Injectable } from "@nestjs/common"; -import { PrismaService } from "../db/prisma.service"; import { User } from "@prisma/client"; +import { PrismaService } from "src/db/prisma.service"; @Injectable() -export class AuthService { +export class UsersService { constructor(private prismaService: PrismaService) {} - async findOrCreateUser(idToken: string, nickname: string): Promise { - const socialUid = idToken; + async findOrCreate( + socialProvider: string, + socialUid: string, + nickname: string + ): Promise { return this.prismaService.user.upsert({ where: { + socialProvider, socialUid, }, update: {}, create: { + socialProvider, socialUid, nickname, }, diff --git a/backend/src/utils/decorators/auth.decorator.ts b/backend/src/utils/decorators/auth.decorator.ts new file mode 100644 index 00000000..4af64666 --- /dev/null +++ b/backend/src/utils/decorators/auth.decorator.ts @@ -0,0 +1,4 @@ +import { SetMetadata } from "@nestjs/common"; + +export const IS_PUBLIC_PATH = "isPublicPath"; +export const Public = () => SetMetadata(IS_PUBLIC_PATH, true); diff --git a/backend/src/utils/types/auth.type.ts b/backend/src/utils/types/auth.type.ts new file mode 100644 index 00000000..ec2dd34e --- /dev/null +++ b/backend/src/utils/types/auth.type.ts @@ -0,0 +1 @@ +export type SocialProvider = "github"; diff --git a/backend/src/utils/types/jwt.type.ts b/backend/src/utils/types/jwt.type.ts new file mode 100644 index 00000000..96f636ba --- /dev/null +++ b/backend/src/utils/types/jwt.type.ts @@ -0,0 +1,4 @@ +export interface JwtPayload { + sub: string; + nickname: string; +}