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>
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user