package auth import ( "sync" "time" ) const maxTrackedClients = 10000 type attemptWindow struct { failures int startedAt time.Time } type LoginLimiter struct { mutex sync.Mutex maxFailures int window time.Duration attempts map[string]*attemptWindow } func NewLoginLimiter(maxFailures int, window time.Duration) *LoginLimiter { return &LoginLimiter{ maxFailures: maxFailures, window: window, attempts: make(map[string]*attemptWindow), } } func (limiter *LoginLimiter) Allowed(client string) bool { limiter.mutex.Lock() defer limiter.mutex.Unlock() attempt := limiter.current(client) return attempt == nil || attempt.failures < limiter.maxFailures } func (limiter *LoginLimiter) RecordFailure(client string) { limiter.mutex.Lock() defer limiter.mutex.Unlock() attempt := limiter.current(client) if attempt == nil { if len(limiter.attempts) >= maxTrackedClients { limiter.removeExpired() } attempt = &attemptWindow{startedAt: time.Now()} limiter.attempts[client] = attempt } attempt.failures++ } func (limiter *LoginLimiter) Reset(client string) { limiter.mutex.Lock() defer limiter.mutex.Unlock() delete(limiter.attempts, client) } func (limiter *LoginLimiter) current(client string) *attemptWindow { attempt, found := limiter.attempts[client] if !found { return nil } if time.Since(attempt.startedAt) > limiter.window { delete(limiter.attempts, client) return nil } return attempt } func (limiter *LoginLimiter) removeExpired() { for client, attempt := range limiter.attempts { if time.Since(attempt.startedAt) > limiter.window { delete(limiter.attempts, client) } } }