auth/src/middleware/adminRateLimit.js

97 lines
2.8 KiB
JavaScript

// src/middleware/adminRateLimit.js
// === ADMIN SECURITY VISUALIZER ===
// Light rate limiting for admin endpoints
// Prevents abuse while allowing legitimate admin access
const { getRedisClient, isRedisReady } = require('../services/redisClient');
// In-memory fallback store
const memoryStore = {};
// Clean up expired entries
setInterval(() => {
const now = Date.now();
Object.keys(memoryStore).forEach((key) => {
if (memoryStore[key].expiresAt && memoryStore[key].expiresAt < now) {
delete memoryStore[key];
}
});
}, 60000);
// Configuration: 100 requests per 15 minutes per user
const ADMIN_RATE_LIMIT = {
maxRequests: parseInt(process.env.ADMIN_RATE_LIMIT_MAX || '100', 10),
windowSeconds: parseInt(process.env.ADMIN_RATE_LIMIT_WINDOW || '900', 10), // 15 minutes
};
/**
* Admin rate limiting middleware
* Limits admin API requests per user
*/
async function adminRateLimit(req, res, next) {
try {
const userId = req.user?.id;
if (!userId) {
return res.status(401).json({ error: 'Authentication required' });
}
const key = `admin_rate_limit:${userId}`;
const redis = await getRedisClient();
let count;
if (isRedisReady() && redis) {
try {
count = await redis.incr(key);
if (count === 1) {
await redis.expire(key, ADMIN_RATE_LIMIT.windowSeconds);
}
} catch (err) {
console.error('Redis error in admin rate limit, falling back to memory:', err);
// Fall through to memory store
count = (memoryStore[key]?.count || 0) + 1;
memoryStore[key] = {
count,
expiresAt: Date.now() + (ADMIN_RATE_LIMIT.windowSeconds * 1000),
};
}
} else {
// Memory store fallback
const stored = memoryStore[key];
if (stored && stored.expiresAt > Date.now()) {
count = stored.count + 1;
stored.count = count;
} else {
count = 1;
memoryStore[key] = {
count: 1,
expiresAt: Date.now() + (ADMIN_RATE_LIMIT.windowSeconds * 1000),
};
}
}
// Check if limit exceeded
if (count > ADMIN_RATE_LIMIT.maxRequests) {
return res.status(429).json({
error: 'Too many requests',
message: 'Rate limit exceeded. Please try again later.',
retry_after: ADMIN_RATE_LIMIT.windowSeconds,
});
}
// Add rate limit headers
res.setHeader('X-RateLimit-Limit', ADMIN_RATE_LIMIT.maxRequests);
res.setHeader('X-RateLimit-Remaining', Math.max(0, ADMIN_RATE_LIMIT.maxRequests - count));
res.setHeader('X-RateLimit-Reset', new Date(Date.now() + (ADMIN_RATE_LIMIT.windowSeconds * 1000)).toISOString());
next();
} catch (err) {
console.error('Admin rate limit error:', err);
// On error, allow the request (fail open for admin access)
next();
}
}
module.exports = adminRateLimit;