MBO-Tech-IT-Webseite/lib/rate-limit.ts

72 lines
1.9 KiB
TypeScript

interface RateLimitEntry {
count: number;
lastAttempt: number;
locked: boolean;
lockedUntil?: number;
}
const attempts = new Map<string, RateLimitEntry>();
const CLEANUP_INTERVAL = 10 * 60 * 1000;
const RESET_WINDOW = 15 * 60 * 1000;
const MAX_ATTEMPTS = 5;
const LOCK_THRESHOLD = 10;
const LOCK_DURATION = 15 * 60 * 1000;
setInterval(() => {
const now = Date.now();
for (const [key, entry] of attempts.entries()) {
if (now - entry.lastAttempt > RESET_WINDOW) {
attempts.delete(key);
}
}
}, CLEANUP_INTERVAL);
export function checkRateLimit(identifier: string) {
const now = Date.now();
let entry = attempts.get(identifier);
if (!entry) {
attempts.set(identifier, { count: 1, lastAttempt: now, locked: false });
return { allowed: true, delayMs: 0, locked: false };
}
if (now - entry.lastAttempt > RESET_WINDOW) {
attempts.set(identifier, { count: 1, lastAttempt: now, locked: false });
return { allowed: true, delayMs: 0, locked: false };
}
if (entry.locked && entry.lockedUntil) {
if (now < entry.lockedUntil) {
return { allowed: false, delayMs: entry.lockedUntil - now, locked: true };
} else {
entry.locked = false;
entry.count = 1;
entry.lastAttempt = now;
return { allowed: true, delayMs: 0, locked: false };
}
}
entry.count++;
entry.lastAttempt = now;
const delayMs = Math.min(Math.pow(2, entry.count - 2) * 1000, 30000);
if (entry.count >= LOCK_THRESHOLD) {
entry.locked = true;
entry.lockedUntil = now + LOCK_DURATION;
return { allowed: false, delayMs: LOCK_DURATION, locked: true };
}
const allowed = entry.count <= MAX_ATTEMPTS;
return { allowed, delayMs: allowed ? 0 : delayMs, locked: false };
}
export function resetRateLimit(identifier: string) {
attempts.delete(identifier);
}
export function getAttemptCount(identifier: string): number {
return attempts.get(identifier)?.count ?? 0;
}