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

- 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:
2025-07-17 08:43:00 +02:00
parent 8cbe97d2f4
commit 9d0607f4f0
5 changed files with 394 additions and 83 deletions
+23 -83
View File
@@ -7,8 +7,6 @@ validateEnvironment();
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const rateLimit = require('express-rate-limit');
const jwt = require('jsonwebtoken');
const path = require('path');
const { initializeDatabase, db } = require('./src/database/db');
const { startFileWatcher } = require('./src/services/fileWatcher');
@@ -16,6 +14,7 @@ const { startExpirationChecker } = require('./src/services/expirationChecker');
const { initializeTransporter, startEmailQueueProcessor } = require('./src/services/emailProcessor');
const { maintenanceMiddleware } = require('./src/middleware/maintenance');
const { sessionTimeoutMiddleware } = require('./src/middleware/sessionTimeout');
const { createRateLimiter, createAuthRateLimiter } = require('./src/services/rateLimitService');
const logger = require('./src/utils/logger');
// Import routes
@@ -93,86 +92,23 @@ const corsOptions = {
app.use(cors(corsOptions));
// Rate limiting with admin bypass
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
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;
}
});
// Initialize rate limiters (they will be created dynamically)
let generalRateLimiter;
let authRateLimiter;
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5, // limit auth attempts
// Use correct client IP when behind proxy
keyGenerator: (req) => {
// Get the real client IP from proxy headers
return req.headers['x-forwarded-for']?.split(',')[0]?.trim() ||
req.headers['x-real-ip'] ||
req.connection.remoteAddress ||
req.ip;
},
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.'
});
}
});
// Function to initialize rate limiters
async function initializeRateLimiters() {
generalRateLimiter = await createRateLimiter();
authRateLimiter = await createAuthRateLimiter();
// Apply rate limiting
app.use('/api/', generalRateLimiter);
app.use('/api/auth', authRateLimiter);
app.use('/api/gallery/:slug/verify', authRateLimiter);
app.use('/api/admin/auth/login', authRateLimiter);
}
// Apply rate limiting - admin routes check will skip for valid admin tokens
app.use('/api/', limiter);
app.use('/api/auth', authLimiter);
// Note: Rate limiters will be initialized after database connection
// Body parsing middleware with increased limits for large uploads
app.use(express.json({ limit: '100mb' }));
@@ -274,9 +210,13 @@ async function startServer() {
// Initialize database
await initializeDatabase();
// Initialize auth security cleanup job
const { initializeCleanupJob } = require('./src/utils/authSecurity');
initializeCleanupJob();
// Initialize rate limiters after database is ready
await initializeRateLimiters();
logger.info('Rate limiters initialized with database configuration');
// Initialize auth security cleanup job
const { initializeCleanupJob } = require('./src/utils/authSecurity');
initializeCleanupJob();
// Start file watcher
startFileWatcher();