freealberta/influence/app/utils/rate-limiter.js
admin e5c32ad25a Add health check utility, logger, metrics, backup, and SMTP toggle scripts
- Implemented a comprehensive health check utility to monitor system dependencies including NocoDB, SMTP, Represent API, disk space, and memory usage.
- Created a logger utility using Winston for structured logging with daily rotation and various log levels.
- Developed a metrics utility using Prometheus client to track application performance metrics such as email sends, HTTP requests, and user activity.
- Added a backup script for automated backups of NocoDB data, uploaded files, and environment configurations with optional S3 support.
- Introduced a toggle script to switch between development (MailHog) and production (ProtonMail) SMTP configurations.
2025-10-23 11:33:00 -06:00

211 lines
5.8 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);
// Custom key generator that's safer with trust proxy
const safeKeyGenerator = (req) => {
return req.ip || req.connection?.remoteAddress || 'unknown';
};
// Production-grade rate limiting configuration
const rateLimitConfig = {
// Email sending - very restrictive (5 emails per hour per IP)
emailSend: {
windowMs: 60 * 60 * 1000, // 1 hour
max: 5,
message: {
error: 'Email rate limit exceeded',
message: 'Too many emails sent from this IP. Maximum 5 emails per hour allowed.',
retryAfter: 3600
}
},
// Representative lookup - moderate (30 lookups per minute)
representativeLookup: {
windowMs: 60 * 1000, // 1 minute
max: 30,
message: {
error: 'Representative lookup rate limit exceeded',
message: 'Too many representative lookups. Maximum 30 per minute allowed.',
retryAfter: 60
}
},
// Login attempts - strict (5 attempts per 15 minutes)
login: {
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5,
message: {
error: 'Login rate limit exceeded',
message: 'Too many login attempts. Please try again in 15 minutes.',
retryAfter: 900
}
},
// Public API - standard (100 requests per minute)
publicAPI: {
windowMs: 60 * 1000, // 1 minute
max: 100,
message: {
error: 'API rate limit exceeded',
message: 'Too many requests from this IP. Please try again later.',
retryAfter: 60
}
},
// Campaign creation/editing - moderate (10 per hour)
campaignMutation: {
windowMs: 60 * 60 * 1000, // 1 hour
max: 10,
message: {
error: 'Campaign mutation rate limit exceeded',
message: 'Too many campaign operations. Maximum 10 per hour allowed.',
retryAfter: 3600
}
},
// General fallback - legacy compatibility
general: {
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100,
message: {
error: 'Too many requests from this IP, please try again later.',
retryAfter: 900
}
}
};
// Create rate limiter instances
const emailSend = rateLimit({
...rateLimitConfig.emailSend,
standardHeaders: true,
legacyHeaders: false,
skipSuccessfulRequests: false,
keyGenerator: safeKeyGenerator
});
const representativeLookup = rateLimit({
...rateLimitConfig.representativeLookup,
standardHeaders: true,
legacyHeaders: false,
keyGenerator: safeKeyGenerator
});
const login = rateLimit({
...rateLimitConfig.login,
standardHeaders: true,
legacyHeaders: false,
skipSuccessfulRequests: false, // Count all attempts
keyGenerator: safeKeyGenerator
});
const publicAPI = rateLimit({
...rateLimitConfig.publicAPI,
standardHeaders: true,
legacyHeaders: false,
keyGenerator: safeKeyGenerator
});
const campaignMutation = rateLimit({
...rateLimitConfig.campaignMutation,
standardHeaders: true,
legacyHeaders: false,
skipSuccessfulRequests: false,
keyGenerator: safeKeyGenerator
});
// General API rate limiter (legacy - kept for backward compatibility)
const general = rateLimit({
...rateLimitConfig.general,
standardHeaders: true,
legacyHeaders: false,
keyGenerator: safeKeyGenerator
});
// Email sending rate limiter (legacy - kept for backward compatibility)
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,
keyGenerator: safeKeyGenerator
});
// 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,
keyGenerator: safeKeyGenerator
});
module.exports = {
// Legacy exports (backward compatibility)
general,
email,
representAPI,
// New granular rate limiters
emailSend,
representativeLookup,
login,
publicAPI,
campaignMutation,
perRecipientEmailLimiter,
// Export config for testing/monitoring
rateLimitConfig
};