namespace QuantEngine.Web.Services; public interface IIpLockoutService { bool IsLockedOut(string ipAddress); void RecordFailedAttempt(string ipAddress); void ClearFailedAttempts(string ipAddress); } public class IpLockoutService : IIpLockoutService { private readonly Dictionary _attemptLog = []; private const int MaxFailedAttempts = 3; private const int LockoutDurationMinutes = 15; private const int MaxLockoutDurationMinutes = 1440; // 24 hours cap private readonly object _lock = new(); public bool IsLockedOut(string ipAddress) { lock (_lock) { if (_attemptLog.TryGetValue(ipAddress, out var record)) { if (DateTime.UtcNow < record.LockedUntil) return true; // Reset failed attempts but keep the LockoutCount to calculate progressive delay on next failure if (record.Attempts >= MaxFailedAttempts) { _attemptLog[ipAddress] = (0, record.LockoutCount, DateTime.MinValue); } } return false; } } public void RecordFailedAttempt(string ipAddress) { lock (_lock) { if (_attemptLog.TryGetValue(ipAddress, out var record)) { record.Attempts++; if (record.Attempts >= MaxFailedAttempts) { // Calculate exponential progressive lockout duration: 15 * 2^LockoutCount var durationMinutes = LockoutDurationMinutes * Math.Pow(2, record.LockoutCount); durationMinutes = Math.Min(durationMinutes, MaxLockoutDurationMinutes); record.LockedUntil = DateTime.UtcNow.AddMinutes(durationMinutes); record.LockoutCount++; } _attemptLog[ipAddress] = record; } else { _attemptLog[ipAddress] = (1, 0, DateTime.MinValue); } } } public void ClearFailedAttempts(string ipAddress) { lock (_lock) { _attemptLog.Remove(ipAddress); } } }