cart and checkout
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
package customerauth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Limiter throttles repeated failures for a key (login email or "reset:"-prefixed
|
||||
// email). It is satisfied by the in-memory LoginLimiter (single-instance / dev)
|
||||
// and by RedisLoginLimiter (shared, horizontally scalable).
|
||||
type Limiter interface {
|
||||
// Allowed reports whether an attempt for key may proceed, and when locked the
|
||||
// remaining lockout duration (for a Retry-After header).
|
||||
Allowed(ctx context.Context, key string) (bool, time.Duration)
|
||||
// RecordFailure registers a failed attempt for key.
|
||||
RecordFailure(ctx context.Context, key string)
|
||||
// Reset clears the failure history for key (call on a successful auth).
|
||||
Reset(ctx context.Context, key string)
|
||||
}
|
||||
|
||||
// LoginLimiter is an in-memory failed-attempt limiter that locks a key out after
|
||||
// too many failures inside a rolling window. It is per-process — matching the
|
||||
// credential store's single-instance scope; a horizontally-scaled deployment
|
||||
// would need a shared backing store. Keys are typically the normalized email
|
||||
// (login) or a "reset:"-prefixed email (reset requests).
|
||||
type LoginLimiter struct {
|
||||
mu sync.Mutex
|
||||
attempts map[string]*attemptState
|
||||
max int // failures allowed in the window before lockout
|
||||
window time.Duration // rolling window and lockout duration
|
||||
}
|
||||
|
||||
type attemptState struct {
|
||||
count int
|
||||
first time.Time
|
||||
lockedUntil time.Time
|
||||
}
|
||||
|
||||
// NewLoginLimiter builds a limiter allowing max failures per window before a
|
||||
// lockout of window duration. Zero/negative values fall back to 5 per 15m.
|
||||
func NewLoginLimiter(max int, window time.Duration) *LoginLimiter {
|
||||
if max <= 0 {
|
||||
max = 5
|
||||
}
|
||||
if window <= 0 {
|
||||
window = 15 * time.Minute
|
||||
}
|
||||
return &LoginLimiter{attempts: make(map[string]*attemptState), max: max, window: window}
|
||||
}
|
||||
|
||||
// Allowed reports whether an attempt for key may proceed. When locked it also
|
||||
// returns the remaining lockout duration (suitable for a Retry-After header). A
|
||||
// nil limiter always allows (disabled).
|
||||
func (l *LoginLimiter) Allowed(_ context.Context, key string) (bool, time.Duration) {
|
||||
if l == nil {
|
||||
return true, 0
|
||||
}
|
||||
now := time.Now()
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
st := l.attempts[key]
|
||||
if st == nil {
|
||||
return true, 0
|
||||
}
|
||||
if now.Before(st.lockedUntil) {
|
||||
return false, st.lockedUntil.Sub(now)
|
||||
}
|
||||
// Window elapsed since the first failure: forget the history.
|
||||
if now.Sub(st.first) > l.window {
|
||||
delete(l.attempts, key)
|
||||
}
|
||||
return true, 0
|
||||
}
|
||||
|
||||
// RecordFailure registers a failed attempt for key, locking it for window once
|
||||
// max failures accumulate inside the window.
|
||||
func (l *LoginLimiter) RecordFailure(_ context.Context, key string) {
|
||||
if l == nil {
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
st := l.attempts[key]
|
||||
if st == nil || now.Sub(st.first) > l.window {
|
||||
l.attempts[key] = &attemptState{count: 1, first: now}
|
||||
return
|
||||
}
|
||||
st.count++
|
||||
if st.count >= l.max {
|
||||
st.lockedUntil = now.Add(l.window)
|
||||
}
|
||||
}
|
||||
|
||||
// Reset clears the failure history for key. Call it on a successful auth.
|
||||
func (l *LoginLimiter) Reset(_ context.Context, key string) {
|
||||
if l == nil {
|
||||
return
|
||||
}
|
||||
l.mu.Lock()
|
||||
delete(l.attempts, key)
|
||||
l.mu.Unlock()
|
||||
}
|
||||
Reference in New Issue
Block a user