const crypto = require('crypto'); const { db } = require('../database/db'); const logger = require('../utils/logger'); /** * Generate a unique identifier for the guest */ function generateGuestIdentifier(req) { const ip = req.ip || req.connection.remoteAddress || 'unknown'; const userAgent = req.headers['user-agent'] || 'unknown'; return crypto .createHash('sha256') .update(`${ip}:${userAgent}`) .digest('hex'); } /** * Get rate limit settings from app_settings */ async function getRateLimitSettings() { try { const settings = await db('app_settings') .where('setting_key', 'feedback_rate_limits') .first(); if (settings && settings.setting_value) { return JSON.parse(settings.setting_value); } // Default settings return { rating: { max: 100, window: 3600 }, // 100 ratings per hour comment: { max: 20, window: 3600 }, // 20 comments per hour like: { max: 200, window: 3600 }, // 200 likes per hour favorite: { max: 100, window: 3600 } // 100 favorites per hour }; } catch (error) { logger.error('Error getting rate limit settings:', error); // Return defaults on error return { rating: { max: 100, window: 3600 }, comment: { max: 20, window: 3600 }, like: { max: 200, window: 3600 }, favorite: { max: 100, window: 3600 } }; } } /** * Check if action is rate limited */ async function checkRateLimit(identifier, eventId, actionType) { try { const settings = await getRateLimitSettings(); const limit = settings[actionType] || { max: 100, window: 3600 }; // Clean old entries (older than window) const cutoff = new Date(Date.now() - limit.window * 1000); await db('feedback_rate_limits') .where('window_start', '<', cutoff) .delete(); // Count recent actions const recentActions = await db('feedback_rate_limits') .where({ identifier, event_id: eventId, action_type: actionType }) .where('window_start', '>', cutoff) .sum('action_count as total') .first(); const currentCount = recentActions?.total || 0; if (currentCount >= limit.max) { return { limited: true, limit: limit.max, window: limit.window, current: currentCount, resetAt: new Date(Date.now() + limit.window * 1000) }; } return { limited: false, limit: limit.max, window: limit.window, current: currentCount, remaining: limit.max - currentCount }; } catch (error) { logger.error('Error checking rate limit:', error); // Allow action on error to avoid blocking legitimate users return { limited: false }; } } /** * Record an action for rate limiting */ async function recordAction(identifier, eventId, actionType) { try { await db('feedback_rate_limits').insert({ identifier, event_id: eventId, action_type: actionType, action_count: 1, window_start: new Date() }); } catch (error) { logger.error('Error recording rate limit action:', error); } } /** * Middleware factory for feedback rate limiting */ function feedbackRateLimit(actionType) { return async (req, res, next) => { try { // Extract event ID from params or body const eventId = req.params.eventId || req.body?.event_id; if (!eventId) { return res.status(400).json({ error: 'Event ID required' }); } // Generate guest identifier const identifier = generateGuestIdentifier(req); req.guestIdentifier = identifier; // Check rate limit const rateLimitStatus = await checkRateLimit(identifier, eventId, actionType); // Set rate limit headers res.set({ 'X-RateLimit-Limit': rateLimitStatus.limit, 'X-RateLimit-Remaining': rateLimitStatus.remaining || 0, 'X-RateLimit-Reset': rateLimitStatus.resetAt ? rateLimitStatus.resetAt.toISOString() : new Date().toISOString() }); if (rateLimitStatus.limited) { logger.warn(`Rate limit exceeded for ${actionType}`, { identifier: identifier.substring(0, 16) + '...', eventId, actionType }); return res.status(429).json({ error: 'Too many requests', message: `Rate limit exceeded. Please try again later.`, retryAfter: rateLimitStatus.window }); } // Record the action after successful processing res.on('finish', async () => { if (res.statusCode >= 200 && res.statusCode < 300) { await recordAction(identifier, eventId, actionType); } }); next(); } catch (error) { logger.error('Error in rate limit middleware:', error); // Allow request to proceed on error next(); } }; } /** * IP-based rate limiting for more strict control */ function strictRateLimit(options = {}) { const { windowMs = 15 * 60 * 1000, // 15 minutes max = 100, // limit each IP to 100 requests per windowMs message = 'Too many requests from this IP, please try again later.', skipSuccessfulRequests = false } = options; const store = new Map(); // Clean up old entries periodically setInterval(() => { const now = Date.now(); for (const [key, data] of store.entries()) { if (data.resetTime < now) { store.delete(key); } } }, windowMs); return (req, res, next) => { const ip = req.ip || req.connection.remoteAddress; const now = Date.now(); const resetTime = now + windowMs; let data = store.get(ip); if (!data || data.resetTime < now) { data = { count: 0, resetTime }; store.set(ip, data); } if (data.count >= max) { return res.status(429).json({ error: 'Too many requests', message, retryAfter: Math.ceil((data.resetTime - now) / 1000) }); } if (!skipSuccessfulRequests || res.statusCode >= 400) { data.count++; } res.setHeader('X-RateLimit-Limit', max); res.setHeader('X-RateLimit-Remaining', Math.max(0, max - data.count)); res.setHeader('X-RateLimit-Reset', new Date(data.resetTime).toISOString()); next(); }; } module.exports = { feedbackRateLimit, strictRateLimit, generateGuestIdentifier, checkRateLimit, recordAction };