package loginlimit import ( "sync" "time" ) // Limiter tracks failed admin login attempts per client IP. type Limiter struct { mu sync.Mutex attempts map[string]*record maxAttempts int lockout time.Duration } type record struct { failures int lockedUntil time.Time } func New(maxAttempts int, lockout time.Duration) *Limiter { if maxAttempts <= 0 { maxAttempts = 5 } if lockout <= 0 { lockout = 15 * time.Minute } return &Limiter{ attempts: make(map[string]*record), maxAttempts: maxAttempts, lockout: lockout, } } // Check reports whether the IP is locked and how long to wait. func (l *Limiter) Check(ip string) (locked bool, retryAfter time.Duration) { l.mu.Lock() defer l.mu.Unlock() r := l.attempts[ip] if r == nil { return false, 0 } now := time.Now() if now.Before(r.lockedUntil) { return true, r.lockedUntil.Sub(now) } if r.failures >= l.maxAttempts { delete(l.attempts, ip) } return false, 0 } // RecordFailure increments the failure count and locks the IP when the limit is reached. func (l *Limiter) RecordFailure(ip string) { l.mu.Lock() defer l.mu.Unlock() now := time.Now() r := l.attempts[ip] if r == nil { r = &record{} l.attempts[ip] = r } if now.Before(r.lockedUntil) { return } if r.failures >= l.maxAttempts && !r.lockedUntil.IsZero() && now.After(r.lockedUntil) { r.failures = 0 r.lockedUntil = time.Time{} } r.failures++ if r.failures >= l.maxAttempts { r.lockedUntil = now.Add(l.lockout) } } // Configure updates rate limit parameters at runtime. func (l *Limiter) Configure(maxAttempts int, lockout time.Duration) { l.mu.Lock() defer l.mu.Unlock() if maxAttempts > 0 { l.maxAttempts = maxAttempts } if lockout > 0 { l.lockout = lockout } } // Reset clears failure state after a successful login. func (l *Limiter) Reset(ip string) { l.mu.Lock() defer l.mu.Unlock() delete(l.attempts, ip) }