diff --git a/Dockerfile b/Dockerfile index 87fb782..4ebd247 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,5 +10,6 @@ RUN pnpm install COPY . . RUN pnpm run build +EXPOSE 3000 CMD [ "pnpm", "start:dev" ] \ No newline at end of file diff --git a/package.json b/package.json index 7a28ad1..84c35cc 100644 --- a/package.json +++ b/package.json @@ -28,9 +28,12 @@ "@nestjs/swagger": "^7.4.2", "@nestjs/typeorm": "^10.0.2", "axios": "^1.7.7", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", "moralis": "^2.27.2", "nodemailer": "^6.9.15", "pg": "^8.13.0", + "rate-limiter-flexible": "^5.0.3", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", "typeorm": "^0.3.20" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6e9f3c9..922ec2e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,28 +10,34 @@ importers: dependencies: '@nestjs/common': specifier: ^10.0.0 - version: 10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1) + version: 10.4.4(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/config': specifier: ^3.2.3 - version: 3.2.3(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(rxjs@7.8.1) + version: 3.2.3(@nestjs/common@10.4.4(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(rxjs@7.8.1) '@nestjs/core': specifier: ^10.0.0 - version: 10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4)(reflect-metadata@0.2.2)(rxjs@7.8.1) + version: 10.4.4(@nestjs/common@10.4.4(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4)(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/platform-express': specifier: ^10.0.0 - version: 10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.4) + version: 10.4.4(@nestjs/common@10.4.4(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.4) '@nestjs/schedule': specifier: ^4.1.1 - version: 4.1.1(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4)(reflect-metadata@0.2.2)(rxjs@7.8.1)) + version: 4.1.1(@nestjs/common@10.4.4(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.4(@nestjs/common@10.4.4(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4)(reflect-metadata@0.2.2)(rxjs@7.8.1)) '@nestjs/swagger': specifier: ^7.4.2 - version: 7.4.2(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4)(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2) + version: 7.4.2(@nestjs/common@10.4.4(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.4(@nestjs/common@10.4.4(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4)(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/typeorm': specifier: ^10.0.2 - version: 10.0.2(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4)(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.0)(ts-node@10.9.2(@types/node@20.16.11)(typescript@5.6.3))) + version: 10.0.2(@nestjs/common@10.4.4(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.4(@nestjs/common@10.4.4(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4)(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.0)(ts-node@10.9.2(@types/node@20.16.11)(typescript@5.6.3))) axios: specifier: ^1.7.7 version: 1.7.7 + class-transformer: + specifier: ^0.5.1 + version: 0.5.1 + class-validator: + specifier: ^0.14.1 + version: 0.14.1 moralis: specifier: ^2.27.2 version: 2.27.2 @@ -41,6 +47,9 @@ importers: pg: specifier: ^8.13.0 version: 8.13.0 + rate-limiter-flexible: + specifier: ^5.0.3 + version: 5.0.3 reflect-metadata: specifier: ^0.2.0 version: 0.2.2 @@ -59,7 +68,7 @@ importers: version: 10.1.4(chokidar@3.6.0)(typescript@5.6.3) '@nestjs/testing': specifier: ^10.0.0 - version: 10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.4)) + version: 10.4.4(@nestjs/common@10.4.4(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.4(@nestjs/common@10.4.4(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4(@nestjs/common@10.4.4(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.4)) '@types/express': specifier: ^4.17.17 version: 4.17.21 @@ -848,6 +857,9 @@ packages: '@types/supertest@6.0.2': resolution: {integrity: sha512-137ypx2lk/wTQbW6An6safu9hXmajAifU/s7szAHLN/FeIm5w7yR0Wkl9fdJMRSHwOn4HLAI0DaB2TOORuhPDg==} + '@types/validator@13.12.2': + resolution: {integrity: sha512-6SlHBzUW8Jhf3liqrGGXyTJSIFe4nqlJ5A5KaMZ2l/vbM3Wh3KSybots/wfWVzNLK4D1NZluDlSQIbIEPx6oyA==} + '@types/yargs-parser@21.0.3': resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} @@ -1258,6 +1270,12 @@ packages: cjs-module-lexer@1.4.1: resolution: {integrity: sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==} + class-transformer@0.5.1: + resolution: {integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==} + + class-validator@0.14.1: + resolution: {integrity: sha512-2VEG9JICxIqTpoK1eMzZqaV+u/EiwEJkMGzTrZf6sU/fwsnOITVgYJ8yojSy6CaXtO9V0Cc6ZQZ8h8m4UBuLwQ==} + cli-cursor@3.1.0: resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} engines: {node: '>=8'} @@ -2239,6 +2257,9 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + libphonenumber-js@1.11.11: + resolution: {integrity: sha512-mF3KaORjJQR6JBNcOkluDcJKhtoQT4VTLRMrX1v/wlBayL4M8ybwEDeryyPcrSEJmD0rVwHUbBarpZwN5NfPFQ==} + lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -2692,6 +2713,9 @@ packages: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} + rate-limiter-flexible@5.0.3: + resolution: {integrity: sha512-lWx2y8NBVlTOLPyqs+6y7dxfEpT6YFqKy3MzWbCy95sTTOhOuxufP2QvRyOHpfXpB9OUJPbVLybw3z3AVAS5fA==} + raw-body@2.5.2: resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} engines: {node: '>= 0.8'} @@ -3244,6 +3268,10 @@ packages: resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} engines: {node: '>=10.12.0'} + validator@13.12.0: + resolution: {integrity: sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==} + engines: {node: '>= 0.10'} + vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -4127,25 +4155,28 @@ snapshots: - uglify-js - webpack-cli - '@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1)': + '@nestjs/common@10.4.4(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)': dependencies: iterare: 1.2.1 reflect-metadata: 0.2.2 rxjs: 7.8.1 tslib: 2.7.0 uid: 2.0.2 + optionalDependencies: + class-transformer: 0.5.1 + class-validator: 0.14.1 - '@nestjs/config@3.2.3(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(rxjs@7.8.1)': + '@nestjs/config@3.2.3(@nestjs/common@10.4.4(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(rxjs@7.8.1)': dependencies: - '@nestjs/common': 10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/common': 10.4.4(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) dotenv: 16.4.5 dotenv-expand: 10.0.0 lodash: 4.17.21 rxjs: 7.8.1 - '@nestjs/core@10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4)(reflect-metadata@0.2.2)(rxjs@7.8.1)': + '@nestjs/core@10.4.4(@nestjs/common@10.4.4(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4)(reflect-metadata@0.2.2)(rxjs@7.8.1)': dependencies: - '@nestjs/common': 10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/common': 10.4.4(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nuxtjs/opencollective': 0.3.2 fast-safe-stringify: 2.1.1 iterare: 1.2.1 @@ -4155,19 +4186,22 @@ snapshots: tslib: 2.7.0 uid: 2.0.2 optionalDependencies: - '@nestjs/platform-express': 10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.4) + '@nestjs/platform-express': 10.4.4(@nestjs/common@10.4.4(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.4) transitivePeerDependencies: - encoding - '@nestjs/mapped-types@2.0.5(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2)': + '@nestjs/mapped-types@2.0.5(@nestjs/common@10.4.4(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.4(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/common': 10.4.4(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) reflect-metadata: 0.2.2 + optionalDependencies: + class-transformer: 0.5.1 + class-validator: 0.14.1 - '@nestjs/platform-express@10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.4)': + '@nestjs/platform-express@10.4.4(@nestjs/common@10.4.4(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.4)': dependencies: - '@nestjs/common': 10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1) - '@nestjs/core': 10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/common': 10.4.4(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/core': 10.4.4(@nestjs/common@10.4.4(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4)(reflect-metadata@0.2.2)(rxjs@7.8.1) body-parser: 1.20.3 cors: 2.8.5 express: 4.21.0 @@ -4176,10 +4210,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@nestjs/schedule@4.1.1(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4)(reflect-metadata@0.2.2)(rxjs@7.8.1))': + '@nestjs/schedule@4.1.1(@nestjs/common@10.4.4(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.4(@nestjs/common@10.4.4(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4)(reflect-metadata@0.2.2)(rxjs@7.8.1))': dependencies: - '@nestjs/common': 10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1) - '@nestjs/core': 10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/common': 10.4.4(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/core': 10.4.4(@nestjs/common@10.4.4(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4)(reflect-metadata@0.2.2)(rxjs@7.8.1) cron: 3.1.7 uuid: 10.0.0 @@ -4205,30 +4239,33 @@ snapshots: transitivePeerDependencies: - chokidar - '@nestjs/swagger@7.4.2(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4)(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2)': + '@nestjs/swagger@7.4.2(@nestjs/common@10.4.4(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.4(@nestjs/common@10.4.4(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4)(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: '@microsoft/tsdoc': 0.15.0 - '@nestjs/common': 10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1) - '@nestjs/core': 10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4)(reflect-metadata@0.2.2)(rxjs@7.8.1) - '@nestjs/mapped-types': 2.0.5(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2) + '@nestjs/common': 10.4.4(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/core': 10.4.4(@nestjs/common@10.4.4(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/mapped-types': 2.0.5(@nestjs/common@10.4.4(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) js-yaml: 4.1.0 lodash: 4.17.21 path-to-regexp: 3.3.0 reflect-metadata: 0.2.2 swagger-ui-dist: 5.17.14 + optionalDependencies: + class-transformer: 0.5.1 + class-validator: 0.14.1 - '@nestjs/testing@10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.4))': + '@nestjs/testing@10.4.4(@nestjs/common@10.4.4(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.4(@nestjs/common@10.4.4(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4(@nestjs/common@10.4.4(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.4))': dependencies: - '@nestjs/common': 10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1) - '@nestjs/core': 10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/common': 10.4.4(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/core': 10.4.4(@nestjs/common@10.4.4(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4)(reflect-metadata@0.2.2)(rxjs@7.8.1) tslib: 2.7.0 optionalDependencies: - '@nestjs/platform-express': 10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.4) + '@nestjs/platform-express': 10.4.4(@nestjs/common@10.4.4(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.4) - '@nestjs/typeorm@10.0.2(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4)(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.0)(ts-node@10.9.2(@types/node@20.16.11)(typescript@5.6.3)))': + '@nestjs/typeorm@10.0.2(@nestjs/common@10.4.4(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.4(@nestjs/common@10.4.4(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4)(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.0)(ts-node@10.9.2(@types/node@20.16.11)(typescript@5.6.3)))': dependencies: - '@nestjs/common': 10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1) - '@nestjs/core': 10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/common': 10.4.4(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/core': 10.4.4(@nestjs/common@10.4.4(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4)(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.0)(ts-node@10.9.2(@types/node@20.16.11)(typescript@5.6.3)) @@ -4424,6 +4461,8 @@ snapshots: '@types/methods': 1.1.4 '@types/superagent': 8.1.9 + '@types/validator@13.12.2': {} + '@types/yargs-parser@21.0.3': {} '@types/yargs@17.0.33': @@ -4927,6 +4966,14 @@ snapshots: cjs-module-lexer@1.4.1: {} + class-transformer@0.5.1: {} + + class-validator@0.14.1: + dependencies: + '@types/validator': 13.12.2 + libphonenumber-js: 1.11.11 + validator: 13.12.0 + cli-cursor@3.1.0: dependencies: restore-cursor: 3.1.0 @@ -6187,6 +6234,8 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + libphonenumber-js@1.11.11: {} + lines-and-columns@1.2.4: {} loader-runner@4.3.0: {} @@ -6590,6 +6639,8 @@ snapshots: range-parser@1.2.1: {} + rate-limiter-flexible@5.0.3: {} + raw-body@2.5.2: dependencies: bytes: 3.1.2 @@ -7086,6 +7137,8 @@ snapshots: '@types/istanbul-lib-coverage': 2.0.6 convert-source-map: 2.0.0 + validator@13.12.0: {} + vary@1.1.2: {} walker@1.0.8: diff --git a/src/alert/alert.controller.ts b/src/alert/alert.controller.ts index 27e3b8b..2d3ed31 100644 --- a/src/alert/alert.controller.ts +++ b/src/alert/alert.controller.ts @@ -1,6 +1,13 @@ -import { Controller, Post, Body } from '@nestjs/common'; +import { + Controller, + Post, + Body, + ValidationPipe, + UsePipes, +} from '@nestjs/common'; import { AlertService } from './alert.service'; -import { ApiTags, ApiOperation } from '@nestjs/swagger'; +import { CreateAlertDto } from './dto/create-alert.dto'; +import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; @ApiTags('alerts') @Controller('alerts') @@ -9,13 +16,17 @@ export class AlertController { @Post() @ApiOperation({ summary: 'Create a new price alert' }) - async createAlert( - @Body() alertData: { chain: string; targetPrice: number; email: string }, - ) { + @ApiResponse({ + status: 201, + description: 'The alert has been successfully created.', + }) + @ApiResponse({ status: 400, description: 'Invalid input data.' }) + @UsePipes(new ValidationPipe({ transform: true })) + async createAlert(@Body() createAlertDto: CreateAlertDto) { return this.alertService.createAlert( - alertData.chain, - alertData.targetPrice, - alertData.email, + createAlertDto.chain, + createAlertDto.targetPrice, + createAlertDto.email, ); } } diff --git a/src/alert/alert.service.ts b/src/alert/alert.service.ts index 9746a2c..0ab0d13 100644 --- a/src/alert/alert.service.ts +++ b/src/alert/alert.service.ts @@ -1,4 +1,4 @@ -import { Injectable, Logger } from '@nestjs/common'; +import { Injectable, Logger, HttpException, HttpStatus } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Alert } from './entities/alert.entity'; @@ -32,8 +32,16 @@ export class AlertService { targetPrice: number, email: string, ): Promise { - const alert = this.alertRepository.create({ chain, targetPrice, email }); - return this.alertRepository.save(alert); + try { + const alert = this.alertRepository.create({ chain, targetPrice, email }); + return await this.alertRepository.save(alert); + } catch (error) { + this.logger.error(`Error creating alert: ${error.message}`, error.stack); + throw new HttpException( + 'Failed to create alert', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } } async checkAlerts() { @@ -50,7 +58,7 @@ export class AlertService { } } } catch (error) { - this.logger.error('Error checking alerts', error.stack); + this.logger.error(`Error checking alerts: ${error.message}`, error.stack); } } @@ -76,7 +84,7 @@ export class AlertService { } } catch (error) { this.logger.error( - `Error checking price increase for ${chain}`, + `Error checking price increase for ${chain}: ${error.message}`, error.stack, ); } @@ -85,7 +93,10 @@ export class AlertService { private async getCurrentPrice(chain: string): Promise { const prices = await this.priceService.getPricesLastHour(chain); - return prices[0]?.price || 0; + if (prices.length === 0) { + throw new Error(`No recent prices found for ${chain}`); + } + return prices[0].price; } private async sendAlertEmail(alert: Alert, currentPrice: number) { @@ -115,7 +126,8 @@ export class AlertService { }); this.logger.log(`Email sent to ${to}: ${subject}`); } catch (error) { - this.logger.error('Error sending email', error.stack); + this.logger.error(`Error sending email: ${error.message}`, error.stack); + throw new Error('Failed to send email'); } } } diff --git a/src/alert/dto/create-alert.dto.ts b/src/alert/dto/create-alert.dto.ts index e379fd7..975f506 100644 --- a/src/alert/dto/create-alert.dto.ts +++ b/src/alert/dto/create-alert.dto.ts @@ -1 +1,20 @@ -export class CreateAlertDto {} +import { IsString, IsNumber, IsEmail, IsIn } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class CreateAlertDto { + @ApiProperty({ + description: 'The blockchain to monitor', + enum: ['ethereum', 'polygon'], + }) + @IsString() + @IsIn(['ethereum', 'polygon']) + chain: string; + + @ApiProperty({ description: 'The target price to trigger the alert' }) + @IsNumber() + targetPrice: number; + + @ApiProperty({ description: 'The email address to send the alert to' }) + @IsEmail() + email: string; +} diff --git a/src/app.module.ts b/src/app.module.ts index 3677724..72f57e3 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -5,6 +5,8 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { ScheduleModule } from '@nestjs/schedule'; import { PriceModule } from './price/price.module'; import { AlertModule } from './alert/alert.module'; +import { HealthController } from './health/health.controller'; +import { ScheduledTasksService } from './scheduled-tasks.service'; import configuration from './config/configuration'; @Module({ @@ -31,5 +33,7 @@ import configuration from './config/configuration'; PriceModule, AlertModule, ], + controllers: [HealthController], + providers: [ScheduledTasksService], }) export class AppModule {} diff --git a/src/common/http-exception.filter.ts b/src/common/http-exception.filter.ts new file mode 100644 index 0000000..30b58c4 --- /dev/null +++ b/src/common/http-exception.filter.ts @@ -0,0 +1,42 @@ +import { + ExceptionFilter, + Catch, + ArgumentsHost, + HttpException, + HttpStatus, + Logger, +} from '@nestjs/common'; +import { Request, Response } from 'express'; + +@Catch() +export class HttpExceptionFilter implements ExceptionFilter { + private readonly logger = new Logger(HttpExceptionFilter.name); + + catch(exception: unknown, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + const request = ctx.getRequest(); + const status = + exception instanceof HttpException + ? exception.getStatus() + : HttpStatus.INTERNAL_SERVER_ERROR; + + const message = + exception instanceof HttpException + ? exception.getResponse() + : 'Internal server error'; + + this.logger.error( + `Http Status: ${status} Error Message: ${JSON.stringify(message)}`, + exception instanceof Error ? exception.stack : undefined, + `${request.method} ${request.url}`, + ); + + response.status(status).json({ + statusCode: status, + timestamp: new Date().toISOString(), + path: request.url, + message: message, + }); + } +} diff --git a/src/common/rate-limiting.guard.ts b/src/common/rate-limiting.guard.ts new file mode 100644 index 0000000..7582a51 --- /dev/null +++ b/src/common/rate-limiting.guard.ts @@ -0,0 +1,38 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + HttpException, + HttpStatus, +} from '@nestjs/common'; +import { Observable } from 'rxjs'; +import { RateLimiterMemory } from 'rate-limiter-flexible'; + +@Injectable() +export class RateLimitingGuard implements CanActivate { + private rateLimiter: RateLimiterMemory; + + constructor() { + this.rateLimiter = new RateLimiterMemory({ + points: 10, // Number of points + duration: 1, // Per second + }); + } + + canActivate( + context: ExecutionContext, + ): boolean | Promise | Observable { + const request = context.switchToHttp().getRequest(); + const key = request.ip; + + return this.rateLimiter + .consume(key) + .then(() => true) + .catch(() => { + throw new HttpException( + 'Too Many Requests', + HttpStatus.TOO_MANY_REQUESTS, + ); + }); + } +} diff --git a/src/health/health.controller.ts b/src/health/health.controller.ts new file mode 100644 index 0000000..9dceaa1 --- /dev/null +++ b/src/health/health.controller.ts @@ -0,0 +1,13 @@ +import { Controller, Get } from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; + +@ApiTags('health') +@Controller('health') +export class HealthController { + @Get() + @ApiOperation({ summary: 'Check application health' }) + @ApiResponse({ status: 200, description: 'Application is healthy' }) + check() { + return { status: 'ok' }; + } +} diff --git a/src/main.ts b/src/main.ts index e2a554f..bcfbf70 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,14 +1,24 @@ +import { RateLimitingGuard } from './common/rate-limiting.guard'; import { NestFactory } from '@nestjs/core'; -import { AppModule } from './app.module'; import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; +import { ValidationPipe } from '@nestjs/common'; +import { AppModule } from './app.module'; +import { HttpExceptionFilter } from './common/http-exception.filter'; async function bootstrap() { const app = await NestFactory.create(AppModule); + app.useGlobalPipes(new ValidationPipe()); + app.useGlobalFilters(new HttpExceptionFilter()); + app.useGlobalGuards(new RateLimitingGuard()); + const config = new DocumentBuilder() .setTitle('Blockchain Price Tracker') .setDescription('API for tracking Ethereum and Polygon prices') .setVersion('1.0') + .addTag('prices') + .addTag('alerts') + .addTag('health') .build(); const document = SwaggerModule.createDocument(app, config); SwaggerModule.setup('api', app, document); diff --git a/src/price/price.service.ts b/src/price/price.service.ts index 39d53d6..e5947f6 100644 --- a/src/price/price.service.ts +++ b/src/price/price.service.ts @@ -1,6 +1,6 @@ -import { Injectable, Logger } from '@nestjs/common'; +import { Injectable, Logger, HttpException, HttpStatus } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { MoreThan, Repository } from 'typeorm'; +import { Repository, MoreThan } from 'typeorm'; import { Price } from './entities/price.entity'; import { ConfigService } from '@nestjs/config'; import axios from 'axios'; @@ -28,7 +28,11 @@ export class PriceService { this.logger.log(`Saved ${chain} price: $${price}`); } } catch (error) { - this.logger.error('Error saving prices', error.stack); + this.logger.error(`Error saving prices: ${error.message}`, error.stack); + throw new HttpException( + 'Failed to save prices', + HttpStatus.INTERNAL_SERVER_ERROR, + ); } } @@ -44,18 +48,23 @@ export class PriceService { }); return parseFloat(response.data.usdPrice); } catch (error) { - this.logger.error(`Error fetching ${chain} price`, error.stack); - throw error; + this.logger.error( + `Error fetching ${chain} price: ${error.message}`, + error.stack, + ); + throw new HttpException( + `Failed to fetch ${chain} price`, + HttpStatus.INTERNAL_SERVER_ERROR, + ); } } private getAddress(chain: string): string { - // You might want to store these in a configuration file const addresses = { ethereum: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', // WETH polygon: '0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270', // WMATIC }; - return addresses[chain]; + return addresses[chain] || ''; } async getPricesLastHour(chain: string): Promise {