From c037c19ef18d296107bd591151df5d2930fe8393 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 2 Feb 2026 19:38:55 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[HIGH]=20En?= =?UTF-8?q?able=20rate=20limiting=20and=20fix=20dependency=20mismatch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enabled global rate limiting in `backend/src/app.ts` to protect against DoS and brute force attacks. Fixed `@fastify/jwt` dependency version mismatch (v10 -> v8) to allow backend to start on Fastify v4. Updated global error handler to correctly process rate limit (429) errors. Added Jest configuration and security verification test suite. Co-authored-by: Magnopiro-oficial <122941268+Magnopiro-oficial@users.noreply.github.com> --- .jules/sentinel.md | 4 + backend/jest.config.js | 6 + backend/jest.setup.js | 20 +++ backend/package-lock.json | 105 ++++---------- backend/package.json | 2 +- backend/src/__tests__/rate_limit.test.ts | 56 ++++++++ backend/src/app.ts | 22 +-- backend/src/config/env.config.ts | 131 ++++++++++-------- backend/src/shared/database.ts | 85 +++++++----- .../src/shared/middleware/error.middleware.ts | 2 + 10 files changed, 244 insertions(+), 189 deletions(-) create mode 100644 .jules/sentinel.md create mode 100644 backend/jest.config.js create mode 100644 backend/jest.setup.js create mode 100644 backend/src/__tests__/rate_limit.test.ts diff --git a/.jules/sentinel.md b/.jules/sentinel.md new file mode 100644 index 0000000..2e1756c --- /dev/null +++ b/.jules/sentinel.md @@ -0,0 +1,4 @@ +## 2026-02-02 - Fastify Rate Limit Error Handling +**Vulnerability:** API endpoints were vulnerable to DoS/brute force because rate limiting was disabled. +**Learning:** `@fastify/rate-limit` errors (code 429) are not automatically mapped to HTTP 429 in a custom global error handler if the handler relies on `error.statusCode` which might be missing on the error object thrown by the plugin. It requires explicit checks for `error.code === 429` (numeric) or ensuring the plugin configuration sets the statusCode on the error object. +**Prevention:** Always verify that security middleware errors (like 429, 401, 403) are correctly intercepted and returned with the proper status code by the global error handler. diff --git a/backend/jest.config.js b/backend/jest.config.js new file mode 100644 index 0000000..0663393 --- /dev/null +++ b/backend/jest.config.js @@ -0,0 +1,6 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/__tests__/**/*.test.ts'], + setupFiles: ['./jest.setup.js'], +}; diff --git a/backend/jest.setup.js b/backend/jest.setup.js new file mode 100644 index 0000000..a2d3046 --- /dev/null +++ b/backend/jest.setup.js @@ -0,0 +1,20 @@ +process.env.NODE_ENV = 'test'; +process.env.JWT_SECRET = 'test-secret-key-at-least-32-chars-long-12345'; +process.env.DATABASE_URL = 'postgresql://test:test@localhost:5432/test_db'; +// Add other required vars to prevent env.config.ts from throwing +process.env.LUNES_RPC_URL = 'http://localhost:9933'; +process.env.CONTRACT_ADDRESS = '0x123'; +process.env.PRIVATE_KEY = '0x123'; +process.env.AWS_ACCESS_KEY_ID = 'test'; +process.env.AWS_SECRET_ACCESS_KEY = 'test'; +process.env.AWS_BUCKET_NAME = 'test'; +process.env.AWS_REGION = 'us-east-1'; +process.env.SMTP_USER = 'test'; +process.env.SMTP_PASS = 'test'; +process.env.KYC_API_KEY = 'test'; +process.env.PRICE_ORACLE_API = 'test'; +process.env.REDIS_PASSWORD = 'test'; + +// Rate limit settings for testing +process.env.RATE_LIMIT_WINDOW_MS = '1000'; // 1 second +process.env.RATE_LIMIT_MAX_REQUESTS = '5'; // 5 requests diff --git a/backend/package-lock.json b/backend/package-lock.json index c5266d5..187d6a3 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@fastify/cors": "^8.4.0", "@fastify/helmet": "^11.1.1", - "@fastify/jwt": "^10.0.0", + "@fastify/jwt": "8.0.1", "@fastify/multipart": "^8.0.0", "@fastify/rate-limit": "^9.0.1", "@fastify/swagger": "^8.12.0", @@ -781,7 +781,6 @@ "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -1536,60 +1535,18 @@ } }, "node_modules/@fastify/jwt": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@fastify/jwt/-/jwt-10.0.0.tgz", - "integrity": "sha512-2Qka3NiyNNcsfejMUvyzot1T4UYIzzcbkFGDdVyrl344fRZ/WkD6VFXOoXhxe2Pzf3LpJNkoSxUM4Ru4DVgkYA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@fastify/jwt/-/jwt-8.0.1.tgz", + "integrity": "sha512-295bd7V6bDCnZOu8MAQgM6r7V1KILB+kdEq1q6nbHfXCnML569n7NSo3WzeLDG6IAqDl+Rhzi1vjxwaNHhRCBA==", "license": "MIT", "dependencies": { - "@fastify/error": "^4.2.0", - "@lukeed/ms": "^2.0.2", - "fast-jwt": "^6.0.2", - "fastify-plugin": "^5.0.1", + "@fastify/error": "^3.0.0", + "@lukeed/ms": "^2.0.0", + "fast-jwt": "^4.0.0", + "fastify-plugin": "^4.0.0", "steed": "^1.1.3" } }, - "node_modules/@fastify/jwt/node_modules/@fastify/error": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz", - "integrity": "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT" - }, - "node_modules/@fastify/jwt/node_modules/fastify-plugin": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.1.0.tgz", - "integrity": "sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT" - }, "node_modules/@fastify/merge-json-schemas": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.1.1.tgz", @@ -3181,7 +3138,6 @@ "resolved": "https://registry.npmjs.org/@polkadot/util/-/util-12.6.2.tgz", "integrity": "sha512-l8TubR7CLEY47240uki0TQzFvtnxFIO7uI/0GoWzpYD/O62EIAMRsuY01N4DuwgKq2ZWD59WhzsLYmA5K6ksdw==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@polkadot/x-bigint": "12.6.2", "@polkadot/x-global": "12.6.2", @@ -3367,7 +3323,6 @@ "resolved": "https://registry.npmjs.org/@polkadot/x-randomvalues/-/x-randomvalues-12.6.2.tgz", "integrity": "sha512-Vr8uG7rH2IcNJwtyf5ebdODMcr0XjoCpUbI91Zv6AlKVYOGKZlKLYJHIwpTaKKB+7KPWyQrk4Mlym/rS7v9feg==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@polkadot/x-global": "12.6.2", "tslib": "^2.6.2" @@ -3497,7 +3452,6 @@ "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz", "integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==", "license": "MIT", - "peer": true, "dependencies": { "cluster-key-slot": "1.1.2", "generic-pool": "3.9.0", @@ -4630,7 +4584,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.17.tgz", "integrity": "sha512-gfehUI8N1z92kygssiuWvLiwcbOB3IRktR6hTDgJlXMYh5OvkPSRmgfoBUmfZt+vhwJtX7v1Yw4KvvAf7c5QKQ==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -4850,7 +4803,6 @@ "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "6.21.0", "@typescript-eslint/types": "6.21.0", @@ -5041,7 +4993,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5541,7 +5492,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001741", @@ -6459,7 +6409,6 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -6860,27 +6809,18 @@ "license": "MIT" }, "node_modules/fast-jwt": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/fast-jwt/-/fast-jwt-6.0.2.tgz", - "integrity": "sha512-dTF4bhYnuXhZYQUaxsHKqAyA5y/L/kQc4fUu0wQ0BSA0dMfcNrcv0aqR2YnVi4f7e1OnzDVU7sDsNdzl1O5EVA==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/fast-jwt/-/fast-jwt-4.0.5.tgz", + "integrity": "sha512-QnpNdn0955GT7SlT8iMgYfhTsityUWysrQjM+Q7bGFijLp6+TNWzlbSMPvgalbrQGRg4ZaHZgMcns5fYOm5avg==", "license": "Apache-2.0", "dependencies": { - "@lukeed/ms": "^2.0.2", + "@lukeed/ms": "^2.0.1", "asn1.js": "^5.4.1", "ecdsa-sig-formatter": "^1.0.11", - "mnemonist": "^0.40.0" + "mnemonist": "^0.39.5" }, "engines": { - "node": ">=20" - } - }, - "node_modules/fast-jwt/node_modules/mnemonist": { - "version": "0.40.3", - "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.40.3.tgz", - "integrity": "sha512-Vjyr90sJ23CKKH/qPAgUKicw/v6pRoamxIEDFOF8uSgFME7DqPRpHgRTejWVjkdGg5dXj0/NyxZHZ9bcjH+2uQ==", - "license": "MIT", - "dependencies": { - "obliterator": "^2.0.4" + "node": ">=16" } }, "node_modules/fast-levenshtein": { @@ -7978,7 +7918,6 @@ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -9945,7 +9884,6 @@ "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@prisma/engines": "5.22.0" }, @@ -10383,7 +10321,6 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -10699,6 +10636,16 @@ "node": ">=8" } }, + "node_modules/smoldot": { + "version": "2.0.22", + "resolved": "https://registry.npmjs.org/smoldot/-/smoldot-2.0.22.tgz", + "integrity": "sha512-B50vRgTY6v3baYH6uCgL15tfaag5tcS2o/P5q1OiXcKGv1axZDfz2dzzMuIkVpyMR2ug11F6EAtQlmYBQd292g==", + "license": "GPL-3.0-or-later WITH Classpath-exception-2.0", + "optional": true, + "dependencies": { + "ws": "^8.8.1" + } + }, "node_modules/sonic-boom": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz", @@ -11241,7 +11188,6 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -11369,7 +11315,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12067,7 +12012,6 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", "license": "MIT", - "peer": true, "engines": { "node": ">=10.0.0" }, @@ -12178,7 +12122,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/backend/package.json b/backend/package.json index 11cd17a..0e1c557 100644 --- a/backend/package.json +++ b/backend/package.json @@ -38,7 +38,7 @@ "dependencies": { "@fastify/cors": "^8.4.0", "@fastify/helmet": "^11.1.1", - "@fastify/jwt": "^10.0.0", + "@fastify/jwt": "8.0.1", "@fastify/multipart": "^8.0.0", "@fastify/rate-limit": "^9.0.1", "@fastify/swagger": "^8.12.0", diff --git a/backend/src/__tests__/rate_limit.test.ts b/backend/src/__tests__/rate_limit.test.ts new file mode 100644 index 0000000..57eb21f --- /dev/null +++ b/backend/src/__tests__/rate_limit.test.ts @@ -0,0 +1,56 @@ +import { app } from "../app"; + +describe("Rate Limit Security", () => { + beforeAll(async () => { + // Mock environment variables are set in jest.setup.js + // RATE_LIMIT_MAX_REQUESTS = 5 + // RATE_LIMIT_WINDOW_MS = 1000 + + // Initialize app but don't listen + // Note: app.start() calls listen, app.initialize() is private in App class. + // However, app.server is exposed. + // We might need to access the underlying instance or force initialization if it's not done in constructor. + // Looking at app.ts, initialize() is called in start(). + // We can't easily call private methods. + // But we can verify if rate limit plugin is registered or just try to inject. + // If initialize() is not called, plugins aren't registered. + + // We need to modify App class to allow testing or expose initialize. + // Or we can try to cast app to any to call initialize. + await (app as any).initialize(); + }); + + afterAll(async () => { + await app.stop(); + }); + + it("should allow requests within limit", async () => { + const response = await app.server.inject({ + method: "GET", + url: "/health", + }); + expect(response.statusCode).toBe(200); + }); + + it("should block excessive requests", async () => { + // Send 10 requests, limit is 5 + const requests = []; + for (let i = 0; i < 10; i++) { + requests.push( + app.server.inject({ + method: "GET", + url: "/health", // Use health check as it's lightweight + }), + ); + } + + const responses = await Promise.all(requests); + const success = responses.filter((r) => r.statusCode === 200); + const blocked = responses.filter((r) => r.statusCode === 429); + + console.log(`Success: ${success.length}, Blocked: ${blocked.length}`); + + // If rate limiting is working, we should see some 429s + expect(blocked.length).toBeGreaterThan(0); + }); +}); diff --git a/backend/src/app.ts b/backend/src/app.ts index 22bd2dd..79e1270 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -64,17 +64,17 @@ class App { }, }); - // Rate Limiting - DESABILITADO PARA DESENVOLVIMENTO - // await this.server.register(rateLimit, { - // max: envConfig.RATE_LIMIT_MAX_REQUESTS, - // timeWindow: envConfig.RATE_LIMIT_WINDOW_MS, - // errorResponseBuilder: (request, context) => ({ - // code: 429, - // error: 'Rate Limit Exceeded', - // message: `Muitas tentativas. Tente novamente em ${Math.round(context.ttl / 1000)} segundos.`, - // expiresIn: context.ttl, - // }), - // }); + // Rate Limiting + await this.server.register(rateLimit, { + max: envConfig.RATE_LIMIT_MAX_REQUESTS, + timeWindow: envConfig.RATE_LIMIT_WINDOW_MS, + errorResponseBuilder: (request, context) => ({ + code: 429, + error: 'Rate Limit Exceeded', + message: `Muitas tentativas. Tente novamente em ${Math.round(context.ttl / 1000)} segundos.`, + expiresIn: context.ttl, + }), + }); // Swagger Documentation if (envConfig.ENABLE_SWAGGER) { diff --git a/backend/src/config/env.config.ts b/backend/src/config/env.config.ts index fd0be76..815033d 100644 --- a/backend/src/config/env.config.ts +++ b/backend/src/config/env.config.ts @@ -1,4 +1,4 @@ -import { config } from 'dotenv'; +import { config } from "dotenv"; // Carrega variáveis de ambiente config(); @@ -99,7 +99,7 @@ const getEnvNumber = (key: string, defaultValue?: number): number => { const getEnvBoolean = (key: string, defaultValue = false): boolean => { const value = process.env[key]; if (!value) return defaultValue; - return value.toLowerCase() === 'true'; + return value.toLowerCase() === "true"; }; const getEnvFloat = (key: string, defaultValue?: number): number => { @@ -110,95 +110,108 @@ const getEnvFloat = (key: string, defaultValue?: number): number => { return value ? parseFloat(value) : defaultValue!; }; -const isDevelopment = process.env.NODE_ENV === 'development' || !process.env.NODE_ENV; +const isDevelopment = + process.env.NODE_ENV === "development" || !process.env.NODE_ENV; export const envConfig: EnvConfig = { // Servidor - NODE_ENV: getEnvVar('NODE_ENV', 'development'), - PORT: getEnvNumber('PORT', 3000), - HOST: getEnvVar('HOST', '0.0.0.0'), + NODE_ENV: getEnvVar("NODE_ENV", "development"), + PORT: getEnvNumber("PORT", 3000), + HOST: getEnvVar("HOST", "0.0.0.0"), // Database - opcional em desenvolvimento - DATABASE_URL: isDevelopment - ? getEnvVar('DATABASE_URL', 'postgresql://user:password@localhost:5432/launchpad_dev') - : getEnvVar('DATABASE_URL'), + DATABASE_URL: isDevelopment + ? getEnvVar( + "DATABASE_URL", + "postgresql://user:password@localhost:5432/launchpad_dev", + ) + : getEnvVar("DATABASE_URL"), // Redis - REDIS_URL: getEnvVar('REDIS_URL', 'redis://localhost:6379'), + REDIS_URL: getEnvVar("REDIS_URL", "redis://localhost:6379"), REDIS_PASSWORD: process.env.REDIS_PASSWORD, // JWT - JWT_SECRET: getEnvVar('JWT_SECRET', 'dev-jwt-secret-change-in-production'), - JWT_EXPIRES_IN: getEnvVar('JWT_EXPIRES_IN', '24h'), - JWT_REFRESH_EXPIRES_IN: getEnvVar('JWT_REFRESH_EXPIRES_IN', '7d'), + JWT_SECRET: getEnvVar("JWT_SECRET", "dev-jwt-secret-change-in-production"), + JWT_EXPIRES_IN: getEnvVar("JWT_EXPIRES_IN", "24h"), + JWT_REFRESH_EXPIRES_IN: getEnvVar("JWT_REFRESH_EXPIRES_IN", "7d"), // Blockchain - valores padrão para desenvolvimento - LUNES_RPC_URL: getEnvVar('LUNES_RPC_URL', 'https://rpc.lunes.io'), - LUNES_HTTP_URL: getEnvVar('LUNES_HTTP_URL', 'https://api.lunes.io'), - CONTRACT_ADDRESS: getEnvVar('CONTRACT_ADDRESS', '0x1234567890123456789012345678901234567890'), - PRIVATE_KEY: getEnvVar('PRIVATE_KEY', 'dev-private-key'), + LUNES_RPC_URL: getEnvVar("LUNES_RPC_URL", "https://rpc.lunes.io"), + LUNES_HTTP_URL: getEnvVar("LUNES_HTTP_URL", "https://api.lunes.io"), + CONTRACT_ADDRESS: getEnvVar( + "CONTRACT_ADDRESS", + "0x1234567890123456789012345678901234567890", + ), + PRIVATE_KEY: getEnvVar("PRIVATE_KEY", "dev-private-key"), MNEMONIC: process.env.MNEMONIC, // Email - valores padrão para desenvolvimento - SMTP_HOST: getEnvVar('SMTP_HOST', 'smtp.gmail.com'), - SMTP_PORT: getEnvNumber('SMTP_PORT', 587), - SMTP_SECURE: getEnvBoolean('SMTP_SECURE', false), - SMTP_USER: getEnvVar('SMTP_USER', 'dev@example.com'), - SMTP_PASS: getEnvVar('SMTP_PASS', 'dev-password'), - FROM_EMAIL: getEnvVar('FROM_EMAIL', 'noreply@launchpad.com'), - FROM_NAME: getEnvVar('FROM_NAME', 'Lunes Launchpad'), + SMTP_HOST: getEnvVar("SMTP_HOST", "smtp.gmail.com"), + SMTP_PORT: getEnvNumber("SMTP_PORT", 587), + SMTP_SECURE: getEnvBoolean("SMTP_SECURE", false), + SMTP_USER: getEnvVar("SMTP_USER", "dev@example.com"), + SMTP_PASS: getEnvVar("SMTP_PASS", "dev-password"), + FROM_EMAIL: getEnvVar("FROM_EMAIL", "noreply@launchpad.com"), + FROM_NAME: getEnvVar("FROM_NAME", "Lunes Launchpad"), // AWS - valores padrão para desenvolvimento - AWS_ACCESS_KEY_ID: getEnvVar('AWS_ACCESS_KEY_ID', 'dev-access-key'), - AWS_SECRET_ACCESS_KEY: getEnvVar('AWS_SECRET_ACCESS_KEY', 'dev-secret-key'), - AWS_BUCKET_NAME: getEnvVar('AWS_BUCKET_NAME', 'launchpad-uploads-dev'), - AWS_REGION: getEnvVar('AWS_REGION', 'us-east-1'), + AWS_ACCESS_KEY_ID: getEnvVar("AWS_ACCESS_KEY_ID", "dev-access-key"), + AWS_SECRET_ACCESS_KEY: getEnvVar("AWS_SECRET_ACCESS_KEY", "dev-secret-key"), + AWS_BUCKET_NAME: getEnvVar("AWS_BUCKET_NAME", "launchpad-uploads-dev"), + AWS_REGION: getEnvVar("AWS_REGION", "us-east-1"), // APIs Externas - valores padrão para desenvolvimento - KYC_API_KEY: getEnvVar('KYC_API_KEY', 'dev-kyc-key'), - KYC_API_URL: getEnvVar('KYC_API_URL', 'https://api.kyc-provider.com'), - PRICE_ORACLE_API: getEnvVar('PRICE_ORACLE_API', 'dev-oracle-key'), - PRICE_ORACLE_URL: getEnvVar('PRICE_ORACLE_URL', 'https://api.coingecko.com/api/v3'), + KYC_API_KEY: getEnvVar("KYC_API_KEY", "dev-kyc-key"), + KYC_API_URL: getEnvVar("KYC_API_URL", "https://api.kyc-provider.com"), + PRICE_ORACLE_API: getEnvVar("PRICE_ORACLE_API", "dev-oracle-key"), + PRICE_ORACLE_URL: getEnvVar( + "PRICE_ORACLE_URL", + "https://api.coingecko.com/api/v3", + ), // Configurações - BCRYPT_ROUNDS: getEnvNumber('BCRYPT_ROUNDS', 12), - RATE_LIMIT_WINDOW_MS: getEnvNumber('RATE_LIMIT_WINDOW_MS', 900000), - RATE_LIMIT_MAX_REQUESTS: getEnvNumber('RATE_LIMIT_MAX_REQUESTS', 100), - CORS_ORIGIN: getEnvVar('CORS_ORIGIN', 'http://localhost:3000,http://localhost:5173'), + BCRYPT_ROUNDS: getEnvNumber("BCRYPT_ROUNDS", 12), + RATE_LIMIT_WINDOW_MS: getEnvNumber("RATE_LIMIT_WINDOW_MS", 900000), + RATE_LIMIT_MAX_REQUESTS: getEnvNumber("RATE_LIMIT_MAX_REQUESTS", 100), + CORS_ORIGIN: getEnvVar( + "CORS_ORIGIN", + "http://localhost:3000,http://localhost:5173", + ), // Logs e Debug - LOG_LEVEL: getEnvVar('LOG_LEVEL', 'info'), - ENABLE_SWAGGER: getEnvBoolean('ENABLE_SWAGGER', true), - ENABLE_METRICS: getEnvBoolean('ENABLE_METRICS', true), + LOG_LEVEL: getEnvVar("LOG_LEVEL", "info"), + ENABLE_SWAGGER: getEnvBoolean("ENABLE_SWAGGER", true), + ENABLE_METRICS: getEnvBoolean("ENABLE_METRICS", true), // Queue - QUEUE_REDIS_URL: getEnvVar('QUEUE_REDIS_URL', 'redis://localhost:6379'), - QUEUE_CONCURRENCY: getEnvNumber('QUEUE_CONCURRENCY', 5), + QUEUE_REDIS_URL: getEnvVar("QUEUE_REDIS_URL", "redis://localhost:6379"), + QUEUE_CONCURRENCY: getEnvNumber("QUEUE_CONCURRENCY", 5), // Blockchain Sync - BLOCK_CONFIRMATION_COUNT: getEnvNumber('BLOCK_CONFIRMATION_COUNT', 3), - EVENT_SYNC_BATCH_SIZE: getEnvNumber('EVENT_SYNC_BATCH_SIZE', 100), - SYNC_INTERVAL_MS: getEnvNumber('SYNC_INTERVAL_MS', 5000), + BLOCK_CONFIRMATION_COUNT: getEnvNumber("BLOCK_CONFIRMATION_COUNT", 3), + EVENT_SYNC_BATCH_SIZE: getEnvNumber("EVENT_SYNC_BATCH_SIZE", 100), + SYNC_INTERVAL_MS: getEnvNumber("SYNC_INTERVAL_MS", 5000), // AMA - AMA_DEFAULT_PRICE_USD: getEnvNumber('AMA_DEFAULT_PRICE_USD', 200), - AMA_LUNES_QUESTION_COST: getEnvFloat('AMA_LUNES_QUESTION_COST', 0.5), - AMA_LUNES_VOTE_COST: getEnvFloat('AMA_LUNES_VOTE_COST', 0.5), + AMA_DEFAULT_PRICE_USD: getEnvNumber("AMA_DEFAULT_PRICE_USD", 200), + AMA_LUNES_QUESTION_COST: getEnvFloat("AMA_LUNES_QUESTION_COST", 0.5), + AMA_LUNES_VOTE_COST: getEnvFloat("AMA_LUNES_VOTE_COST", 0.5), // Fees - PLATFORM_FEE_PERCENT: getEnvFloat('PLATFORM_FEE_PERCENT', 2.5), - LISTING_FEE_LUNES: getEnvNumber('LISTING_FEE_LUNES', 1000), - STAKING_REWARD_RATE: getEnvFloat('STAKING_REWARD_RATE', 0.12), + PLATFORM_FEE_PERCENT: getEnvFloat("PLATFORM_FEE_PERCENT", 2.5), + LISTING_FEE_LUNES: getEnvNumber("LISTING_FEE_LUNES", 1000), + STAKING_REWARD_RATE: getEnvFloat("STAKING_REWARD_RATE", 0.12), }; // Validação adicional para ambiente de produção -if (envConfig.NODE_ENV === 'production') { +if (envConfig.NODE_ENV === "production") { const requiredProdVars = [ - 'DATABASE_URL', - 'JWT_SECRET', - 'LUNES_RPC_URL', - 'CONTRACT_ADDRESS', - 'PRIVATE_KEY', + "DATABASE_URL", + "JWT_SECRET", + "LUNES_RPC_URL", + "CONTRACT_ADDRESS", + "PRIVATE_KEY", ]; for (const varName of requiredProdVars) { @@ -209,10 +222,10 @@ if (envConfig.NODE_ENV === 'production') { // Validações de segurança if (envConfig.JWT_SECRET.length < 32) { - throw new Error('JWT_SECRET deve ter pelo menos 32 caracteres em produção'); + throw new Error("JWT_SECRET deve ter pelo menos 32 caracteres em produção"); } if (envConfig.BCRYPT_ROUNDS < 10) { - throw new Error('BCRYPT_ROUNDS deve ser pelo menos 10 em produção'); + throw new Error("BCRYPT_ROUNDS deve ser pelo menos 10 em produção"); } -} \ No newline at end of file +} diff --git a/backend/src/shared/database.ts b/backend/src/shared/database.ts index 5ddbf12..a592d73 100644 --- a/backend/src/shared/database.ts +++ b/backend/src/shared/database.ts @@ -1,9 +1,9 @@ -import { PrismaClient } from '@prisma/client'; -import { logger } from './logger'; +import { PrismaClient } from "@prisma/client"; +import { logger } from "./logger"; /** * Cliente Prisma configurado para a aplicação - * + * * Features: * - Logging de queries em desenvolvimento * - Tratamento de erros de conexão @@ -16,10 +16,11 @@ class DatabaseService { private constructor() { this.prisma = new PrismaClient({ - log: process.env.NODE_ENV === 'development' - ? ['query', 'info', 'warn', 'error'] - : ['error'], - errorFormat: 'pretty', + log: + process.env.NODE_ENV === "development" + ? ["query", "info", "warn", "error"] + : ["error"], + errorFormat: "pretty", }); this.setupMiddleware(); @@ -40,11 +41,11 @@ class DatabaseService { const result = await next(params); const after = Date.now(); - logger.debug('Database query executed', { + logger.debug("Database query executed", { model: params.model, action: params.action, duration: `${after - before}ms`, - args: process.env.NODE_ENV === 'development' ? params.args : undefined + args: process.env.NODE_ENV === "development" ? params.args : undefined, }); return result; @@ -53,17 +54,17 @@ class DatabaseService { // Middleware para soft delete (se necessário) this.prisma.$use(async (params, next) => { // Interceptar operações de delete para implementar soft delete - if (params.action === 'delete') { - params.action = 'update'; - params.args['data'] = { deletedAt: new Date() }; + if (params.action === "delete") { + params.action = "update"; + params.args["data"] = { deletedAt: new Date() }; } - - if (params.action === 'deleteMany') { - params.action = 'updateMany'; + + if (params.action === "deleteMany") { + params.action = "updateMany"; if (params.args.data != undefined) { - params.args.data['deletedAt'] = new Date(); + params.args.data["deletedAt"] = new Date(); } else { - params.args['data'] = { deletedAt: new Date() }; + params.args["data"] = { deletedAt: new Date() }; } } @@ -104,9 +105,9 @@ class DatabaseService { public async connect(): Promise { try { await this.prisma.$connect(); - logger.info('Database connected successfully'); + logger.info("Database connected successfully"); } catch (error) { - logger.error('Failed to connect to database', { error }); + logger.error("Failed to connect to database", { error }); throw error; } } @@ -117,9 +118,9 @@ class DatabaseService { public async disconnect(): Promise { try { await this.prisma.$disconnect(); - logger.info('Database disconnected successfully'); + logger.info("Database disconnected successfully"); } catch (error) { - logger.error('Failed to disconnect from database', { error }); + logger.error("Failed to disconnect from database", { error }); throw error; } } @@ -132,7 +133,7 @@ class DatabaseService { await this.prisma.$queryRaw`SELECT 1`; return true; } catch (error) { - logger.error('Database health check failed', { error }); + logger.error("Database health check failed", { error }); return false; } } @@ -141,7 +142,17 @@ class DatabaseService { * Executa transação */ public async transaction( - fn: (prisma: Omit) => Promise + fn: ( + prisma: Omit< + PrismaClient, + | "$connect" + | "$disconnect" + | "$on" + | "$transaction" + | "$use" + | "$extends" + >, + ) => Promise, ): Promise { return this.prisma.$transaction(fn); } @@ -151,7 +162,7 @@ class DatabaseService { */ public async clearCache(): Promise { // Implementar limpeza de cache se necessário - logger.info('Database cache cleared'); + logger.info("Database cache cleared"); } } @@ -183,10 +194,10 @@ export interface PaginationResult { export function createPaginationResult( data: T[], total: number, - options: PaginationOptions + options: PaginationOptions, ): PaginationResult { const totalPages = Math.ceil(total / options.limit); - + return { data, pagination: { @@ -195,8 +206,8 @@ export function createPaginationResult( total, totalPages, hasNext: options.page < totalPages, - hasPrev: options.page > 1 - } + hasPrev: options.page > 1, + }, }; } @@ -208,16 +219,16 @@ export interface DateFilter { export function createDateFilter(filter?: DateFilter) { if (!filter) return undefined; - + const dateFilter: any = {}; if (filter.from) dateFilter.gte = filter.from; if (filter.to) dateFilter.lte = filter.to; - + return Object.keys(dateFilter).length > 0 ? dateFilter : undefined; } // Utilitários para ordenação -export type SortOrder = 'asc' | 'desc'; +export type SortOrder = "asc" | "desc"; export interface SortOptions { field: string; @@ -225,11 +236,11 @@ export interface SortOptions { } export function createSortOptions(sort?: string): SortOptions { - if (!sort) return { field: 'createdAt', order: 'desc' }; - - const [field, order] = sort.split(':'); + if (!sort) return { field: "createdAt", order: "desc" }; + + const [field, order] = sort.split(":"); return { - field: field || 'createdAt', - order: (order as SortOrder) || 'desc' + field: field || "createdAt", + order: (order as SortOrder) || "desc", }; -} \ No newline at end of file +} diff --git a/backend/src/shared/middleware/error.middleware.ts b/backend/src/shared/middleware/error.middleware.ts index 57b3abf..9be6bdd 100644 --- a/backend/src/shared/middleware/error.middleware.ts +++ b/backend/src/shared/middleware/error.middleware.ts @@ -46,6 +46,8 @@ export const errorHandler = (server: FastifyInstance) => { let statusCode = 500; if (error.statusCode) { statusCode = error.statusCode; + } else if (error.code === 429 as any || error.code === '429') { + statusCode = 429; } else if (error.code === 'FST_JWT_NO_AUTHORIZATION_IN_HEADER') { statusCode = 401; } else if (error.code === 'FST_JWT_AUTHORIZATION_TOKEN_EXPIRED') {