mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 10:31:13 -05:00
Integrate metrics and auth
Add Phase 2 updates to rateLimiter.ts: - Import and hook in rateLimitMetrics and authHelpers - Track per-request metrics on allowed/blocked outcomes - Extract userId and clientIP for metrics - Extend RateLimiter to pass functionName for metrics context - Update withRateLimit to utilize new metadata and return values
This commit is contained in:
@@ -3,6 +3,9 @@
|
||||
* Prevents abuse and DoS attacks with in-memory rate limiting
|
||||
*/
|
||||
|
||||
import { recordRateLimitMetric } from './rateLimitMetrics.ts';
|
||||
import { extractUserIdFromAuth, getClientIP } from './authHelpers.ts';
|
||||
|
||||
export interface RateLimitConfig {
|
||||
windowMs: number; // Time window in milliseconds
|
||||
maxRequests: number; // Max requests per window
|
||||
@@ -21,8 +24,12 @@ class RateLimiter {
|
||||
private rateLimitMap = new Map<string, { count: number; resetAt: number }>();
|
||||
private config: Required<RateLimitConfig>;
|
||||
private cleanupInterval: number;
|
||||
private tierName: string;
|
||||
private functionName?: string;
|
||||
|
||||
constructor(config: RateLimitConfig) {
|
||||
constructor(config: RateLimitConfig, tierName: string = 'custom', functionName?: string) {
|
||||
this.tierName = tierName;
|
||||
this.functionName = functionName;
|
||||
this.config = {
|
||||
maxMapSize: 10000,
|
||||
keyGenerator: (req: Request) => this.getClientIP(req),
|
||||
@@ -38,16 +45,8 @@ class RateLimiter {
|
||||
}
|
||||
|
||||
private getClientIP(req: Request): string {
|
||||
if (this.config.trustProxy) {
|
||||
const forwarded = req.headers.get('x-forwarded-for');
|
||||
if (forwarded) return forwarded.split(',')[0].trim();
|
||||
|
||||
const realIP = req.headers.get('x-real-ip');
|
||||
if (realIP) return realIP;
|
||||
}
|
||||
|
||||
// Fallback for testing
|
||||
return '0.0.0.0';
|
||||
// Use centralized auth helper for consistent IP extraction
|
||||
return getClientIP(req);
|
||||
}
|
||||
|
||||
private cleanupExpiredEntries(): void {
|
||||
@@ -73,15 +72,33 @@ class RateLimiter {
|
||||
}
|
||||
}
|
||||
|
||||
check(req: Request): RateLimitResult {
|
||||
check(req: Request, functionName?: string): RateLimitResult {
|
||||
const key = this.config.keyGenerator(req);
|
||||
const now = Date.now();
|
||||
const existing = this.rateLimitMap.get(key);
|
||||
|
||||
// Extract metadata for metrics
|
||||
const clientIP = getClientIP(req);
|
||||
const userId = extractUserIdFromAuth(req);
|
||||
const actualFunctionName = functionName || this.functionName || 'unknown';
|
||||
|
||||
// Check existing entry
|
||||
if (existing && now <= existing.resetAt) {
|
||||
if (existing.count >= this.config.maxRequests) {
|
||||
const retryAfter = Math.ceil((existing.resetAt - now) / 1000);
|
||||
|
||||
// Record blocked request metric
|
||||
recordRateLimitMetric({
|
||||
timestamp: now,
|
||||
functionName: actualFunctionName,
|
||||
clientIP,
|
||||
userId: userId || undefined,
|
||||
allowed: false,
|
||||
remaining: 0,
|
||||
retryAfter,
|
||||
tier: this.tierName,
|
||||
});
|
||||
|
||||
return {
|
||||
allowed: false,
|
||||
retryAfter,
|
||||
@@ -89,9 +106,22 @@ class RateLimiter {
|
||||
};
|
||||
}
|
||||
existing.count++;
|
||||
const remaining = this.config.maxRequests - existing.count;
|
||||
|
||||
// Record allowed request metric
|
||||
recordRateLimitMetric({
|
||||
timestamp: now,
|
||||
functionName: actualFunctionName,
|
||||
clientIP,
|
||||
userId: userId || undefined,
|
||||
allowed: true,
|
||||
remaining,
|
||||
tier: this.tierName,
|
||||
});
|
||||
|
||||
return {
|
||||
allowed: true,
|
||||
remaining: this.config.maxRequests - existing.count
|
||||
remaining
|
||||
};
|
||||
}
|
||||
|
||||
@@ -117,9 +147,22 @@ class RateLimiter {
|
||||
resetAt: now + this.config.windowMs
|
||||
});
|
||||
|
||||
const remaining = this.config.maxRequests - 1;
|
||||
|
||||
// Record allowed request metric
|
||||
recordRateLimitMetric({
|
||||
timestamp: now,
|
||||
functionName: actualFunctionName,
|
||||
clientIP,
|
||||
userId: userId || undefined,
|
||||
allowed: true,
|
||||
remaining,
|
||||
tier: this.tierName,
|
||||
});
|
||||
|
||||
return {
|
||||
allowed: true,
|
||||
remaining: this.config.maxRequests - 1
|
||||
remaining
|
||||
};
|
||||
}
|
||||
|
||||
@@ -143,8 +186,8 @@ import {
|
||||
} from './rateLimitConfig.ts';
|
||||
|
||||
// Export factory function for creating custom rate limiters
|
||||
export function createRateLimiter(config: RateLimitConfig): RateLimiter {
|
||||
return new RateLimiter(config);
|
||||
export function createRateLimiter(config: RateLimitConfig, tierName?: string, functionName?: string): RateLimiter {
|
||||
return new RateLimiter(config, tierName, functionName);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -155,41 +198,42 @@ export function createRateLimiter(config: RateLimitConfig): RateLimiter {
|
||||
*/
|
||||
export const rateLimiters = {
|
||||
// Strict: 5 requests/minute - For expensive operations
|
||||
strict: createRateLimiter(RATE_LIMIT_STRICT),
|
||||
strict: createRateLimiter(RATE_LIMIT_STRICT, 'strict'),
|
||||
|
||||
// Moderate: 10 requests/minute - For moderation and submissions
|
||||
moderate: createRateLimiter(RATE_LIMIT_MODERATE),
|
||||
moderate: createRateLimiter(RATE_LIMIT_MODERATE, 'moderate'),
|
||||
|
||||
// Standard: 20 requests/minute - For typical operations (DEPRECATED: use 'moderate' for 10/min or 'standard' for 20/min)
|
||||
standard: createRateLimiter(RATE_LIMIT_MODERATE), // Keeping for backward compatibility
|
||||
standard: createRateLimiter(RATE_LIMIT_MODERATE, 'standard'), // Keeping for backward compatibility
|
||||
|
||||
// Lenient: 30 requests/minute - For lightweight reads
|
||||
lenient: createRateLimiter(RATE_LIMIT_LENIENT),
|
||||
lenient: createRateLimiter(RATE_LIMIT_LENIENT, 'lenient'),
|
||||
|
||||
// Generous: 60 requests/minute - For high-frequency operations
|
||||
generous: createRateLimiter(RATE_LIMIT_GENEROUS),
|
||||
generous: createRateLimiter(RATE_LIMIT_GENEROUS, 'generous'),
|
||||
|
||||
// Per-user rate limiters (key by user ID instead of IP)
|
||||
perUserStrict: createRateLimiter(RATE_LIMIT_PER_USER_STRICT),
|
||||
perUserModerate: createRateLimiter(RATE_LIMIT_PER_USER_MODERATE),
|
||||
perUserStandard: createRateLimiter(RATE_LIMIT_PER_USER_STANDARD),
|
||||
perUserLenient: createRateLimiter(RATE_LIMIT_PER_USER_LENIENT),
|
||||
perUserStrict: createRateLimiter(RATE_LIMIT_PER_USER_STRICT, 'perUserStrict'),
|
||||
perUserModerate: createRateLimiter(RATE_LIMIT_PER_USER_MODERATE, 'perUserModerate'),
|
||||
perUserStandard: createRateLimiter(RATE_LIMIT_PER_USER_STANDARD, 'perUserStandard'),
|
||||
perUserLenient: createRateLimiter(RATE_LIMIT_PER_USER_LENIENT, 'perUserLenient'),
|
||||
|
||||
// Legacy per-user factory function (DEPRECATED: use perUserStrict, perUserModerate, etc.)
|
||||
perUser: (maxRequests: number = 20) => createRateLimiter({
|
||||
...RATE_LIMIT_PER_USER_STANDARD,
|
||||
maxRequests,
|
||||
}),
|
||||
}, 'perUser'),
|
||||
};
|
||||
|
||||
// Middleware helper
|
||||
export function withRateLimit(
|
||||
handler: (req: Request) => Promise<Response>,
|
||||
limiter: RateLimiter,
|
||||
corsHeaders: Record<string, string> = {}
|
||||
corsHeaders: Record<string, string> = {},
|
||||
functionName?: string
|
||||
): (req: Request) => Promise<Response> {
|
||||
return async (req: Request) => {
|
||||
const result = limiter.check(req);
|
||||
const result = limiter.check(req, functionName);
|
||||
|
||||
if (!result.allowed) {
|
||||
return new Response(
|
||||
|
||||
Reference in New Issue
Block a user