freealberta/influence/app/utils/rate-limiter.js

105 lines
3.6 KiB
JavaScript

const rateLimit = require('express-rate-limit');
// In-memory store for per-recipient email tracking
const emailTracker = new Map();
// Helper function to clean up expired entries
function cleanupExpiredEntries() {
const now = Date.now();
for (const [key, timestamp] of emailTracker.entries()) {
if (now - timestamp > 5 * 60 * 1000) { // 5 minutes
emailTracker.delete(key);
}
}
}
// Clean up expired entries every minute
setInterval(cleanupExpiredEntries, 60 * 1000);
// General API rate limiter
const general = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: {
error: 'Too many requests from this IP, please try again later.',
retryAfter: 15 * 60 // 15 minutes in seconds
},
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
// Use a custom key generator that's safer with trust proxy
keyGenerator: (req) => {
// Fallback to connection remote address if req.ip is not available
return req.ip || req.connection?.remoteAddress || 'unknown';
},
});
// Email sending rate limiter (general - keeps existing behavior)
const email = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 10, // limit each IP to 10 emails per hour
message: {
error: 'Too many emails sent from this IP, please try again later.',
retryAfter: 60 * 60 // 1 hour in seconds
},
standardHeaders: true,
legacyHeaders: false,
skipSuccessfulRequests: false, // Don't skip counting successful requests
// Use a custom key generator that's safer with trust proxy
keyGenerator: (req) => {
// Fallback to connection remote address if req.ip is not available
return req.ip || req.connection?.remoteAddress || 'unknown';
},
});
// Custom middleware for per-recipient email rate limiting
const perRecipientEmailLimiter = (req, res, next) => {
const clientIp = req.ip || req.connection?.remoteAddress || 'unknown';
const recipientEmail = req.body.recipientEmail;
if (!recipientEmail) {
return next(); // Let validation middleware handle missing recipient
}
const trackingKey = `${clientIp}:${recipientEmail}`;
const now = Date.now();
const lastSent = emailTracker.get(trackingKey);
if (lastSent && (now - lastSent) < 5 * 60 * 1000) { // 5 minutes
const timeRemaining = Math.ceil((5 * 60 * 1000 - (now - lastSent)) / 1000);
return res.status(429).json({
success: false,
error: 'Rate limit exceeded',
message: `You can only send one email per representative every 5 minutes. Please wait ${Math.ceil(timeRemaining / 60)} more minutes before sending another email to this representative.`,
retryAfter: timeRemaining,
rateLimitType: 'per-recipient'
});
}
// Store the current timestamp for this IP-recipient combination
emailTracker.set(trackingKey, now);
next();
};
// Represent API rate limiter (more restrictive)
const representAPI = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 60, // match the Represent API limit of 60 requests per minute
message: {
error: 'Represent API rate limit exceeded, please try again later.',
retryAfter: 60 // 1 minute in seconds
},
standardHeaders: true,
legacyHeaders: false,
// Use a custom key generator that's safer with trust proxy
keyGenerator: (req) => {
// Fallback to connection remote address if req.ip is not available
return req.ip || req.connection?.remoteAddress || 'unknown';
},
});
module.exports = {
general,
email,
perRecipientEmailLimiter,
representAPI
};