feat(api): API keys + Redis rate limiting + usage stats
Add an optional API-key layer to the public read-only API. Keys grant higher per-minute rate limits and attribute usage; anonymous callers are still allowed at a lower IP-based budget. - migration 0008_api_key: api_key table (sha256 hash only, plaintext shown once) - apikey pkg: key generation + hashing - ratelimit pkg: Redis fixed-window limiter + per-key usage counters; fails open - public API middleware: X-API-Key / Bearer auth, X-RateLimit-* headers, 429+Retry-After - admin: issue/list/revoke keys + usage view (API + UI tab) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
|
||||
"github.com/baicai2026-baicai/goods/api/internal/ratelimit"
|
||||
"github.com/baicai2026-baicai/goods/api/internal/store"
|
||||
)
|
||||
|
||||
@@ -24,17 +25,35 @@ const APIVersion = "v1"
|
||||
const (
|
||||
defaultPageSize = 20
|
||||
maxPageSize = 100
|
||||
|
||||
// defaultAnonLimit is the per-minute request budget for unauthenticated
|
||||
// callers (identified by client IP) when none is configured.
|
||||
defaultAnonLimit = 60
|
||||
)
|
||||
|
||||
// Handler holds dependencies shared by the HTTP routes.
|
||||
type Handler struct {
|
||||
store *store.Store
|
||||
spa fs.FS
|
||||
store *store.Store
|
||||
spa fs.FS
|
||||
limiter *ratelimit.Limiter
|
||||
anonLimit int
|
||||
}
|
||||
|
||||
// New constructs a Handler backed by the given store. spa may be nil (JSON-only).
|
||||
// Rate limiting is disabled until WithRateLimit is called.
|
||||
func New(s *store.Store, spa fs.FS) *Handler {
|
||||
return &Handler{store: s, spa: spa}
|
||||
return &Handler{store: s, spa: spa, anonLimit: defaultAnonLimit}
|
||||
}
|
||||
|
||||
// WithRateLimit attaches a Redis-backed limiter and the anonymous per-minute
|
||||
// budget, enabling rate limiting + usage tracking on the public API routes.
|
||||
// A non-positive anonPerMin keeps the default.
|
||||
func (h *Handler) WithRateLimit(l *ratelimit.Limiter, anonPerMin int) *Handler {
|
||||
h.limiter = l
|
||||
if anonPerMin > 0 {
|
||||
h.anonLimit = anonPerMin
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
// Router builds the top-level HTTP handler with middleware and routes mounted.
|
||||
@@ -47,6 +66,7 @@ func (h *Handler) Router() http.Handler {
|
||||
r.Get("/healthz", h.Healthz)
|
||||
|
||||
r.Route("/api/"+APIVersion, func(r chi.Router) {
|
||||
r.Use(h.rateLimit)
|
||||
r.Route("/products", func(r chi.Router) {
|
||||
r.Get("/barcode/{gtin}", h.ProductByBarcode)
|
||||
r.Get("/search", h.SearchProducts)
|
||||
|
||||
Reference in New Issue
Block a user