- 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.
211 lines
5.8 KiB
JavaScript
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
|
|
}; |