package middleware import ( "log" "net/http" "github.com/gin-gonic/gin" "switchboard-core/store" ) // RateLimiter implements per-IP rate limiting backed by a shared store. // // Fail-open: if the store is unavailable (PG down), requests are allowed. // Auth endpoints have their own protections (bcrypt, lockout); blocking // legitimate logins due to a rate-limit DB failure is worse than a brief burst. type RateLimiter struct { store store.RateLimitStore rate float64 burst int } // NewRateLimiter creates a rate limiter backed by the given store. // rate is requests per second, burst is max requests per window. func NewRateLimiter(s store.RateLimitStore, rate float64, burst int) *RateLimiter { return &RateLimiter{store: s, rate: rate, burst: burst} } // Limit returns a Gin middleware that enforces the rate limit. func (rl *RateLimiter) Limit() gin.HandlerFunc { return func(c *gin.Context) { key := "auth:" + c.ClientIP() allowed, err := rl.store.Allow(c.Request.Context(), key, rl.rate, rl.burst) if err != nil { // Fail open — don't block auth on rate limit DB failure. log.Printf("[ratelimit] store error (allowing request): %v", err) c.Next() return } if !allowed { c.Header("Retry-After", "1") c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{ "error": "rate limit exceeded", }) return } c.Next() } }