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:
Binary file not shown.
@@ -0,0 +1,63 @@
|
|||||||
|
exports.up = async function(knex) {
|
||||||
|
// Add rate limit settings to app_settings
|
||||||
|
const rateLimitSettings = [
|
||||||
|
{
|
||||||
|
setting_key: 'rate_limit_enabled',
|
||||||
|
setting_value: JSON.stringify(true),
|
||||||
|
setting_type: 'security'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
setting_key: 'rate_limit_window_minutes',
|
||||||
|
setting_value: JSON.stringify(15),
|
||||||
|
setting_type: 'security'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
setting_key: 'rate_limit_max_requests',
|
||||||
|
setting_value: JSON.stringify(1000),
|
||||||
|
setting_type: 'security'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
setting_key: 'rate_limit_auth_max_requests',
|
||||||
|
setting_value: JSON.stringify(5),
|
||||||
|
setting_type: 'security'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
setting_key: 'rate_limit_skip_authenticated',
|
||||||
|
setting_value: JSON.stringify(true),
|
||||||
|
setting_type: 'security'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
setting_key: 'rate_limit_public_endpoints_only',
|
||||||
|
setting_value: JSON.stringify(false),
|
||||||
|
setting_type: 'security'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Insert settings if they don't exist
|
||||||
|
for (const setting of rateLimitSettings) {
|
||||||
|
const exists = await knex('app_settings')
|
||||||
|
.where('setting_key', setting.setting_key)
|
||||||
|
.first();
|
||||||
|
|
||||||
|
if (!exists) {
|
||||||
|
await knex('app_settings').insert({
|
||||||
|
...setting,
|
||||||
|
updated_at: knex.fn.now()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = async function(knex) {
|
||||||
|
// Remove rate limit settings
|
||||||
|
await knex('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'
|
||||||
|
])
|
||||||
|
.del();
|
||||||
|
};
|
||||||
+23
-83
@@ -7,8 +7,6 @@ validateEnvironment();
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const helmet = require('helmet');
|
const helmet = require('helmet');
|
||||||
const cors = require('cors');
|
const cors = require('cors');
|
||||||
const rateLimit = require('express-rate-limit');
|
|
||||||
const jwt = require('jsonwebtoken');
|
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { initializeDatabase, db } = require('./src/database/db');
|
const { initializeDatabase, db } = require('./src/database/db');
|
||||||
const { startFileWatcher } = require('./src/services/fileWatcher');
|
const { startFileWatcher } = require('./src/services/fileWatcher');
|
||||||
@@ -16,6 +14,7 @@ const { startExpirationChecker } = require('./src/services/expirationChecker');
|
|||||||
const { initializeTransporter, startEmailQueueProcessor } = require('./src/services/emailProcessor');
|
const { initializeTransporter, startEmailQueueProcessor } = require('./src/services/emailProcessor');
|
||||||
const { maintenanceMiddleware } = require('./src/middleware/maintenance');
|
const { maintenanceMiddleware } = require('./src/middleware/maintenance');
|
||||||
const { sessionTimeoutMiddleware } = require('./src/middleware/sessionTimeout');
|
const { sessionTimeoutMiddleware } = require('./src/middleware/sessionTimeout');
|
||||||
|
const { createRateLimiter, createAuthRateLimiter } = require('./src/services/rateLimitService');
|
||||||
const logger = require('./src/utils/logger');
|
const logger = require('./src/utils/logger');
|
||||||
|
|
||||||
// Import routes
|
// Import routes
|
||||||
@@ -93,86 +92,23 @@ const corsOptions = {
|
|||||||
|
|
||||||
app.use(cors(corsOptions));
|
app.use(cors(corsOptions));
|
||||||
|
|
||||||
// Rate limiting with admin bypass
|
// Initialize rate limiters (they will be created dynamically)
|
||||||
const limiter = rateLimit({
|
let generalRateLimiter;
|
||||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
let authRateLimiter;
|
||||||
max: process.env.NODE_ENV === 'development' ? 1000 : 100, // More lenient in development
|
|
||||||
// Use correct client IP when behind proxy
|
|
||||||
keyGenerator: (req) => {
|
|
||||||
// Get the real client IP from proxy headers
|
|
||||||
const clientIp = req.headers['x-forwarded-for']?.split(',')[0]?.trim() ||
|
|
||||||
req.headers['x-real-ip'] ||
|
|
||||||
req.connection.remoteAddress ||
|
|
||||||
req.ip;
|
|
||||||
|
|
||||||
// Log rate limit key for debugging (only in development)
|
|
||||||
if (process.env.NODE_ENV === 'development' && req.path.includes('/api/')) {
|
|
||||||
logger.debug('Rate limit key generated', {
|
|
||||||
path: req.path,
|
|
||||||
clientIp,
|
|
||||||
headers: {
|
|
||||||
'x-forwarded-for': req.headers['x-forwarded-for'],
|
|
||||||
'x-real-ip': req.headers['x-real-ip']
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return clientIp;
|
|
||||||
},
|
|
||||||
handler: (req, res) => {
|
|
||||||
logger.warn('Rate limit exceeded', {
|
|
||||||
ip: req.headers['x-forwarded-for']?.split(',')[0]?.trim() || req.ip,
|
|
||||||
path: req.path,
|
|
||||||
method: req.method
|
|
||||||
});
|
|
||||||
res.status(429).json({
|
|
||||||
error: 'Too many requests, please try again later.'
|
|
||||||
});
|
|
||||||
},
|
|
||||||
skip: (req) => {
|
|
||||||
// Skip rate limiting for authenticated admin users
|
|
||||||
if (req.path.startsWith('/api/admin/') && req.headers.authorization) {
|
|
||||||
const token = req.headers.authorization.replace('Bearer ', '');
|
|
||||||
try {
|
|
||||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
|
||||||
return decoded.type === 'admin';
|
|
||||||
} catch (err) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Also skip rate limiting for public settings endpoint in development
|
|
||||||
if (process.env.NODE_ENV === 'development' && req.path === '/api/public/settings') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const authLimiter = rateLimit({
|
// Function to initialize rate limiters
|
||||||
windowMs: 15 * 60 * 1000,
|
async function initializeRateLimiters() {
|
||||||
max: 5, // limit auth attempts
|
generalRateLimiter = await createRateLimiter();
|
||||||
// Use correct client IP when behind proxy
|
authRateLimiter = await createAuthRateLimiter();
|
||||||
keyGenerator: (req) => {
|
|
||||||
// Get the real client IP from proxy headers
|
// Apply rate limiting
|
||||||
return req.headers['x-forwarded-for']?.split(',')[0]?.trim() ||
|
app.use('/api/', generalRateLimiter);
|
||||||
req.headers['x-real-ip'] ||
|
app.use('/api/auth', authRateLimiter);
|
||||||
req.connection.remoteAddress ||
|
app.use('/api/gallery/:slug/verify', authRateLimiter);
|
||||||
req.ip;
|
app.use('/api/admin/auth/login', authRateLimiter);
|
||||||
},
|
}
|
||||||
handler: (req, res) => {
|
|
||||||
logger.warn('Auth rate limit exceeded', {
|
|
||||||
ip: req.headers['x-forwarded-for']?.split(',')[0]?.trim() || req.ip,
|
|
||||||
path: req.path
|
|
||||||
});
|
|
||||||
res.status(429).json({
|
|
||||||
error: 'Too many authentication attempts, please try again later.'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Apply rate limiting - admin routes check will skip for valid admin tokens
|
// Note: Rate limiters will be initialized after database connection
|
||||||
app.use('/api/', limiter);
|
|
||||||
app.use('/api/auth', authLimiter);
|
|
||||||
|
|
||||||
// Body parsing middleware with increased limits for large uploads
|
// Body parsing middleware with increased limits for large uploads
|
||||||
app.use(express.json({ limit: '100mb' }));
|
app.use(express.json({ limit: '100mb' }));
|
||||||
@@ -274,9 +210,13 @@ async function startServer() {
|
|||||||
// Initialize database
|
// Initialize database
|
||||||
await initializeDatabase();
|
await initializeDatabase();
|
||||||
|
|
||||||
// Initialize auth security cleanup job
|
// Initialize rate limiters after database is ready
|
||||||
const { initializeCleanupJob } = require('./src/utils/authSecurity');
|
await initializeRateLimiters();
|
||||||
initializeCleanupJob();
|
logger.info('Rate limiters initialized with database configuration');
|
||||||
|
|
||||||
|
// Initialize auth security cleanup job
|
||||||
|
const { initializeCleanupJob } = require('./src/utils/authSecurity');
|
||||||
|
initializeCleanupJob();
|
||||||
|
|
||||||
// Start file watcher
|
// Start file watcher
|
||||||
startFileWatcher();
|
startFileWatcher();
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const { db, logActivity } = require('../database/db');
|
|||||||
const { formatBoolean } = require('../utils/dbCompat');
|
const { formatBoolean } = require('../utils/dbCompat');
|
||||||
const { adminAuth } = require('../middleware/auth');
|
const { adminAuth } = require('../middleware/auth');
|
||||||
const { clearMaintenanceCache } = require('../middleware/maintenance');
|
const { clearMaintenanceCache } = require('../middleware/maintenance');
|
||||||
|
const { clearSettingsCache } = require('../services/rateLimitService');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
// Configure multer for logo uploads
|
// Configure multer for logo uploads
|
||||||
@@ -582,4 +583,68 @@ router.post('/favicon', adminAuth, faviconUpload.single('favicon'), async (req,
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Update rate limit settings
|
||||||
|
router.put('/security/rate-limit', adminAuth, [
|
||||||
|
body('rate_limit_enabled').isBoolean().withMessage('Enabled must be a boolean'),
|
||||||
|
body('rate_limit_window_minutes').isInt({ min: 1, max: 60 }).withMessage('Window must be between 1 and 60 minutes'),
|
||||||
|
body('rate_limit_max_requests').isInt({ min: 10, max: 10000 }).withMessage('Max requests must be between 10 and 10000'),
|
||||||
|
body('rate_limit_auth_max_requests').isInt({ min: 1, max: 100 }).withMessage('Auth max requests must be between 1 and 100'),
|
||||||
|
body('rate_limit_skip_authenticated').isBoolean().withMessage('Skip authenticated must be a boolean'),
|
||||||
|
body('rate_limit_public_endpoints_only').isBoolean().withMessage('Public endpoints only must be a boolean')
|
||||||
|
], async (req, res) => {
|
||||||
|
try {
|
||||||
|
const errors = validationResult(req);
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return res.status(400).json({ errors: errors.array() });
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
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
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
// Update each setting
|
||||||
|
const settings = [
|
||||||
|
{ key: 'rate_limit_enabled', value: rate_limit_enabled },
|
||||||
|
{ key: 'rate_limit_window_minutes', value: rate_limit_window_minutes },
|
||||||
|
{ key: 'rate_limit_max_requests', value: rate_limit_max_requests },
|
||||||
|
{ key: 'rate_limit_auth_max_requests', value: rate_limit_auth_max_requests },
|
||||||
|
{ key: 'rate_limit_skip_authenticated', value: rate_limit_skip_authenticated },
|
||||||
|
{ key: 'rate_limit_public_endpoints_only', value: rate_limit_public_endpoints_only }
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const { key, value } of settings) {
|
||||||
|
await db('app_settings')
|
||||||
|
.where('setting_key', key)
|
||||||
|
.update({
|
||||||
|
setting_value: JSON.stringify(value),
|
||||||
|
updated_at: new Date()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the rate limit settings cache to apply changes immediately
|
||||||
|
clearSettingsCache();
|
||||||
|
|
||||||
|
// Log activity
|
||||||
|
await logActivity('settings_updated',
|
||||||
|
{
|
||||||
|
category: 'security',
|
||||||
|
subcategory: 'rate_limit',
|
||||||
|
changes: settings.length
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
{ type: 'admin', id: req.admin.id, name: req.admin.username }
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({ message: 'Rate limit settings updated successfully' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Rate limit settings update error:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to update rate limit settings' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@@ -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