package httpapi import ( "sync" "time" ) type loginAttempt struct { failures int windowStart time.Time blockedTill time.Time } type loginLimiter struct { mu sync.Mutex attempts map[string]loginAttempt now func() time.Time } type queryLimitEntry struct { windowStart time.Time count int } // queryLimiter is deliberately small and in-memory for the temporary public // phone lookup. The production replacement will be an OTP or external // identity provider, so this limiter is only a safety net for the test flow. type queryLimiter struct { mu sync.Mutex entries map[string]queryLimitEntry now func() time.Time limit int window time.Duration } func newQueryLimiter(now func() time.Time, limit int, window time.Duration) *queryLimiter { return &queryLimiter{ entries: make(map[string]queryLimitEntry), now: now, limit: limit, window: window, } } func (l *queryLimiter) allow(key string) (bool, time.Duration) { if l == nil || l.limit <= 0 || l.window <= 0 { return true, 0 } l.mu.Lock() defer l.mu.Unlock() now := l.now() entry, ok := l.entries[key] if !ok || now.Sub(entry.windowStart) >= l.window { entry = queryLimitEntry{windowStart: now} } if entry.count >= l.limit { retry := l.window - now.Sub(entry.windowStart) if retry < 0 { retry = 0 } return false, retry } entry.count++ l.entries[key] = entry return true, 0 } func newLoginLimiter(now func() time.Time) *loginLimiter { return &loginLimiter{attempts: make(map[string]loginAttempt), now: now} } func (l *loginLimiter) allow(key string) (bool, time.Duration) { l.mu.Lock() defer l.mu.Unlock() now := l.now() attempt, ok := l.attempts[key] if !ok { return true, 0 } if now.Before(attempt.blockedTill) { return false, attempt.blockedTill.Sub(now) } if now.Sub(attempt.windowStart) > 10*time.Minute { delete(l.attempts, key) } return true, 0 } func (l *loginLimiter) failure(key string) { l.mu.Lock() defer l.mu.Unlock() now := l.now() attempt := l.attempts[key] if attempt.windowStart.IsZero() || now.Sub(attempt.windowStart) > 10*time.Minute { attempt = loginAttempt{windowStart: now} } attempt.failures++ if attempt.failures >= 5 { attempt.blockedTill = now.Add(10 * time.Minute) } l.attempts[key] = attempt } func (l *loginLimiter) success(key string) { l.mu.Lock() delete(l.attempts, key) l.mu.Unlock() }