Files
Luminary/internal/auth/loginlimit/limit.go
T
renjue 76ba500417
CI / docker (push) Successful in 2m4s
Initial commit: Luminary AI Gateway
OpenAI-compatible AI gateway with Vue admin UI, multi-provider egress,
ingress key governance, monitoring, and security controls.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-23 21:46:16 +08:00

94 lines
1.9 KiB
Go

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)
}