Files
livedash-node/lib/rateLimiter.ts
Kaj Kowalski 1eea2cc3e4 refactor: fix biome linting issues and update project documentation
- Fix 36+ biome linting issues reducing errors/warnings from 227 to 191
- Replace explicit 'any' types with proper TypeScript interfaces
- Fix React hooks dependencies and useCallback patterns
- Resolve unused variables and parameter assignment issues
- Improve accessibility with proper label associations
- Add comprehensive API documentation for admin and security features
- Update README.md with accurate PostgreSQL setup and current tech stack
- Create complete documentation for audit logging, CSP monitoring, and batch processing
- Fix outdated project information and missing developer workflows
2025-07-12 00:28:09 +02:00

161 lines
3.8 KiB
TypeScript

// Shared rate limiting utility to prevent code duplication
export interface RateLimitConfig {
maxAttempts: number;
windowMs: number;
maxEntries?: number;
cleanupIntervalMs?: number;
}
export interface RateLimitAttempt {
count: number;
resetTime: number;
}
export class InMemoryRateLimiter {
private attempts = new Map<string, RateLimitAttempt>();
private cleanupInterval: NodeJS.Timeout;
constructor(private config: RateLimitConfig) {
const cleanupMs = config.cleanupIntervalMs || 5 * 60 * 1000; // 5 minutes default
// Clean up expired entries periodically
this.cleanupInterval = setInterval(() => {
this.cleanup();
}, cleanupMs);
}
/**
* Check if a key (e.g., IP address) is rate limited
*/
checkRateLimit(key: string): { allowed: boolean; resetTime?: number } {
const now = Date.now();
const attempt = this.attempts.get(key);
if (!attempt || now > attempt.resetTime) {
// No previous attempt or window expired - allow and start new window
this.attempts.set(key, {
count: 1,
resetTime: now + this.config.windowMs,
});
return { allowed: true };
}
if (attempt.count >= this.config.maxAttempts) {
// Rate limit exceeded
return { allowed: false, resetTime: attempt.resetTime };
}
// Increment counter
attempt.count++;
return { allowed: true };
}
/**
* Clean up expired entries and prevent unbounded growth
*/
private cleanup(): void {
const now = Date.now();
const maxEntries = this.config.maxEntries || 10000;
// Remove expired entries
for (const [key, attempt] of Array.from(this.attempts.entries())) {
if (now > attempt.resetTime) {
this.attempts.delete(key);
}
}
// If still too many entries, remove oldest half
if (this.attempts.size > maxEntries) {
const entries = Array.from(this.attempts.entries());
entries.sort((a, b) => a[1].resetTime - b[1].resetTime);
const toRemove = Math.floor(entries.length / 2);
for (let i = 0; i < toRemove; i++) {
this.attempts.delete(entries[i][0]);
}
}
}
/**
* Check rate limit with custom parameters
*/
async check(
key: string,
maxAttempts: number,
windowMs: number
): Promise<{
success: boolean;
remaining: number;
}> {
const now = Date.now();
let attempt = this.attempts.get(key);
if (!attempt || now > attempt.resetTime) {
// Initialize or reset the attempt
attempt = {
count: 1,
resetTime: now + windowMs,
};
this.attempts.set(key, attempt);
return {
success: true,
remaining: maxAttempts - 1,
};
}
if (attempt.count >= maxAttempts) {
return {
success: false,
remaining: 0,
};
}
attempt.count++;
this.attempts.set(key, attempt);
return {
success: true,
remaining: maxAttempts - attempt.count,
};
}
/**
* Clean up resources
*/
destroy(): void {
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
}
}
}
/**
* Default rate limiter instance for general use
*/
export const rateLimiter = new InMemoryRateLimiter({
maxAttempts: 100,
windowMs: 15 * 60 * 1000, // 15 minutes
maxEntries: 10000,
cleanupIntervalMs: 5 * 60 * 1000, // 5 minutes
});
/**
* Extract client IP address from request headers
*/
export function extractClientIP(request: Request): string {
// Check multiple possible headers in order of preference
const forwarded = request.headers.get("x-forwarded-for");
if (forwarded) {
// Take the first IP from comma-separated list
return forwarded.split(",")[0].trim();
}
return (
request.headers.get("x-real-ip") ||
request.headers.get("x-client-ip") ||
request.headers.get("cf-connecting-ip") ||
"unknown"
);
}