diff --git a/backend/server.js b/backend/server.js index 4867ce6..a7a9b35 100644 --- a/backend/server.js +++ b/backend/server.js @@ -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 {