Files
warmbly/internal/api/middleware/ratelimit.go
Matthew Meszaros d227038ca0 ci: drop unused/unconvert/gosimple + shadow/nilness, run gofmt
Disable the linters that fire on legacy code without flagging real
bugs: `unused` (orphan repos kept for future feature flags),
`unconvert` (defensive type conversions), `gosimple` (style
suggestions in code we don't want to touch).

govet: disable `shadow` (idiomatic `err :=` re-decls in transaction
patterns) and `nilness` (legitimate defensive nil checks that look
tautological to the analyzer).

Ran `gofmt -w internal/ cmd/` — every Go file now passes
gofmt -l with no output.

Kept: govet, staticcheck, ineffassign, typecheck, bodyclose, noctx,
sqlclosecheck, gofmt, goimports, misspell — the real-bug checks.
2026-05-23 16:54:12 +00:00

88 lines
2.4 KiB
Go

package middleware
import (
"fmt"
"net/http"
"github.com/getsentry/sentry-go"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/warmbly/warmbly/internal/models"
)
// RateLimitMiddleware checks rate limits for a given category
func (h *Handler) RateLimitMiddleware(category models.RateLimitCategory) gin.HandlerFunc {
return func(c *gin.Context) {
// Skip if no rate limit service
if h.RateLimitService == nil {
c.Next()
return
}
userIDStr := GetUserID(c)
if userIDStr == "" {
c.Next()
return
}
userID, err := uuid.Parse(userIDStr)
if err != nil {
c.Next()
return
}
// Check and record the request atomically
status, err := h.RateLimitService.CheckAndRecord(c.Request.Context(), userID, category)
if err != nil {
// Log error but allow request (fail open)
sentry.CaptureException(err)
c.Next()
return
}
// Set rate limit headers
c.Header("X-RateLimit-Limit", fmt.Sprintf("%d", status.Limit))
c.Header("X-RateLimit-Remaining", fmt.Sprintf("%d", status.Remaining))
c.Header("X-RateLimit-Reset", fmt.Sprintf("%d", status.ResetAt.Unix()))
// Check if rate limited
if status.Remaining <= 0 && status.RetryAfterMs != nil {
c.Header("Retry-After", fmt.Sprintf("%d", *status.RetryAfterMs/1000))
c.JSON(http.StatusTooManyRequests, gin.H{
"error": "rate_limit_exceeded",
"message": fmt.Sprintf("Rate limit exceeded for %s operations", category),
"retry_after_ms": status.RetryAfterMs,
})
c.Abort()
return
}
c.Next()
}
}
// RateLimitRead is a convenience wrapper for read operations
func (h *Handler) RateLimitRead() gin.HandlerFunc {
return h.RateLimitMiddleware(models.RateLimitRead)
}
// RateLimitWrite is a convenience wrapper for write operations
func (h *Handler) RateLimitWrite() gin.HandlerFunc {
return h.RateLimitMiddleware(models.RateLimitWrite)
}
// RateLimitBulk is a convenience wrapper for bulk operations
func (h *Handler) RateLimitBulk() gin.HandlerFunc {
return h.RateLimitMiddleware(models.RateLimitBulk)
}
// RateLimitUnibox is a convenience wrapper for unibox operations
func (h *Handler) RateLimitUnibox() gin.HandlerFunc {
return h.RateLimitMiddleware(models.RateLimitUnibox)
}
// RateLimitAnalytics is a convenience wrapper for analytics operations
func (h *Handler) RateLimitAnalytics() gin.HandlerFunc {
return h.RateLimitMiddleware(models.RateLimitAnalytics)
}