diff --git a/package.json b/package.json index d50f11c..da895c6 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "@nestjs-modules/mailer": "^2.0.2", "@nestjs/axios": "^3.0.3", "@nestjs/bullmq": "^10.2.1", + "@nestjs/cache-manager": "^2.2.2", "@nestjs/common": "^10.4.1", "@nestjs/config": "^3.2.3", "@nestjs/core": "^10.4.1", @@ -48,6 +49,8 @@ "argon2": "^0.41.1", "axios": "^1.7.7", "bullmq": "^5.12.14", + "cache-manager": "^5.7.6", + "cache-manager-redis-yet": "^5.1.4", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "compression": "^1.7.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 917c4f6..5131bb1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: '@nestjs/bullmq': specifier: ^10.2.1 version: 10.2.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(bullmq@5.12.14) + '@nestjs/cache-manager': + specifier: ^2.2.2 + version: 2.2.2(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(cache-manager@5.7.6)(rxjs@7.8.1) '@nestjs/common': specifier: ^10.4.1 version: 10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -40,10 +43,10 @@ importers: version: 7.4.0(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(encoding@0.1.13)(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/terminus': specifier: ^10.2.3 - version: 10.2.3(bq7pyh5konlacyjccn47tf7plq) + version: 10.2.3(a5lhekddtlsvoqvynenfvvi6la) '@nestjs/typeorm': specifier: ^10.0.2 - version: 10.0.2(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(encoding@0.1.13)(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.1)(pg@8.12.0)(ts-node@10.9.2(@swc/core@1.7.22)(@types/node@20.16.2)(typescript@5.5.4))) + version: 10.0.2(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(encoding@0.1.13)(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.1)(pg@8.12.0)(redis@4.7.0)(ts-node@10.9.2(@swc/core@1.7.22)(@types/node@20.16.2)(typescript@5.5.4))) argon2: specifier: ^0.41.1 version: 0.41.1 @@ -53,6 +56,12 @@ importers: bullmq: specifier: ^5.12.14 version: 5.12.14 + cache-manager: + specifier: ^5.7.6 + version: 5.7.6 + cache-manager-redis-yet: + specifier: ^5.1.4 + version: 5.1.4 class-transformer: specifier: ^0.5.1 version: 0.5.1 @@ -100,10 +109,10 @@ importers: version: 2.7.0 typeorm: specifier: ^0.3.20 - version: 0.3.20(ioredis@5.4.1)(pg@8.12.0)(ts-node@10.9.2(@swc/core@1.7.22)(@types/node@20.16.2)(typescript@5.5.4)) + version: 0.3.20(ioredis@5.4.1)(pg@8.12.0)(redis@4.7.0)(ts-node@10.9.2(@swc/core@1.7.22)(@types/node@20.16.2)(typescript@5.5.4)) typeorm-extension: specifier: ^3.6.1 - version: 3.6.1(typeorm@0.3.20(ioredis@5.4.1)(pg@8.12.0)(ts-node@10.9.2(@swc/core@1.7.22)(@types/node@20.16.2)(typescript@5.5.4))) + version: 3.6.1(typeorm@0.3.20(ioredis@5.4.1)(pg@8.12.0)(redis@4.7.0)(ts-node@10.9.2(@swc/core@1.7.22)(@types/node@20.16.2)(typescript@5.5.4))) uuid: specifier: ^10.0.0 version: 10.0.0 @@ -1167,6 +1176,14 @@ packages: '@nestjs/core': ^8.0.0 || ^9.0.0 || ^10.0.0 bullmq: ^3.0.0 || ^4.0.0 || ^5.0.0 + '@nestjs/cache-manager@2.2.2': + resolution: {integrity: sha512-+n7rpU1QABeW2WV17Dl1vZCG3vWjJU1MaamWgZvbGxYE9EeCM0lVLfw3z7acgDTNwOy+K68xuQPoIMxD0bhjlA==} + peerDependencies: + '@nestjs/common': ^9.0.0 || ^10.0.0 + '@nestjs/core': ^9.0.0 || ^10.0.0 + cache-manager: <=5 + rxjs: ^7.0.0 + '@nestjs/cli@10.4.5': resolution: {integrity: sha512-FP7Rh13u8aJbHe+zZ7hM0CC4785g9Pw4lz4r2TTgRtf0zTxSWMkJaPEwyjX8SK9oWK2GsYxl+fKpwVZNbmnj9A==} engines: {node: '>= 16.14'} @@ -1364,6 +1381,35 @@ packages: resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@redis/bloom@1.2.0': + resolution: {integrity: sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==} + peerDependencies: + '@redis/client': ^1.0.0 + + '@redis/client@1.6.0': + resolution: {integrity: sha512-aR0uffYI700OEEH4gYnitAnv3vzVGXCFvYfdpu/CJKvk4pHfLPEy/JSZyrpQ+15WhXe1yJRXLtfQ84s4mEXnPg==} + engines: {node: '>=14'} + + '@redis/graph@1.1.1': + resolution: {integrity: sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==} + peerDependencies: + '@redis/client': ^1.0.0 + + '@redis/json@1.0.7': + resolution: {integrity: sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==} + peerDependencies: + '@redis/client': ^1.0.0 + + '@redis/search@1.2.0': + resolution: {integrity: sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==} + peerDependencies: + '@redis/client': ^1.0.0 + + '@redis/time-series@1.1.0': + resolution: {integrity: sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==} + peerDependencies: + '@redis/client': ^1.0.0 + '@rollup/rollup-android-arm-eabi@4.20.0': resolution: {integrity: sha512-TSpWzflCc4VGAUJZlPpgAJE1+V60MePDQnBd7PPkpuEmOy8i87aL6tinFGKBFKuEDikYpig72QzdT3QPYIi+oA==} cpu: [arm] @@ -2348,6 +2394,14 @@ packages: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} + cache-manager-redis-yet@5.1.4: + resolution: {integrity: sha512-2mXZjo+txfH2m+mSTHTITNq8c5SssU2nP7NutzrocO3Mw/SbjHcDo+mriI3ZuR63ov/oUUIaF9iF+MzDqVzMoQ==} + engines: {node: '>= 18'} + + cache-manager@5.7.6: + resolution: {integrity: sha512-wBxnBHjDxF1RXpHCBD6HGvKER003Ts7IIm0CHpggliHzN1RZditb7rXoduE1rplc2DEFYKxhLKgFuchXMJje9w==} + engines: {node: '>= 18'} + cacheable-lookup@5.0.4: resolution: {integrity: sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==} engines: {node: '>=10.6.0'} @@ -3334,6 +3388,10 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + generic-pool@3.9.0: + resolution: {integrity: sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==} + engines: {node: '>= 4'} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -4151,6 +4209,9 @@ packages: lodash.camelcase@4.3.0: resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + lodash.clonedeep@4.5.0: + resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} + lodash.defaults@4.2.0: resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} @@ -5032,6 +5093,10 @@ packages: resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} engines: {node: '>= 0.6.0'} + promise-coalesce@1.1.2: + resolution: {integrity: sha512-zLaJ9b8hnC564fnJH6NFSOGZYYdzrAJn2JUUIwzoQb32fG2QAakpDNM+CZo1km6keXkRXRM+hml1BFAPVnPkxg==} + engines: {node: '>=16'} + promise@7.3.1: resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==} @@ -5172,6 +5237,9 @@ packages: resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} engines: {node: '>=4'} + redis@4.7.0: + resolution: {integrity: sha512-zvmkHEAdGMn+hMRXuMBtu4Vo5P6rHQjLoHftu+lBqq8ZTA3RCVC/WzD790bkKKiNFp7d5/9PcSD19fJyyRvOdQ==} + reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} @@ -6163,6 +6231,9 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + yaml@2.5.0: resolution: {integrity: sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==} engines: {node: '>= 14'} @@ -7216,6 +7287,13 @@ snapshots: bullmq: 5.12.14 tslib: 2.6.3 + '@nestjs/cache-manager@2.2.2(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(cache-manager@5.7.6)(rxjs@7.8.1)': + dependencies: + '@nestjs/common': 10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/core': 10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1) + cache-manager: 5.7.6 + rxjs: 7.8.1 + '@nestjs/cli@10.4.5(@swc/cli@0.4.0(@swc/core@1.7.22)(chokidar@3.6.0))(@swc/core@1.7.22)': dependencies: '@angular-devkit/core': 17.3.8(chokidar@3.6.0) @@ -7343,7 +7421,7 @@ snapshots: class-transformer: 0.5.1 class-validator: 0.14.1 - '@nestjs/terminus@10.2.3(bq7pyh5konlacyjccn47tf7plq)': + '@nestjs/terminus@10.2.3(a5lhekddtlsvoqvynenfvvi6la)': dependencies: '@nestjs/common': 10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/core': 10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -7353,8 +7431,8 @@ snapshots: rxjs: 7.8.1 optionalDependencies: '@nestjs/axios': 3.0.3(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(axios@1.7.7)(rxjs@7.8.1) - '@nestjs/typeorm': 10.0.2(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(encoding@0.1.13)(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.1)(pg@8.12.0)(ts-node@10.9.2(@swc/core@1.7.22)(@types/node@20.16.2)(typescript@5.5.4))) - typeorm: 0.3.20(ioredis@5.4.1)(pg@8.12.0)(ts-node@10.9.2(@swc/core@1.7.22)(@types/node@20.16.2)(typescript@5.5.4)) + '@nestjs/typeorm': 10.0.2(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(encoding@0.1.13)(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.1)(pg@8.12.0)(redis@4.7.0)(ts-node@10.9.2(@swc/core@1.7.22)(@types/node@20.16.2)(typescript@5.5.4))) + typeorm: 0.3.20(ioredis@5.4.1)(pg@8.12.0)(redis@4.7.0)(ts-node@10.9.2(@swc/core@1.7.22)(@types/node@20.16.2)(typescript@5.5.4)) '@nestjs/testing@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1))': dependencies: @@ -7364,13 +7442,13 @@ snapshots: optionalDependencies: '@nestjs/platform-express': 10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1) - '@nestjs/typeorm@10.0.2(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(encoding@0.1.13)(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.1)(pg@8.12.0)(ts-node@10.9.2(@swc/core@1.7.22)(@types/node@20.16.2)(typescript@5.5.4)))': + '@nestjs/typeorm@10.0.2(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(encoding@0.1.13)(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.1)(pg@8.12.0)(redis@4.7.0)(ts-node@10.9.2(@swc/core@1.7.22)(@types/node@20.16.2)(typescript@5.5.4)))': dependencies: '@nestjs/common': 10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/core': 10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(encoding@0.1.13)(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.1)(pg@8.12.0)(ts-node@10.9.2(@swc/core@1.7.22)(@types/node@20.16.2)(typescript@5.5.4)) + typeorm: 0.3.20(ioredis@5.4.1)(pg@8.12.0)(redis@4.7.0)(ts-node@10.9.2(@swc/core@1.7.22)(@types/node@20.16.2)(typescript@5.5.4)) uuid: 9.0.1 '@nodelib/fs.scandir@2.1.5': @@ -7403,6 +7481,32 @@ snapshots: '@pkgr/core@0.1.1': {} + '@redis/bloom@1.2.0(@redis/client@1.6.0)': + dependencies: + '@redis/client': 1.6.0 + + '@redis/client@1.6.0': + dependencies: + cluster-key-slot: 1.1.2 + generic-pool: 3.9.0 + yallist: 4.0.0 + + '@redis/graph@1.1.1(@redis/client@1.6.0)': + dependencies: + '@redis/client': 1.6.0 + + '@redis/json@1.0.7(@redis/client@1.6.0)': + dependencies: + '@redis/client': 1.6.0 + + '@redis/search@1.2.0(@redis/client@1.6.0)': + dependencies: + '@redis/client': 1.6.0 + + '@redis/time-series@1.1.0(@redis/client@1.6.0)': + dependencies: + '@redis/client': 1.6.0 + '@rollup/rollup-android-arm-eabi@4.20.0': optional: true @@ -8666,6 +8770,24 @@ snapshots: cac@6.7.14: {} + cache-manager-redis-yet@5.1.4: + dependencies: + '@redis/bloom': 1.2.0(@redis/client@1.6.0) + '@redis/client': 1.6.0 + '@redis/graph': 1.1.1(@redis/client@1.6.0) + '@redis/json': 1.0.7(@redis/client@1.6.0) + '@redis/search': 1.2.0(@redis/client@1.6.0) + '@redis/time-series': 1.1.0(@redis/client@1.6.0) + cache-manager: 5.7.6 + redis: 4.7.0 + + cache-manager@5.7.6: + dependencies: + eventemitter3: 5.0.1 + lodash.clonedeep: 4.5.0 + lru-cache: 10.2.2 + promise-coalesce: 1.1.2 + cacheable-lookup@5.0.4: {} cacheable-request@7.0.4: @@ -9781,6 +9903,8 @@ snapshots: function-bind@1.1.2: {} + generic-pool@3.9.0: {} + gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} @@ -10826,6 +10950,8 @@ snapshots: lodash.camelcase@4.3.0: {} + lodash.clonedeep@4.5.0: {} + lodash.defaults@4.2.0: {} lodash.includes@4.3.0: {} @@ -11897,6 +12023,8 @@ snapshots: process@0.11.10: {} + promise-coalesce@1.1.2: {} + promise@7.3.1: dependencies: asap: 2.0.6 @@ -12091,6 +12219,15 @@ snapshots: dependencies: redis-errors: 1.2.0 + redis@4.7.0: + dependencies: + '@redis/bloom': 1.2.0(@redis/client@1.6.0) + '@redis/client': 1.6.0 + '@redis/graph': 1.1.1(@redis/client@1.6.0) + '@redis/json': 1.0.7(@redis/client@1.6.0) + '@redis/search': 1.2.0(@redis/client@1.6.0) + '@redis/time-series': 1.1.0(@redis/client@1.6.0) + reflect-metadata@0.2.2: {} regenerator-runtime@0.14.1: @@ -12652,7 +12789,7 @@ snapshots: typedarray@0.0.6: {} - typeorm-extension@3.6.1(typeorm@0.3.20(ioredis@5.4.1)(pg@8.12.0)(ts-node@10.9.2(@swc/core@1.7.22)(@types/node@20.16.2)(typescript@5.5.4))): + typeorm-extension@3.6.1(typeorm@0.3.20(ioredis@5.4.1)(pg@8.12.0)(redis@4.7.0)(ts-node@10.9.2(@swc/core@1.7.22)(@types/node@20.16.2)(typescript@5.5.4))): dependencies: '@faker-js/faker': 8.4.1 consola: 3.2.3 @@ -12662,10 +12799,10 @@ snapshots: rapiq: 0.9.0 reflect-metadata: 0.2.2 smob: 1.5.0 - typeorm: 0.3.20(ioredis@5.4.1)(pg@8.12.0)(ts-node@10.9.2(@swc/core@1.7.22)(@types/node@20.16.2)(typescript@5.5.4)) + typeorm: 0.3.20(ioredis@5.4.1)(pg@8.12.0)(redis@4.7.0)(ts-node@10.9.2(@swc/core@1.7.22)(@types/node@20.16.2)(typescript@5.5.4)) yargs: 17.7.2 - typeorm@0.3.20(ioredis@5.4.1)(pg@8.12.0)(ts-node@10.9.2(@swc/core@1.7.22)(@types/node@20.16.2)(typescript@5.5.4)): + typeorm@0.3.20(ioredis@5.4.1)(pg@8.12.0)(redis@4.7.0)(ts-node@10.9.2(@swc/core@1.7.22)(@types/node@20.16.2)(typescript@5.5.4)): dependencies: '@sqltools/formatter': 1.2.5 app-root-path: 3.1.0 @@ -12685,6 +12822,7 @@ snapshots: optionalDependencies: ioredis: 5.4.1 pg: 8.12.0 + redis: 4.7.0 ts-node: 10.9.2(@swc/core@1.7.22)(@types/node@20.16.2)(typescript@5.5.4) transitivePeerDependencies: - supports-color @@ -13004,6 +13142,8 @@ snapshots: yallist@3.1.1: {} + yallist@4.0.0: {} + yaml@2.5.0: {} yargs-parser@20.2.9: {} diff --git a/src/api/auth/auth.controller.ts b/src/api/auth/auth.controller.ts index a2ab1d4..c14e503 100644 --- a/src/api/auth/auth.controller.ts +++ b/src/api/auth/auth.controller.ts @@ -1,6 +1,6 @@ import { CurrentUser } from '@/decorators/current-user.decorator'; import { ApiAuth, ApiPublic } from '@/decorators/http.decorators'; -import { Body, Controller, Post } from '@nestjs/common'; +import { Body, Controller, Get, Post } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { AuthService } from './auth.service'; import { LoginReqDto } from './dto/login.req.dto'; @@ -70,7 +70,7 @@ export class AuthController { } @ApiPublic() - @Post('verify/email') + @Get('verify/email') async verifyEmail() { return 'verify-email'; } diff --git a/src/api/auth/auth.service.spec.ts b/src/api/auth/auth.service.spec.ts index c041e93..865ab99 100644 --- a/src/api/auth/auth.service.spec.ts +++ b/src/api/auth/auth.service.spec.ts @@ -1,4 +1,5 @@ import { getQueueToken } from '@nestjs/bullmq'; +import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { ConfigService } from '@nestjs/config'; import { JwtService } from '@nestjs/jwt'; import { Test, TestingModule } from '@nestjs/testing'; @@ -50,6 +51,12 @@ describe('AuthService', () => { add: jest.fn(), }, }, + { + provide: CACHE_MANAGER, + useValue: { + set: jest.fn(), + }, + }, ], }).compile(); diff --git a/src/api/auth/auth.service.ts b/src/api/auth/auth.service.ts index 71f7ab4..01a718e 100644 --- a/src/api/auth/auth.service.ts +++ b/src/api/auth/auth.service.ts @@ -1,19 +1,20 @@ +import { IEmailJob, IVerifyEmailJob } from '@/common/interfaces/job.interface'; import { Branded } from '@/common/types/types'; import { AllConfigType } from '@/config/config.type'; import { SYSTEM_USER_ID } from '@/constants/app.constant'; +import { ErrorCode } from '@/constants/error-code.constant'; import { JobName, QueueName } from '@/constants/job.constant'; +import { ValidationException } from '@/exceptions/validation.exception'; import { verifyPassword } from '@/utils/password.util'; import { InjectQueue } from '@nestjs/bullmq'; -import { - BadRequestException, - Injectable, - UnauthorizedException, -} from '@nestjs/common'; +import { CACHE_MANAGER } from '@nestjs/cache-manager'; +import { Inject, Injectable, UnauthorizedException } from '@nestjs/common'; import { randomStringGenerator } from '@nestjs/common/utils/random-string-generator.util'; import { ConfigService } from '@nestjs/config'; import { JwtService } from '@nestjs/jwt'; import { InjectRepository } from '@nestjs/typeorm'; import { Queue } from 'bullmq'; +import { Cache } from 'cache-manager'; import { plainToInstance } from 'class-transformer'; import crypto from 'crypto'; import ms from 'ms'; @@ -46,7 +47,9 @@ export class AuthService { @InjectRepository(UserEntity) private readonly userRepository: Repository, @InjectQueue(QueueName.EMAIL) - private readonly emailQueue: Queue, + private readonly emailQueue: Queue, + @Inject(CACHE_MANAGER) + private readonly cacheManager: Cache, ) {} /** @@ -94,14 +97,17 @@ export class AuthService { } async register(dto: RegisterReqDto): Promise { - const existUser = await UserEntity.findOne({ where: { email: dto.email } }); + // Check if the user already exists + const isExistUser = await UserEntity.exists({ + where: { email: dto.email }, + }); - if (existUser) { - throw new BadRequestException('Account with this email already exists'); + if (isExistUser) { + throw new ValidationException(ErrorCode.E003); } + // Register user const user = new UserEntity({ - username: dto.email.split('@')[0], email: dto.email, password: dto.password, createdBy: SYSTEM_USER_ID, @@ -110,11 +116,25 @@ export class AuthService { await user.save(); + // Send email verification + const token = await this.createVerificationToken({ id: user.id }); + const tokenExpiresIn = this.configService.getOrThrow( + 'auth.confirmEmailExpires', + { + infer: true, + }, + ); + await this.cacheManager.set( + `auth:token:${user.id}:email-verification`, + token, + ms(tokenExpiresIn), + ); await this.emailQueue.add( JobName.EMAIL_VERIFICATION, { email: dto.email, - }, + token, + } as IVerifyEmailJob, { attempts: 3, backoff: { type: 'exponential', delay: 60000 } }, ); @@ -182,6 +202,22 @@ export class AuthService { } } + private async createVerificationToken(data: { id: string }): Promise { + return await this.jwtService.signAsync( + { + id: data.id, + }, + { + secret: this.configService.getOrThrow('auth.confirmEmailSecret', { + infer: true, + }), + expiresIn: this.configService.getOrThrow('auth.confirmEmailExpires', { + infer: true, + }), + }, + ); + } + private async createToken(data: { id: string; sessionId: string; diff --git a/src/api/post/entities/post.entity.ts b/src/api/post/entities/post.entity.ts index 502815c..a2402ae 100644 --- a/src/api/post/entities/post.entity.ts +++ b/src/api/post/entities/post.entity.ts @@ -5,6 +5,7 @@ import { Column, DeleteDateColumn, Entity, + JoinColumn, ManyToOne, PrimaryGeneratedColumn, Relation, @@ -32,6 +33,14 @@ export class PostEntity extends AbstractEntity { @Column({ nullable: true }) content?: string; + @Column({ name: 'user_id' }) + userId!: Uuid; + + @JoinColumn({ + name: 'user_id', + referencedColumnName: 'id', + foreignKeyConstraintName: 'FK_post_user_id', + }) @ManyToOne(() => UserEntity, (user) => user.posts) user: Relation; diff --git a/src/api/user/entities/user.entity.ts b/src/api/user/entities/user.entity.ts index 7d79698..ad11af0 100644 --- a/src/api/user/entities/user.entity.ts +++ b/src/api/user/entities/user.entity.ts @@ -25,12 +25,15 @@ export class UserEntity extends AbstractEntity { @PrimaryGeneratedColumn('uuid', { primaryKeyConstraintName: 'PK_user_id' }) id!: Uuid; - @Column() + @Column({ + length: 50, + nullable: true, + }) @Index('UQ_user_username', { where: '"deleted_at" IS NULL', unique: true, }) - username!: string; + username: string; @Column() @Index('UQ_user_email', { where: '"deleted_at" IS NULL', unique: true }) diff --git a/src/background/queues/email-queue/email-queue.service.ts b/src/background/queues/email-queue/email-queue.service.ts index 266e9c9..165c776 100644 --- a/src/background/queues/email-queue/email-queue.service.ts +++ b/src/background/queues/email-queue/email-queue.service.ts @@ -1,3 +1,4 @@ +import { IVerifyEmailJob } from '@/common/interfaces/job.interface'; import { MailService } from '@/mail/mail.service'; import { Injectable, Logger } from '@nestjs/common'; @@ -7,10 +8,8 @@ export class EmailQueueService { constructor(private readonly mailService: MailService) {} - async sendEmailVerification(data: any): Promise { + async sendEmailVerification(data: IVerifyEmailJob): Promise { this.logger.debug(`Sending email verification to ${data.email}`); - await this.mailService.sendEmailVerification(data.email, 'test'); // TODO: Update logic when sending email verification - - return null; + await this.mailService.sendEmailVerification(data.email, data.token); } } diff --git a/src/background/queues/email-queue/email.processor.ts b/src/background/queues/email-queue/email.processor.ts index 0eae047..b72b8ac 100644 --- a/src/background/queues/email-queue/email.processor.ts +++ b/src/background/queues/email-queue/email.processor.ts @@ -1,3 +1,4 @@ +import { IEmailJob, IVerifyEmailJob } from '@/common/interfaces/job.interface'; import { JobName, QueueName } from '@/constants/job.constant'; import { OnWorkerEvent, Processor, WorkerHost } from '@nestjs/bullmq'; import { Logger } from '@nestjs/common'; @@ -22,14 +23,19 @@ export class EmailProcessor extends WorkerHost { constructor(private readonly emailQueueService: EmailQueueService) { super(); } - async process(job: Job, _token?: string): Promise { + async process( + job: Job, + _token?: string, + ): Promise { this.logger.debug( `Processing job ${job.id} of type ${job.name} with data ${JSON.stringify(job.data)}...`, ); switch (job.name) { case JobName.EMAIL_VERIFICATION: - return await this.emailQueueService.sendEmailVerification(job.data); + return await this.emailQueueService.sendEmailVerification( + job.data as unknown as IVerifyEmailJob, + ); default: throw new Error(`Unknown job name: ${job.name}`); } diff --git a/src/common/interfaces/job.interface.ts b/src/common/interfaces/job.interface.ts new file mode 100644 index 0000000..fce5d7c --- /dev/null +++ b/src/common/interfaces/job.interface.ts @@ -0,0 +1,7 @@ +export interface IEmailJob { + email: string; +} + +export interface IVerifyEmailJob extends IEmailJob { + token: string; +} diff --git a/src/constants/error-code.constant.ts b/src/constants/error-code.constant.ts index 1d302d4..f63adfe 100644 --- a/src/constants/error-code.constant.ts +++ b/src/constants/error-code.constant.ts @@ -9,4 +9,5 @@ export enum ErrorCode { // Error E001 = 'user.error.username_or_email_exists', E002 = 'user.error.not_found', + E003 = 'user.error.email_exists', } diff --git a/src/database/migrations/1721488504685-create-user-table.ts b/src/database/migrations/1721488504685-create-user-table.ts index aecb433..28d16f5 100644 --- a/src/database/migrations/1721488504685-create-user-table.ts +++ b/src/database/migrations/1721488504685-create-user-table.ts @@ -7,7 +7,7 @@ export class CreateUserTable1720105653064 implements MigrationInterface { await queryRunner.query(` CREATE TABLE "user" ( "id" uuid NOT NULL DEFAULT uuid_generate_v4(), - "username" character varying NOT NULL, + "username" character varying(50), "email" character varying NOT NULL, "password" character varying NOT NULL, "bio" character varying NOT NULL DEFAULT '', diff --git a/src/database/migrations/1722352657866-create-post-table.ts b/src/database/migrations/1722352657866-create-post-table.ts index d9c31c6..da4a226 100644 --- a/src/database/migrations/1722352657866-create-post-table.ts +++ b/src/database/migrations/1722352657866-create-post-table.ts @@ -7,10 +7,11 @@ export class CreatePostTable1722352657866 implements MigrationInterface { await queryRunner.query(` CREATE TABLE "post" ( "id" uuid NOT NULL DEFAULT uuid_generate_v4(), - "slug" character varying NOT NULL, "title" character varying NOT NULL, + "slug" character varying NOT NULL, "description" character varying, "content" character varying, + "user_id" uuid NOT NULL, "deleted_at" TIMESTAMP WITH TIME ZONE, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "created_by" character varying NOT NULL, @@ -18,10 +19,18 @@ export class CreatePostTable1722352657866 implements MigrationInterface { "updated_by" character varying NOT NULL, CONSTRAINT "PK_post_id" PRIMARY KEY ("id") ) - `); + `); + + await queryRunner.query(` + ALTER TABLE "post" + ADD CONSTRAINT "FK_post_user_id" FOREIGN KEY ("user_id") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION + `); } public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE "post" DROP CONSTRAINT "FK_post_user_id" + `); await queryRunner.query(` DROP TABLE "post" `); diff --git a/src/database/seeds/1722382752268-post-seeder.ts b/src/database/seeds/1722382752268-post-seeder.ts index 060350b..0dcb785 100644 --- a/src/database/seeds/1722382752268-post-seeder.ts +++ b/src/database/seeds/1722382752268-post-seeder.ts @@ -1,4 +1,5 @@ import { PostEntity } from '@/api/post/entities/post.entity'; +import { UserEntity } from '@/api/user/entities/user.entity'; import { DataSource } from 'typeorm'; import { Seeder, SeederFactoryManager } from 'typeorm-extension'; @@ -6,11 +7,14 @@ export class PostSeeder1722382752268 implements Seeder { track = false; public async run( - _dataSource: DataSource, + dataSource: DataSource, factoryManager: SeederFactoryManager, ): Promise { - const postFactory = factoryManager.get(PostEntity); - - await postFactory.saveMany(10); + const userRepository = dataSource.getRepository(UserEntity); + const adminUser = await userRepository.findOneBy({ username: 'admin' }); + if (adminUser) { + const postFactory = factoryManager.get(PostEntity); + await postFactory.saveMany(10, { userId: adminUser.id }); + } } } diff --git a/src/generated/i18n.generated.ts b/src/generated/i18n.generated.ts index 61e481f..266befa 100644 --- a/src/generated/i18n.generated.ts +++ b/src/generated/i18n.generated.ts @@ -23,6 +23,7 @@ export type I18nTranslations = { }; "error": { "username_or_email_exists": string; + "email_exists": string; "not_found": string; "invalid_password": string; "invalid_token": string; diff --git a/src/i18n/en/user.json b/src/i18n/en/user.json index 3daaea3..d43e4c4 100644 --- a/src/i18n/en/user.json +++ b/src/i18n/en/user.json @@ -8,6 +8,7 @@ }, "error": { "username_or_email_exists": "Username or email already exists", + "email_exists": "Account with this email already exists", "not_found": "User not found", "invalid_password": "Invalid password", "invalid_token": "Invalid token" diff --git a/src/i18n/jp/user.json b/src/i18n/jp/user.json index 6b6ae69..2d7c18d 100644 --- a/src/i18n/jp/user.json +++ b/src/i18n/jp/user.json @@ -5,5 +5,12 @@ }, "validation": { "is_empty": "は必須です" + }, + "error": { + "username_or_email_exists": "ユーザー名またはメールアドレスは既に存在します", + "email_exists": "このメールアドレスを持つアカウントは既に存在します", + "not_found": "ユーザーが見つかりません", + "invalid_password": "無効なパスワード", + "invalid_token": "無効なトークン" } } diff --git a/src/i18n/vi/user.json b/src/i18n/vi/user.json index 7c40bcd..9d5b834 100644 --- a/src/i18n/vi/user.json +++ b/src/i18n/vi/user.json @@ -5,5 +5,12 @@ }, "validation": { "is_empty": "Trường này không được để trống" + }, + "error": { + "username_or_email_exists": "Tên người dùng hoặc email đã tồn tại", + "email_exists": "Tài khoản với email này đã tồn tại", + "not_found": "Người dùng không tồn tại", + "invalid_password": "Mật khẩu không hợp lệ", + "invalid_token": "Token không hợp lệ" } } diff --git a/src/mail/mail.service.ts b/src/mail/mail.service.ts index 55544bd..a778e31 100644 --- a/src/mail/mail.service.ts +++ b/src/mail/mail.service.ts @@ -1,12 +1,18 @@ +import { AllConfigType } from '@/config/config.type'; import { MailerService } from '@nestjs-modules/mailer'; import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; @Injectable() export class MailService { - constructor(private readonly mailerService: MailerService) {} + constructor( + private readonly configService: ConfigService, + private readonly mailerService: MailerService, + ) {} async sendEmailVerification(email: string, token: string) { - const url = `example.com/auth/verify-email?token=${token}`; + // Please replace the URL with your own frontend URL + const url = `${this.configService.get('app.url', { infer: true })}/api/v1/auth/verify/email?token=${token}`; await this.mailerService.sendMail({ to: email, diff --git a/src/mail/templates/email-verification.hbs b/src/mail/templates/email-verification.hbs index bb3a8f1..173ff90 100644 --- a/src/mail/templates/email-verification.hbs +++ b/src/mail/templates/email-verification.hbs @@ -4,7 +4,7 @@ - Simple Transactional Email + Email Verification