feat: implement dynamic rate limiting with database configuration
Mirror to GitHub / mirror (push) Successful in 21s
Test and Lint / backend-test (push) Successful in 1m10s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m8s
Version and Release / version-bump (push) Successful in 34s
Version and Release / trigger-drone (push) Successful in 3s
Mirror to GitHub / mirror (push) Successful in 21s
Test and Lint / backend-test (push) Successful in 1m10s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m8s
Version and Release / version-bump (push) Successful in 34s
Version and Release / trigger-drone (push) Successful in 3s
- Add database migration for rate limit settings - Create rate limit service with dynamic configuration from database - Implement proper authentication detection for admin and gallery tokens - Skip rate limiting for authenticated users (configurable) - Add admin API endpoint to update rate limit settings - Use correct client IP detection with proxy support - Cache settings for performance (1 minute cache) - Default to 1000 requests per 15 minutes for better UX - Apply auth-specific limits only to login endpoints Key improvements: - No more rate limiting for authenticated gallery/admin users - Configurable via admin settings page - Immediate effect when settings change - Better handling of proxied requests 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
const rateLimit = require('express-rate-limit');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const { db } = require('../database/db');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
// Cache for rate limit settings
|
||||
let settingsCache = null;
|
||||
let cacheExpiry = 0;
|
||||
const CACHE_DURATION = 60000; // 1 minute cache
|
||||
|
||||
/**
|
||||
* Get rate limit settings from database with caching
|
||||
*/
|
||||
async function getRateLimitSettings() {
|
||||
try {
|
||||
// Check cache
|
||||
if (settingsCache && Date.now() < cacheExpiry) {
|
||||
return settingsCache;
|
||||
}
|
||||
|
||||
// Fetch from database
|
||||
const settings = await db('app_settings')
|
||||
.whereIn('setting_key', [
|
||||
'rate_limit_enabled',
|
||||
'rate_limit_window_minutes',
|
||||
'rate_limit_max_requests',
|
||||
'rate_limit_auth_max_requests',
|
||||
'rate_limit_skip_authenticated',
|
||||
'rate_limit_public_endpoints_only'
|
||||
]);
|
||||
|
||||
// Parse settings into object
|
||||
const config = {
|
||||
enabled: true,
|
||||
windowMinutes: 15,
|
||||
maxRequests: 100,
|
||||
authMaxRequests: 5,
|
||||
skipAuthenticated: true,
|
||||
publicEndpointsOnly: false
|
||||
};
|
||||
|
||||
settings.forEach(setting => {
|
||||
const value = JSON.parse(setting.setting_value);
|
||||
switch (setting.setting_key) {
|
||||
case 'rate_limit_enabled':
|
||||
config.enabled = value;
|
||||
break;
|
||||
case 'rate_limit_window_minutes':
|
||||
config.windowMinutes = value;
|
||||
break;
|
||||
case 'rate_limit_max_requests':
|
||||
config.maxRequests = value;
|
||||
break;
|
||||
case 'rate_limit_auth_max_requests':
|
||||
config.authMaxRequests = value;
|
||||
break;
|
||||
case 'rate_limit_skip_authenticated':
|
||||
config.skipAuthenticated = value;
|
||||
break;
|
||||
case 'rate_limit_public_endpoints_only':
|
||||
config.publicEndpointsOnly = value;
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// Update cache
|
||||
settingsCache = config;
|
||||
cacheExpiry = Date.now() + CACHE_DURATION;
|
||||
|
||||
return config;
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch rate limit settings:', error);
|
||||
// Return defaults on error
|
||||
return {
|
||||
enabled: true,
|
||||
windowMinutes: 15,
|
||||
maxRequests: 100,
|
||||
authMaxRequests: 5,
|
||||
skipAuthenticated: true,
|
||||
publicEndpointsOnly: false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear settings cache (call when settings are updated)
|
||||
*/
|
||||
function clearSettingsCache() {
|
||||
settingsCache = null;
|
||||
cacheExpiry = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if request has valid authentication
|
||||
*/
|
||||
function isAuthenticated(req) {
|
||||
try {
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const token = authHeader.substring(7);
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
|
||||
// Check if token is valid
|
||||
if (!decoded || typeof decoded !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Valid token found - check type
|
||||
req.tokenType = decoded.type; // 'admin' or 'gallery'
|
||||
req.tokenPayload = decoded;
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if rate limiting should be applied to this request
|
||||
*/
|
||||
function shouldSkipRateLimit(req, config) {
|
||||
// If rate limiting is disabled globally
|
||||
if (!config.enabled) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Never skip rate limiting for auth endpoints
|
||||
const isAuthEndpoint = req.path.match(/\/(auth|login|gallery\/[^/]+\/verify)$/);
|
||||
if (isAuthEndpoint) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if we should skip authenticated requests
|
||||
if (config.skipAuthenticated && isAuthenticated(req)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if we only rate limit public endpoints
|
||||
if (config.publicEndpointsOnly) {
|
||||
const isPublicEndpoint = req.path.startsWith('/api/public/') ||
|
||||
req.path.startsWith('/api/gallery/') ||
|
||||
isAuthEndpoint;
|
||||
return !isPublicEndpoint;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create dynamic rate limiter
|
||||
*/
|
||||
async function createRateLimiter() {
|
||||
const config = await getRateLimitSettings();
|
||||
|
||||
return rateLimit({
|
||||
windowMs: config.windowMinutes * 60 * 1000,
|
||||
max: async (req) => {
|
||||
// Refresh config for each request
|
||||
const currentConfig = await getRateLimitSettings();
|
||||
|
||||
// Different limits for auth endpoints
|
||||
const isAuthEndpoint = req.path.match(/\/(auth|login|gallery\/[^/]+\/verify)$/);
|
||||
return isAuthEndpoint ? currentConfig.authMaxRequests : currentConfig.maxRequests;
|
||||
},
|
||||
keyGenerator: (req) => {
|
||||
// Use correct client IP when behind proxy
|
||||
return req.headers['x-forwarded-for']?.split(',')[0]?.trim() ||
|
||||
req.headers['x-real-ip'] ||
|
||||
req.connection.remoteAddress ||
|
||||
req.ip;
|
||||
},
|
||||
skip: async (req) => {
|
||||
const currentConfig = await getRateLimitSettings();
|
||||
return shouldSkipRateLimit(req, currentConfig);
|
||||
},
|
||||
handler: (req, res) => {
|
||||
const clientIp = req.headers['x-forwarded-for']?.split(',')[0]?.trim() || req.ip;
|
||||
logger.warn('Rate limit exceeded', {
|
||||
ip: clientIp,
|
||||
path: req.path,
|
||||
method: req.method,
|
||||
authenticated: isAuthenticated(req),
|
||||
tokenType: req.tokenType
|
||||
});
|
||||
|
||||
res.status(429).json({
|
||||
error: 'Too many requests, please try again later.',
|
||||
retryAfter: res.getHeader('Retry-After')
|
||||
});
|
||||
},
|
||||
standardHeaders: true, // Return rate limit info in headers
|
||||
legacyHeaders: false, // Disable X-RateLimit headers
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create auth-specific rate limiter
|
||||
*/
|
||||
async function createAuthRateLimiter() {
|
||||
const config = await getRateLimitSettings();
|
||||
|
||||
return rateLimit({
|
||||
windowMs: config.windowMinutes * 60 * 1000,
|
||||
max: config.authMaxRequests,
|
||||
keyGenerator: (req) => {
|
||||
// Use correct client IP when behind proxy
|
||||
return req.headers['x-forwarded-for']?.split(',')[0]?.trim() ||
|
||||
req.headers['x-real-ip'] ||
|
||||
req.connection.remoteAddress ||
|
||||
req.ip;
|
||||
},
|
||||
skip: async () => {
|
||||
const currentConfig = await getRateLimitSettings();
|
||||
return !currentConfig.enabled;
|
||||
},
|
||||
handler: (req, res) => {
|
||||
const clientIp = req.headers['x-forwarded-for']?.split(',')[0]?.trim() || req.ip;
|
||||
logger.warn('Auth rate limit exceeded', {
|
||||
ip: clientIp,
|
||||
path: req.path
|
||||
});
|
||||
|
||||
res.status(429).json({
|
||||
error: 'Too many authentication attempts, please try again later.',
|
||||
retryAfter: res.getHeader('Retry-After')
|
||||
});
|
||||
},
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getRateLimitSettings,
|
||||
clearSettingsCache,
|
||||
createRateLimiter,
|
||||
createAuthRateLimiter,
|
||||
isAuthenticated,
|
||||
shouldSkipRateLimit
|
||||
};
|
||||
Reference in New Issue
Block a user