| |
|
|
| const MAX_FAILS_BEFORE_LOCK = 5; |
| const LOCK_STEPS_MS = [30_000, 120_000, 600_000, 1_800_000]; |
| const FAIL_WINDOW_MS = 60 * 60 * 1000; |
|
|
| const attempts = new Map(); |
|
|
| function now() { return Date.now(); } |
|
|
| function getEntry(ip) { |
| const e = attempts.get(ip); |
| if (!e) return null; |
| |
| if (e.lastFailAt && now() - e.lastFailAt > FAIL_WINDOW_MS && (!e.lockUntil || now() >= e.lockUntil)) { |
| attempts.delete(ip); |
| return null; |
| } |
| return e; |
| } |
|
|
| export function checkLock(ip) { |
| const e = getEntry(ip); |
| if (!e || !e.lockUntil) return { locked: false }; |
| const remaining = e.lockUntil - now(); |
| if (remaining <= 0) return { locked: false }; |
| return { locked: true, retryAfter: Math.ceil(remaining / 1000) }; |
| } |
|
|
| export function recordFail(ip) { |
| const e = getEntry(ip) || { fails: 0, lockUntil: 0, lockLevel: 0, lastFailAt: 0 }; |
| e.fails += 1; |
| e.lastFailAt = now(); |
| if (e.fails >= MAX_FAILS_BEFORE_LOCK) { |
| const step = LOCK_STEPS_MS[Math.min(e.lockLevel, LOCK_STEPS_MS.length - 1)]; |
| e.lockUntil = now() + step; |
| e.lockLevel += 1; |
| e.fails = 0; |
| } |
| attempts.set(ip, e); |
| return { remainingBeforeLock: Math.max(0, MAX_FAILS_BEFORE_LOCK - e.fails) }; |
| } |
|
|
| export function recordSuccess(ip) { |
| attempts.delete(ip); |
| } |
|
|
| export function getClientIp(request) { |
| |
| const realIp = request.headers.get("x-9r-real-ip"); |
| if (realIp) return realIp; |
| |
| if (process.env.TRUST_PROXY === "true") { |
| const xff = request.headers.get("x-forwarded-for"); |
| if (xff) return xff.split(",")[0].trim(); |
| } |
| |
| |
| return "unknown"; |
| } |
|
|