diff --git a/backend/package-lock.json b/backend/package-lock.json index 30c1fd95..d2266ef9 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 32f363f8..07c4bc7c 100644 --- a/backend/package.json +++ b/backend/package.json @@ -18,7 +18,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/src/index.js b/backend/src/index.js index fe832123..26605292 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -2,8 +2,8 @@ const express = require('express'); 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'); @@ -12,17 +12,24 @@ 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'); const { OrganizationWebhook } = models; @@ -47,7 +54,6 @@ app.post('/api/admin/webhooks', async (req, res) => { } }); -// Services const indexingService = require('./services/indexingService'); const adminService = require('./services/adminService'); const vestingService = require('./services/vestingService'); @@ -57,7 +63,6 @@ const tvlService = require('./services/tvlService'); const vaultExportService = require('./services/vaultExportService'); const notificationService = require('./services/notificationService'); -// Routes app.get('/', (req, res) => { res.json({ message: 'Vesting Vault API is running!' }); }); @@ -66,46 +71,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 }); } }); @@ -113,24 +105,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; @@ -138,10 +124,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 }); } }); @@ -152,10 +135,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 }); } }); @@ -166,10 +146,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 }); } }); @@ -180,14 +157,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; @@ -195,10 +168,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 }); } }); @@ -209,10 +179,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 }); } }); @@ -223,10 +190,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 }); } }); @@ -237,14 +201,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(); @@ -259,60 +219,29 @@ 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 -// Vault export with admin/org check -app.get('/api/vault/:id/export', async (req, res) => { - try { - const { id } = req.params; - const { admin_address } = req.query; - if (!admin_address) { - return res.status(400).json({ success: false, error: 'admin_address is required' }); - } - // Get vault and org_id - const vault = await require('./models').Vault.findOne({ where: { id } }); - if (!vault) { - return res.status(404).json({ success: false, error: 'Vault not found' }); - } - const orgId = vault.org_id; - const isAdmin = await isAdminOfOrg(admin_address, orgId); - if (!isAdmin) { - return res.status(403).json({ success: false, error: 'Forbidden: admin_address does not belong to organization' }); - } - // 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 (!res.headersSent) { - res.status(500).json({ - success: false, - error: error.message - }); + res.status(500).json({ success: false, error: error.message }); } else { res.destroy(error); } } }); -// Start server const startServer = async () => { try { await sequelize.authenticate(); console.log('Database connection established successfully.'); - - await sequelize.sync({ alter: true }); + console.log('Database synchronized successfully.'); - - // Initialize Redis Cache + try { await cacheService.connect(); if (cacheService.isReady()) { @@ -324,15 +253,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}`); @@ -340,30 +266,21 @@ 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...'); } - - // Initialize Notification Service (Cron Job) - try { - notificationService.start(); - } catch (notificationError) { - console.error('Failed to start Notification Service:', notificationError); - } - - // 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`); } - + }); } catch (error) { console.error('Unable to start server:', error);