import type Database from 'better-sqlite3'; import type { Config } from '../config.ts'; import { getLoginAttempt, recordFailure as dbRecordFailure, resetLoginAttempts, } from '../db/login-attempts.ts'; export interface LockoutCheckResult { locked: boolean; retryAfterSeconds?: number; } export interface LockoutFailureResult { locked: boolean; durationSeconds: number; failedCount: number; } export interface LockoutService { check(username: string): LockoutCheckResult; recordFailure(username: string): LockoutFailureResult; recordSuccess(username: string): void; } interface LockoutDeps { db: Database.Database; config: Config; now?: () => Date; } function computeDurationSeconds(failedCount: number, config: Config): number { const { lockoutThreshold, lockoutBaseSeconds, lockoutMaxSeconds } = config; if (failedCount < lockoutThreshold) return 0; const exponent = failedCount - lockoutThreshold; const raw = lockoutBaseSeconds * 2 ** exponent; return Math.min(lockoutMaxSeconds, raw); } export function createLockoutService(deps: LockoutDeps): LockoutService { const { db, config } = deps; const now = deps.now ?? ((): Date => new Date()); return { check(username: string): LockoutCheckResult { const row = getLoginAttempt(db, username); if (!row?.locked_until) return { locked: false }; const lockedUntilMs = Date.parse(row.locked_until); const remainingMs = lockedUntilMs - now().getTime(); if (remainingMs <= 0) return { locked: false }; return { locked: true, retryAfterSeconds: Math.ceil(remainingMs / 1000) }; }, recordFailure(username: string): LockoutFailureResult { const existing = getLoginAttempt(db, username); const nextCount = (existing?.failed_count ?? 0) + 1; const durationSeconds = computeDurationSeconds(nextCount, config); const lockedUntilIso = durationSeconds > 0 ? new Date(now().getTime() + durationSeconds * 1000).toISOString() : null; dbRecordFailure(db, username, lockedUntilIso); return { locked: durationSeconds > 0, durationSeconds, failedCount: nextCount, }; }, recordSuccess(username: string): void { resetLoginAttempts(db, username); }, }; }