From a82b9dfbfc6191395a86d8fc6887f5afc535291f Mon Sep 17 00:00:00 2001 From: Igor Propisnov Date: Sat, 4 May 2024 00:16:49 +0200 Subject: [PATCH] added basic auth and token handling --- backend/package.json | 9 + backend/pnpm-lock.yaml | 419 ++++++++++++++++-- backend/src/app.module.ts | 5 +- backend/src/entities/user.entity.ts | 28 ++ backend/src/main.ts | 11 + .../csp-middleware/csp.middleware.ts | 4 +- .../https-redirect.middleware.ts | 6 +- .../security.middleware.ts | 4 +- .../src/modules/auth-module/auth.module.ts | 24 + .../decorators/get-user-id.decorator.ts | 10 + .../common/decorators/get-user.decorator.ts | 13 + .../auth-module/common/decorators/index.ts | 3 + .../common/decorators/public.decorator.ts | 3 + .../common/guards/access-token.guard.ts | 26 ++ .../auth-module/common/guards/index.ts | 2 + .../common/guards/refresh-token.guard.ts | 7 + .../auth-module/controller/auth.controller.ts | 49 ++ .../modules/auth-module/models/dto/index.ts | 1 + .../models/dto/user-credentials.dto.ts | 11 + .../modules/auth-module/models/types/index.ts | 3 + .../jwt-payload-with-refresh-token.type.ts | 3 + .../models/types/jwt-payload.type.ts | 4 + .../auth-module/models/types/tokens.type.ts | 4 + .../repositories/user.repository.ts | 33 ++ .../auth-module/services/auth.service.ts | 85 ++++ .../services/encryption.service.ts | 13 + .../services/token-management.service.ts | 37 ++ .../strategies/access-token.strategie.ts | 22 + .../modules/auth-module/strategies/index.ts | 2 + .../strategies/refresh-token.strategie.ts | 35 ++ .../database-module/database-config.ts | 8 +- 31 files changed, 844 insertions(+), 40 deletions(-) create mode 100644 backend/src/entities/user.entity.ts create mode 100644 backend/src/modules/auth-module/auth.module.ts create mode 100644 backend/src/modules/auth-module/common/decorators/get-user-id.decorator.ts create mode 100644 backend/src/modules/auth-module/common/decorators/get-user.decorator.ts create mode 100644 backend/src/modules/auth-module/common/decorators/index.ts create mode 100644 backend/src/modules/auth-module/common/decorators/public.decorator.ts create mode 100644 backend/src/modules/auth-module/common/guards/access-token.guard.ts create mode 100644 backend/src/modules/auth-module/common/guards/index.ts create mode 100644 backend/src/modules/auth-module/common/guards/refresh-token.guard.ts create mode 100644 backend/src/modules/auth-module/controller/auth.controller.ts create mode 100644 backend/src/modules/auth-module/models/dto/index.ts create mode 100644 backend/src/modules/auth-module/models/dto/user-credentials.dto.ts create mode 100644 backend/src/modules/auth-module/models/types/index.ts create mode 100644 backend/src/modules/auth-module/models/types/jwt-payload-with-refresh-token.type.ts create mode 100644 backend/src/modules/auth-module/models/types/jwt-payload.type.ts create mode 100644 backend/src/modules/auth-module/models/types/tokens.type.ts create mode 100644 backend/src/modules/auth-module/repositories/user.repository.ts create mode 100644 backend/src/modules/auth-module/services/auth.service.ts create mode 100644 backend/src/modules/auth-module/services/encryption.service.ts create mode 100644 backend/src/modules/auth-module/services/token-management.service.ts create mode 100644 backend/src/modules/auth-module/strategies/access-token.strategie.ts create mode 100644 backend/src/modules/auth-module/strategies/index.ts create mode 100644 backend/src/modules/auth-module/strategies/refresh-token.strategie.ts diff --git a/backend/package.json b/backend/package.json index ddde228..fd52f86 100644 --- a/backend/package.json +++ b/backend/package.json @@ -23,10 +23,18 @@ "@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", + "bcrypt": "^5.1.1", + "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 +45,7 @@ "@nestjs/cli": "^10.0.0", "@nestjs/schematics": "^10.0.0", "@nestjs/testing": "^10.0.0", + "@types/bcrypt": "^5.0.2", "@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..3c8ea11 100644 --- a/backend/pnpm-lock.yaml +++ b/backend/pnpm-lock.yaml @@ -7,25 +7,49 @@ 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) + bcrypt: + specifier: ^5.1.1 + version: 5.1.1 + 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 +76,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/bcrypt': + specifier: ^5.0.2 + version: 5.0.2 '@types/express': specifier: ^4.17.17 version: 4.17.21 @@ -861,6 +888,24 @@ packages: resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} engines: {node: '>=8'} + /@mapbox/node-pre-gyp@1.0.11: + resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==} + hasBin: true + dependencies: + detect-libc: 2.0.3 + https-proxy-agent: 5.0.1 + make-dir: 3.1.0 + node-fetch: 2.7.0 + nopt: 5.0.0 + npmlog: 5.0.1 + rimraf: 3.0.2 + semver: 7.6.0 + tar: 6.2.1 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + /@microsoft/tsdoc@0.14.2: resolution: {integrity: sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==} dev: false @@ -906,7 +951,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 +964,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 +978,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 +1004,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 +1017,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 +1040,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 +1102,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 +1120,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 +1145,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 +1160,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 @@ -1201,6 +1272,12 @@ packages: '@babel/types': 7.24.0 dev: true + /@types/bcrypt@5.0.2: + resolution: {integrity: sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==} + dependencies: + '@types/node': 20.12.4 + dev: true + /@types/body-parser@1.19.5: resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} dependencies: @@ -1291,6 +1368,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 +1433,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 @@ -1610,6 +1696,10 @@ packages: resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} dev: true + /abbrev@1.1.1: + resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + dev: false + /accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -1642,6 +1732,15 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + /agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + dependencies: + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: false + /ajv-formats@2.1.1(ajv@8.12.0): resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} peerDependencies: @@ -1741,6 +1840,18 @@ packages: /append-field@1.0.0: resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} + /aproba@2.0.0: + resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==} + dev: false + + /are-we-there-yet@2.0.0: + resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==} + engines: {node: '>=10'} + dependencies: + delegates: 1.0.0 + readable-stream: 3.6.2 + dev: false + /arg@4.1.3: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} @@ -1851,6 +1962,18 @@ packages: /base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + /bcrypt@5.1.1: + resolution: {integrity: sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==} + engines: {node: '>= 10.0.0'} + requiresBuild: true + dependencies: + '@mapbox/node-pre-gyp': 1.0.11 + node-addon-api: 5.1.0 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + /binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -1888,7 +2011,6 @@ packages: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 - dev: true /brace-expansion@2.0.1: resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} @@ -1926,6 +2048,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==} @@ -2027,6 +2153,11 @@ packages: fsevents: 2.3.3 dev: true + /chownr@2.0.0: + resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} + engines: {node: '>=10'} + dev: false + /chrome-trace-event@1.0.3: resolution: {integrity: sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==} engines: {node: '>=6.0'} @@ -2041,6 +2172,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'} @@ -2134,6 +2275,11 @@ packages: /color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + /color-support@1.1.3: + resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} + hasBin: true + dev: false + /combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -2167,7 +2313,6 @@ packages: /concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - dev: true /concat-stream@1.6.2: resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==} @@ -2181,6 +2326,10 @@ packages: /consola@2.15.3: resolution: {integrity: sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==} + /console-control-strings@1.1.0: + resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} + dev: false + /content-disposition@0.5.4: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} @@ -2324,6 +2473,10 @@ packages: engines: {node: '>=0.4.0'} dev: true + /delegates@1.0.0: + resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} + dev: false + /depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -2332,6 +2485,11 @@ packages: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + /detect-libc@2.0.3: + resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} + engines: {node: '>=8'} + dev: false + /detect-newline@3.1.0: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} @@ -2380,6 +2538,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==} @@ -2865,13 +3029,19 @@ packages: universalify: 2.0.1 dev: true + /fs-minipass@2.1.0: + resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} + engines: {node: '>= 8'} + dependencies: + minipass: 3.3.6 + dev: false + /fs-monkey@1.0.5: resolution: {integrity: sha512-8uMbBjrhzW76TYgEV27Y5E//W2f/lTFmx78P2w19FZSxarhI/798APGQyuGCwmkNxgwGRhrLfvWyLBvNtuOmew==} dev: true /fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - dev: true /fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} @@ -2884,6 +3054,21 @@ packages: /function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + /gauge@3.0.2: + resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==} + engines: {node: '>=10'} + dependencies: + aproba: 2.0.0 + color-support: 1.1.3 + console-control-strings: 1.1.0 + has-unicode: 2.0.1 + object-assign: 4.1.1 + signal-exit: 3.0.7 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wide-align: 1.1.5 + dev: false + /gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -2951,7 +3136,6 @@ packages: minimatch: 3.1.2 once: 1.4.0 path-is-absolute: 1.0.1 - dev: true /glob@9.3.5: resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==} @@ -3027,6 +3211,10 @@ packages: resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} engines: {node: '>= 0.4'} + /has-unicode@2.0.1: + resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} + dev: false + /hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} @@ -3056,6 +3244,16 @@ packages: statuses: 2.0.1 toidentifier: 1.0.1 + /https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + dependencies: + agent-base: 6.0.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: false + /human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} @@ -3102,7 +3300,6 @@ packages: dependencies: once: 1.4.0 wrappy: 1.0.2 - dev: true /inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -3794,6 +3991,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 +4046,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 +4072,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 +4104,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 +4134,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==} @@ -3884,6 +4142,13 @@ packages: '@jridgewell/sourcemap-codec': 1.4.15 dev: true + /make-dir@3.1.0: + resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} + engines: {node: '>=8'} + dependencies: + semver: 6.3.1 + dev: false + /make-dir@4.0.0: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} @@ -3965,7 +4230,6 @@ packages: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} dependencies: brace-expansion: 1.1.11 - dev: true /minimatch@8.0.4: resolution: {integrity: sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==} @@ -3990,21 +4254,47 @@ packages: /minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + /minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + dependencies: + yallist: 4.0.0 + dev: false + /minipass@4.2.8: resolution: {integrity: sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==} engines: {node: '>=8'} dev: true + /minipass@5.0.0: + resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} + engines: {node: '>=8'} + dev: false + /minipass@7.0.4: resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==} engines: {node: '>=16 || 14 >=14.17'} + /minizlib@2.1.2: + resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} + engines: {node: '>= 8'} + dependencies: + minipass: 3.3.6 + yallist: 4.0.0 + dev: false + /mkdirp@0.5.6: resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} hasBin: true dependencies: minimist: 1.2.8 + /mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + dev: false + /mkdirp@2.1.6: resolution: {integrity: sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==} engines: {node: '>=10'} @@ -4065,6 +4355,10 @@ packages: resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} dev: true + /node-addon-api@5.1.0: + resolution: {integrity: sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==} + dev: false + /node-emoji@1.11.0: resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==} dependencies: @@ -4090,6 +4384,14 @@ packages: resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} dev: true + /nopt@5.0.0: + resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==} + engines: {node: '>=6'} + hasBin: true + dependencies: + abbrev: 1.1.1 + dev: false + /normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} @@ -4102,6 +4404,15 @@ packages: path-key: 3.1.1 dev: true + /npmlog@5.0.1: + resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==} + dependencies: + are-we-there-yet: 2.0.0 + console-control-strings: 1.1.0 + gauge: 3.0.2 + set-blocking: 2.0.0 + dev: false + /object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -4119,7 +4430,6 @@ packages: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} dependencies: wrappy: 1.0.2 - dev: true /onetime@5.1.2: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} @@ -4228,6 +4538,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'} @@ -4236,7 +4574,6 @@ packages: /path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} - dev: true /path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} @@ -4264,6 +4601,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 @@ -4491,7 +4832,6 @@ packages: inherits: 2.0.4 string_decoder: 1.3.0 util-deprecate: 1.0.2 - dev: true /readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} @@ -4573,7 +4913,6 @@ packages: hasBin: true dependencies: glob: 7.2.3 - dev: true /rimraf@4.4.1: resolution: {integrity: sha512-Gk8NlF062+T9CqNGn6h4tls3k6T1+/nXdOcSZVikNVtlRdYpA7wRJJMoXmuvOnLW844rPjdQ7JgXCYM6PPC/og==} @@ -4625,7 +4964,6 @@ packages: /semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - dev: true /semver@7.6.0: resolution: {integrity: sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==} @@ -4633,7 +4971,6 @@ packages: hasBin: true dependencies: lru-cache: 6.0.0 - dev: true /send@0.18.0: resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} @@ -4672,6 +5009,10 @@ packages: transitivePeerDependencies: - supports-color + /set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + dev: false + /set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -4725,7 +5066,6 @@ packages: /signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} - dev: true /signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} @@ -4821,7 +5161,6 @@ packages: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} dependencies: safe-buffer: 5.2.1 - dev: true /strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} @@ -4944,6 +5283,18 @@ packages: engines: {node: '>=6'} dev: true + /tar@6.2.1: + resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} + engines: {node: '>=10'} + dependencies: + chownr: 2.0.0 + fs-minipass: 2.1.0 + minipass: 5.0.0 + minizlib: 2.1.2 + mkdirp: 1.0.4 + yallist: 4.0.0 + dev: false + /terser-webpack-plugin@5.3.10(webpack@5.90.1): resolution: {integrity: sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==} engines: {node: '>= 10.13.0'} @@ -5359,6 +5710,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'} @@ -5489,6 +5844,12 @@ packages: dependencies: isexe: 2.0.0 + /wide-align@1.1.5: + resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} + dependencies: + string-width: 4.2.3 + dev: false + /wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} @@ -5516,7 +5877,6 @@ packages: /wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - dev: true /write-file-atomic@4.0.2: resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} @@ -5540,7 +5900,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 2b7d263..2b39120 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -6,6 +6,8 @@ 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: [ @@ -13,9 +15,10 @@ import { HttpsRedirectMiddleware } from './middleware/https-middlware/https-redi isGlobal: true, }), DatabaseModule, + AuthModule, ], controllers: [AppController], - providers: [AppService], + providers: [AppService, { provide: 'APP_GUARD', useClass: AccessTokenGuard }], }) export class AppModule { configure(consumer: MiddlewareConsumer) { diff --git a/backend/src/entities/user.entity.ts b/backend/src/entities/user.entity.ts new file mode 100644 index 0000000..960a8cf --- /dev/null +++ b/backend/src/entities/user.entity.ts @@ -0,0 +1,28 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, +} from 'typeorm'; + +@Entity() +export class User { + @PrimaryGeneratedColumn() + 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 index b01790e..d77e38c 100644 --- a/backend/src/middleware/csp-middleware/csp.middleware.ts +++ b/backend/src/middleware/csp-middleware/csp.middleware.ts @@ -4,9 +4,9 @@ import { ConfigService } from '@nestjs/config'; @Injectable() export class CspMiddleware implements NestMiddleware { - constructor(private configService: ConfigService) {} + constructor(private readonly configService: ConfigService) {} - use(req: Request, res: Response, next: NextFunction): void { + public use(req: Request, res: Response, next: NextFunction): void { const cspDirectives = this.configService.get('CSP_DIRECTIVES'); if (cspDirectives) { res.setHeader('Content-Security-Policy', cspDirectives); diff --git a/backend/src/middleware/https-middlware/https-redirect.middleware.ts b/backend/src/middleware/https-middlware/https-redirect.middleware.ts index 59f0bb5..0a02027 100644 --- a/backend/src/middleware/https-middlware/https-redirect.middleware.ts +++ b/backend/src/middleware/https-middlware/https-redirect.middleware.ts @@ -4,9 +4,9 @@ import { NextFunction, Request, Response } from 'express'; @Injectable() export class HttpsRedirectMiddleware implements NestMiddleware { - constructor(private configService: ConfigService) {} + constructor(private readonly configService: ConfigService) {} - use(req: Request, res: Response, next: NextFunction) { + 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}`; @@ -14,6 +14,8 @@ export class HttpsRedirectMiddleware implements NestMiddleware { } else { next(); } + } else { + next(); } } } diff --git a/backend/src/middleware/security-middleware/security.middleware.ts b/backend/src/middleware/security-middleware/security.middleware.ts index b766628..e85536d 100644 --- a/backend/src/middleware/security-middleware/security.middleware.ts +++ b/backend/src/middleware/security-middleware/security.middleware.ts @@ -4,9 +4,9 @@ import { ConfigService } from '@nestjs/config'; @Injectable() export class SecurityHeadersMiddleware implements NestMiddleware { - constructor(private configService: ConfigService) {} + constructor(private readonly configService: ConfigService) {} - use(req: Request, res: Response, next: NextFunction): void { + public use(req: Request, res: Response, next: NextFunction): void { if (this.configService.get('NODE_ENV') === 'production') { res.setHeader( 'Strict-Transport-Security', 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..171e9c8 --- /dev/null +++ b/backend/src/modules/auth-module/auth.module.ts @@ -0,0 +1,24 @@ +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 { User } from 'src/entities/user.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([User])], + 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..d4e38d9 --- /dev/null +++ b/backend/src/modules/auth-module/common/guards/access-token.guard.ts @@ -0,0 +1,26 @@ +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 { + const isPublic = this.reflector.getAllAndOverride('isPublic', [ + context.getHandler(), + context.getClass(), + ]); + + 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..239674d --- /dev/null +++ b/backend/src/modules/auth-module/controller/auth.controller.ts @@ -0,0 +1,49 @@ +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('local/signup') + @HttpCode(HttpStatus.CREATED) + public async signup(@Body() userCredentials: UserCredentialsDto): Promise { + return this.authService.signup(userCredentials); + } + + @Public() + @Post('local/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..1b35eec --- /dev/null +++ b/backend/src/modules/auth-module/models/dto/user-credentials.dto.ts @@ -0,0 +1,11 @@ +import { IsEmail, IsNotEmpty, IsString } from 'class-validator'; + +export class UserCredentialsDto { + @IsNotEmpty() + @IsEmail() + public email: string; + + @IsNotEmpty() + @IsString() + 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..64a5428 --- /dev/null +++ b/backend/src/modules/auth-module/repositories/user.repository.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { User } from 'src/entities/user.entity'; +import { Repository } from 'typeorm'; + +@Injectable() +export class UserRepository { + constructor( + @InjectRepository(User) + 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..2533b64 --- /dev/null +++ b/backend/src/modules/auth-module/services/encryption.service.ts @@ -0,0 +1,13 @@ +import { Injectable } from '@nestjs/common'; +import * as bcrypt from 'bcrypt'; + +@Injectable() +export class EncryptionService { + public async hashData(data: string): Promise { + return bcrypt.hash(data, 10); + } + + public async compareHash(data: string, encrypted: string): Promise { + return bcrypt.compare(data, encrypted); + } +} 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..a7136c6 --- /dev/null +++ b/backend/src/modules/auth-module/services/token-management.service.ts @@ -0,0 +1,37 @@ +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 = '15m'; + private readonly REFRESH_TOKEN_EXPIRY = '7d'; + + constructor( + private readonly jwt: JwtService, + private readonly configService: ConfigService + ) {} + + public async generateTokens(userId: number, email: string): Promise { + const [access_token, refresh_token] = await Promise.all([ + this.jwt.signAsync( + { sub: userId, email }, + { + expiresIn: this.ACCESS_TOKEN_EXPIRY, + secret: this.configService.get('JWT_SECRET_AT'), + } + ), + this.jwt.signAsync( + { sub: userId, email }, + { + expiresIn: this.REFRESH_TOKEN_EXPIRY, + secret: this.configService.get('JWT_SECRET_RT'), + } + ), + ]); + + return { access_token, refresh_token }; + } +} 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..250b162 --- /dev/null +++ b/backend/src/modules/auth-module/strategies/access-token.strategie.ts @@ -0,0 +1,22 @@ +import { PassportStrategy } from '@nestjs/passport'; +import { Strategy, ExtractJwt } from 'passport-jwt'; +import { ConfigService } from '@nestjs/config'; +import { Injectable } from '@nestjs/common'; +import { JwtPayload } from '../models/types'; + +@Injectable() +export class AccessTokenStrategy extends PassportStrategy( + Strategy, + 'jwt-access-token' +) { + constructor(private readonly configService: ConfigService) { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + secretOrKey: configService.get('JWT_SECRET_AT'), + }); + } + + public async validate(payload: JwtPayload) { + 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..dc7ee43 --- /dev/null +++ b/backend/src/modules/auth-module/strategies/refresh-token.strategie.ts @@ -0,0 +1,35 @@ +import { PassportStrategy } from '@nestjs/passport'; +import { Strategy, ExtractJwt } from 'passport-jwt'; +import { ConfigService } from '@nestjs/config'; +import { ForbiddenException, Injectable } from '@nestjs/common'; +import { Request } from 'express'; + +@Injectable() +export class RefreshTokenStrategy extends PassportStrategy( + Strategy, + 'jwt-refresh-token' +) { + constructor(private readonly configService: ConfigService) { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + secretOrKey: configService.get('JWT_SECRET_RT'), + passReqToCallback: true, + }); + } + + public async validate(req: Request, payload: any) { + const refresh_token = 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..0f955ee 100644 --- a/backend/src/modules/database-module/database-config.ts +++ b/backend/src/modules/database-module/database-config.ts @@ -1,5 +1,7 @@ -import { ConfigService } from "@nestjs/config"; -import { TypeOrmModuleOptions } from "@nestjs/typeorm"; +import { ConfigService } from '@nestjs/config'; +import { TypeOrmModuleOptions } from '@nestjs/typeorm'; +import { resolve } from 'path'; +import { User } from '../../entities/user.entity'; export const databaseConfigFactory = ( configService: ConfigService @@ -12,5 +14,5 @@ export const databaseConfigFactory = ( database: configService.get('DB_NAME'), synchronize: true, logging: true, - entities: [], + entities: [User], });