Files
CU_Points/backend/internal/middleware/rate_limit.go
T
emilandClaude Sonnet 4.6 50b3c4198a feat: initial commit — backend API + student cabinet frontend
- Go backend: auth (JWT), points earn/spend, QR token generation,
  partners, admin grant/stats endpoints with chi router
- Next.js 14 frontend: login, student dashboard, transaction history,
  QR display, partners list
- PostgreSQL migrations (4 tables), Redis cache, Docker Compose
- CORS middleware, role-based route protection, Zustand auth store

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 10:03:27 +03:00

48 lines
1.6 KiB
Go

package middleware
import (
"context"
"net/http"
"time"
"github.com/redis/go-redis/v9"
)
// SpendRateLimit returns middleware that limits requests to maxPerMinute per
// authenticated partner (identified by user_id in context). It must be placed
// after Auth + RequireRole("partner") so that UserIDFromContext is populated.
//
// Implementation: Redis INCR + EXPIRE sliding-window counter.
// Key: rate_limit:spend:<partner_id> — expires after 1 minute.
// On Redis failure the middleware fails open (lets the request through) so that
// a Redis outage does not take down point transactions.
func SpendRateLimit(rdb *redis.Client, maxPerMinute int) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
partnerID := UserIDFromContext(r.Context())
key := "rate_limit:spend:" + partnerID
// Use a short-lived context for Redis so a slow Redis doesn't stall the request.
rCtx, cancel := context.WithTimeout(r.Context(), 200*time.Millisecond)
defer cancel()
count, err := rdb.Incr(rCtx, key).Result()
if err != nil {
// Fail open: Redis unavailable should not block transactions.
next.ServeHTTP(w, r)
return
}
// Set the expiry only on the first increment so the window resets each minute.
if count == 1 {
rdb.Expire(rCtx, key, time.Minute) //nolint:errcheck
}
if count > int64(maxPerMinute) {
http.Error(w, `{"error":"rate limit exceeded, max 10 requests per minute"}`, http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}
}