- Create Economist SubmissionTracking package correctly: * mainArticle = full blog post content * coverLetter = 216-word SIR— letter * Links to blog post via blogPostId - Archive 'Letter to The Economist' from blog posts (it's the cover letter) - Fix date display on article cards (use published_at) - Target publication already displaying via blue badge Database changes: - Make blogPostId optional in SubmissionTracking model - Economist package ID: 68fa85ae49d4900e7f2ecd83 - Le Monde package ID: 68fa2abd2e6acd5691932150 Next: Enhanced modal with tabs, validation, export 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
97 lines
2.7 KiB
JavaScript
97 lines
2.7 KiB
JavaScript
/**
|
|
* Rate Limiting Middleware (inst_045 - Quick Win Version)
|
|
* Prevents brute force, DoS, and spam attacks
|
|
*
|
|
* QUICK WIN: In-memory rate limiting (no Redis required)
|
|
* Full version in Phase 4 will use Redis for distributed rate limiting
|
|
*/
|
|
|
|
const rateLimit = require('express-rate-limit');
|
|
const { logSecurityEvent, getClientIp } = require('../utils/security-logger');
|
|
|
|
/**
|
|
* Create rate limiter with custom handler for security logging
|
|
*/
|
|
function createRateLimiter(options) {
|
|
const {
|
|
windowMs,
|
|
max,
|
|
tier,
|
|
message,
|
|
skipSuccessfulRequests = false
|
|
} = options;
|
|
|
|
return rateLimit({
|
|
windowMs,
|
|
max,
|
|
skipSuccessfulRequests,
|
|
standardHeaders: true,
|
|
legacyHeaders: false,
|
|
handler: async (req, res) => {
|
|
const clientIp = getClientIp(req);
|
|
|
|
await logSecurityEvent({
|
|
type: 'rate_limit_exceeded',
|
|
sourceIp: clientIp,
|
|
userId: req.user?.id || req.user?.userId,
|
|
endpoint: req.path,
|
|
userAgent: req.get('user-agent'),
|
|
details: {
|
|
tier,
|
|
limit: max,
|
|
window_ms: windowMs,
|
|
window_display: `${windowMs / 1000} seconds`
|
|
},
|
|
action: 'blocked',
|
|
severity: 'medium'
|
|
});
|
|
|
|
res.status(429).json({
|
|
error: 'Rate limit exceeded',
|
|
message: message || `Too many requests. Limit: ${max} per ${windowMs / 1000} seconds`,
|
|
retryAfter: Math.ceil(windowMs / 1000)
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Public endpoints: 1000 requests per 15 minutes per IP (inst_045)
|
|
* Increased for local development - production should use lower limits
|
|
*/
|
|
const publicRateLimiter = createRateLimiter({
|
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
max: 1000, // Increased from 100 to 1000
|
|
tier: 'public',
|
|
message: 'Too many requests from this IP. Please try again later.'
|
|
});
|
|
|
|
/**
|
|
* Form submissions: 50 requests per minute per IP (inst_043)
|
|
* Increased for local development - production should use lower limits
|
|
*/
|
|
const formRateLimiter = createRateLimiter({
|
|
windowMs: 60 * 1000, // 1 minute
|
|
max: 50, // Increased from 5 to 50
|
|
tier: 'form',
|
|
message: 'Too many form submissions. Please wait before submitting again.'
|
|
});
|
|
|
|
/**
|
|
* Authentication endpoints: 100 attempts per 5 minutes
|
|
* Increased for local development - production should use lower limits
|
|
*/
|
|
const authRateLimiter = createRateLimiter({
|
|
windowMs: 5 * 60 * 1000, // 5 minutes
|
|
max: 100, // Increased from 10 to 100
|
|
tier: 'auth',
|
|
message: 'Too many authentication attempts. Please try again later.',
|
|
skipSuccessfulRequests: true // Don't count successful logins
|
|
});
|
|
|
|
module.exports = {
|
|
publicRateLimiter,
|
|
formRateLimiter,
|
|
authRateLimiter,
|
|
createRateLimiter
|
|
};
|