From 72b6cb35a0e6b5c6fba2f5030dfd4e3181a6b7a2 Mon Sep 17 00:00:00 2001 From: mahyargdz Date: Wed, 22 Jan 2025 09:50:03 +0330 Subject: [PATCH] chore: add user get profile and check validity route --- .env.example | 5 +- .github/workflows/deploy.yaml | 47 +++ package.json | 6 + pnpm-lock.yaml | 281 +++++++++++++++++- src/app.module.ts | 7 +- src/common/constants/index.ts | 4 + src/common/decorators/auth-guard.decorator.ts | 9 + src/common/decorators/roles.decorator.ts | 4 + src/common/decorators/user.decorator.ts | 15 + src/common/enums/message.enum.ts | 13 +- src/configs/jwt.config.ts | 11 + src/configs/rateLimit.config.ts | 21 ++ src/configs/sms.config.ts | 2 +- src/configs/swagger.config.ts | 11 + src/configs/typeorm.config.ts | 2 +- src/core/exceptions/http.exceptions.ts | 6 +- src/modules/auth/DTO/complete-register.dto.ts | 10 +- src/modules/auth/DTO/verify-otp.dto.ts | 17 ++ src/modules/auth/auth.controller.ts | 45 ++- src/modules/auth/auth.module.ts | 12 +- src/modules/auth/auth.service.ts | 58 ---- src/modules/auth/guards/auth.guard.ts | 7 + src/modules/auth/guards/role.guard.ts | 27 ++ src/modules/auth/interfaces/IToken-payload.ts | 6 + src/modules/auth/providers/auth.service.ts | 125 ++++++++ src/modules/auth/providers/tokens.service.ts | 53 ++++ src/modules/auth/strategies/jwt.strategy.ts | 22 ++ src/modules/users/DTO/check-validity.dto.ts | 17 ++ src/modules/users/DTO/update-profile.dto.ts | 3 + src/modules/users/entities/role.entity.ts | 2 +- src/modules/users/entities/user.entity.ts | 9 +- src/modules/users/enums/validity-type.enum.ts | 3 + .../users/providers/users.repository.ts | 8 +- src/modules/users/providers/users.service.ts | 65 +++- src/modules/users/users.controller.ts | 31 +- src/modules/users/users.module.ts | 6 +- src/modules/utils/providers/cache.service.ts | 4 + src/modules/utils/providers/otp.service.ts | 21 +- src/modules/utils/utils.module.ts | 6 +- 39 files changed, 874 insertions(+), 127 deletions(-) create mode 100644 .github/workflows/deploy.yaml create mode 100644 src/common/constants/index.ts create mode 100644 src/common/decorators/auth-guard.decorator.ts create mode 100644 src/common/decorators/roles.decorator.ts create mode 100644 src/common/decorators/user.decorator.ts create mode 100644 src/configs/jwt.config.ts create mode 100644 src/configs/rateLimit.config.ts create mode 100644 src/modules/auth/DTO/verify-otp.dto.ts delete mode 100644 src/modules/auth/auth.service.ts create mode 100644 src/modules/auth/guards/auth.guard.ts create mode 100644 src/modules/auth/guards/role.guard.ts create mode 100644 src/modules/auth/interfaces/IToken-payload.ts create mode 100644 src/modules/auth/providers/auth.service.ts create mode 100644 src/modules/auth/providers/tokens.service.ts create mode 100644 src/modules/auth/strategies/jwt.strategy.ts create mode 100644 src/modules/users/DTO/check-validity.dto.ts create mode 100644 src/modules/users/DTO/update-profile.dto.ts create mode 100644 src/modules/users/enums/validity-type.enum.ts diff --git a/.env.example b/.env.example index f206590..295917b 100644 --- a/.env.example +++ b/.env.example @@ -27,4 +27,7 @@ MAIL_FROM= REDIS_URI=redis://:@localhost:6379/0 -CACHE_TTL=120000 \ No newline at end of file +CACHE_TTL=120000 + +THROTTLE_TTL=300000 +THROTTLE_LIMIT=10 \ No newline at end of file diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml new file mode 100644 index 0000000..8d5a2e9 --- /dev/null +++ b/.github/workflows/deploy.yaml @@ -0,0 +1,47 @@ +name: Build and Deploy Docker Images +on: + push: + branches: + - main + +jobs: + build_and_deploy: + runs-on: ubuntu-latest + + env: + DEP_SERVER: "https://captain.run.danakcorp.com" + APP_TOKEN: 443dca14bd870283926a9ab3a0c6b41c463cb9423c654e9fbd9069059df9021a + APP_NAME: danak-dsc-api + GITHUB_TOKEN: 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 + + steps: + - name: Check out repositorys + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: zmihamid + password: ${{ env.GITHUB_TOKEN }} + + - name: Preset Image Name + run: echo "IMAGE_URL=$(echo ghcr.io/zmihamid/${{ github.event.repository.name }}:$(echo ${{ github.sha }} | cut -c1-7) | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV + + - name: Build and push Docker Image + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + push: true + tags: ${{ env.IMAGE_URL }} + + - name: Install CapRover CLI + run: npm install -g caprover + + - name: deploy to server + run: | + caprover deploy -a $CAPROVER_APP_NAME -u $DANAK_SERVER --appToken $APP_TOKEN -i "$IMAGE_URL" diff --git a/package.json b/package.json index 1f5d8c9..fab61b2 100644 --- a/package.json +++ b/package.json @@ -29,13 +29,17 @@ "dependencies": { "@fastify/static": "^7.0.4", "@keyv/redis": "^4.2.0", + "@nest-lab/throttler-storage-redis": "^1.0.0", "@nestjs-modules/mailer": "^2.0.2", "@nestjs/cache-manager": "^3.0.0", "@nestjs/common": "^10.4.15", "@nestjs/config": "^4.0.0", "@nestjs/core": "^10.4.15", + "@nestjs/jwt": "^11.0.0", + "@nestjs/passport": "^11.0.4", "@nestjs/platform-fastify": "^10.4.15", "@nestjs/swagger": "^8.1.1", + "@nestjs/throttler": "^6.3.0", "@nestjs/typeorm": "^10.0.2", "@types/bcrypt": "^5.0.2", "axios": "^1.7.9", @@ -46,6 +50,7 @@ "fastify": "^5.2.1", "handlebars": "^4.7.8", "nodemailer": "^6.9.16", + "passport-jwt": "^4.0.1", "pg": "^8.13.1", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", @@ -60,6 +65,7 @@ "@types/express": "^5.0.0", "@types/jest": "^29.5.14", "@types/node": "^20.17.14", + "@types/passport-jwt": "^4.0.1", "@types/supertest": "^6.0.2", "@typescript-eslint/eslint-plugin": "^8.20.0", "@typescript-eslint/parser": "^8.20.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 18bf7a6..af2cea6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: '@keyv/redis': specifier: ^4.2.0 version: 4.2.0 + '@nest-lab/throttler-storage-redis': + specifier: ^1.0.0 + version: 1.0.0(@nestjs/common@10.4.15(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.15(@nestjs/common@10.4.15(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/throttler@6.3.0(@nestjs/common@10.4.15(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.15(@nestjs/common@10.4.15(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2))(ioredis@5.4.2)(reflect-metadata@0.2.2) '@nestjs-modules/mailer': specifier: ^2.0.2 version: 2.0.2(@nestjs/common@10.4.15(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.15(@nestjs/common@10.4.15(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2)(rxjs@7.8.1))(nodemailer@6.9.16) @@ -29,15 +32,24 @@ importers: '@nestjs/core': specifier: ^10.4.15 version: 10.4.15(@nestjs/common@10.4.15(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/jwt': + specifier: ^11.0.0 + version: 11.0.0(@nestjs/common@10.4.15(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)) + '@nestjs/passport': + specifier: ^11.0.4 + version: 11.0.4(@nestjs/common@10.4.15(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(passport@0.7.0) '@nestjs/platform-fastify': specifier: ^10.4.15 version: 10.4.15(@fastify/static@7.0.4)(@nestjs/common@10.4.15(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.15(@nestjs/common@10.4.15(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2)(rxjs@7.8.1)) '@nestjs/swagger': specifier: ^8.1.1 version: 8.1.1(@fastify/static@7.0.4)(@nestjs/common@10.4.15(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.15(@nestjs/common@10.4.15(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.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) + '@nestjs/throttler': + specifier: ^6.3.0 + version: 6.3.0(@nestjs/common@10.4.15(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.15(@nestjs/common@10.4.15(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2) '@nestjs/typeorm': specifier: ^10.0.2 - version: 10.0.2(@nestjs/common@10.4.15(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.15(@nestjs/common@10.4.15(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2)(rxjs@7.8.1)(typeorm@0.3.20(pg@8.13.1)(redis@4.7.0)(ts-node@10.9.2(@types/node@20.17.14)(typescript@5.7.3))) + version: 10.0.2(@nestjs/common@10.4.15(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.15(@nestjs/common@10.4.15(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2)(rxjs@7.8.1)(typeorm@0.3.20(ioredis@5.4.2)(pg@8.13.1)(redis@4.7.0)(ts-node@10.9.2(@types/node@20.17.14)(typescript@5.7.3))) '@types/bcrypt': specifier: ^5.0.2 version: 5.0.2 @@ -65,6 +77,9 @@ importers: nodemailer: specifier: ^6.9.16 version: 6.9.16 + passport-jwt: + specifier: ^4.0.1 + version: 4.0.1 pg: specifier: ^8.13.1 version: 8.13.1 @@ -76,7 +91,7 @@ importers: version: 7.8.1 typeorm: specifier: ^0.3.20 - version: 0.3.20(pg@8.13.1)(redis@4.7.0)(ts-node@10.9.2(@types/node@20.17.14)(typescript@5.7.3)) + version: 0.3.20(ioredis@5.4.2)(pg@8.13.1)(redis@4.7.0)(ts-node@10.9.2(@types/node@20.17.14)(typescript@5.7.3)) devDependencies: '@commitlint/cli': specifier: ^19.6.1 @@ -102,6 +117,9 @@ importers: '@types/node': specifier: ^20.17.14 version: 20.17.14 + '@types/passport-jwt': + specifier: ^4.0.1 + version: 4.0.1 '@types/supertest': specifier: ^6.0.2 version: 6.0.2 @@ -567,6 +585,9 @@ packages: resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} deprecated: Use @eslint/object-schema instead + '@ioredis/commands@1.2.0': + resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==} + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -695,6 +716,15 @@ packages: '@microsoft/tsdoc@0.15.1': resolution: {integrity: sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==} + '@nest-lab/throttler-storage-redis@1.0.0': + resolution: {integrity: sha512-k0eVe1yzTF8kgi5xQ+httwVBsNGDcq2liBLYK++ROpqiHUb7QxXet3JgERTQ4PWusyacPKz5DCtw9ijmGBx5YQ==} + peerDependencies: + '@nestjs/common': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 + '@nestjs/core': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 + '@nestjs/throttler': '>=6.0.0' + ioredis: '>=5.0.0' + reflect-metadata: ^0.2.1 + '@nestjs-modules/mailer@2.0.2': resolution: {integrity: sha512-+z4mADQasg0H1ZaGu4zZTuKv2pu+XdErqx99PLFPzCDNTN/q9U59WPgkxVaHnsvKHNopLj5Xap7G4ZpptduoYw==} peerDependencies: @@ -759,6 +789,11 @@ packages: '@nestjs/websockets': optional: true + '@nestjs/jwt@11.0.0': + resolution: {integrity: sha512-v7YRsW3Xi8HNTsO+jeHSEEqelX37TVWgwt+BcxtkG/OfXJEOs6GZdbdza200d6KqId1pJQZ6UPj1F0M6E+mxaA==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + '@nestjs/mapped-types@2.0.6': resolution: {integrity: sha512-84ze+CPfp1OWdpRi1/lOu59hOhTz38eVzJvRKrg9ykRFwDz+XleKfMsG0gUqNZYFa6v53XYzeD+xItt8uDW7NQ==} peerDependencies: @@ -772,6 +807,12 @@ packages: class-validator: optional: true + '@nestjs/passport@11.0.4': + resolution: {integrity: sha512-XpqLKPlo6yt0tFSAKgh1Q2j442UWhaL8qPh6GoMVTi5MsvK/uYD2aaMr3wrwyFIpH0/hjOqKC7TI5+Dw9e7/ng==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + passport: ^0.5.0 || ^0.6.0 || ^0.7.0 + '@nestjs/platform-fastify@10.4.15': resolution: {integrity: sha512-h1raWeoQEI3Q1wLOLDTbwpObFBMbdq22DlJNyTNY1pyNDyc5K99mnhFnk+eAiM/Di+pjewv5Nh2CzPUZIguA/g==} peerDependencies: @@ -820,6 +861,13 @@ packages: '@nestjs/platform-express': optional: true + '@nestjs/throttler@6.3.0': + resolution: {integrity: sha512-IqTMbl5Iyxjts7NwbVriDND0Cnr8rwNqAPpF5HJE+UV+2VrVUBwCfDXKEiXu47vzzaQLlWPYegBsGO9OXxa+oQ==} + peerDependencies: + '@nestjs/common': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 + '@nestjs/core': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 + reflect-metadata: ^0.1.13 || ^0.2.0 + '@nestjs/typeorm@10.0.2': resolution: {integrity: sha512-H738bJyydK4SQkRCTeh1aFBxoO1E9xdL/HaLGThwrqN95os5mEyAtK7BLADOS+vldP4jDZ2VQPLj4epWwRqCeQ==} peerDependencies: @@ -988,6 +1036,9 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + '@types/jsonwebtoken@9.0.7': + resolution: {integrity: sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==} + '@types/methods@1.1.4': resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} @@ -1003,6 +1054,15 @@ packages: '@types/node@20.17.14': resolution: {integrity: sha512-w6qdYetNL5KRBiSClK/KWai+2IMEJuAj+EujKCumalFOwXtvOXaEan9AuwcRID2IcOIAWSIfR495hBtgKlx2zg==} + '@types/passport-jwt@4.0.1': + resolution: {integrity: sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==} + + '@types/passport-strategy@0.2.38': + resolution: {integrity: sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==} + + '@types/passport@1.0.17': + resolution: {integrity: sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==} + '@types/pug@2.0.10': resolution: {integrity: sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==} @@ -1403,6 +1463,9 @@ packages: bser@2.1.1: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -1776,6 +1839,10 @@ packages: delegates@1.0.0: resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -1871,6 +1938,9 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + editorconfig@1.0.4: resolution: {integrity: sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==} engines: {node: '>=14'} @@ -2553,6 +2623,10 @@ packages: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} + ioredis@5.4.2: + resolution: {integrity: sha512-0SZXGNGZ+WzISQ67QDyZ2x0+wVxjjUndtD8oSeik/4ajifeiRufed8fCb8QW8VMyi4MXcS+UO1k/0NGhvq1PAg==} + engines: {node: '>=12.22.0'} + ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} @@ -2981,6 +3055,10 @@ packages: resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} engines: {'0': node >= 0.2.0} + jsonwebtoken@9.0.2: + resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} + engines: {node: '>=12', npm: '>=6'} + jstransformer@1.0.0: resolution: {integrity: sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A==} @@ -2989,6 +3067,12 @@ packages: engines: {node: '>=10.0.0'} hasBin: true + jwa@1.4.1: + resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==} + + jws@3.2.2: + resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -3071,9 +3155,30 @@ packages: lodash.camelcase@4.3.0: resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + lodash.defaults@4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isarguments@3.1.0: + resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + lodash.isplainobject@4.0.6: resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + lodash.kebabcase@4.1.1: resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==} @@ -3086,6 +3191,9 @@ packages: lodash.mergewith@4.6.2: resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + lodash.snakecase@4.1.1: resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} @@ -3580,6 +3688,17 @@ packages: parseley@0.12.1: resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==} + passport-jwt@4.0.1: + resolution: {integrity: sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==} + + passport-strategy@1.0.0: + resolution: {integrity: sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==} + engines: {node: '>= 0.4.0'} + + passport@0.7.0: + resolution: {integrity: sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==} + engines: {node: '>= 0.4.0'} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -3621,6 +3740,9 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + pause@0.0.1: + resolution: {integrity: sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==} + peberminta@0.9.0: resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==} @@ -3839,6 +3961,14 @@ packages: resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} engines: {node: '>= 12.13.0'} + redis-errors@1.2.0: + resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} + engines: {node: '>=4'} + + redis-parser@3.0.0: + resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} + engines: {node: '>=4'} + redis@4.7.0: resolution: {integrity: sha512-zvmkHEAdGMn+hMRXuMBtu4Vo5P6rHQjLoHftu+lBqq8ZTA3RCVC/WzD790bkKKiNFp7d5/9PcSD19fJyyRvOdQ==} @@ -4110,6 +4240,9 @@ packages: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} + standard-as-callback@2.1.0: + resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + statuses@2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} @@ -4512,6 +4645,10 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + uuid@9.0.1: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} hasBin: true @@ -5183,6 +5320,8 @@ snapshots: '@humanwhocodes/object-schema@2.0.3': {} + '@ioredis/commands@1.2.0': {} + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -5426,6 +5565,15 @@ snapshots: '@microsoft/tsdoc@0.15.1': {} + '@nest-lab/throttler-storage-redis@1.0.0(@nestjs/common@10.4.15(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.15(@nestjs/common@10.4.15(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/throttler@6.3.0(@nestjs/common@10.4.15(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.15(@nestjs/common@10.4.15(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2))(ioredis@5.4.2)(reflect-metadata@0.2.2)': + dependencies: + '@nestjs/common': 10.4.15(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/core': 10.4.15(@nestjs/common@10.4.15(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/throttler': 6.3.0(@nestjs/common@10.4.15(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.15(@nestjs/common@10.4.15(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2) + ioredis: 5.4.2 + reflect-metadata: 0.2.2 + tslib: 2.8.1 + '@nestjs-modules/mailer@2.0.2(@nestjs/common@10.4.15(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.15(@nestjs/common@10.4.15(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2)(rxjs@7.8.1))(nodemailer@6.9.16)': dependencies: '@css-inline/css-inline': 0.14.1 @@ -5512,6 +5660,12 @@ snapshots: transitivePeerDependencies: - encoding + '@nestjs/jwt@11.0.0(@nestjs/common@10.4.15(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))': + dependencies: + '@nestjs/common': 10.4.15(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@types/jsonwebtoken': 9.0.7 + jsonwebtoken: 9.0.2 + '@nestjs/mapped-types@2.0.6(@nestjs/common@10.4.15(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)': dependencies: '@nestjs/common': 10.4.15(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -5520,6 +5674,11 @@ snapshots: class-transformer: 0.5.1 class-validator: 0.14.1 + '@nestjs/passport@11.0.4(@nestjs/common@10.4.15(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(passport@0.7.0)': + dependencies: + '@nestjs/common': 10.4.15(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) + passport: 0.7.0 + '@nestjs/platform-fastify@10.4.15(@fastify/static@7.0.4)(@nestjs/common@10.4.15(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.15(@nestjs/common@10.4.15(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2)(rxjs@7.8.1))': dependencies: '@fastify/cors': 9.0.1 @@ -5578,13 +5737,19 @@ snapshots: '@nestjs/core': 10.4.15(@nestjs/common@10.4.15(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2)(rxjs@7.8.1) tslib: 2.8.1 - '@nestjs/typeorm@10.0.2(@nestjs/common@10.4.15(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.15(@nestjs/common@10.4.15(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2)(rxjs@7.8.1)(typeorm@0.3.20(pg@8.13.1)(redis@4.7.0)(ts-node@10.9.2(@types/node@20.17.14)(typescript@5.7.3)))': + '@nestjs/throttler@6.3.0(@nestjs/common@10.4.15(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.15(@nestjs/common@10.4.15(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2)': + dependencies: + '@nestjs/common': 10.4.15(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/core': 10.4.15(@nestjs/common@10.4.15(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2)(rxjs@7.8.1) + reflect-metadata: 0.2.2 + + '@nestjs/typeorm@10.0.2(@nestjs/common@10.4.15(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.15(@nestjs/common@10.4.15(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2)(rxjs@7.8.1)(typeorm@0.3.20(ioredis@5.4.2)(pg@8.13.1)(redis@4.7.0)(ts-node@10.9.2(@types/node@20.17.14)(typescript@5.7.3)))': dependencies: '@nestjs/common': 10.4.15(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/core': 10.4.15(@nestjs/common@10.4.15(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2)(rxjs@7.8.1) reflect-metadata: 0.2.2 rxjs: 7.8.1 - typeorm: 0.3.20(pg@8.13.1)(redis@4.7.0)(ts-node@10.9.2(@types/node@20.17.14)(typescript@5.7.3)) + typeorm: 0.3.20(ioredis@5.4.2)(pg@8.13.1)(redis@4.7.0)(ts-node@10.9.2(@types/node@20.17.14)(typescript@5.7.3)) uuid: 9.0.1 '@nodelib/fs.scandir@2.1.5': @@ -5765,6 +5930,10 @@ snapshots: '@types/json5@0.0.29': {} + '@types/jsonwebtoken@9.0.7': + dependencies: + '@types/node': 20.17.14 + '@types/methods@1.1.4': {} '@types/mime@1.3.5': {} @@ -5781,6 +5950,20 @@ snapshots: dependencies: undici-types: 6.19.8 + '@types/passport-jwt@4.0.1': + dependencies: + '@types/jsonwebtoken': 9.0.7 + '@types/passport-strategy': 0.2.38 + + '@types/passport-strategy@0.2.38': + dependencies: + '@types/express': 5.0.0 + '@types/passport': 1.0.17 + + '@types/passport@1.0.17': + dependencies: + '@types/express': 5.0.0 + '@types/pug@2.0.10': optional: true @@ -6300,6 +6483,8 @@ snapshots: dependencies: node-int64: 0.4.0 + buffer-equal-constant-time@1.0.1: {} + buffer-from@1.1.2: {} buffer@5.7.1: @@ -6693,6 +6878,8 @@ snapshots: delegates@1.0.0: {} + denque@2.1.0: {} + depd@2.0.0: {} dequal@2.0.3: {} @@ -6797,6 +6984,10 @@ snapshots: eastasianwidth@0.2.0: {} + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + editorconfig@1.0.4: dependencies: '@one-ini/wasm': 0.1.1 @@ -7683,6 +7874,20 @@ snapshots: hasown: 2.0.2 side-channel: 1.1.0 + ioredis@5.4.2: + dependencies: + '@ioredis/commands': 1.2.0 + cluster-key-slot: 1.1.2 + debug: 4.4.0 + denque: 2.1.0 + lodash.defaults: 4.2.0 + lodash.isarguments: 3.1.0 + redis-errors: 1.2.0 + redis-parser: 3.0.0 + standard-as-callback: 2.1.0 + transitivePeerDependencies: + - supports-color + ipaddr.js@1.9.1: {} ipaddr.js@2.2.0: {} @@ -8290,6 +8495,19 @@ snapshots: jsonparse@1.3.1: {} + jsonwebtoken@9.0.2: + 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.3 + jstransformer@1.0.0: dependencies: is-promise: 2.2.2 @@ -8307,6 +8525,17 @@ snapshots: - encoding optional: true + jwa@1.4.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@3.2.2: + dependencies: + jwa: 1.4.1 + safe-buffer: 5.2.1 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -8409,8 +8638,22 @@ snapshots: lodash.camelcase@4.3.0: {} + lodash.defaults@4.2.0: {} + + lodash.includes@4.3.0: {} + + lodash.isarguments@3.1.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.kebabcase@4.1.1: {} lodash.memoize@4.1.2: {} @@ -8419,6 +8662,8 @@ snapshots: lodash.mergewith@4.6.2: {} + lodash.once@4.1.1: {} + lodash.snakecase@4.1.1: {} lodash.startcase@4.4.0: {} @@ -9152,6 +9397,19 @@ snapshots: peberminta: 0.9.0 optional: true + passport-jwt@4.0.1: + dependencies: + jsonwebtoken: 9.0.2 + passport-strategy: 1.0.0 + + passport-strategy@1.0.0: {} + + passport@0.7.0: + dependencies: + passport-strategy: 1.0.0 + pause: 0.0.1 + utils-merge: 1.0.1 + path-exists@4.0.0: {} path-exists@5.0.0: {} @@ -9178,6 +9436,8 @@ snapshots: path-type@4.0.0: {} + pause@0.0.1: {} + peberminta@0.9.0: optional: true @@ -9437,6 +9697,12 @@ snapshots: real-require@0.2.0: {} + redis-errors@1.2.0: {} + + redis-parser@3.0.0: + dependencies: + redis-errors: 1.2.0 + redis@4.7.0: dependencies: '@redis/bloom': 1.2.0(@redis/client@1.6.0) @@ -9723,6 +9989,8 @@ snapshots: dependencies: escape-string-regexp: 2.0.0 + standard-as-callback@2.1.0: {} + statuses@2.0.1: {} string-argv@0.3.2: {} @@ -10036,7 +10304,7 @@ snapshots: possible-typed-array-names: 1.0.0 reflect.getprototypeof: 1.0.10 - typeorm@0.3.20(pg@8.13.1)(redis@4.7.0)(ts-node@10.9.2(@types/node@20.17.14)(typescript@5.7.3)): + typeorm@0.3.20(ioredis@5.4.2)(pg@8.13.1)(redis@4.7.0)(ts-node@10.9.2(@types/node@20.17.14)(typescript@5.7.3)): dependencies: '@sqltools/formatter': 1.2.5 app-root-path: 3.1.0 @@ -10054,6 +10322,7 @@ snapshots: uuid: 9.0.1 yargs: 17.7.2 optionalDependencies: + ioredis: 5.4.2 pg: 8.13.1 redis: 4.7.0 ts-node: 10.9.2(@types/node@20.17.14)(typescript@5.7.3) @@ -10102,6 +10371,8 @@ snapshots: util-deprecate@1.0.2: {} + utils-merge@1.0.1: {} + uuid@9.0.1: {} v8-compile-cache-lib@3.0.1: {} diff --git a/src/app.module.ts b/src/app.module.ts index d09f81a..8ee0b4c 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,10 +1,12 @@ import { CacheModule } from "@nestjs/cache-manager"; import { MiddlewareConsumer, Module, NestModule } from "@nestjs/common"; import { ConfigModule } from "@nestjs/config"; +import { ThrottlerModule } from "@nestjs/throttler"; import { TypeOrmModule } from "@nestjs/typeorm"; import { cacheConfig } from "./configs/cache.config"; -import { DatabaseConfigs } from "./configs/typeorm.config"; +import { rateLimitConfig } from "./configs/rateLimit.config"; +import { databaseConfigs } from "./configs/typeorm.config"; import { HTTPLogger } from "./core/middlewares/logger.middleware"; import { AuthModule } from "./modules/auth/auth.module"; import { TicketsModule } from "./modules/tickets/tickets.module"; @@ -12,9 +14,10 @@ import { UsersModule } from "./modules/users/users.module"; @Module({ imports: [ + ThrottlerModule.forRootAsync(rateLimitConfig()), ConfigModule.forRoot({ cache: true, isGlobal: true }), CacheModule.registerAsync(cacheConfig()), - TypeOrmModule.forRootAsync(DatabaseConfigs()), + TypeOrmModule.forRootAsync(databaseConfigs()), UsersModule, TicketsModule, AuthModule, diff --git a/src/common/constants/index.ts b/src/common/constants/index.ts new file mode 100644 index 0000000..8fb1921 --- /dev/null +++ b/src/common/constants/index.ts @@ -0,0 +1,4 @@ +// export const AUTH_THROTTLE = "AUTH_THROTTLE"; +export const AUTH_THROTTLE_TTL = 5 * 60 * 1000; +export const AUTH_THROTTLE_LIMIT = 5; +export const JWT_STRATEGY_NAME = "jwt_Strategy"; diff --git a/src/common/decorators/auth-guard.decorator.ts b/src/common/decorators/auth-guard.decorator.ts new file mode 100644 index 0000000..19bf11b --- /dev/null +++ b/src/common/decorators/auth-guard.decorator.ts @@ -0,0 +1,9 @@ +import { UseGuards, applyDecorators } from "@nestjs/common"; +import { ApiBearerAuth } from "@nestjs/swagger"; + +import { JwtAuthGuard } from "../../modules/auth/guards/auth.guard"; +import { RoleGuard } from "../../modules/auth/guards/role.guard"; + +export function AuthGuards() { + return applyDecorators(UseGuards(JwtAuthGuard, RoleGuard), ApiBearerAuth("authorization")); +} diff --git a/src/common/decorators/roles.decorator.ts b/src/common/decorators/roles.decorator.ts new file mode 100644 index 0000000..3d51da8 --- /dev/null +++ b/src/common/decorators/roles.decorator.ts @@ -0,0 +1,4 @@ +import { SetMetadata } from "@nestjs/common"; + +export const ROLES_KEY = "roles"; +export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles); diff --git a/src/common/decorators/user.decorator.ts b/src/common/decorators/user.decorator.ts new file mode 100644 index 0000000..8fca5f3 --- /dev/null +++ b/src/common/decorators/user.decorator.ts @@ -0,0 +1,15 @@ +import { ExecutionContext, createParamDecorator } from "@nestjs/common"; +import { FastifyRequest } from "fastify"; + +import { User } from "../../modules/users/entities/user.entity"; + +declare module "fastify" { + interface FastifyRequest { + user?: Omit; + } +} + +export const UserDec = createParamDecorator((_data: unknown, ctx: ExecutionContext) => { + const req = ctx.switchToHttp().getRequest(); + return req.user; +}); diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index 527cbe0..8e167d9 100644 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -1,4 +1,5 @@ export const enum AuthMessage { + UNAUTHORIZED_ACCESS = "شما دسترسی لازم برای این عملیات را ندارید.", PHONE_REGISTERED = "شماره ثبت شد", INVALID_PHONE_FORMAT = "فرمت شماره تلفن صحیح نیست", OTP_SENT = "کد یکبار مصرف به شماره شما ارسال شد.", @@ -20,7 +21,7 @@ export const enum AuthMessage { USER_EXISTS = "با این شماره قبلا ثبت نام شده است", USER_REGISTER_SUCCESS = "ثبت نام موفقیت آمیز بود", INVALID_USER = "کاربری با این مشخصات یافت نشد", - PHONE_NOT_FOUND = "کاربری با این آیدی یافت نشد", + PHONE_NOT_FOUND = "کاربری با این شماره یافت نشد", TOKEN_EXPIRED = "توکن منقضی شده است", TOKEN_INVALID = "توکن نامعتبر است", BANNED = "در حال حاضر شما امکان دسترسی به این سرویس را ندارید", @@ -46,9 +47,19 @@ export const enum AuthMessage { BIRTH_DATE_NOT_EMPTY = "تاریخ تولد نمی‌تواند خالی باشد", NATIONAL_CODE_INCORRECT = "کد ملی باید ۱۰ رقمی باشد", NATIONAL_NOT_EMPTY = "کد ملی نمی‌تواند خالی باشد", + OTP_ALREADY_SENT = "کد یکبار مصرف قبلا ارسال شده است", + TOO_MANY_REQUESTS = "تعداد درخواست های شما بیش از حد مجاز است", } export const enum UserMessage { USER_NOT_FOUND = "کاربری با این مشخصات یافت نشد", EMAIL_EXIST = "ایمیل قبلا ثبت شده است", + ROLE_NOT_FOUND = "نقش یافت نشد", + USER_EXISTS = "کاربر با این مشخصات وجود دارد", +} + +export const enum CommonMessage { + VALIDITY_TYPE_REQUIRED = "نوع اعتبار سنجی مورد نیاز است", + THIS_FILED_IS_REQUIRED = "این فیلد الزامی است", + VALID_FOR_CHOOSE = "معتبر برای انتخاب", } diff --git a/src/configs/jwt.config.ts b/src/configs/jwt.config.ts new file mode 100644 index 0000000..882203b --- /dev/null +++ b/src/configs/jwt.config.ts @@ -0,0 +1,11 @@ +import { ConfigService } from "@nestjs/config"; +import { JwtModuleAsyncOptions } from "@nestjs/jwt"; + +export function jwtConfig(): JwtModuleAsyncOptions { + return { + inject: [ConfigService], + useFactory: (configService: ConfigService) => ({ + secret: configService.getOrThrow("JWT_SECRET_KEY"), + }), + }; +} diff --git a/src/configs/rateLimit.config.ts b/src/configs/rateLimit.config.ts new file mode 100644 index 0000000..3499c1d --- /dev/null +++ b/src/configs/rateLimit.config.ts @@ -0,0 +1,21 @@ +import { ThrottlerStorageRedisService } from "@nest-lab/throttler-storage-redis"; +import { ConfigService } from "@nestjs/config"; +import { ThrottlerAsyncOptions } from "@nestjs/throttler"; + +import { AuthMessage } from "../common/enums/message.enum"; + +export function rateLimitConfig(): ThrottlerAsyncOptions { + return { + inject: [ConfigService], + useFactory: (configService: ConfigService) => ({ + throttlers: [ + { + ttl: configService.getOrThrow("THROTTLE_TTL"), + limit: configService.getOrThrow("THROTTLE_LIMIT"), + }, + ], + errorMessage: AuthMessage.TOO_MANY_REQUESTS, + storage: new ThrottlerStorageRedisService(configService.getOrThrow("REDIS_URI")), + }), + }; +} diff --git a/src/configs/sms.config.ts b/src/configs/sms.config.ts index b68e510..0d9e735 100644 --- a/src/configs/sms.config.ts +++ b/src/configs/sms.config.ts @@ -1,6 +1,6 @@ import { ConfigService } from "@nestjs/config"; -export function SmsConfigs() { +export function smsConfigs() { return { inject: [ConfigService], useFactory(configService: ConfigService) { diff --git a/src/configs/swagger.config.ts b/src/configs/swagger.config.ts index 72349b6..1b123bf 100644 --- a/src/configs/swagger.config.ts +++ b/src/configs/swagger.config.ts @@ -5,6 +5,17 @@ export function getSwaggerDocument(app: NestFastifyApplication) { const swaggerConfig = new DocumentBuilder() .setTitle("The Danak dsc api document") .setDescription("The Danak dsc API description") + .addBearerAuth( + { + type: "http", + scheme: "bearer", + bearerFormat: "JWT", + name: "authorization", + in: "header", + }, + "authorization", + ) + .setVersion("1.0.0") .build(); diff --git a/src/configs/typeorm.config.ts b/src/configs/typeorm.config.ts index f642b8f..69adcce 100644 --- a/src/configs/typeorm.config.ts +++ b/src/configs/typeorm.config.ts @@ -1,7 +1,7 @@ import { ConfigService } from "@nestjs/config"; import { TypeOrmModuleAsyncOptions } from "@nestjs/typeorm"; -export function DatabaseConfigs(): TypeOrmModuleAsyncOptions { +export function databaseConfigs(): TypeOrmModuleAsyncOptions { return { inject: [ConfigService], useFactory(configService: ConfigService) { diff --git a/src/core/exceptions/http.exceptions.ts b/src/core/exceptions/http.exceptions.ts index 2b7db3e..d959e9a 100644 --- a/src/core/exceptions/http.exceptions.ts +++ b/src/core/exceptions/http.exceptions.ts @@ -1,12 +1,12 @@ import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus } from "@nestjs/common"; -import { FastifyReply, FastifyRequest } from "fastify"; +import { FastifyReply } from "fastify"; @Catch(HttpException) export class HttpExceptionFilter implements ExceptionFilter { catch(exception: HttpException, host: ArgumentsHost) { const ctx = host.switchToHttp(); const reply = ctx.getResponse(); - const request = ctx.getRequest(); + // const request = ctx.getRequest(); const status = exception.getStatus() || HttpStatus.INTERNAL_SERVER_ERROR; const exceptionResponse = exception.getResponse(); @@ -18,7 +18,7 @@ export class HttpExceptionFilter implements ExceptionFilter { error: { message, // timestamp: new Date().toISOString(), - path: request.url, + // path: request.url, }, }); } diff --git a/src/modules/auth/DTO/complete-register.dto.ts b/src/modules/auth/DTO/complete-register.dto.ts index e4a0f53..b05bde3 100644 --- a/src/modules/auth/DTO/complete-register.dto.ts +++ b/src/modules/auth/DTO/complete-register.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from "@nestjs/swagger"; -import { IsEmail, IsMobilePhone, IsNotEmpty, IsNumberString, IsString, Length, MinLength } from "class-validator"; +import { IsMobilePhone, IsNotEmpty, IsNumberString, IsString, Length, MinLength } from "class-validator"; import { AuthMessage } from "../../../common/enums/message.enum"; @@ -27,10 +27,10 @@ export class CompleteRegistrationDto { @ApiProperty({ description: "Last name", example: "jamshidi" }) lastName: string; - @IsNotEmpty({ message: AuthMessage.EMAIL_NOT_EMPTY }) - @IsEmail({}, { message: AuthMessage.INVALID_EMAIL_FORMAT }) - @ApiProperty({ description: "Email", example: "ma@gmail.com" }) - email: string; + // @IsNotEmpty({ message: AuthMessage.EMAIL_NOT_EMPTY }) + // @IsEmail({}, { message: AuthMessage.INVALID_EMAIL_FORMAT }) + // @ApiProperty({ description: "Email", example: "ma@gmail.com" }) + // email: string; @IsNotEmpty({ message: AuthMessage.BIRTH_DATE_NOT_EMPTY }) @IsString({ message: AuthMessage.BIRTH_DATE_NOT_EMPTY }) diff --git a/src/modules/auth/DTO/verify-otp.dto.ts b/src/modules/auth/DTO/verify-otp.dto.ts new file mode 100644 index 0000000..116f768 --- /dev/null +++ b/src/modules/auth/DTO/verify-otp.dto.ts @@ -0,0 +1,17 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsMobilePhone, IsNotEmpty, IsNumberString, Length } from "class-validator"; + +import { AuthMessage } from "../../../common/enums/message.enum"; + +export class VerifyOtpDto { + @IsNotEmpty({ message: AuthMessage.PHONE_NOT_EMPTY }) + @IsMobilePhone("fa-IR", {}, { message: AuthMessage.INVALID_PHONE_FORMAT }) + @ApiProperty({ description: "phone number", default: "09123456789" }) + phone: string; + + @ApiProperty({ description: "OTP code received via SMS", example: "56893" }) + @IsNotEmpty({ message: AuthMessage.OTP_NOT_EMPTY }) + @IsNumberString(undefined, { message: AuthMessage.OTP_FORMAT_INVALID }) + @Length(5, 5, { message: AuthMessage.OTP_FORMAT_INVALID }) + code: string; +} diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index b49ae30..6ef1e8d 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -1,41 +1,54 @@ -import { Body, Controller, Post } from "@nestjs/common"; +import { Body, Controller, HttpCode, HttpStatus, Post, UseGuards } from "@nestjs/common"; import { ApiOperation, ApiTags } from "@nestjs/swagger"; +import { Throttle, ThrottlerGuard } from "@nestjs/throttler"; -import { AuthService } from "./auth.service"; import { CompleteRegistrationDto } from "./DTO/complete-register.dto"; +import { LoginPasswordDTO } from "./DTO/loginPassword.dto"; import { RequestOtpDto } from "./DTO/request-otp.dto"; +import { VerifyOtpDto } from "./DTO/verify-otp.dto"; +import { AuthService } from "./providers/auth.service"; +import { AUTH_THROTTLE_LIMIT, AUTH_THROTTLE_TTL } from "../../common/constants"; @ApiTags("Auth") @Controller("auth") +@Throttle({ default: { limit: AUTH_THROTTLE_LIMIT, ttl: AUTH_THROTTLE_TTL } }) +@UseGuards(ThrottlerGuard) export class AuthController { constructor(private authService: AuthService) {} @ApiOperation({ summary: "Initiate registration" }) + @HttpCode(HttpStatus.OK) @Post("register/initiate") - async register(@Body() registerDto: RequestOtpDto) { + register(@Body() registerDto: RequestOtpDto) { return this.authService.initiateRegistration(registerDto); } @ApiOperation({ summary: "complete registration ==> step 2" }) @Post("register/complete") - async completeRegistration(@Body() completeRegistrationDto: CompleteRegistrationDto) { + completeRegistration(@Body() completeRegistrationDto: CompleteRegistrationDto) { return this.authService.completeRegistration(completeRegistrationDto); } - // @Post("login") - // async login(@Body() loginDto: LoginDto) { - // return this.authService.login(loginDto); - // } + @ApiOperation({ summary: "request to login with otp" }) + @HttpCode(HttpStatus.OK) + @Post("otp/send") + sendOtp(@Body() requestOtpDto: RequestOtpDto) { + return this.authService.requestLoginOtp(requestOtpDto); + } - // @Post("otp/register/send") - // async sendOtp(@Body() sendOtpDto: RequestOtpDto) { - // return this.authService.requestRegisterOTP(sendOtpDto); - // } + @ApiOperation({ summary: "verify otp for login" }) + @HttpCode(HttpStatus.OK) + @Post("otp/verify") + verifyOtp(@Body() verifyOtpDto: VerifyOtpDto) { + return this.authService.verifyLoginOtp(verifyOtpDto); + } - // @Post("otp/verify") - // async verifyOtp(@Body() verifyOtpDto: VerifyOtpDto) { - // return this.authService.verifyOtp(verifyOtpDto); - // } + @ApiOperation({ summary: "login with password" }) + @HttpCode(HttpStatus.OK) + @Post("login/password") + loginWithPassword(@Body() loginPasswordDto: LoginPasswordDTO) { + return this.authService.loginWithPassword(loginPasswordDto); + } // @Post("forgot-password") // async forgotPassword(@Body() forgotPasswordDto: ForgotPasswordDto) { diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts index cc39e8f..7e96ebf 100644 --- a/src/modules/auth/auth.module.ts +++ b/src/modules/auth/auth.module.ts @@ -1,13 +1,19 @@ import { Module } from "@nestjs/common"; +import { JwtModule } from "@nestjs/jwt"; +import { PassportModule } from "@nestjs/passport"; import { AuthController } from "./auth.controller"; -import { AuthService } from "./auth.service"; +import { jwtConfig } from "../../configs/jwt.config"; import { UsersModule } from "../users/users.module"; import { UtilsModule } from "../utils/utils.module"; +import { AuthService } from "./providers/auth.service"; +import { TokensService } from "./providers/tokens.service"; +import { JwtStrategy } from "./strategies/jwt.strategy"; @Module({ - imports: [UtilsModule, UsersModule], + imports: [UtilsModule, UsersModule, PassportModule, JwtModule.registerAsync(jwtConfig())], controllers: [AuthController], - providers: [AuthService], + providers: [AuthService, TokensService, JwtStrategy], + exports: [AuthService], }) export class AuthModule {} diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts deleted file mode 100644 index c234e8d..0000000 --- a/src/modules/auth/auth.service.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { BadRequestException, Injectable, Logger } from "@nestjs/common"; - -import { CompleteRegistrationDto } from "./DTO/complete-register.dto"; -import { LoginPasswordDTO } from "./DTO/loginPassword.dto"; -import { RequestOtpDto } from "./DTO/request-otp.dto"; -import { AuthMessage, UserMessage } from "../../common/enums/message.enum"; -import { UsersService } from "../users/providers/users.service"; -import { OTPService } from "../utils/providers/otp.service"; -import { PasswordService } from "../utils/providers/password.service"; - -@Injectable() -export class AuthService { - private readonly logger = new Logger(AuthService.name); - constructor( - private readonly userService: UsersService, - private readonly passwordService: PasswordService, - private readonly otpService: OTPService, - ) {} - //****************** */ - async initiateRegistration(requestOtpDto: RequestOtpDto) { - const { phone } = requestOtpDto; - const otpCode = await this.otpService.generateAndSetInCache(phone, "REGISTER"); - // await SmsService.sendOtp(phone, otpCode); - this.logger.log(`OTP sent to ${phone}: ${otpCode}`); - - return { - message: AuthMessage.OTP_SENT, - otpCode, - }; - } - //****************** */ - async completeRegistration(completeRegistrationDto: CompleteRegistrationDto) { - const { phone, code } = completeRegistrationDto; - const isValid = await this.otpService.verifyOtp(phone, code, "REGISTER"); - - if (!isValid) throw new BadRequestException(AuthMessage.INVALID_OTP); - - await this.otpService.delOtpFormCache(phone, "REGISTER"); - const hashedPassword = await this.passwordService.hashPassword(completeRegistrationDto.password); - const user = await this.userService.createUser(completeRegistrationDto, hashedPassword); - - return { - message: AuthMessage.USER_REGISTER_SUCCESS, - user, - }; - } - //****************** */ - - async loginWithPassword(loginDto: LoginPasswordDTO) { - const { email, password } = loginDto; - - const user = await this.userService.findOneWithEmail(email); - if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND); - - const passCompare = await this.passwordService.comparePassword(password, user.password); - if (!passCompare) throw new BadRequestException(AuthMessage.INVALID_PASSWORD); - } -} diff --git a/src/modules/auth/guards/auth.guard.ts b/src/modules/auth/guards/auth.guard.ts new file mode 100644 index 0000000..908f55c --- /dev/null +++ b/src/modules/auth/guards/auth.guard.ts @@ -0,0 +1,7 @@ +import { Injectable } from "@nestjs/common"; +import { AuthGuard } from "@nestjs/passport"; + +import { JWT_STRATEGY_NAME } from "../../../common/constants"; + +@Injectable() +export class JwtAuthGuard extends AuthGuard(JWT_STRATEGY_NAME) {} diff --git a/src/modules/auth/guards/role.guard.ts b/src/modules/auth/guards/role.guard.ts new file mode 100644 index 0000000..fd3356e --- /dev/null +++ b/src/modules/auth/guards/role.guard.ts @@ -0,0 +1,27 @@ +import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from "@nestjs/common"; +import { Reflector } from "@nestjs/core"; +import { FastifyRequest } from "fastify"; + +import { ROLES_KEY } from "../../../common/decorators/roles.decorator"; +import { AuthMessage } from "../../../common/enums/message.enum"; +import { RoleEnum } from "../../users/enums/role.enum"; + +@Injectable() +export class RoleGuard implements CanActivate { + constructor(private reflector: Reflector) {} + + canActivate(context: ExecutionContext): boolean { + const requiredRole = this.reflector.getAllAndOverride(ROLES_KEY, [context.getHandler(), context.getClass()]); + if (!requiredRole) return true; + + const req = context.switchToHttp().getRequest(); + const user = req.user; + + if (!user) throw new ForbiddenException(AuthMessage.UNAUTHORIZED_ACCESS); + + const hasRequiredRole = requiredRole.includes(user.role.name); + if (!hasRequiredRole) throw new ForbiddenException(AuthMessage.UNAUTHORIZED_ACCESS); + + return true; + } +} diff --git a/src/modules/auth/interfaces/IToken-payload.ts b/src/modules/auth/interfaces/IToken-payload.ts new file mode 100644 index 0000000..ce50705 --- /dev/null +++ b/src/modules/auth/interfaces/IToken-payload.ts @@ -0,0 +1,6 @@ +import { RoleEnum } from "../../users/enums/role.enum"; + +export interface ITokenPayload { + sub: string; + role: RoleEnum; +} diff --git a/src/modules/auth/providers/auth.service.ts b/src/modules/auth/providers/auth.service.ts new file mode 100644 index 0000000..e8a84d1 --- /dev/null +++ b/src/modules/auth/providers/auth.service.ts @@ -0,0 +1,125 @@ +import { BadRequestException, Injectable, Logger } from "@nestjs/common"; + +import { TokensService } from "./tokens.service"; +import { AuthMessage, UserMessage } from "../../../common/enums/message.enum"; +import { UsersService } from "../../users/providers/users.service"; +import { OTPService } from "../../utils/providers/otp.service"; +import { PasswordService } from "../../utils/providers/password.service"; +import { CompleteRegistrationDto } from "../DTO/complete-register.dto"; +import { LoginPasswordDTO } from "../DTO/loginPassword.dto"; +import { RequestOtpDto } from "../DTO/request-otp.dto"; +import { VerifyOtpDto } from "../DTO/verify-otp.dto"; + +@Injectable() +export class AuthService { + private readonly logger = new Logger(AuthService.name); + constructor( + private readonly usersService: UsersService, + private readonly passwordService: PasswordService, + private readonly otpService: OTPService, + private readonly tokensService: TokensService, + ) {} + //****************** */ + async initiateRegistration(requestOtpDto: RequestOtpDto) { + const { phone } = requestOtpDto; + const existUser = await this.usersService.findOneWithPhone(phone); + if (existUser) throw new BadRequestException(AuthMessage.PHONE_EXISTS); + + const existCode = await this.otpService.checkExistOtp(phone, "REGISTER"); + if (existCode) { + return { + message: AuthMessage.OTP_ALREADY_SENT, + ttlSecond: existCode, + }; + } + const otpCode = await this.otpService.generateAndSetInCache(phone, "REGISTER"); + // await SmsService.sendOtp(phone, otpCode); + this.logger.log(`OTP sent to ${phone}: ${otpCode}`); + + return { + message: AuthMessage.OTP_SENT, + otpCode, + }; + } + //****************** */ + async completeRegistration(completeRegistrationDto: CompleteRegistrationDto) { + const { phone, code } = completeRegistrationDto; + const isValid = await this.otpService.verifyOtp(phone, code, "REGISTER"); + + if (!isValid) throw new BadRequestException(AuthMessage.INVALID_OTP); + + await this.otpService.delOtpFormCache(phone, "REGISTER"); + const hashedPassword = await this.passwordService.hashPassword(completeRegistrationDto.password); + const user = await this.usersService.createUser(completeRegistrationDto, hashedPassword); + + const tokens = this.tokensService.generateAccessAndRefreshToken({ sub: user.id, role: user.role.name }); + + return { + message: AuthMessage.USER_REGISTER_SUCCESS, + ...tokens, + }; + } + //****************** */ + + async loginWithPassword(loginDto: LoginPasswordDTO) { + const { email, password } = loginDto; + + const user = await this.usersService.findOneWithEmail(email); + if (!user) throw new BadRequestException(AuthMessage.INVALID_PASSWORD); + + const passCompare = await this.passwordService.comparePassword(password, user.password); + if (!passCompare) throw new BadRequestException(AuthMessage.INVALID_PASSWORD); + + const tokens = this.tokensService.generateAccessAndRefreshToken({ sub: user.id, role: user.role.name }); + + return { + message: AuthMessage.PASSWORD_LOGIN_SUCCESS, + ...tokens, + }; + } + + //****************** */ + + async requestLoginOtp(requestOtpDto: RequestOtpDto) { + const { phone } = requestOtpDto; + const user = await this.usersService.findOneWithPhone(phone); + if (!user) throw new BadRequestException(AuthMessage.PHONE_NOT_FOUND); + + const existCode = await this.otpService.checkExistOtp(phone, "LOGIN"); + if (existCode) { + return { + message: AuthMessage.OTP_ALREADY_SENT, + ttlSecond: existCode, + }; + } + + const otpCode = await this.otpService.generateAndSetInCache(phone, "LOGIN"); + // await SmsService.sendOtp(phone, otpCode); + this.logger.log(`OTP sent to ${phone}: ${otpCode}`); + + return { + message: AuthMessage.OTP_SENT, + otpCode, + }; + } + + //****************** */ + async verifyLoginOtp(verifyOtpDto: VerifyOtpDto) { + const { code, phone } = verifyOtpDto; + + const isValid = await this.otpService.verifyOtp(phone, code, "LOGIN"); + if (!isValid) throw new BadRequestException(AuthMessage.INVALID_OTP); + + await this.otpService.delOtpFormCache(phone, "LOGIN"); + + const user = await this.usersService.findOneWithPhone(phone); + if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND); + + const tokens = this.tokensService.generateAccessAndRefreshToken({ sub: user.id, role: user.role.name }); + return { + message: AuthMessage.LOGIN_SUCCESS, + ...tokens, + }; + } + //****************** */ +} diff --git a/src/modules/auth/providers/tokens.service.ts b/src/modules/auth/providers/tokens.service.ts new file mode 100644 index 0000000..fd441a2 --- /dev/null +++ b/src/modules/auth/providers/tokens.service.ts @@ -0,0 +1,53 @@ +import { Injectable } from "@nestjs/common"; +import { ConfigService } from "@nestjs/config"; +import { JwtService } from "@nestjs/jwt"; + +import { ITokenPayload } from "../interfaces/IToken-payload"; +// import { UserRepository } from "../users/providers/users.repository"; + +@Injectable() +export class TokensService { + constructor( + private readonly configService: ConfigService, + private readonly jwtService: JwtService, + // private readonly userRepository: UserRepository, + ) {} + // ----------- generate token ----------------- + generateAccessAndRefreshToken(payload: ITokenPayload) { + const access_expire = this.configService.getOrThrow("ACCESS_TOKEN_EXPIRE"); + const refresh_expire = this.configService.getOrThrow("REFRESH_TOKEN_EXPIRE"); + + const accessToken = this.jwtService.sign(payload, { expiresIn: `${access_expire}h` }); + + const refreshToken = this.jwtService.sign(payload, { expiresIn: `${refresh_expire}d` }); + + return { + accessToken: { token: accessToken, expire: Date.now() + 1000 * 60 * 60 * access_expire }, + refreshToken: { token: refreshToken, expire: Date.now() + 1000 * 60 * 60 * 24 * refresh_expire }, + }; + } + + //****************************** */ + // validateRefreshToken(token: string) { + // const { sub } = this.verifyToken(token, "refresh"); + // return this.generateAccessAndRefreshToken({ sub: sub }); + // } + + // verifyToken(token: string, type: "access" | "refresh"): ITokenPayload { + // try { + // if (type === "access") { + // return this.jwtService.verify(token, { + // secret: process.env.ACCESS_TOKEN_SECRET, + // ignoreExpiration: false, + // }); + // } else if (type === "refresh") { + // return this.jwtService.verify(token, { + // secret: process.env.REFRESH_TOKEN_SECRET, + // ignoreExpiration: false, + // }); + // } else throw new Error("Invalid token type => access / refresh"); + // } catch { + // throw new HttpException(AuthMessage.LoginAgain, HttpStatus.UNAUTHORIZED); + // } + // } +} diff --git a/src/modules/auth/strategies/jwt.strategy.ts b/src/modules/auth/strategies/jwt.strategy.ts new file mode 100644 index 0000000..1bf5ede --- /dev/null +++ b/src/modules/auth/strategies/jwt.strategy.ts @@ -0,0 +1,22 @@ +import { Injectable } from "@nestjs/common"; +import { ConfigService } from "@nestjs/config"; +import { PassportStrategy } from "@nestjs/passport"; +import { ExtractJwt, Strategy } from "passport-jwt"; + +import { JWT_STRATEGY_NAME } from "../../../common/constants"; +import { ITokenPayload } from "../interfaces/IToken-payload"; + +@Injectable() +export class JwtStrategy extends PassportStrategy(Strategy, JWT_STRATEGY_NAME) { + constructor(configService: ConfigService) { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + ignoreExpiration: false, + secretOrKey: configService.getOrThrow("JWT_SECRET_KEY"), + }); + } + + async validate(payload: ITokenPayload) { + return { id: payload.sub, role: payload.role }; + } +} diff --git a/src/modules/users/DTO/check-validity.dto.ts b/src/modules/users/DTO/check-validity.dto.ts new file mode 100644 index 0000000..be8ef7a --- /dev/null +++ b/src/modules/users/DTO/check-validity.dto.ts @@ -0,0 +1,17 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsEnum, IsNotEmpty, IsString } from "class-validator"; + +import { CommonMessage } from "../../../common/enums/message.enum"; +import { ValidityType } from "../enums/validity-type.enum"; + +export class CheckValidityDTO { + @IsNotEmpty({ message: CommonMessage.VALIDITY_TYPE_REQUIRED }) + @IsEnum(ValidityType) + @ApiProperty({ enum: ValidityType, example: ValidityType.USERNAME }) + type: ValidityType; + + @IsNotEmpty({ message: CommonMessage.THIS_FILED_IS_REQUIRED }) + @IsString({ message: CommonMessage.THIS_FILED_IS_REQUIRED }) + @ApiProperty({ example: "mahyargdz" }) + value: string; +} diff --git a/src/modules/users/DTO/update-profile.dto.ts b/src/modules/users/DTO/update-profile.dto.ts new file mode 100644 index 0000000..175b672 --- /dev/null +++ b/src/modules/users/DTO/update-profile.dto.ts @@ -0,0 +1,3 @@ +export class UpdateProfileDto { + userName: string; +} diff --git a/src/modules/users/entities/role.entity.ts b/src/modules/users/entities/role.entity.ts index e1377c9..2a02ac1 100644 --- a/src/modules/users/entities/role.entity.ts +++ b/src/modules/users/entities/role.entity.ts @@ -5,6 +5,6 @@ import { RoleEnum } from "../enums/role.enum"; @Entity() export class Role extends BaseEntity { - @Column({ type: "enum", enum: RoleEnum, default: RoleEnum.USER, nullable: false }) + @Column({ type: "enum", enum: RoleEnum, default: RoleEnum.USER, nullable: false, unique: true }) name: RoleEnum; } diff --git a/src/modules/users/entities/user.entity.ts b/src/modules/users/entities/user.entity.ts index b3f2a39..0717ee2 100644 --- a/src/modules/users/entities/user.entity.ts +++ b/src/modules/users/entities/user.entity.ts @@ -5,11 +5,14 @@ import { BaseEntity } from "../../../common/entities/base.entity"; @Entity() export class User extends BaseEntity { - @Column({ type: "varchar", length: 150, unique: true, nullable: true }) + @Column({ type: "varchar", length: 150, nullable: true }) email: string; - @Column({ type: "varchar", length: 11, unique: true, nullable: true }) - phoneNumber: string; + @Column({ type: "varchar", length: 11, unique: true, nullable: false }) + phone: string; + + @Column({ type: "varchar", length: 150, unique: true, nullable: true }) + userName: string; @Column({ type: "varchar", length: 150 }) password: string; diff --git a/src/modules/users/enums/validity-type.enum.ts b/src/modules/users/enums/validity-type.enum.ts new file mode 100644 index 0000000..7984dd7 --- /dev/null +++ b/src/modules/users/enums/validity-type.enum.ts @@ -0,0 +1,3 @@ +export enum ValidityType { + USERNAME = "userName", +} diff --git a/src/modules/users/providers/users.repository.ts b/src/modules/users/providers/users.repository.ts index 7918c72..1e7a911 100644 --- a/src/modules/users/providers/users.repository.ts +++ b/src/modules/users/providers/users.repository.ts @@ -11,8 +11,14 @@ export class UserRepository extends Repository { } async findOneWithEmail(email: string): Promise { - return await this.findOneBy({ + return this.findOneBy({ email, }); } + + async findOneWithPhone(phone: string): Promise { + return this.findOneBy({ + phone, + }); + } } diff --git a/src/modules/users/providers/users.service.ts b/src/modules/users/providers/users.service.ts index ba6d207..516d882 100644 --- a/src/modules/users/providers/users.service.ts +++ b/src/modules/users/providers/users.service.ts @@ -3,35 +3,82 @@ import { BadRequestException, Injectable, Logger } from "@nestjs/common"; import { RoleRepository } from "./roles.repository"; import { UserRepository } from "./users.repository"; import { roles } from "../../../../db/seeders/role.seeder"; -import { UserMessage } from "../../../common/enums/message.enum"; +import { CommonMessage, UserMessage } from "../../../common/enums/message.enum"; import { CompleteRegistrationDto } from "../../auth/DTO/complete-register.dto"; +import { CheckValidityDTO } from "../DTO/check-validity.dto"; import { User } from "../entities/user.entity"; +import { RoleEnum } from "../enums/role.enum"; @Injectable() export class UsersService { + private readonly userSelect = { + id: true, + phone: true, + email: true, + userName: true, + firstName: true, + lastName: true, + nationalCode: true, + birthDate: true, + role: { + id: true, + name: true, + }, + }; private logger = new Logger(UsersService.name); constructor( private userRepository: UserRepository, private roleRepository: RoleRepository, ) {} + /************************************************************ */ + + async getMe(userId: string) { + const user = await this.userRepository.findOne({ where: { id: userId }, select: this.userSelect }); + if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND); + return { user }; + } + /************************************************************ */ + async findOneWithEmail(email: string): Promise { const user = await this.userRepository.findOneWithEmail(email); if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND); return user; } + /************************************************************ */ + async findOneWithPhone(phone: string): Promise { + const user = await this.userRepository.findOneWithPhone(phone); + return user; + } + + /************************************************************ */ + + async createUser(registerDto: CompleteRegistrationDto, hashedPassword: string): Promise { + // const existEmail = await this.userRepository.findOneWithEmail(registerDto.email); + // if (existEmail) throw new BadRequestException(UserMessage.EMAIL_EXIST); + + const role = await this.roleRepository.findOneBy({ name: RoleEnum.USER }); + if (!role) throw new BadRequestException(UserMessage.ROLE_NOT_FOUND); + + const user = this.userRepository.create({ ...registerDto, password: hashedPassword, role }); + return await this.userRepository.save(user); + } + /************************************************************ */ + + async checkValidity(checkDto: CheckValidityDTO) { + const { type, value } = checkDto; + const user = await this.userRepository.findOneBy({ [type]: value }); + if (user) throw new BadRequestException(UserMessage.USER_EXISTS); + return { message: CommonMessage.VALID_FOR_CHOOSE }; + } + + ///****************************************************** */ async roleSeeder() { + const countRole = await this.roleRepository.count(); + if (countRole > 0) return; const createdRoles = await this.roleRepository.insert(roles); this.logger.log({ createdRoles }); return createdRoles; } - - async createUser(registerDto: CompleteRegistrationDto, hashedPassword: string): Promise { - const existEmail = await this.userRepository.findOneWithEmail(registerDto.email); - if (existEmail) throw new BadRequestException(UserMessage.EMAIL_EXIST); - - const user = this.userRepository.create({ ...registerDto, password: hashedPassword }); - return await this.userRepository.save(user); - } } diff --git a/src/modules/users/users.controller.ts b/src/modules/users/users.controller.ts index 487560e..fca1bc1 100644 --- a/src/modules/users/users.controller.ts +++ b/src/modules/users/users.controller.ts @@ -1,6 +1,31 @@ -import { Controller } from "@nestjs/common"; -import { ApiTags } from "@nestjs/swagger"; +import { Body, Controller, Get, Patch, Post } from "@nestjs/common"; +import { ApiOperation, ApiTags } from "@nestjs/swagger"; + +import { CheckValidityDTO } from "./DTO/check-validity.dto"; +import { User } from "./entities/user.entity"; +import { UsersService } from "./providers/users.service"; +import { AuthGuards } from "../../common/decorators/auth-guard.decorator"; +import { UserDec } from "../../common/decorators/user.decorator"; @Controller("users") @ApiTags("users") -export class UsersController {} +export class UsersController { + constructor(private usersService: UsersService) {} + + @AuthGuards() + @ApiOperation({ summary: "Get user profile" }) + @Get("me") + getMe(@UserDec() user: User) { + return this.usersService.getMe(user.id); + } + + @Patch("update-profile") + updateProfile() {} + + @AuthGuards() + @ApiOperation({ summary: "Check validity of user field" }) + @Post("check-validity") + checkValidity(@Body() checkValidityDto: CheckValidityDTO) { + return this.usersService.checkValidity(checkValidityDto); + } +} diff --git a/src/modules/users/users.module.ts b/src/modules/users/users.module.ts index f251cce..e4f2d2f 100644 --- a/src/modules/users/users.module.ts +++ b/src/modules/users/users.module.ts @@ -3,10 +3,10 @@ import { TypeOrmModule } from "@nestjs/typeorm"; import { Role } from "./entities/role.entity"; import { User } from "./entities/user.entity"; +import { RoleRepository } from "./providers/roles.repository"; import { UserRepository } from "./providers/users.repository"; import { UsersService } from "./providers/users.service"; import { UsersController } from "./users.controller"; -import { RoleRepository } from "./providers/roles.repository"; @Module({ imports: [TypeOrmModule.forFeature([User, Role])], @@ -15,8 +15,8 @@ import { RoleRepository } from "./providers/roles.repository"; exports: [UsersService, TypeOrmModule], }) export class UsersModule implements OnApplicationBootstrap { - constructor(private userService: UsersService) {} + // constructor(private userService: UsersService) {} async onApplicationBootstrap() { - await this.userService.roleSeeder(); + // await this.userService.roleSeeder(); } } diff --git a/src/modules/utils/providers/cache.service.ts b/src/modules/utils/providers/cache.service.ts index 62c23d8..f1e745b 100644 --- a/src/modules/utils/providers/cache.service.ts +++ b/src/modules/utils/providers/cache.service.ts @@ -16,4 +16,8 @@ export class CacheService { async del(key: string) { await this.cacheManager.del(key); } + + async getTtl(key: string) { + return await this.cacheManager.ttl(key); + } } diff --git a/src/modules/utils/providers/otp.service.ts b/src/modules/utils/providers/otp.service.ts index f965209..13c3c64 100644 --- a/src/modules/utils/providers/otp.service.ts +++ b/src/modules/utils/providers/otp.service.ts @@ -12,28 +12,33 @@ export class OTPService { private generateOTP(): string { return randomInt(10000, 99999).toString(); } - + // async generateAndSetInCache(identifier: string, cacheKey: OtpCacheKeyType) { const otp = this.generateOTP(); const key = `${identifier}:${cacheKey}:OTP`; await this.cacheService.set(key, otp); return otp; } - + // async verifyOtp(identifier: string, otpCode: string, cacheKey: OtpCacheKeyType) { const key = `${identifier}:${cacheKey}:OTP`; const codeInCache = await this.cacheService.get(key); return codeInCache && otpCode === codeInCache; } - + // async delOtpFormCache(identifier: string, cacheKey: OtpCacheKeyType) { const key = `${identifier}:${cacheKey}:OTP`; await this.cacheService.del(key); } - // async verify(identifier: string, otp: string): Promise { - // // Here you would typically verify the OTP against stored value - // // and check if it's not expired - // return true; // Placeholder implementation - // } + async checkExistOtp(identifier: string, cacheKey: OtpCacheKeyType) { + const key = `${identifier}:${cacheKey}:OTP`; + const codeInCache = await this.cacheService.get(key); + + if (!codeInCache) return null; + + const ttlInMsSeconds = await this.cacheService.getTtl(key); + + return Math.floor((ttlInMsSeconds! - Date.now()) / 1000); + } } diff --git a/src/modules/utils/utils.module.ts b/src/modules/utils/utils.module.ts index cddb935..7673bc9 100644 --- a/src/modules/utils/utils.module.ts +++ b/src/modules/utils/utils.module.ts @@ -5,7 +5,7 @@ import { CacheService } from "./providers/cache.service"; import { OTPService } from "./providers/otp.service"; import { PasswordService } from "./providers/password.service"; import { SmsService } from "./providers/sms.service"; -import { SmsConfigs } from "../../configs/sms.config"; +import { smsConfigs } from "../../configs/sms.config"; @Module({ providers: [ @@ -15,8 +15,8 @@ import { SmsConfigs } from "../../configs/sms.config"; SmsService, { provide: SMS_CONFIG, - useFactory: SmsConfigs().useFactory, - inject: SmsConfigs().inject, + useFactory: smsConfigs().useFactory, + inject: smsConfigs().inject, }, ], exports: [SMS_CONFIG, OTPService, PasswordService, CacheService, SmsService],