93 lines
1.9 KiB
Go
93 lines
1.9 KiB
Go
package auth
|
|
|
|
import (
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
maxAttempts = 5
|
|
windowMinutes = 15
|
|
lockMinutes = 15
|
|
)
|
|
|
|
type entry struct {
|
|
count int
|
|
firstAt time.Time
|
|
lockedUntil time.Time
|
|
}
|
|
|
|
// RateLimiter tracks failed login attempts per Discord ID in memory.
|
|
// Resets on bot restart (by design — avoids DB writes on every auth failure).
|
|
type RateLimiter struct {
|
|
mu sync.Mutex
|
|
entries map[string]*entry
|
|
}
|
|
|
|
func NewRateLimiter() *RateLimiter {
|
|
return &RateLimiter{entries: make(map[string]*entry)}
|
|
}
|
|
|
|
// Allow returns true if the given ID is not currently rate-limited.
|
|
func (r *RateLimiter) Allow(id string) bool {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
e, ok := r.entries[id]
|
|
if !ok {
|
|
return true
|
|
}
|
|
if !e.lockedUntil.IsZero() {
|
|
if time.Now().Before(e.lockedUntil) {
|
|
return false
|
|
}
|
|
// Lock expired — reset
|
|
delete(r.entries, id)
|
|
}
|
|
return true
|
|
}
|
|
|
|
// Record records a failed attempt. Returns true if the account is now locked.
|
|
func (r *RateLimiter) Record(id string) bool {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
e, ok := r.entries[id]
|
|
if !ok {
|
|
e = &entry{firstAt: time.Now()}
|
|
r.entries[id] = e
|
|
}
|
|
// Reset window if outside window duration
|
|
if time.Since(e.firstAt) > windowMinutes*time.Minute {
|
|
e.count = 0
|
|
e.firstAt = time.Now()
|
|
e.lockedUntil = time.Time{}
|
|
}
|
|
e.count++
|
|
if e.count >= maxAttempts {
|
|
e.lockedUntil = time.Now().Add(lockMinutes * time.Minute)
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Reset clears the rate limit entry for a Discord ID (call on successful login).
|
|
func (r *RateLimiter) Reset(id string) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
delete(r.entries, id)
|
|
}
|
|
|
|
// RemainingLock returns the remaining lock duration (zero if not locked).
|
|
func (r *RateLimiter) RemainingLock(id string) time.Duration {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
e, ok := r.entries[id]
|
|
if !ok || e.lockedUntil.IsZero() {
|
|
return 0
|
|
}
|
|
remaining := time.Until(e.lockedUntil)
|
|
if remaining < 0 {
|
|
return 0
|
|
}
|
|
return remaining
|
|
}
|