diff --git a/backend/package.json b/backend/package.json index ddde228..e5bd261 100644 --- a/backend/package.json +++ b/backend/package.json @@ -23,10 +23,17 @@ "@nestjs/common": "^10.0.0", "@nestjs/config": "^3.2.2", "@nestjs/core": "^10.0.0", + "@nestjs/jwt": "^10.2.0", + "@nestjs/passport": "^10.0.3", "@nestjs/platform-express": "^10.0.0", "@nestjs/swagger": "^7.3.1", "@nestjs/typeorm": "^10.0.2", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", "install": "^0.13.0", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", + "passport-local": "^1.0.0", "pg": "^8.11.5", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", @@ -37,6 +44,7 @@ "@nestjs/cli": "^10.0.0", "@nestjs/schematics": "^10.0.0", "@nestjs/testing": "^10.0.0", + "@types/argon2": "^0.15.0", "@types/express": "^4.17.17", "@types/jest": "^29.5.2", "@types/node": "^20.3.1", diff --git a/backend/pnpm-lock.yaml b/backend/pnpm-lock.yaml index 7d8382a..1fdc979 100644 --- a/backend/pnpm-lock.yaml +++ b/backend/pnpm-lock.yaml @@ -7,25 +7,46 @@ settings: dependencies: '@nestjs/common': specifier: ^10.0.0 - version: 10.3.7(reflect-metadata@0.2.2)(rxjs@7.8.1) + version: 10.3.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/config': specifier: ^3.2.2 version: 3.2.2(@nestjs/common@10.3.7)(rxjs@7.8.1) '@nestjs/core': specifier: ^10.0.0 version: 10.3.7(@nestjs/common@10.3.7)(@nestjs/platform-express@10.3.7)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/jwt': + specifier: ^10.2.0 + version: 10.2.0(@nestjs/common@10.3.7) + '@nestjs/passport': + specifier: ^10.0.3 + version: 10.0.3(@nestjs/common@10.3.7)(passport@0.7.0) '@nestjs/platform-express': specifier: ^10.0.0 version: 10.3.7(@nestjs/common@10.3.7)(@nestjs/core@10.3.7) '@nestjs/swagger': specifier: ^7.3.1 - version: 7.3.1(@nestjs/common@10.3.7)(@nestjs/core@10.3.7)(reflect-metadata@0.2.2) + version: 7.3.1(@nestjs/common@10.3.7)(@nestjs/core@10.3.7)(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2) '@nestjs/typeorm': specifier: ^10.0.2 version: 10.0.2(@nestjs/common@10.3.7)(@nestjs/core@10.3.7)(reflect-metadata@0.2.2)(rxjs@7.8.1)(typeorm@0.3.20) + class-transformer: + specifier: ^0.5.1 + version: 0.5.1 + class-validator: + specifier: ^0.14.1 + version: 0.14.1 install: specifier: ^0.13.0 version: 0.13.0 + passport: + specifier: ^0.7.0 + version: 0.7.0 + passport-jwt: + specifier: ^4.0.1 + version: 4.0.1 + passport-local: + specifier: ^1.0.0 + version: 1.0.0 pg: specifier: ^8.11.5 version: 8.11.5 @@ -52,6 +73,9 @@ devDependencies: '@nestjs/testing': specifier: ^10.0.0 version: 10.3.7(@nestjs/common@10.3.7)(@nestjs/core@10.3.7)(@nestjs/platform-express@10.3.7) + '@types/argon2': + specifier: ^0.15.0 + version: 0.15.0 '@types/express': specifier: ^4.17.17 version: 4.17.21 @@ -906,7 +930,7 @@ packages: - webpack-cli dev: true - /@nestjs/common@10.3.7(reflect-metadata@0.2.2)(rxjs@7.8.1): + /@nestjs/common@10.3.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1): resolution: {integrity: sha512-gKFtFzcJznrwsRYjtNZoPAvSOPYdNgxbTYoAyLTpoy393cIKgLmJTHu6ReH8/qIB9AaZLdGaFLkx98W/tFWFUw==} peerDependencies: class-transformer: '*' @@ -919,6 +943,8 @@ packages: class-validator: optional: true dependencies: + class-transformer: 0.5.1 + class-validator: 0.14.1 iterare: 1.2.1 reflect-metadata: 0.2.2 rxjs: 7.8.1 @@ -931,7 +957,7 @@ packages: '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 rxjs: ^7.1.0 dependencies: - '@nestjs/common': 10.3.7(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/common': 10.3.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) dotenv: 16.4.5 dotenv-expand: 10.0.0 lodash: 4.17.21 @@ -957,7 +983,7 @@ packages: '@nestjs/websockets': optional: true dependencies: - '@nestjs/common': 10.3.7(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/common': 10.3.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/platform-express': 10.3.7(@nestjs/common@10.3.7)(@nestjs/core@10.3.7) '@nuxtjs/opencollective': 0.3.2 fast-safe-stringify: 2.1.1 @@ -970,7 +996,17 @@ packages: transitivePeerDependencies: - encoding - /@nestjs/mapped-types@2.0.5(@nestjs/common@10.3.7)(reflect-metadata@0.2.2): + /@nestjs/jwt@10.2.0(@nestjs/common@10.3.7): + resolution: {integrity: sha512-x8cG90SURkEiLOehNaN2aRlotxT0KZESUliOPKKnjWiyJOcWurkF3w345WOX0P4MgFzUjGoZ1Sy0aZnxeihT0g==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 + dependencies: + '@nestjs/common': 10.3.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@types/jsonwebtoken': 9.0.5 + jsonwebtoken: 9.0.2 + dev: false + + /@nestjs/mapped-types@2.0.5(@nestjs/common@10.3.7)(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2): resolution: {integrity: sha512-bSJv4pd6EY99NX9CjBIyn4TVDoSit82DUZlL4I3bqNfy5Gt+gXTa86i3I/i0iIV9P4hntcGM5GyO+FhZAhxtyg==} peerDependencies: '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 @@ -983,17 +1019,29 @@ packages: class-validator: optional: true dependencies: - '@nestjs/common': 10.3.7(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/common': 10.3.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) + class-transformer: 0.5.1 + class-validator: 0.14.1 reflect-metadata: 0.2.2 dev: false + /@nestjs/passport@10.0.3(@nestjs/common@10.3.7)(passport@0.7.0): + resolution: {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 + dependencies: + '@nestjs/common': 10.3.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) + passport: 0.7.0 + dev: false + /@nestjs/platform-express@10.3.7(@nestjs/common@10.3.7)(@nestjs/core@10.3.7): resolution: {integrity: sha512-noNJ+PyIxQJLCKfuXz0tcQtlVAynfLIuKy62g70lEZ86UrIqSrZFqvWs/rFUgkbT6J8H7Rmv11hASOnX+7M2rA==} peerDependencies: '@nestjs/common': ^10.0.0 '@nestjs/core': ^10.0.0 dependencies: - '@nestjs/common': 10.3.7(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/common': 10.3.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/core': 10.3.7(@nestjs/common@10.3.7)(@nestjs/platform-express@10.3.7)(reflect-metadata@0.2.2)(rxjs@7.8.1) body-parser: 1.20.2 cors: 2.8.5 @@ -1033,7 +1081,7 @@ packages: - chokidar dev: true - /@nestjs/swagger@7.3.1(@nestjs/common@10.3.7)(@nestjs/core@10.3.7)(reflect-metadata@0.2.2): + /@nestjs/swagger@7.3.1(@nestjs/common@10.3.7)(@nestjs/core@10.3.7)(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2): resolution: {integrity: sha512-LUC4mr+5oAleEC/a2j8pNRh1S5xhKXJ1Gal5ZdRjt9XebQgbngXCdW7JTA9WOEcwGtFZN9EnKYdquzH971LZfw==} peerDependencies: '@fastify/static': ^6.0.0 || ^7.0.0 @@ -1051,9 +1099,11 @@ packages: optional: true dependencies: '@microsoft/tsdoc': 0.14.2 - '@nestjs/common': 10.3.7(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/common': 10.3.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/core': 10.3.7(@nestjs/common@10.3.7)(@nestjs/platform-express@10.3.7)(reflect-metadata@0.2.2)(rxjs@7.8.1) - '@nestjs/mapped-types': 2.0.5(@nestjs/common@10.3.7)(reflect-metadata@0.2.2) + '@nestjs/mapped-types': 2.0.5(@nestjs/common@10.3.7)(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2) + class-transformer: 0.5.1 + class-validator: 0.14.1 js-yaml: 4.1.0 lodash: 4.17.21 path-to-regexp: 3.2.0 @@ -1074,7 +1124,7 @@ packages: '@nestjs/platform-express': optional: true dependencies: - '@nestjs/common': 10.3.7(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/common': 10.3.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/core': 10.3.7(@nestjs/common@10.3.7)(@nestjs/platform-express@10.3.7)(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/platform-express': 10.3.7(@nestjs/common@10.3.7)(@nestjs/core@10.3.7) tslib: 2.6.2 @@ -1089,7 +1139,7 @@ packages: rxjs: ^7.2.0 typeorm: ^0.3.0 dependencies: - '@nestjs/common': 10.3.7(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/common': 10.3.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/core': 10.3.7(@nestjs/common@10.3.7)(@nestjs/platform-express@10.3.7)(reflect-metadata@0.2.2)(rxjs@7.8.1) reflect-metadata: 0.2.2 rxjs: 7.8.1 @@ -1129,6 +1179,11 @@ packages: transitivePeerDependencies: - encoding + /@phc/format@1.0.0: + resolution: {integrity: sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==} + engines: {node: '>=10'} + dev: true + /@pkgjs/parseargs@0.11.0: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -1172,6 +1227,13 @@ packages: /@tsconfig/node16@1.0.4: resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + /@types/argon2@0.15.0: + resolution: {integrity: sha512-AKQ8LR6bgmNHF7vhIQjD4EEbxITc1+1sTS9OKvkT5SaTfKw9OhFFExriod+H92biWIm23k7UT5VcF5ja9D+FIg==} + deprecated: This is a stub types definition for Argon2 (https://github.com/ranisalt/node-argon2). Argon2 provides its own type definitions, so you don't need @types/argon2 installed! + dependencies: + argon2: 0.40.1 + dev: true + /@types/babel__core@7.20.5: resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} dependencies: @@ -1291,6 +1353,12 @@ packages: resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} dev: true + /@types/jsonwebtoken@9.0.5: + resolution: {integrity: sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==} + dependencies: + '@types/node': 20.12.4 + dev: false + /@types/methods@1.1.4: resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} dev: true @@ -1350,6 +1418,9 @@ packages: '@types/superagent': 8.1.6 dev: true + /@types/validator@13.11.9: + resolution: {integrity: sha512-FCTsikRozryfayPuiI46QzH3fnrOoctTjvOYZkho9BTFLCOZ2rgZJHMOVgCOfttjPJcgOx52EpkY0CMfy87MIw==} + /@types/yargs-parser@21.0.3: resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} dev: true @@ -1744,6 +1815,16 @@ packages: /arg@4.1.3: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + /argon2@0.40.1: + resolution: {integrity: sha512-DjtHDwd7pm12qeWyfihHoM8Bn5vGcgH6sKwgPqwNYroRmxlrzadHEvMyuvQxN/V8YSyRRKD5x6ito09q1e9OyA==} + engines: {node: '>=16.17.0'} + requiresBuild: true + dependencies: + '@phc/format': 1.0.0 + node-addon-api: 7.1.0 + node-gyp-build: 4.8.1 + dev: true + /argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} dependencies: @@ -1926,6 +2007,10 @@ packages: node-int64: 0.4.0 dev: true + /buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + dev: false + /buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -2041,6 +2126,16 @@ packages: resolution: {integrity: sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==} dev: true + /class-transformer@0.5.1: + resolution: {integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==} + + /class-validator@0.14.1: + resolution: {integrity: sha512-2VEG9JICxIqTpoK1eMzZqaV+u/EiwEJkMGzTrZf6sU/fwsnOITVgYJ8yojSy6CaXtO9V0Cc6ZQZ8h8m4UBuLwQ==} + dependencies: + '@types/validator': 13.11.9 + libphonenumber-js: 1.10.61 + validator: 13.11.0 + /cli-cursor@3.1.0: resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} engines: {node: '>=8'} @@ -2380,6 +2475,12 @@ packages: /eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + /ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + dependencies: + safe-buffer: 5.2.1 + dev: false + /ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -3794,6 +3895,37 @@ packages: graceful-fs: 4.2.11 dev: true + /jsonwebtoken@9.0.2: + resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} + engines: {node: '>=12', npm: '>=6'} + 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.1.1 + ms: 2.1.3 + semver: 7.6.0 + dev: false + + /jwa@1.4.1: + resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==} + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + dev: false + + /jws@3.2.2: + resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} + dependencies: + jwa: 1.4.1 + safe-buffer: 5.2.1 + dev: false + /keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} dependencies: @@ -3818,6 +3950,9 @@ packages: type-check: 0.4.0 dev: true + /libphonenumber-js@1.10.61: + resolution: {integrity: sha512-TsQsyzDttDvvzWNkbp/i0fVbzTGJIG0mUu/uNalIaRQEYeJxVQ/FPg+EJgSqfSXezREjM0V3RZ8cLVsKYhhw0Q==} + /lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} dev: true @@ -3841,6 +3976,30 @@ packages: p-locate: 5.0.0 dev: true + /lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + dev: false + + /lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + dev: false + + /lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + dev: false + + /lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + dev: false + + /lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + dev: false + + /lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + dev: false + /lodash.memoize@4.1.2: resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} dev: true @@ -3849,6 +4008,10 @@ packages: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} dev: true + /lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + dev: false + /lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} @@ -3875,7 +4038,6 @@ packages: engines: {node: '>=10'} dependencies: yallist: 4.0.0 - dev: true /magic-string@0.30.5: resolution: {integrity: sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==} @@ -4065,6 +4227,11 @@ packages: resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} dev: true + /node-addon-api@7.1.0: + resolution: {integrity: sha512-mNcltoe1R8o7STTegSOHdnJNN7s5EUvhoS7ShnTHDyOSd+8H+UdWODq6qSv67PjC8Zc5JRT8+oLAMCr0SIXw7g==} + engines: {node: ^16 || ^18 || >= 20} + dev: true + /node-emoji@1.11.0: resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==} dependencies: @@ -4082,6 +4249,11 @@ packages: dependencies: whatwg-url: 5.0.0 + /node-gyp-build@4.8.1: + resolution: {integrity: sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==} + hasBin: true + dev: true + /node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} dev: true @@ -4228,6 +4400,34 @@ packages: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} + /passport-jwt@4.0.1: + resolution: {integrity: sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==} + dependencies: + jsonwebtoken: 9.0.2 + passport-strategy: 1.0.0 + dev: false + + /passport-local@1.0.0: + resolution: {integrity: sha512-9wCE6qKznvf9mQYYbgJ3sVOHmCWoUNMVFoZzNoznmISbhnNNPhN9xfY3sLmScHMetEJeoY7CXwfhCe7argfQow==} + engines: {node: '>= 0.4.0'} + dependencies: + passport-strategy: 1.0.0 + dev: false + + /passport-strategy@1.0.0: + resolution: {integrity: sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==} + engines: {node: '>= 0.4.0'} + dev: false + + /passport@0.7.0: + resolution: {integrity: sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==} + engines: {node: '>= 0.4.0'} + dependencies: + passport-strategy: 1.0.0 + pause: 0.0.1 + utils-merge: 1.0.1 + dev: false + /path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -4264,6 +4464,10 @@ packages: engines: {node: '>=8'} dev: true + /pause@0.0.1: + resolution: {integrity: sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==} + dev: false + /pg-cloudflare@1.1.1: resolution: {integrity: sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==} requiresBuild: true @@ -4633,7 +4837,6 @@ packages: hasBin: true dependencies: lru-cache: 6.0.0 - dev: true /send@0.18.0: resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} @@ -5359,6 +5562,10 @@ packages: convert-source-map: 2.0.0 dev: true + /validator@13.11.0: + resolution: {integrity: sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==} + engines: {node: '>= 0.10'} + /vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -5540,7 +5747,6 @@ packages: /yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - dev: true /yargs-parser@20.2.9: resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index eb7e79c..2b39120 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -1,8 +1,13 @@ -import { Module } from '@nestjs/common'; +import { MiddlewareConsumer, Module, RequestMethod } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { ConfigModule } from '@nestjs/config'; import { DatabaseModule } from './modules/database-module/database.module'; +import { CspMiddleware } from './middleware/csp-middleware/csp.middleware'; +import { SecurityHeadersMiddleware } from './middleware/security-middleware/security.middleware'; +import { HttpsRedirectMiddleware } from './middleware/https-middlware/https-redirect.middleware'; +import { AuthModule } from './modules/auth-module/auth.module'; +import { AccessTokenGuard } from './modules/auth-module/common/guards'; @Module({ imports: [ @@ -10,8 +15,16 @@ import { DatabaseModule } from './modules/database-module/database.module'; isGlobal: true, }), DatabaseModule, + AuthModule, ], controllers: [AppController], - providers: [AppService], + providers: [AppService, { provide: 'APP_GUARD', useClass: AccessTokenGuard }], }) -export class AppModule {} +export class AppModule { + configure(consumer: MiddlewareConsumer) { + consumer + // TODO: Redirect via Reverse Proxy all HTTP requests to HTTPS + .apply(CspMiddleware, SecurityHeadersMiddleware, HttpsRedirectMiddleware) + .forRoutes({ path: '*', method: RequestMethod.ALL }); + } +} diff --git a/backend/src/entities/user-credentials.entity.ts b/backend/src/entities/user-credentials.entity.ts new file mode 100644 index 0000000..a9959b0 --- /dev/null +++ b/backend/src/entities/user-credentials.entity.ts @@ -0,0 +1,28 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, +} from 'typeorm'; + +@Entity() +export class UserCredentials { + @PrimaryGeneratedColumn('uuid') + id: number; + + @Column({ unique: true }) + email: string; + + @Column() + hash: string; + + @Column({ nullable: true }) + hashedRt?: string; + + @CreateDateColumn() + createdAt: Date; + + @UpdateDateColumn() + updatedAt: Date; +} diff --git a/backend/src/main.ts b/backend/src/main.ts index fca788d..e476576 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -1,6 +1,7 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; +import { ValidationPipe } from '@nestjs/common'; async function setupSwagger(app) { const config = new DocumentBuilder().build(); @@ -8,9 +9,19 @@ async function setupSwagger(app) { SwaggerModule.setup('api', app, document); } +async function setupPrefix(app) { + app.setGlobalPrefix('api'); +} + +async function setupClassValidator(app) { + app.useGlobalPipes(new ValidationPipe()); +} + async function bootstrap() { const app = await NestFactory.create(AppModule); await setupSwagger(app); + await setupPrefix(app); + await setupClassValidator(app); await app.listen(3000); } bootstrap(); diff --git a/backend/src/middleware/csp-middleware/csp.middleware.ts b/backend/src/middleware/csp-middleware/csp.middleware.ts new file mode 100644 index 0000000..d77e38c --- /dev/null +++ b/backend/src/middleware/csp-middleware/csp.middleware.ts @@ -0,0 +1,16 @@ +import { Injectable, NestMiddleware } from '@nestjs/common'; +import { Request, Response, NextFunction } from 'express'; +import { ConfigService } from '@nestjs/config'; + +@Injectable() +export class CspMiddleware implements NestMiddleware { + constructor(private readonly configService: ConfigService) {} + + public use(req: Request, res: Response, next: NextFunction): void { + const cspDirectives = this.configService.get('CSP_DIRECTIVES'); + if (cspDirectives) { + res.setHeader('Content-Security-Policy', cspDirectives); + } + next(); + } +} diff --git a/backend/src/middleware/https-middlware/https-redirect.middleware.ts b/backend/src/middleware/https-middlware/https-redirect.middleware.ts new file mode 100644 index 0000000..0a02027 --- /dev/null +++ b/backend/src/middleware/https-middlware/https-redirect.middleware.ts @@ -0,0 +1,21 @@ +import { Injectable, NestMiddleware } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { NextFunction, Request, Response } from 'express'; + +@Injectable() +export class HttpsRedirectMiddleware implements NestMiddleware { + constructor(private readonly configService: ConfigService) {} + + public use(req: Request, res: Response, next: NextFunction) { + if (this.configService.get('NODE_ENV') === 'production') { + if (req.protocol === 'http') { + const httpsUrl = `https://${req.headers.host}${req.url}`; + res.redirect(httpsUrl); + } else { + next(); + } + } else { + next(); + } + } +} diff --git a/backend/src/middleware/security-middleware/security.middleware.ts b/backend/src/middleware/security-middleware/security.middleware.ts new file mode 100644 index 0000000..e85536d --- /dev/null +++ b/backend/src/middleware/security-middleware/security.middleware.ts @@ -0,0 +1,20 @@ +import { Injectable, NestMiddleware } from '@nestjs/common'; +import { Request, Response, NextFunction } from 'express'; +import { ConfigService } from '@nestjs/config'; + +@Injectable() +export class SecurityHeadersMiddleware implements NestMiddleware { + constructor(private readonly configService: ConfigService) {} + + public use(req: Request, res: Response, next: NextFunction): void { + if (this.configService.get('NODE_ENV') === 'production') { + res.setHeader( + 'Strict-Transport-Security', + 'max-age=63072000; includeSubDomains; preload' + ); + } + res.setHeader('X-Content-Type-Options', 'nosniff'); + res.setHeader('X-Frame-Options', 'SAMEORIGIN'); + next(); + } +} diff --git a/backend/src/modules/auth-module/auth.module.ts b/backend/src/modules/auth-module/auth.module.ts new file mode 100644 index 0000000..fa1efaf --- /dev/null +++ b/backend/src/modules/auth-module/auth.module.ts @@ -0,0 +1,27 @@ +import { Module } from '@nestjs/common'; +import { AuthService } from './services/auth.service'; +import { AuthController } from './controller/auth.controller'; +import { JwtModule } from '@nestjs/jwt'; +import { AccessTokenStrategy, RefreshTokenStrategy } from './strategies'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { UserCredentials } from 'src/entities/user-credentials.entity'; +import { UserRepository } from './repositories/user.repository'; +import { EncryptionService } from './services/encryption.service'; +import { TokenManagementService } from './services/token-management.service'; + +@Module({ + imports: [ + JwtModule.register({}), + TypeOrmModule.forFeature([UserCredentials]), + ], + providers: [ + AuthService, + TokenManagementService, + EncryptionService, + UserRepository, + AccessTokenStrategy, + RefreshTokenStrategy, + ], + controllers: [AuthController], +}) +export class AuthModule {} diff --git a/backend/src/modules/auth-module/common/decorators/get-user-id.decorator.ts b/backend/src/modules/auth-module/common/decorators/get-user-id.decorator.ts new file mode 100644 index 0000000..72c035d --- /dev/null +++ b/backend/src/modules/auth-module/common/decorators/get-user-id.decorator.ts @@ -0,0 +1,10 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; +import { JwtPayload } from 'src/modules/auth-module/models/types'; + +export const GetCurrentUserId = createParamDecorator( + (_: undefined, context: ExecutionContext): number => { + const request = context.switchToHttp().getRequest(); + const user = request.user as JwtPayload; + return user.sub; + } +); diff --git a/backend/src/modules/auth-module/common/decorators/get-user.decorator.ts b/backend/src/modules/auth-module/common/decorators/get-user.decorator.ts new file mode 100644 index 0000000..e4cf977 --- /dev/null +++ b/backend/src/modules/auth-module/common/decorators/get-user.decorator.ts @@ -0,0 +1,13 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; +import { JwtPayloadWithRefreshToken } from 'src/modules/auth-module/models/types'; + +export const GetCurrentUser = createParamDecorator( + ( + data: keyof JwtPayloadWithRefreshToken | undefined, + context: ExecutionContext + ) => { + const request = context.switchToHttp().getRequest(); + if (!data) return request.user; + return request.user[data]; + } +); diff --git a/backend/src/modules/auth-module/common/decorators/index.ts b/backend/src/modules/auth-module/common/decorators/index.ts new file mode 100644 index 0000000..61c88bb --- /dev/null +++ b/backend/src/modules/auth-module/common/decorators/index.ts @@ -0,0 +1,3 @@ +export * from './get-user-id.decorator'; +export * from './get-user.decorator'; +export * from './public.decorator'; diff --git a/backend/src/modules/auth-module/common/decorators/public.decorator.ts b/backend/src/modules/auth-module/common/decorators/public.decorator.ts new file mode 100644 index 0000000..5b8bb31 --- /dev/null +++ b/backend/src/modules/auth-module/common/decorators/public.decorator.ts @@ -0,0 +1,3 @@ +import { SetMetadata } from '@nestjs/common'; + +export const Public = () => SetMetadata('isPublic', true); diff --git a/backend/src/modules/auth-module/common/guards/access-token.guard.ts b/backend/src/modules/auth-module/common/guards/access-token.guard.ts new file mode 100644 index 0000000..3ca8986 --- /dev/null +++ b/backend/src/modules/auth-module/common/guards/access-token.guard.ts @@ -0,0 +1,28 @@ +import { Injectable, ExecutionContext } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; +import { Reflector } from '@nestjs/core'; +import { Observable } from 'rxjs'; + +@Injectable() +export class AccessTokenGuard extends AuthGuard('jwt-access-token') { + constructor(private readonly reflector: Reflector) { + super(); + } + + public canActivate( + context: ExecutionContext + ): boolean | Promise | Observable { + // Check if the current route is marked as public + const isPublic = this.reflector.getAllAndOverride('isPublic', [ + context.getHandler(), + context.getClass(), + ]); + + // Allow access if the route is public, otherwise defer to the standard JWT authentication mechanism + if (isPublic) { + return true; + } + + return super.canActivate(context); + } +} diff --git a/backend/src/modules/auth-module/common/guards/index.ts b/backend/src/modules/auth-module/common/guards/index.ts new file mode 100644 index 0000000..50511b9 --- /dev/null +++ b/backend/src/modules/auth-module/common/guards/index.ts @@ -0,0 +1,2 @@ +export * from './access-token.guard'; +export * from './refresh-token.guard'; diff --git a/backend/src/modules/auth-module/common/guards/refresh-token.guard.ts b/backend/src/modules/auth-module/common/guards/refresh-token.guard.ts new file mode 100644 index 0000000..79d73fa --- /dev/null +++ b/backend/src/modules/auth-module/common/guards/refresh-token.guard.ts @@ -0,0 +1,7 @@ +import { AuthGuard } from '@nestjs/passport'; + +export class RefreshTokenGuard extends AuthGuard('jwt-refresh-token') { + constructor() { + super(); + } +} diff --git a/backend/src/modules/auth-module/controller/auth.controller.ts b/backend/src/modules/auth-module/controller/auth.controller.ts new file mode 100644 index 0000000..87607a2 --- /dev/null +++ b/backend/src/modules/auth-module/controller/auth.controller.ts @@ -0,0 +1,53 @@ +import { + Controller, + Post, + Body, + HttpCode, + HttpStatus, + UseGuards, +} from '@nestjs/common'; +import { AuthService } from '../services/auth.service'; +import { UserCredentialsDto } from '../models/dto'; +import { Tokens } from '../models/types'; +import { RefreshTokenGuard } from '../common/guards'; +import { GetCurrentUser, GetCurrentUserId, Public } from '../common/decorators'; + +@Controller('auth') +export class AuthController { + constructor(private readonly authService: AuthService) {} + + @Public() + @Post('signup') + @HttpCode(HttpStatus.CREATED) + public async signup( + @Body() userCredentials: UserCredentialsDto + ): Promise { + return this.authService.signup(userCredentials); + } + + @Public() + @Post('signin') + @HttpCode(HttpStatus.OK) + public async signin( + @Body() userCredentials: UserCredentialsDto + ): Promise { + return this.authService.signin(userCredentials); + } + + @Post('logout') + @HttpCode(HttpStatus.OK) + public async logout(@GetCurrentUserId() userId: number): Promise { + return this.authService.logout(userId); + } + + @Public() + @UseGuards(RefreshTokenGuard) + @Post('refresh') + @HttpCode(HttpStatus.OK) + public async refresh( + @GetCurrentUserId() userId: number, + @GetCurrentUser('refresh_token') refresh_token: string + ): Promise { + return this.authService.refresh(userId, refresh_token); + } +} diff --git a/backend/src/modules/auth-module/models/dto/index.ts b/backend/src/modules/auth-module/models/dto/index.ts new file mode 100644 index 0000000..7d5fd28 --- /dev/null +++ b/backend/src/modules/auth-module/models/dto/index.ts @@ -0,0 +1 @@ +export * from './user-credentials.dto'; diff --git a/backend/src/modules/auth-module/models/dto/user-credentials.dto.ts b/backend/src/modules/auth-module/models/dto/user-credentials.dto.ts new file mode 100644 index 0000000..3fd7563 --- /dev/null +++ b/backend/src/modules/auth-module/models/dto/user-credentials.dto.ts @@ -0,0 +1,12 @@ +import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator'; + +export class UserCredentialsDto { + @IsNotEmpty() + @IsEmail() + public email: string; + + @IsNotEmpty() + @IsString() + @MinLength(8) + public password: string; +} diff --git a/backend/src/modules/auth-module/models/types/index.ts b/backend/src/modules/auth-module/models/types/index.ts new file mode 100644 index 0000000..90e9169 --- /dev/null +++ b/backend/src/modules/auth-module/models/types/index.ts @@ -0,0 +1,3 @@ +export * from './tokens.type'; +export * from './jwt-payload.type'; +export * from './jwt-payload-with-refresh-token.type'; diff --git a/backend/src/modules/auth-module/models/types/jwt-payload-with-refresh-token.type.ts b/backend/src/modules/auth-module/models/types/jwt-payload-with-refresh-token.type.ts new file mode 100644 index 0000000..c2e9746 --- /dev/null +++ b/backend/src/modules/auth-module/models/types/jwt-payload-with-refresh-token.type.ts @@ -0,0 +1,3 @@ +import { JwtPayload } from './jwt-payload.type'; + +export type JwtPayloadWithRefreshToken = JwtPayload & { refresh_token: string }; diff --git a/backend/src/modules/auth-module/models/types/jwt-payload.type.ts b/backend/src/modules/auth-module/models/types/jwt-payload.type.ts new file mode 100644 index 0000000..dce426b --- /dev/null +++ b/backend/src/modules/auth-module/models/types/jwt-payload.type.ts @@ -0,0 +1,4 @@ +export type JwtPayload = { + email: string; + sub: number; +}; diff --git a/backend/src/modules/auth-module/models/types/tokens.type.ts b/backend/src/modules/auth-module/models/types/tokens.type.ts new file mode 100644 index 0000000..1c0a510 --- /dev/null +++ b/backend/src/modules/auth-module/models/types/tokens.type.ts @@ -0,0 +1,4 @@ +export type Tokens = { + access_token: string; + refresh_token: string; +}; diff --git a/backend/src/modules/auth-module/repositories/user.repository.ts b/backend/src/modules/auth-module/repositories/user.repository.ts new file mode 100644 index 0000000..7e0ba22 --- /dev/null +++ b/backend/src/modules/auth-module/repositories/user.repository.ts @@ -0,0 +1,40 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { UserCredentials } from 'src/entities/user-credentials.entity'; +import { Repository } from 'typeorm'; + +@Injectable() +export class UserRepository { + constructor( + @InjectRepository(UserCredentials) + private readonly repository: Repository + ) {} + + public async createUser( + email: string, + hash: string + ): Promise { + const user = this.repository.create({ email, hash }); + return this.repository.save(user); + } + + public async findUserByEmail( + email: string + ): Promise { + return this.repository.findOne({ where: { email } }); + } + + public async findUserById( + userId: number + ): Promise { + return this.repository.findOne({ where: { id: userId } }); + } + + public async updateUserTokenHash( + userId: number, + hashedRt: string | null + ): Promise { + const result = await this.repository.update(userId, { hashedRt }); + return result.affected ?? 0; + } +} diff --git a/backend/src/modules/auth-module/services/auth.service.ts b/backend/src/modules/auth-module/services/auth.service.ts new file mode 100644 index 0000000..272e841 --- /dev/null +++ b/backend/src/modules/auth-module/services/auth.service.ts @@ -0,0 +1,85 @@ +import { ForbiddenException, Injectable } from '@nestjs/common'; +import { UserCredentialsDto } from '../models/dto'; +import { Tokens } from '../models/types'; +import { EncryptionService } from './encryption.service'; +import { UserRepository } from '../repositories/user.repository'; +import { TokenManagementService } from './token-management.service'; + +@Injectable() +export class AuthService { + constructor( + private readonly userRepository: UserRepository, + private readonly tokenManagementService: TokenManagementService, + private readonly encryptionService: EncryptionService + ) {} + + public async signup(userCredentials: UserCredentialsDto): Promise { + const passwordHashed = await this.encryptionService.hashData( + userCredentials.password + ); + const user = await this.userRepository.createUser( + userCredentials.email, + passwordHashed + ); + return this.generateAndPersistTokens(user.id, user.email); + } + + public async signin(userCredentials: UserCredentialsDto): Promise { + const user = await this.userRepository.findUserByEmail( + userCredentials.email + ); + if (!user) { + throw new ForbiddenException('Access Denied'); + } + + const passwordMatch = await this.encryptionService.compareHash( + userCredentials.password, + user.hash + ); + if (!passwordMatch) { + throw new ForbiddenException('Access Denied'); + } + + return this.generateAndPersistTokens(user.id, user.email); + } + + public async refresh(userId: number, refreshToken: string): Promise { + const user = await this.userRepository.findUserById(userId); + if (!user || !user.hashedRt) { + throw new ForbiddenException('Access Denied'); + } + + const refreshTokenMatch = await this.encryptionService.compareHash( + refreshToken, + user.hashedRt + ); + if (!refreshTokenMatch) { + throw new ForbiddenException('Access Denied'); + } + + return this.generateAndPersistTokens(user.id, user.email); + } + + public async logout(userId: number): Promise { + const affected = await this.userRepository.updateUserTokenHash( + userId, + null + ); + return affected > 0; + } + + private async generateAndPersistTokens( + userId: number, + email: string + ): Promise { + const tokens = await this.tokenManagementService.generateTokens( + userId, + email + ); + const hashedRefreshToken = await this.encryptionService.hashData( + tokens.refresh_token + ); + await this.userRepository.updateUserTokenHash(userId, hashedRefreshToken); + return tokens; + } +} diff --git a/backend/src/modules/auth-module/services/encryption.service.ts b/backend/src/modules/auth-module/services/encryption.service.ts new file mode 100644 index 0000000..79182e7 --- /dev/null +++ b/backend/src/modules/auth-module/services/encryption.service.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@nestjs/common'; +import * as argon2 from 'argon2'; +import { Options } from 'argon2'; + +@Injectable() +export class EncryptionService { + private hashOptions: Options = { + type: argon2.argon2id, + memoryCost: 2 ** 16, + timeCost: 3, + parallelism: 1, + }; + + public async hashData(data: string): Promise { + return await argon2.hash(data, this.hashOptions); + } + + public async compareHash(data: string, encrypted: string): Promise { + return await argon2.verify(encrypted, data); + } +} diff --git a/backend/src/modules/auth-module/services/token-management.service.ts b/backend/src/modules/auth-module/services/token-management.service.ts new file mode 100644 index 0000000..73ad211 --- /dev/null +++ b/backend/src/modules/auth-module/services/token-management.service.ts @@ -0,0 +1,58 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { Tokens } from '../models/types'; +import { JwtService } from '@nestjs/jwt'; + +@Injectable() +export class TokenManagementService { + private readonly ACCESS_TOKEN_EXPIRY: string; + private readonly REFRESH_TOKEN_EXPIRY: string; + private readonly JWT_SECRET_AT: string; + private readonly JWT_SECRET_RT: string; + + constructor( + private readonly jwt: JwtService, + private readonly configService: ConfigService + ) { + this.ACCESS_TOKEN_EXPIRY = this.configService.get( + 'ACCESS_TOKEN_EXPIRY' + ); + this.REFRESH_TOKEN_EXPIRY = this.configService.get( + 'REFRESH_TOKEN_EXPIRY' + ); + this.JWT_SECRET_AT = this.configService.get('JWT_SECRET_AT'); + this.JWT_SECRET_RT = this.configService.get('JWT_SECRET_RT'); + } + + public async generateTokens(userId: number, email: string): Promise { + const access_token: string = await this.createAccessToken(userId, email); + const refresh_token: string = await this.createRefreshToken(userId, email); + return { access_token, refresh_token }; + } + + private async createAccessToken( + userId: number, + email: string + ): Promise { + return this.jwt.signAsync( + { sub: userId, email }, + { + expiresIn: this.ACCESS_TOKEN_EXPIRY, + secret: this.JWT_SECRET_AT, + } + ); + } + + private async createRefreshToken( + userId: number, + email: string + ): Promise { + return this.jwt.signAsync( + { sub: userId, email }, + { + expiresIn: this.REFRESH_TOKEN_EXPIRY, + secret: this.JWT_SECRET_RT, + } + ); + } +} diff --git a/backend/src/modules/auth-module/strategies/access-token.strategie.ts b/backend/src/modules/auth-module/strategies/access-token.strategie.ts new file mode 100644 index 0000000..00dea31 --- /dev/null +++ b/backend/src/modules/auth-module/strategies/access-token.strategie.ts @@ -0,0 +1,26 @@ +import { Injectable } from '@nestjs/common'; +import { PassportStrategy } from '@nestjs/passport'; +import { Strategy, ExtractJwt } from 'passport-jwt'; +import { ConfigService } from '@nestjs/config'; +import { JwtPayload } from '../models/types'; + +@Injectable() +export class AccessTokenStrategy extends PassportStrategy( + Strategy, + 'jwt-access-token' +) { + constructor(private readonly configService: ConfigService) { + super(AccessTokenStrategy.getJwtConfig(configService)); + } + + private static getJwtConfig(configService: ConfigService): any { + return { + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + secretOrKey: configService.get('JWT_SECRET_AT'), + }; + } + + public async validate(payload: JwtPayload): Promise { + return payload; + } +} diff --git a/backend/src/modules/auth-module/strategies/index.ts b/backend/src/modules/auth-module/strategies/index.ts new file mode 100644 index 0000000..c0e13f5 --- /dev/null +++ b/backend/src/modules/auth-module/strategies/index.ts @@ -0,0 +1,2 @@ +export * from './access-token.strategie'; +export * from './refresh-token.strategie'; diff --git a/backend/src/modules/auth-module/strategies/refresh-token.strategie.ts b/backend/src/modules/auth-module/strategies/refresh-token.strategie.ts new file mode 100644 index 0000000..e11c3bc --- /dev/null +++ b/backend/src/modules/auth-module/strategies/refresh-token.strategie.ts @@ -0,0 +1,39 @@ +import { Injectable, ForbiddenException } from '@nestjs/common'; +import { PassportStrategy } from '@nestjs/passport'; +import { Strategy, ExtractJwt } from 'passport-jwt'; +import { ConfigService } from '@nestjs/config'; +import { Request } from 'express'; + +@Injectable() +export class RefreshTokenStrategy extends PassportStrategy( + Strategy, + 'jwt-refresh-token' +) { + constructor(private readonly configService: ConfigService) { + super(RefreshTokenStrategy.createJwtStrategyOptions(configService)); + } + + private static createJwtStrategyOptions(configService: ConfigService): any { + return { + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + secretOrKey: configService.get('JWT_SECRET_RT'), + passReqToCallback: true, + }; + } + + public async validate(req: Request, payload: any) { + const refresh_token: string = req + ?.get('authorization') + ?.replace('Bearer', '') + .trim(); + + if (!refresh_token) { + throw new ForbiddenException('Refresh token malformed'); + } + + return { + ...payload, + refresh_token, + }; + } +} diff --git a/backend/src/modules/database-module/database-config.ts b/backend/src/modules/database-module/database-config.ts index d788f72..4d3d2bc 100644 --- a/backend/src/modules/database-module/database-config.ts +++ b/backend/src/modules/database-module/database-config.ts @@ -1,5 +1,6 @@ -import { ConfigService } from "@nestjs/config"; -import { TypeOrmModuleOptions } from "@nestjs/typeorm"; +import { ConfigService } from '@nestjs/config'; +import { TypeOrmModuleOptions } from '@nestjs/typeorm'; +import { UserCredentials } from '../../entities/user-credentials.entity'; export const databaseConfigFactory = ( configService: ConfigService @@ -12,5 +13,5 @@ export const databaseConfigFactory = ( database: configService.get('DB_NAME'), synchronize: true, logging: true, - entities: [], + entities: [UserCredentials], });