- Docker: Multi-stage Dockerfile with security hardening, docker-compose for production and development environments - Environment: Comprehensive .env.example with all config options, lib/config/env.ts for typed environment validation - Logging: Structured JSON logging with request/response middleware - Monitoring: Prometheus metrics endpoint, Grafana dashboard, health checks (liveness/readiness probes) - Security: Security headers, rate limiting, CORS middleware - CI/CD: GitHub Actions workflows for CI, production deploy, and preview deployments - Error tracking: Sentry integration foundation Files created: - Docker: Dockerfile, docker-compose.yml, docker-compose.dev.yml, .dockerignore - Config: lib/config/env.ts, lib/config/index.ts - Logging: lib/logging/logger.ts, lib/logging/middleware.ts - Monitoring: lib/monitoring/sentry.ts, lib/monitoring/metrics.ts, lib/monitoring/health.ts - Security: lib/security/headers.ts, lib/security/rateLimit.ts, lib/security/cors.ts - API: pages/api/health/*, pages/api/metrics.ts - Infra: infra/prometheus/prometheus.yml, infra/grafana/*
151 lines
4 KiB
TypeScript
151 lines
4 KiB
TypeScript
/**
|
|
* Rate Limiting Middleware
|
|
* Agent 4: Production Deployment
|
|
*
|
|
* Provides rate limiting for API routes to prevent abuse.
|
|
*/
|
|
|
|
import type { NextApiRequest, NextApiResponse, NextApiHandler } from 'next';
|
|
import { env } from '../config';
|
|
import { logger } from '../logging';
|
|
|
|
interface RateLimitConfig {
|
|
windowMs: number; // Time window in milliseconds
|
|
maxRequests: number; // Max requests per window
|
|
keyGenerator?: (req: NextApiRequest) => string;
|
|
skipSuccessfulRequests?: boolean;
|
|
skipFailedRequests?: boolean;
|
|
message?: string;
|
|
}
|
|
|
|
interface RateLimitEntry {
|
|
count: number;
|
|
resetTime: number;
|
|
}
|
|
|
|
// In-memory store for rate limiting
|
|
// In production, use Redis for distributed rate limiting
|
|
const store = new Map<string, RateLimitEntry>();
|
|
|
|
// Cleanup old entries periodically
|
|
setInterval(() => {
|
|
const now = Date.now();
|
|
for (const [key, entry] of store.entries()) {
|
|
if (entry.resetTime < now) {
|
|
store.delete(key);
|
|
}
|
|
}
|
|
}, 60000); // Cleanup every minute
|
|
|
|
const DEFAULT_CONFIG: RateLimitConfig = {
|
|
windowMs: env.rateLimitWindowMs,
|
|
maxRequests: env.rateLimitMaxRequests,
|
|
message: 'Too many requests, please try again later.',
|
|
};
|
|
|
|
/**
|
|
* Default key generator - uses IP address
|
|
*/
|
|
function defaultKeyGenerator(req: NextApiRequest): string {
|
|
const forwarded = req.headers['x-forwarded-for'];
|
|
if (typeof forwarded === 'string') {
|
|
return forwarded.split(',')[0].trim();
|
|
}
|
|
if (Array.isArray(forwarded)) {
|
|
return forwarded[0];
|
|
}
|
|
return req.socket?.remoteAddress || 'unknown';
|
|
}
|
|
|
|
/**
|
|
* Check if request is rate limited
|
|
*/
|
|
function checkRateLimit(
|
|
key: string,
|
|
config: RateLimitConfig
|
|
): { limited: boolean; remaining: number; resetTime: number } {
|
|
const now = Date.now();
|
|
let entry = store.get(key);
|
|
|
|
// Create new entry if doesn't exist or expired
|
|
if (!entry || entry.resetTime < now) {
|
|
entry = {
|
|
count: 0,
|
|
resetTime: now + config.windowMs,
|
|
};
|
|
store.set(key, entry);
|
|
}
|
|
|
|
// Increment count
|
|
entry.count += 1;
|
|
|
|
const remaining = Math.max(0, config.maxRequests - entry.count);
|
|
const limited = entry.count > config.maxRequests;
|
|
|
|
return { limited, remaining, resetTime: entry.resetTime };
|
|
}
|
|
|
|
/**
|
|
* Rate limiting middleware
|
|
*/
|
|
export function withRateLimit(
|
|
handler: NextApiHandler,
|
|
config: Partial<RateLimitConfig> = {}
|
|
): NextApiHandler {
|
|
const mergedConfig: RateLimitConfig = { ...DEFAULT_CONFIG, ...config };
|
|
const keyGenerator = mergedConfig.keyGenerator || defaultKeyGenerator;
|
|
|
|
return async (req: NextApiRequest, res: NextApiResponse) => {
|
|
const key = keyGenerator(req);
|
|
const { limited, remaining, resetTime } = checkRateLimit(key, mergedConfig);
|
|
|
|
// Set rate limit headers
|
|
res.setHeader('X-RateLimit-Limit', mergedConfig.maxRequests);
|
|
res.setHeader('X-RateLimit-Remaining', remaining);
|
|
res.setHeader('X-RateLimit-Reset', Math.ceil(resetTime / 1000));
|
|
|
|
if (limited) {
|
|
logger.warn('Rate limit exceeded', {
|
|
ip: key,
|
|
path: req.url,
|
|
method: req.method,
|
|
});
|
|
|
|
res.setHeader('Retry-After', Math.ceil((resetTime - Date.now()) / 1000));
|
|
return res.status(429).json({
|
|
error: 'Too Many Requests',
|
|
message: mergedConfig.message,
|
|
retryAfter: Math.ceil((resetTime - Date.now()) / 1000),
|
|
});
|
|
}
|
|
|
|
return handler(req, res);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Create a rate limiter with custom settings
|
|
*/
|
|
export function createRateLimiter(config: Partial<RateLimitConfig>) {
|
|
return (handler: NextApiHandler) => withRateLimit(handler, config);
|
|
}
|
|
|
|
/**
|
|
* Stricter rate limiter for authentication endpoints
|
|
*/
|
|
export const authRateLimiter = createRateLimiter({
|
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
maxRequests: 5, // 5 attempts per window
|
|
message: 'Too many authentication attempts. Please try again in 15 minutes.',
|
|
});
|
|
|
|
/**
|
|
* API rate limiter for general endpoints
|
|
*/
|
|
export const apiRateLimiter = createRateLimiter({
|
|
windowMs: 60 * 1000, // 1 minute
|
|
maxRequests: 60, // 60 requests per minute
|
|
});
|
|
|
|
// Export types
|
|
export type { RateLimitConfig, RateLimitEntry };
|