From d1e0285365fb987ca9c41465bb8032a91c613ed0 Mon Sep 17 00:00:00 2001 From: Bijin Krishn Date: Tue, 10 Mar 2026 23:00:28 +0530 Subject: [PATCH] feat: add request rate limiting Create infra folder for grouping infrastructure related modules --- .env.example | 3 + docker-compose.dev.yml | 2 + package.json | 3 +- pnpm-lock.yaml | 167 ++++----------------- src/app.module.ts | 37 +++-- src/auth/auth.service.spec.ts | 2 +- src/auth/auth.service.ts | 2 +- src/auth/guard/auth.guard.spec.ts | 4 +- src/auth/guard/auth.guard.ts | 2 +- src/auth/guard/refresh-token.guard.spec.ts | 2 +- src/auth/guard/refresh-token.guard.ts | 2 +- src/config/env.validation.ts | 2 + src/{ => infra}/prisma/prisma.module.ts | 0 src/{ => infra}/prisma/prisma.service.ts | 4 + src/infra/redis/redis.module.ts | 9 ++ src/infra/redis/redis.service.ts | 16 ++ src/otp/otp.service.spec.ts | 4 +- src/otp/otp.service.ts | 5 +- src/token/token.service.spec.ts | 2 +- src/token/token.service.ts | 2 +- src/user/user.service.spec.ts | 2 +- src/user/user.service.ts | 2 +- 22 files changed, 109 insertions(+), 165 deletions(-) rename src/{ => infra}/prisma/prisma.module.ts (100%) rename src/{ => infra}/prisma/prisma.service.ts (87%) create mode 100644 src/infra/redis/redis.module.ts create mode 100644 src/infra/redis/redis.service.ts diff --git a/.env.example b/.env.example index 73812d7..d7beb29 100644 --- a/.env.example +++ b/.env.example @@ -13,3 +13,6 @@ JWT_ACCESS_EXPIRES_IN=1h # JWT - Refresh JWT_REFRESH_SECRET_KEY=CHANGE_ME JWT_REFRESH_EXPIRES_IN=2h +# Throttle - Rate limiting +THROTTLE_TTL=60000 +THROTTLE_LIMIT=10 \ No newline at end of file diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 98b1a8f..c1923dd 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -19,6 +19,8 @@ services: - ./src:/app/src - ./prisma:/app/prisma - ./generated:/app/generated + - ./package.json:/app/package.json + - ./pnpm-lock.yaml:/app/pnpm-lock.yaml - dev_node_modules:/app/node_modules # Prevents local node_modules from overwriting container's networks: - infra_net diff --git a/package.json b/package.json index 46e57e3..5fa96e8 100644 --- a/package.json +++ b/package.json @@ -44,13 +44,14 @@ ] }, "dependencies": { - "@nestjs-modules/ioredis": "^2.0.2", + "@nest-lab/throttler-storage-redis": "^1.2.0", "@nestjs/common": "^11.0.1", "@nestjs/config": "^4.0.3", "@nestjs/core": "^11.0.1", "@nestjs/jwt": "^11.0.2", "@nestjs/mapped-types": "*", "@nestjs/platform-express": "^11.0.1", + "@nestjs/throttler": "^6.5.0", "@prisma/adapter-pg": "^7.4.0", "@prisma/client": "^7.4.2", "argon2": "^0.44.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bc39747..9bef414 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,9 +8,9 @@ importers: .: dependencies: - '@nestjs-modules/ioredis': - specifier: ^2.0.2 - version: 2.0.2(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(ioredis@5.10.0)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nest-lab/throttler-storage-redis': + specifier: ^1.2.0 + version: 1.2.0(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)(@nestjs/throttler@6.5.0(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)(reflect-metadata@0.2.2))(ioredis@5.10.0)(reflect-metadata@0.2.2) '@nestjs/common': specifier: ^11.0.1 version: 11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -29,6 +29,9 @@ importers: '@nestjs/platform-express': specifier: ^11.0.1 version: 11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13) + '@nestjs/throttler': + specifier: ^6.5.0 + version: 6.5.0(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)(reflect-metadata@0.2.2) '@prisma/adapter-pg': specifier: ^7.4.0 version: 7.4.0 @@ -989,12 +992,14 @@ packages: '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} - '@nestjs-modules/ioredis@2.0.2': - resolution: {integrity: sha512-8pzSvT8R3XP6p8ZzQmEN8OnY0yWrJ/elFhwQK+PID2zf1SLBkAZ18bDcx3SKQ2atledt0gd9kBeP5xT4MlyS7Q==} + '@nest-lab/throttler-storage-redis@1.2.0': + resolution: {integrity: sha512-tMkUyo68NCKTR+zILk+EC35SMYBtDPZY2mCj7ZaCietWGVTnuP4zwq9ERYfvU6kJv6h8teNZrC6MJCmY6/dljw==} peerDependencies: - '@nestjs/common': '>=6.7.0' - '@nestjs/core': '>=6.7.0' + '@nestjs/common': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + '@nestjs/core': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + '@nestjs/throttler': '>=6.0.0' ioredis: '>=5.0.0' + reflect-metadata: ^0.2.1 '@nestjs/cli@11.0.16': resolution: {integrity: sha512-P0H+Vcjki6P5160E5QnMt3Q0X5FTg4PZkP99Ig4lm/4JWqfw32j3EXv3YBTJ2DmxLwOQ/IS9F7dzKpMAgzKTGg==} @@ -1075,54 +1080,6 @@ packages: peerDependencies: typescript: '>=4.8.2' - '@nestjs/terminus@10.2.0': - resolution: {integrity: sha512-zPs98xvJ4ogEimRQOz8eU90mb7z+W/kd/mL4peOgrJ/VqER+ibN2Cboj65uJZW3XuNhpOqaeYOJte86InJd44A==} - peerDependencies: - '@grpc/grpc-js': '*' - '@grpc/proto-loader': '*' - '@mikro-orm/core': '*' - '@mikro-orm/nestjs': '*' - '@nestjs/axios': ^1.0.0 || ^2.0.0 || ^3.0.0 - '@nestjs/common': ^9.0.0 || ^10.0.0 - '@nestjs/core': ^9.0.0 || ^10.0.0 - '@nestjs/microservices': ^9.0.0 || ^10.0.0 - '@nestjs/mongoose': ^9.0.0 || ^10.0.0 - '@nestjs/sequelize': ^9.0.0 || ^10.0.0 - '@nestjs/typeorm': ^9.0.0 || ^10.0.0 - '@prisma/client': '*' - mongoose: '*' - reflect-metadata: 0.1.x - rxjs: 7.x - sequelize: '*' - typeorm: '*' - peerDependenciesMeta: - '@grpc/grpc-js': - optional: true - '@grpc/proto-loader': - optional: true - '@mikro-orm/core': - optional: true - '@mikro-orm/nestjs': - optional: true - '@nestjs/axios': - optional: true - '@nestjs/microservices': - optional: true - '@nestjs/mongoose': - optional: true - '@nestjs/sequelize': - optional: true - '@nestjs/typeorm': - optional: true - '@prisma/client': - optional: true - mongoose: - optional: true - sequelize: - optional: true - typeorm: - optional: true - '@nestjs/testing@11.1.13': resolution: {integrity: sha512-bOWP8nLEZAOEEX8jAZGBCc1yU0+nv4g2ipc+QEzkVUe3eEEUKHKaeGafJ3GtDuGavlZKfkXEqflZuICdavu5dQ==} peerDependencies: @@ -1136,6 +1093,13 @@ packages: '@nestjs/platform-express': optional: true + '@nestjs/throttler@6.5.0': + resolution: {integrity: sha512-9j0ZRfH0QE1qyrj9JjIRDz5gQLPqq9yVC2nHsrosDVAfI5HHw08/aUAWx9DZLSdQf4HDkmhTTEGLrRFHENvchQ==} + peerDependencies: + '@nestjs/common': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + '@nestjs/core': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + reflect-metadata: ^0.1.13 || ^0.2.0 + '@noble/hashes@1.8.0': resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} engines: {node: ^14.21.3 || >=16} @@ -1632,9 +1596,6 @@ packages: ajv@8.18.0: resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} - ansi-align@3.0.1: - resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} - ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} @@ -1753,10 +1714,6 @@ packages: resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} - boxen@5.1.2: - resolution: {integrity: sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==} - engines: {node: '>=10'} - brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} @@ -1842,10 +1799,6 @@ packages: chardet@2.1.1: resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} - check-disk-space@3.4.0: - resolution: {integrity: sha512-drVkSqfwA+TvuEhFipiR1OC9boEGZL5RrWvVsOthdcvQNXyCCuKkEiTOTXZ7qxSf/GLwq4GvzfrQD/Wz325hgw==} - engines: {node: '>=16'} - chevrotain@10.5.0: resolution: {integrity: sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A==} @@ -1876,10 +1829,6 @@ packages: class-validator@0.14.3: resolution: {integrity: sha512-rXXekcjofVN1LTOSw+u4u9WXVEUvNBVjORW154q/IdmYWy1nMbOU9aNtZB0t8m+FJQ9q91jlr2f9CwwUFdFMRA==} - cli-boxes@2.2.1: - resolution: {integrity: sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==} - engines: {node: '>=6'} - cli-cursor@3.1.0: resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} engines: {node: '>=8'} @@ -3904,10 +3853,6 @@ packages: resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} engines: {node: '>=4'} - type-fest@0.20.2: - resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} - engines: {node: '>=10'} - type-fest@0.21.3: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} @@ -4038,10 +3983,6 @@ packages: engines: {node: '>= 8'} hasBin: true - widest-line@3.1.0: - resolution: {integrity: sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==} - engines: {node: '>=8'} - word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -5057,29 +4998,14 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true - '@nestjs-modules/ioredis@2.0.2(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(ioredis@5.10.0)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nest-lab/throttler-storage-redis@1.2.0(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)(@nestjs/throttler@6.5.0(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)(reflect-metadata@0.2.2))(ioredis@5.10.0)(reflect-metadata@0.2.2)': dependencies: '@nestjs/common': 11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/core': 11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/throttler': 6.5.0(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)(reflect-metadata@0.2.2) ioredis: 5.10.0 - optionalDependencies: - '@nestjs/terminus': 10.2.0(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(reflect-metadata@0.2.2)(rxjs@7.8.2) - transitivePeerDependencies: - - '@grpc/grpc-js' - - '@grpc/proto-loader' - - '@mikro-orm/core' - - '@mikro-orm/nestjs' - - '@nestjs/axios' - - '@nestjs/microservices' - - '@nestjs/mongoose' - - '@nestjs/sequelize' - - '@nestjs/typeorm' - - '@prisma/client' - - mongoose - - reflect-metadata - - rxjs - - sequelize - - typeorm + reflect-metadata: 0.2.2 + tslib: 2.8.1 '@nestjs/cli@11.0.16(@types/node@22.19.11)': dependencies: @@ -5181,25 +5107,19 @@ snapshots: transitivePeerDependencies: - chokidar - '@nestjs/terminus@10.2.0(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/testing@11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)(@nestjs/platform-express@11.1.13)': dependencies: '@nestjs/common': 11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/core': 11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) - boxen: 5.1.2 - check-disk-space: 3.4.0 - reflect-metadata: 0.2.2 - rxjs: 7.8.2 + tslib: 2.8.1 optionalDependencies: - '@prisma/client': 7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3) - optional: true + '@nestjs/platform-express': 11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13) - '@nestjs/testing@11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)(@nestjs/platform-express@11.1.13)': + '@nestjs/throttler@6.5.0(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)(reflect-metadata@0.2.2)': dependencies: '@nestjs/common': 11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/core': 11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) - tslib: 2.8.1 - optionalDependencies: - '@nestjs/platform-express': 11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13) + reflect-metadata: 0.2.2 '@noble/hashes@1.8.0': {} @@ -5764,11 +5684,6 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 - ansi-align@3.0.1: - dependencies: - string-width: 4.2.3 - optional: true - ansi-colors@4.1.3: {} ansi-escapes@4.3.2: @@ -5907,18 +5822,6 @@ snapshots: transitivePeerDependencies: - supports-color - boxen@5.1.2: - dependencies: - ansi-align: 3.0.1 - camelcase: 6.3.0 - chalk: 4.1.2 - cli-boxes: 2.2.1 - string-width: 4.2.3 - type-fest: 0.20.2 - widest-line: 3.1.0 - wrap-ansi: 7.0.0 - optional: true - brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 @@ -6009,9 +5912,6 @@ snapshots: chardet@2.1.1: {} - check-disk-space@3.4.0: - optional: true - chevrotain@10.5.0: dependencies: '@chevrotain/cst-dts-gen': 10.5.0 @@ -6045,9 +5945,6 @@ snapshots: libphonenumber-js: 1.12.36 validator: 13.15.26 - cli-boxes@2.2.1: - optional: true - cli-cursor@3.1.0: dependencies: restore-cursor: 3.1.0 @@ -8211,9 +8108,6 @@ snapshots: type-detect@4.0.8: {} - type-fest@0.20.2: - optional: true - type-fest@0.21.3: {} type-fest@4.41.0: {} @@ -8366,11 +8260,6 @@ snapshots: dependencies: isexe: 2.0.0 - widest-line@3.1.0: - dependencies: - string-width: 4.2.3 - optional: true - word-wrap@1.2.5: {} wordwrap@1.0.0: {} diff --git a/src/app.module.ts b/src/app.module.ts index f575b3d..bb96373 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,30 +1,49 @@ -import { RedisModule } from '@nestjs-modules/ioredis' +import { ThrottlerStorageRedisService } from '@nest-lab/throttler-storage-redis' import { Module } from '@nestjs/common' -import { ConfigModule } from '@nestjs/config' +import { ConfigModule, ConfigService } from '@nestjs/config' +import { APP_GUARD } from '@nestjs/core' +import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler' import { AppController } from './app.controller' import { AppService } from './app.service' import { AuthInfrastructureModule } from './auth/auth-infrastructure.module' import { AuthModule } from './auth/auth.module' import envValidation from './config/env.validation' -import { PrismaModule } from './prisma/prisma.module' +import { PrismaModule } from './infra/prisma/prisma.module' +import { RedisModule } from './infra/redis/redis.module' +import { RedisService } from './infra/redis/redis.service' import { UserModule } from './user/user.module' @Module({ imports: [ ConfigModule.forRoot({ - isGlobal: true, validationSchema: envValidation, }), PrismaModule, UserModule, AuthModule, - RedisModule.forRoot({ - type: 'single', - url: process.env.REDIS_URL, - }), + RedisModule, AuthInfrastructureModule, + ThrottlerModule.forRootAsync({ + imports: [RedisModule], + inject: [RedisService, ConfigService], + useFactory: (redis: RedisService, config: ConfigService) => ({ + throttlers: [ + { + ttl: config.get('THROTTLE_TTL') as number, + limit: config.get('THROTTLE_LIMIT') as number, + }, + ], + storage: new ThrottlerStorageRedisService(redis), + }), + }), ], controllers: [AppController], - providers: [AppService], + providers: [ + AppService, + { + provide: APP_GUARD, + useClass: ThrottlerGuard, + }, + ], }) export class AppModule {} diff --git a/src/auth/auth.service.spec.ts b/src/auth/auth.service.spec.ts index b75196b..3ac204a 100644 --- a/src/auth/auth.service.spec.ts +++ b/src/auth/auth.service.spec.ts @@ -5,8 +5,8 @@ import { } from '@nestjs/common' import { Test, TestingModule } from '@nestjs/testing' import * as argon from 'argon2' +import { PrismaService } from 'src/infra/prisma/prisma.service' import { OtpService } from 'src/otp/otp.service' -import { PrismaService } from 'src/prisma/prisma.service' import { TokenService } from 'src/token/token.service' import { UserService } from 'src/user/user.service' import { AuthService } from './auth.service' diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index fc37b28..f7cce31 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -5,8 +5,8 @@ import { NotFoundException, } from '@nestjs/common' import * as argon from 'argon2' +import { PrismaService } from 'src/infra/prisma/prisma.service' import { OtpService } from 'src/otp/otp.service' -import { PrismaService } from 'src/prisma/prisma.service' import { TokenService } from 'src/token/token.service' import { CreateUserDto } from 'src/user/dto' import { UserService } from 'src/user/user.service' diff --git a/src/auth/guard/auth.guard.spec.ts b/src/auth/guard/auth.guard.spec.ts index c895f63..d7dc592 100644 --- a/src/auth/guard/auth.guard.spec.ts +++ b/src/auth/guard/auth.guard.spec.ts @@ -1,8 +1,8 @@ import { + ExecutionContext, ForbiddenException, NotFoundException, UnauthorizedException, - ExecutionContext, } from '@nestjs/common' import { Reflector } from '@nestjs/core' import { JsonWebTokenError, JwtService } from '@nestjs/jwt' @@ -10,7 +10,7 @@ import { Test, TestingModule } from '@nestjs/testing' import { Request } from 'express' import authConfig from 'src/auth/config/auth.config' import { IS_PUBLIC_KEY } from 'src/auth/decorator' -import { PrismaService } from 'src/prisma/prisma.service' +import { PrismaService } from 'src/infra/prisma/prisma.service' import { AuthGuard, REQUEST_USER_KEY } from './auth.guard' describe('AuthGuard', () => { diff --git a/src/auth/guard/auth.guard.ts b/src/auth/guard/auth.guard.ts index 353d752..68253a2 100644 --- a/src/auth/guard/auth.guard.ts +++ b/src/auth/guard/auth.guard.ts @@ -11,7 +11,7 @@ import type { ConfigType } from '@nestjs/config' import { Reflector } from '@nestjs/core' import { JsonWebTokenError, JwtService } from '@nestjs/jwt' import { Request } from 'express' -import { PrismaService } from 'src/prisma/prisma.service' +import { PrismaService } from 'src/infra/prisma/prisma.service' import authConfig from '../config/auth.config' import { IS_PUBLIC_KEY } from '../decorator' import { ActiveUser, JwtAccessPayload } from '../interface' diff --git a/src/auth/guard/refresh-token.guard.spec.ts b/src/auth/guard/refresh-token.guard.spec.ts index 2830518..d1bf514 100644 --- a/src/auth/guard/refresh-token.guard.spec.ts +++ b/src/auth/guard/refresh-token.guard.spec.ts @@ -4,7 +4,7 @@ import { Test, TestingModule } from '@nestjs/testing' import * as argon from 'argon2' import { Request } from 'express' import authConfig from 'src/auth/config/auth.config' -import { PrismaService } from 'src/prisma/prisma.service' +import { PrismaService } from 'src/infra/prisma/prisma.service' import { REQUEST_USER_KEY } from './auth.guard' import { RefreshTokenGuard } from './refresh-token.guard' diff --git a/src/auth/guard/refresh-token.guard.ts b/src/auth/guard/refresh-token.guard.ts index 9ac9567..39b6921 100644 --- a/src/auth/guard/refresh-token.guard.ts +++ b/src/auth/guard/refresh-token.guard.ts @@ -9,7 +9,7 @@ import type { ConfigType } from '@nestjs/config' import { JsonWebTokenError, JwtService } from '@nestjs/jwt' import * as argon from 'argon2' import { Request } from 'express' -import { PrismaService } from 'src/prisma/prisma.service' +import { PrismaService } from 'src/infra/prisma/prisma.service' import authConfig from '../config/auth.config' import { ActiveUser, JwtRefreshPayload } from '../interface' import { extractTokenFromHeader } from '../util' diff --git a/src/config/env.validation.ts b/src/config/env.validation.ts index e35944f..a21730b 100644 --- a/src/config/env.validation.ts +++ b/src/config/env.validation.ts @@ -10,4 +10,6 @@ export default Joi.object({ JWT_ACCESS_EXPIRES_IN: Joi.string().default('1w'), JWT_REFRESH_SECRET_KEY: Joi.string().required(), JWT_REFRESH_EXPIRES_IN: Joi.string().default('2w'), + THROTTLE_TTL: Joi.number().default(60000), + THROTTLE_LIMIT: Joi.number().default(10), }) diff --git a/src/prisma/prisma.module.ts b/src/infra/prisma/prisma.module.ts similarity index 100% rename from src/prisma/prisma.module.ts rename to src/infra/prisma/prisma.module.ts diff --git a/src/prisma/prisma.service.ts b/src/infra/prisma/prisma.service.ts similarity index 87% rename from src/prisma/prisma.service.ts rename to src/infra/prisma/prisma.service.ts index 247b101..814823e 100644 --- a/src/prisma/prisma.service.ts +++ b/src/infra/prisma/prisma.service.ts @@ -11,4 +11,8 @@ export class PrismaService extends PrismaClient { }) super({ adapter }) } + + async onModuleDestroy() { + await this.$disconnect() + } } diff --git a/src/infra/redis/redis.module.ts b/src/infra/redis/redis.module.ts new file mode 100644 index 0000000..63a215d --- /dev/null +++ b/src/infra/redis/redis.module.ts @@ -0,0 +1,9 @@ +import { Global, Module } from '@nestjs/common' +import { RedisService } from './redis.service' + +@Global() +@Module({ + providers: [RedisService], + exports: [RedisService], +}) +export class RedisModule {} diff --git a/src/infra/redis/redis.service.ts b/src/infra/redis/redis.service.ts new file mode 100644 index 0000000..8b5724f --- /dev/null +++ b/src/infra/redis/redis.service.ts @@ -0,0 +1,16 @@ +import { Injectable, OnModuleDestroy } from '@nestjs/common' +import { ConfigService } from '@nestjs/config' +import Redis from 'ioredis' + +@Injectable() +export class RedisService extends Redis implements OnModuleDestroy { + constructor(config: ConfigService) { + super(config.get('REDIS_URL') as string, { + maxRetriesPerRequest: null, + }) + } + + async onModuleDestroy() { + await this.quit() + } +} diff --git a/src/otp/otp.service.spec.ts b/src/otp/otp.service.spec.ts index 6827fc8..26bd8b3 100644 --- a/src/otp/otp.service.spec.ts +++ b/src/otp/otp.service.spec.ts @@ -1,7 +1,7 @@ -import { getRedisConnectionToken } from '@nestjs-modules/ioredis' import { UnauthorizedException } from '@nestjs/common' import { Test, TestingModule } from '@nestjs/testing' import * as argon from 'argon2' +import { RedisService } from 'src/infra/redis/redis.service' import { OtpService } from './otp.service' import * as util from './util' @@ -31,7 +31,7 @@ describe('OtpService', () => { providers: [ OtpService, { - provide: getRedisConnectionToken(), + provide: RedisService, useValue: mockRedis, }, ], diff --git a/src/otp/otp.service.ts b/src/otp/otp.service.ts index 45043f4..fbae78b 100644 --- a/src/otp/otp.service.ts +++ b/src/otp/otp.service.ts @@ -1,8 +1,7 @@ -import { InjectRedis } from '@nestjs-modules/ioredis' import { Injectable, UnauthorizedException } from '@nestjs/common' import * as argon from 'argon2' -import Redis from 'ioredis' import { VerifyOtpDto } from 'src/auth/dto' +import { RedisService } from 'src/infra/redis/redis.service' import { v7 as uuidv7 } from 'uuid' import { generateOtp, generateRedisKey } from './util' @@ -12,7 +11,7 @@ const EMAILED_OTP_REDIS_EX = 180 // 3 minutes @Injectable() export class OtpService { - constructor(@InjectRedis() private readonly redis: Redis) {} + constructor(private readonly redis: RedisService) {} public async emailOtp(email: string, guarded = false) { const otp = await this.generateAndCacheOtp(email, guarded) diff --git a/src/token/token.service.spec.ts b/src/token/token.service.spec.ts index e873e0b..d7016db 100644 --- a/src/token/token.service.spec.ts +++ b/src/token/token.service.spec.ts @@ -9,7 +9,7 @@ import { Test, TestingModule } from '@nestjs/testing' import * as argon from 'argon2' import { Prisma, type User } from 'generated/prisma/client' import authConfig from 'src/auth/config/auth.config' -import { PrismaService } from 'src/prisma/prisma.service' +import { PrismaService } from 'src/infra/prisma/prisma.service' import { TokenService } from './token.service' jest.mock('argon2') diff --git a/src/token/token.service.ts b/src/token/token.service.ts index 69b1813..404f08f 100644 --- a/src/token/token.service.ts +++ b/src/token/token.service.ts @@ -12,7 +12,7 @@ import * as argon from 'argon2' import { Prisma, User } from 'generated/prisma/client' import authConfig from 'src/auth/config/auth.config' import { JwtAccessPayload, JwtRefreshPayload } from 'src/auth/interface' -import { PrismaService } from 'src/prisma/prisma.service' +import { PrismaService } from 'src/infra/prisma/prisma.service' import { v7 as uuidv7 } from 'uuid' @Injectable() diff --git a/src/user/user.service.spec.ts b/src/user/user.service.spec.ts index 85dc4d3..b30d2a2 100644 --- a/src/user/user.service.spec.ts +++ b/src/user/user.service.spec.ts @@ -7,8 +7,8 @@ import { Test, TestingModule } from '@nestjs/testing' import * as argon from 'argon2' import { Prisma } from 'generated/prisma/client' import { Role } from 'generated/prisma/enums' +import { PrismaService } from 'src/infra/prisma/prisma.service' import { OtpService } from 'src/otp/otp.service' -import { PrismaService } from 'src/prisma/prisma.service' import { TokenService } from 'src/token/token.service' import { UserService } from './user.service' diff --git a/src/user/user.service.ts b/src/user/user.service.ts index 19ccb33..4203654 100644 --- a/src/user/user.service.ts +++ b/src/user/user.service.ts @@ -7,8 +7,8 @@ import { } from '@nestjs/common' import * as argon from 'argon2' import { Prisma } from 'generated/prisma/client' +import { PrismaService } from 'src/infra/prisma/prisma.service' import { OtpService } from 'src/otp/otp.service' -import { PrismaService } from 'src/prisma/prisma.service' import { TokenService } from 'src/token/token.service' import { ChangeUserRoleDto,