package middleware import ( "sync" "time" ) // LoginAttemptTracker tracks failed login attempts per IP and username type LoginAttemptTracker struct { mu sync.RWMutex attempts map[string]*AttemptInfo maxAttempts int lockoutDuration time.Duration cleanupInterval time.Duration } // AttemptInfo stores information about login attempts type AttemptInfo struct { AttemptCount int LockedUntil time.Time LastAttempt time.Time } // NewLoginAttemptTracker creates a new login attempt tracker func NewLoginAttemptTracker(maxAttempts int, lockoutDuration, cleanupInterval time.Duration) *LoginAttemptTracker { tracker := &LoginAttemptTracker{ attempts: make(map[string]*AttemptInfo), maxAttempts: maxAttempts, lockoutDuration: lockoutDuration, cleanupInterval: cleanupInterval, } // Start cleanup goroutine go tracker.cleanup() return tracker } // RecordFailedAttempt records a failed login attempt func (t *LoginAttemptTracker) RecordFailedAttempt(identifier string) (locked bool, remainingTime time.Duration) { t.mu.Lock() defer t.mu.Unlock() now := time.Now() info, exists := t.attempts[identifier] if !exists { info = &AttemptInfo{ AttemptCount: 1, LastAttempt: now, } t.attempts[identifier] = info return false, 0 } // Check if currently locked if now.Before(info.LockedUntil) { return true, time.Until(info.LockedUntil) } // Reset attempts if last attempt was more than lockoutDuration ago if now.Sub(info.LastAttempt) > t.lockoutDuration { info.AttemptCount = 1 } else { info.AttemptCount++ } info.LastAttempt = now // Check if max attempts reached if info.AttemptCount >= t.maxAttempts { info.LockedUntil = now.Add(t.lockoutDuration) return true, t.lockoutDuration } return false, 0 } // IsLocked checks if an identifier is currently locked out func (t *LoginAttemptTracker) IsLocked(identifier string) (locked bool, remainingTime time.Duration) { t.mu.Lock() defer t.mu.Unlock() info, exists := t.attempts[identifier] if !exists { return false, 0 } now := time.Now() if now.Before(info.LockedUntil) { return true, time.Until(info.LockedUntil) } return false, 0 } // ClearAttempts clears failed login attempts for an identifier func (t *LoginAttemptTracker) ClearAttempts(identifier string) { t.mu.Lock() defer t.mu.Unlock() delete(t.attempts, identifier) } // cleanup removes old attempt records periodically func (t *LoginAttemptTracker) cleanup() { ticker := time.NewTicker(t.cleanupInterval) defer ticker.Stop() for range ticker.C { t.mu.Lock() now := time.Now() for identifier, info := range t.attempts { // Remove records that are not locked and haven't had recent attempts if now.After(info.LockedUntil) && now.Sub(info.LastAttempt) > t.lockoutDuration*2 { delete(t.attempts, identifier) } } t.mu.Unlock() } }