mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 11:31:11 -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
|
* Prevents abuse and DoS attacks with in-memory rate limiting
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { recordRateLimitMetric } from './rateLimitMetrics.ts';
|
||||||
|
import { extractUserIdFromAuth, getClientIP } from './authHelpers.ts';
|
||||||
|
|
||||||
export interface RateLimitConfig {
|
export interface RateLimitConfig {
|
||||||
windowMs: number; // Time window in milliseconds
|
windowMs: number; // Time window in milliseconds
|
||||||
maxRequests: number; // Max requests per window
|
maxRequests: number; // Max requests per window
|
||||||
@@ -21,8 +24,12 @@ class RateLimiter {
|
|||||||
private rateLimitMap = new Map<string, { count: number; resetAt: number }>();
|
private rateLimitMap = new Map<string, { count: number; resetAt: number }>();
|
||||||
private config: Required<RateLimitConfig>;
|
private config: Required<RateLimitConfig>;
|
||||||
private cleanupInterval: number;
|
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 = {
|
this.config = {
|
||||||
maxMapSize: 10000,
|
maxMapSize: 10000,
|
||||||
keyGenerator: (req: Request) => this.getClientIP(req),
|
keyGenerator: (req: Request) => this.getClientIP(req),
|
||||||
@@ -38,16 +45,8 @@ class RateLimiter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getClientIP(req: Request): string {
|
private getClientIP(req: Request): string {
|
||||||
if (this.config.trustProxy) {
|
// Use centralized auth helper for consistent IP extraction
|
||||||
const forwarded = req.headers.get('x-forwarded-for');
|
return getClientIP(req);
|
||||||
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';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private cleanupExpiredEntries(): void {
|
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 key = this.config.keyGenerator(req);
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const existing = this.rateLimitMap.get(key);
|
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
|
// Check existing entry
|
||||||
if (existing && now <= existing.resetAt) {
|
if (existing && now <= existing.resetAt) {
|
||||||
if (existing.count >= this.config.maxRequests) {
|
if (existing.count >= this.config.maxRequests) {
|
||||||
const retryAfter = Math.ceil((existing.resetAt - now) / 1000);
|
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 {
|
return {
|
||||||
allowed: false,
|
allowed: false,
|
||||||
retryAfter,
|
retryAfter,
|
||||||
@@ -89,9 +106,22 @@ class RateLimiter {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
existing.count++;
|
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 {
|
return {
|
||||||
allowed: true,
|
allowed: true,
|
||||||
remaining: this.config.maxRequests - existing.count
|
remaining
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,9 +147,22 @@ class RateLimiter {
|
|||||||
resetAt: now + this.config.windowMs
|
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 {
|
return {
|
||||||
allowed: true,
|
allowed: true,
|
||||||
remaining: this.config.maxRequests - 1
|
remaining
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,8 +186,8 @@ import {
|
|||||||
} from './rateLimitConfig.ts';
|
} from './rateLimitConfig.ts';
|
||||||
|
|
||||||
// Export factory function for creating custom rate limiters
|
// Export factory function for creating custom rate limiters
|
||||||
export function createRateLimiter(config: RateLimitConfig): RateLimiter {
|
export function createRateLimiter(config: RateLimitConfig, tierName?: string, functionName?: string): RateLimiter {
|
||||||
return new RateLimiter(config);
|
return new RateLimiter(config, tierName, functionName);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -155,41 +198,42 @@ export function createRateLimiter(config: RateLimitConfig): RateLimiter {
|
|||||||
*/
|
*/
|
||||||
export const rateLimiters = {
|
export const rateLimiters = {
|
||||||
// Strict: 5 requests/minute - For expensive operations
|
// 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: 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: 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: 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: 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)
|
// Per-user rate limiters (key by user ID instead of IP)
|
||||||
perUserStrict: createRateLimiter(RATE_LIMIT_PER_USER_STRICT),
|
perUserStrict: createRateLimiter(RATE_LIMIT_PER_USER_STRICT, 'perUserStrict'),
|
||||||
perUserModerate: createRateLimiter(RATE_LIMIT_PER_USER_MODERATE),
|
perUserModerate: createRateLimiter(RATE_LIMIT_PER_USER_MODERATE, 'perUserModerate'),
|
||||||
perUserStandard: createRateLimiter(RATE_LIMIT_PER_USER_STANDARD),
|
perUserStandard: createRateLimiter(RATE_LIMIT_PER_USER_STANDARD, 'perUserStandard'),
|
||||||
perUserLenient: createRateLimiter(RATE_LIMIT_PER_USER_LENIENT),
|
perUserLenient: createRateLimiter(RATE_LIMIT_PER_USER_LENIENT, 'perUserLenient'),
|
||||||
|
|
||||||
// Legacy per-user factory function (DEPRECATED: use perUserStrict, perUserModerate, etc.)
|
// Legacy per-user factory function (DEPRECATED: use perUserStrict, perUserModerate, etc.)
|
||||||
perUser: (maxRequests: number = 20) => createRateLimiter({
|
perUser: (maxRequests: number = 20) => createRateLimiter({
|
||||||
...RATE_LIMIT_PER_USER_STANDARD,
|
...RATE_LIMIT_PER_USER_STANDARD,
|
||||||
maxRequests,
|
maxRequests,
|
||||||
}),
|
}, 'perUser'),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Middleware helper
|
// Middleware helper
|
||||||
export function withRateLimit(
|
export function withRateLimit(
|
||||||
handler: (req: Request) => Promise<Response>,
|
handler: (req: Request) => Promise<Response>,
|
||||||
limiter: RateLimiter,
|
limiter: RateLimiter,
|
||||||
corsHeaders: Record<string, string> = {}
|
corsHeaders: Record<string, string> = {},
|
||||||
|
functionName?: string
|
||||||
): (req: Request) => Promise<Response> {
|
): (req: Request) => Promise<Response> {
|
||||||
return async (req: Request) => {
|
return async (req: Request) => {
|
||||||
const result = limiter.check(req);
|
const result = limiter.check(req, functionName);
|
||||||
|
|
||||||
if (!result.allowed) {
|
if (!result.allowed) {
|
||||||
return new Response(
|
return new Response(
|
||||||
|
|||||||
Reference in New Issue
Block a user