oups
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user