package middleware import ( "net/http" "sync" "time" "github.com/prabhupadasays/spsays-bff/pkg/response" ) // RateLimiter is a simple in-memory token bucket rate limiter. // In production, replace with DragonflyDB/Redis-based limiter. type RateLimiter struct { mu sync.Mutex visitors map[string]*visitor rate int // tokens per interval interval time.Duration // refill interval } type visitor struct { tokens int lastSeen time.Time } // NewRateLimiter creates a new in-memory rate limiter. func NewRateLimiter(rate int, interval time.Duration) *RateLimiter { rl := &RateLimiter{ visitors: make(map[string]*visitor), rate: rate, interval: interval, } // Clean up stale visitors every minute go rl.cleanup() return rl } // Limit returns a middleware that rate-limits requests by IP. func (rl *RateLimiter) Limit(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ip := r.RemoteAddr rl.mu.Lock() v, exists := rl.visitors[ip] if !exists { v = &visitor{tokens: rl.rate, lastSeen: time.Now()} rl.visitors[ip] = v } // Refill tokens elapsed := time.Since(v.lastSeen) refill := int(elapsed / rl.interval) if refill > 0 { v.tokens = min(v.tokens+refill*rl.rate, rl.rate) v.lastSeen = time.Now() } if v.tokens <= 0 { rl.mu.Unlock() response.Error(w, http.StatusTooManyRequests, "rate limit exceeded") return } v.tokens-- rl.mu.Unlock() next.ServeHTTP(w, r) }) } func (rl *RateLimiter) cleanup() { for { time.Sleep(time.Minute) rl.mu.Lock() for ip, v := range rl.visitors { if time.Since(v.lastSeen) > 3*time.Minute { delete(rl.visitors, ip) } } rl.mu.Unlock() } }