saveinmed/backend/internal/http/middleware/ratelimit.go
Gabbriiel 90467db1ec refactor: substitui backend Medusa por backend Go e corrige testes do marketplace
- Remove backend Medusa.js (TypeScript) e substitui pelo backend Go (saveinmed-performance-core)
- Corrige testes auth.test.ts: alinha paths de API (v1/ sem barra inicial) e campo access_token
- Corrige GroupedProductCard.test.tsx: ajusta distância formatada (toFixed) e troca userEvent por fireEvent com fakeTimers
- Corrige AuthContext.test.tsx: usa vi.hoisted() para mocks e corrige parênteses no waitFor

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 04:56:37 -06:00

103 lines
2.2 KiB
Go

package middleware
import (
"net/http"
"sync"
"time"
)
// RateLimiter provides token bucket rate limiting per IP.
type RateLimiter struct {
buckets map[string]*bucket
mu sync.Mutex
rate int // tokens per interval
burst int // max tokens
per time.Duration // refill interval
}
type bucket struct {
tokens int
lastFill time.Time
}
// NewRateLimiter creates a rate limiter.
// Default: 100 requests per minute per IP.
func NewRateLimiter(rate, burst int, per time.Duration) *RateLimiter {
return &RateLimiter{
buckets: make(map[string]*bucket),
rate: rate,
burst: burst,
per: per,
}
}
// DefaultRateLimiter returns a limiter with sensible defaults.
func DefaultRateLimiter() *RateLimiter {
return NewRateLimiter(100, 100, time.Minute)
}
// Middleware returns an HTTP middleware that enforces rate limiting.
func (rl *RateLimiter) Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ip := getClientIP(r)
if !rl.allow(ip) {
w.Header().Set("Retry-After", "60")
w.WriteHeader(http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}
func (rl *RateLimiter) allow(key string) bool {
rl.mu.Lock()
defer rl.mu.Unlock()
b, exists := rl.buckets[key]
if !exists {
b = &bucket{tokens: rl.burst, lastFill: time.Now()}
rl.buckets[key] = b
}
// Refill tokens based on elapsed time
now := time.Now()
elapsed := now.Sub(b.lastFill)
tokensToAdd := int(elapsed/rl.per) * rl.rate
if tokensToAdd > 0 {
b.tokens = min(b.tokens+tokensToAdd, rl.burst)
b.lastFill = now
}
if b.tokens > 0 {
b.tokens--
return true
}
return false
}
func getClientIP(r *http.Request) string {
// Check common proxy headers
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
return xff
}
if xri := r.Header.Get("X-Real-IP"); xri != "" {
return xri
}
return r.RemoteAddr
}
// Cleanup removes stale buckets (call periodically)
func (rl *RateLimiter) Cleanup(maxAge time.Duration) {
rl.mu.Lock()
defer rl.mu.Unlock()
now := time.Now()
for key, b := range rl.buckets {
if now.Sub(b.lastFill) > maxAge {
delete(rl.buckets, key)
}
}
}