mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 15:12:09 +01:00
- 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
161 lines
3.8 KiB
TypeScript
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"
|
|
);
|
|
}
|