All checks were successful
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com> Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
53 lines
1.3 KiB
Go
53 lines
1.3 KiB
Go
package middleware
|
|
|
|
import (
|
|
"log"
|
|
"net/http"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"armature/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()
|
|
}
|
|
}
|