Compare commits

...

2 Commits

Author SHA1 Message Date
Gitea Actions Bot ffcfd9766d chore: bump backend version to 1.0.60
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-07-16 21:41:10 +00:00
paul 3501a52f0e fix: rate limiting issues with reverse proxy setup
Mirror to GitHub / mirror (push) Successful in 27s
Test and Lint / backend-test (push) Successful in 1m12s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m13s
Version and Release / version-bump (push) Successful in 40s
Version and Release / trigger-drone (push) Successful in 3s
- Add keyGenerator function to properly detect client IP behind proxy
- Support X-Forwarded-For and X-Real-IP headers from Traefik/nginx
- Add custom handlers with better error messages
- Add debug endpoint (dev only) to verify IP detection
- Improve logging for rate limit debugging

This fixes the 429 errors when multiple requests come from same proxy IP.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-16 23:36:04 +02:00
3 changed files with 75 additions and 4 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "picpeak-backend",
"version": "1.0.59",
"version": "1.0.60",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "picpeak-backend",
"version": "1.0.59",
"version": "1.0.60",
"dependencies": {
"adm-zip": "^0.5.16",
"archiver": "^5.3.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "picpeak-backend",
"version": "1.0.59",
"version": "1.0.60",
"description": "Backend for PicPeak event photo sharing platform",
"main": "server.js",
"scripts": {
+72 -1
View File
@@ -97,6 +97,38 @@ app.use(cors(corsOptions));
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) {
@@ -118,7 +150,24 @@ const limiter = rateLimit({
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5 // limit auth attempts
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.'
});
}
});
// Apply rate limiting - admin routes check will skip for valid admin tokens
@@ -158,6 +207,28 @@ app.use('/thumbnails', require('./src/middleware/photoAuth'), setCorsHeaders, se
// Static file serving for uploads (public - logos, favicons)
app.use('/uploads', setCorsHeaders, secureStatic(path.join(storagePath, 'uploads')));
// Debug endpoint to check IP detection (only in development)
if (process.env.NODE_ENV === 'development') {
app.get('/api/debug/ip', (req, res) => {
const clientIp = req.headers['x-forwarded-for']?.split(',')[0]?.trim() ||
req.headers['x-real-ip'] ||
req.connection.remoteAddress ||
req.ip;
res.json({
detectedIp: clientIp,
reqIp: req.ip,
headers: {
'x-forwarded-for': req.headers['x-forwarded-for'],
'x-real-ip': req.headers['x-real-ip'],
'x-forwarded-proto': req.headers['x-forwarded-proto'],
'x-forwarded-host': req.headers['x-forwarded-host']
},
trustProxy: app.get('trust proxy')
});
});
}
// Health check endpoint
app.get('/health', async (req, res) => {
try {