From b2d969a2ee22819dd532e39c19ab623be854ff99 Mon Sep 17 00:00:00 2001 From: JojoFlex1 Date: Mon, 23 Feb 2026 07:45:40 +0300 Subject: [PATCH] feat: add rate limiting to claim endpoints (1 req/min per IP) --- backend/package-lock.json | 2 +- backend/package.json | 8 +-- backend/src/index.js | 134 ++++++++++---------------------------- 3 files changed, 41 insertions(+), 103 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index ea70a180..e4b36fcc 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -16,7 +16,7 @@ "discord.js": "^14.14.1", "dotenv": "^16.3.1", "express": "^4.18.2", - "express-rate-limit": "^7.1.5", + "express-rate-limit": "^7.5.1", "graphql": "^16.8.1", "graphql-subscriptions": "^2.0.0", "graphql-ws": "^5.14.3", diff --git a/backend/package.json b/backend/package.json index 7216b2d7..741677cc 100644 --- a/backend/package.json +++ b/backend/package.json @@ -15,19 +15,19 @@ "@graphql-tools/schema": "^10.0.2", "axios": "^1.6.2", "cors": "^2.8.5", + "discord.js": "^14.14.1", "dotenv": "^16.3.1", "express": "^4.18.2", - "express-rate-limit": "^7.1.5", + "express-rate-limit": "^7.5.1", "graphql": "^16.8.1", "graphql-subscriptions": "^2.0.0", "graphql-ws": "^5.14.3", "pg": "^8.11.3", + "redis": "^4.6.12", "sequelize": "^6.35.2", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.1", - "ws": "^8.14.2", - "discord.js": "^14.14.1", - "redis": "^4.6.12" + "ws": "^8.14.2" }, "devDependencies": { "jest": "^29.7.0", diff --git a/backend/src/index.js b/backend/src/index.js index 73f716e8..a669eec5 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -4,8 +4,8 @@ const { RedisIoAdapter } = require('./websocket/redis.adapter'); const cors = require('cors'); const dotenv = require('dotenv'); const http = require('http'); +const { rateLimit } = require('express-rate-limit'); -// Import swagger documentation const swaggerUi = require('swagger-ui-express'); const swaggerSpecs = require('./swagger/options'); @@ -14,21 +14,27 @@ dotenv.config(); const app = express(); const PORT = process.env.PORT || 3000; -// Create HTTP server for GraphQL subscriptions const httpServer = http.createServer(app); -// Middleware app.use(cors()); app.use(express.json()); -// Swagger UI middleware app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpecs)); -// Database connection and models +const claimRateLimiter = rateLimit({ + windowMs: 60 * 1000, + limit: 1, + message: { + success: false, + error: 'Too many claim requests. Please wait 1 minute before trying again.' + }, + standardHeaders: 'draft-7', + legacyHeaders: false, +}); + const { sequelize } = require('./database/connection'); const models = require('./models'); -// Services const indexingService = require('./services/indexingService'); const adminService = require('./services/adminService'); const vestingService = require('./services/vestingService'); @@ -37,7 +43,6 @@ const cacheService = require('./services/cacheService'); const tvlService = require('./services/tvlService'); const vaultExportService = require('./services/vaultExportService'); -// Routes app.get('/', (req, res) => { res.json({ message: 'Vesting Vault API is running!' }); }); @@ -46,46 +51,33 @@ app.get('/health', (req, res) => { res.json({ status: 'OK', timestamp: new Date().toISOString() }); }); -// API Routes for claims and indexing -app.post('/api/claims', async (req, res) => { +app.post('/api/claims', claimRateLimiter, async (req, res) => { try { const claim = await indexingService.processClaim(req.body); res.status(201).json({ success: true, data: claim }); } catch (error) { console.error('Error processing claim:', error); - res.status(500).json({ - success: false, - error: error.message - }); + res.status(500).json({ success: false, error: error.message }); } }); -app.post('/api/claims/batch', async (req, res) => { +app.post('/api/claims/batch', claimRateLimiter, async (req, res) => { try { const result = await indexingService.processBatchClaims(req.body.claims); res.json({ success: true, data: result }); } catch (error) { console.error('Error processing batch claims:', error); - res.status(500).json({ - success: false, - error: error.message - }); + res.status(500).json({ success: false, error: error.message }); } }); -app.post('/api/claims/backfill-prices', async (req, res) => { +app.post('/api/claims/backfill-prices', claimRateLimiter, async (req, res) => { try { const processedCount = await indexingService.backfillMissingPrices(); - res.json({ - success: true, - message: `Backfilled prices for ${processedCount} claims` - }); + res.json({ success: true, message: `Backfilled prices for ${processedCount} claims` }); } catch (error) { console.error('Error backfilling prices:', error); - res.status(500).json({ - success: false, - error: error.message - }); + res.status(500).json({ success: false, error: error.message }); } }); @@ -93,24 +85,18 @@ app.get('/api/claims/:userAddress/realized-gains', async (req, res) => { try { const { userAddress } = req.params; const { startDate, endDate } = req.query; - const gains = await indexingService.getRealizedGains( - userAddress, + userAddress, startDate ? new Date(startDate) : null, endDate ? new Date(endDate) : null ); - res.json({ success: true, data: gains }); } catch (error) { console.error('Error calculating realized gains:', error); - res.status(500).json({ - success: false, - error: error.message - }); + res.status(500).json({ success: false, error: error.message }); } }); -// Admin Routes app.post('/api/admin/revoke', async (req, res) => { try { const { adminAddress, targetVault, reason } = req.body; @@ -118,10 +104,7 @@ app.post('/api/admin/revoke', async (req, res) => { res.json({ success: true, data: result }); } catch (error) { console.error('Error revoking access:', error); - res.status(500).json({ - success: false, - error: error.message - }); + res.status(500).json({ success: false, error: error.message }); } }); @@ -132,10 +115,7 @@ app.post('/api/admin/create', async (req, res) => { res.json({ success: true, data: result }); } catch (error) { console.error('Error creating vault:', error); - res.status(500).json({ - success: false, - error: error.message - }); + res.status(500).json({ success: false, error: error.message }); } }); @@ -146,10 +126,7 @@ app.post('/api/admin/transfer', async (req, res) => { res.json({ success: true, data: result }); } catch (error) { console.error('Error transferring vault:', error); - res.status(500).json({ - success: false, - error: error.message - }); + res.status(500).json({ success: false, error: error.message }); } }); @@ -160,14 +137,10 @@ app.get('/api/admin/audit-logs', async (req, res) => { res.json({ success: true, data: result }); } catch (error) { console.error('Error fetching audit logs:', error); - res.status(500).json({ - success: false, - error: error.message - }); + res.status(500).json({ success: false, error: error.message }); } }); -// Admin Key Management Routes app.post('/api/admin/propose-new-admin', async (req, res) => { try { const { currentAdminAddress, newAdminAddress, contractAddress } = req.body; @@ -175,10 +148,7 @@ app.post('/api/admin/propose-new-admin', async (req, res) => { res.json({ success: true, data: result }); } catch (error) { console.error('Error proposing new admin:', error); - res.status(500).json({ - success: false, - error: error.message - }); + res.status(500).json({ success: false, error: error.message }); } }); @@ -189,10 +159,7 @@ app.post('/api/admin/accept-ownership', async (req, res) => { res.json({ success: true, data: result }); } catch (error) { console.error('Error accepting ownership:', error); - res.status(500).json({ - success: false, - error: error.message - }); + res.status(500).json({ success: false, error: error.message }); } }); @@ -203,10 +170,7 @@ app.post('/api/admin/transfer-ownership', async (req, res) => { res.json({ success: true, data: result }); } catch (error) { console.error('Error transferring ownership:', error); - res.status(500).json({ - success: false, - error: error.message - }); + res.status(500).json({ success: false, error: error.message }); } }); @@ -217,14 +181,10 @@ app.get('/api/admin/pending-transfers', async (req, res) => { res.json({ success: true, data: result }); } catch (error) { console.error('Error fetching pending transfers:', error); - res.status(500).json({ - success: false, - error: error.message - }); + res.status(500).json({ success: false, error: error.message }); } }); -// Stats Routes app.get('/api/stats/tvl', async (req, res) => { try { const tvlStats = await tvlService.getTVLStats(); @@ -239,50 +199,34 @@ app.get('/api/stats/tvl', async (req, res) => { }); } catch (error) { console.error('Error fetching TVL stats:', error); - res.status(500).json({ - success: false, - error: error.message - }); + res.status(500).json({ success: false, error: error.message }); } }); -// Vault Export Routes app.get('/api/vault/:id/export', async (req, res) => { try { const { id } = req.params; - - // Set response headers for CSV download res.setHeader('Content-Type', 'text/csv'); res.setHeader('Content-Disposition', `attachment; filename="vault-${id}-export-${new Date().toISOString().split('T')[0]}.csv"`); - - // Stream the CSV data await vaultExportService.streamVaultAsCSV(id, res); } catch (error) { console.error('Error exporting vault:', error); - - // If headers haven't been sent yet, send JSON error response if (!res.headersSent) { - res.status(500).json({ - success: false, - error: error.message - }); + res.status(500).json({ success: false, error: error.message }); } else { - // If streaming already started, destroy the stream res.destroy(error); } } }); -// Start server const startServer = async () => { try { await sequelize.authenticate(); console.log('Database connection established successfully.'); - + await sequelize.sync(); console.log('Database synchronized successfully.'); - - // Initialize Redis Cache + try { await cacheService.connect(); if (cacheService.isReady()) { @@ -294,15 +238,12 @@ const startServer = async () => { console.error('Failed to connect to Redis:', cacheError); console.log('Continuing without Redis cache...'); } - - // Initialize GraphQL Server + let graphQLServer = null; try { - // Import GraphQL server (using require for CommonJS compatibility) const { createGraphQLServer } = require('./graphql/server'); graphQLServer = await createGraphQLServer(app); console.log('GraphQL Server initialized successfully.'); - const serverInfo = graphQLServer.getServerInfo(); console.log(`GraphQL Playground available at: ${serverInfo.playgroundUrl}`); console.log(`GraphQL Subscriptions available at: ${serverInfo.subscriptionEndpoint}`); @@ -310,23 +251,20 @@ const startServer = async () => { console.error('Failed to initialize GraphQL Server:', graphqlError); console.log('Continuing with REST API only...'); } - - // Initialize Discord Bot + try { await discordBotService.start(); } catch (discordError) { console.error('Failed to initialize Discord Bot:', discordError); console.log('Continuing without Discord bot...'); } - - // Start the HTTP server + httpServer.listen(PORT, () => { console.log(`Server is running on port ${PORT}`); console.log(`REST API available at: http://localhost:${PORT}`); if (graphQLServer) { console.log(`GraphQL API available at: http://localhost:${PORT}/graphql`); } - // Initialize WebSocket Gateway with Redis adapter const redisIoAdapter = new RedisIoAdapter(httpServer); const claimGateway = new ClaimGateway(); claimGateway.server = redisIoAdapter.createIOServer(PORT, { transports: ['websocket'] });