Documentation
¶
Overview ¶
Rate limiting middleware with pluggable backends. Supports in-memory (single instance) and Redis (multi-instance) backends. Set REDIS_URL to enable Redis backend; falls back to in-memory if not set.
Index ¶
- Variables
- func APIKeyAuth(s *store.Store) gin.HandlerFunc
- func AdminOnly() gin.HandlerFunc
- func Idempotency(s *store.Store) gin.HandlerFunc
- func IssueJWT(secret, userID, email, name string, isAdmin bool, ttl time.Duration) (string, error)
- func LicenseBruteForceGuard(bf *BruteForceProtection) gin.HandlerFunc
- func PrometheusMetrics() gin.HandlerFunc
- func RateLimit(rate int, window time.Duration) gin.HandlerFunc
- func RateLimitByIP(rate int, window time.Duration) gin.HandlerFunc
- func RequestID() gin.HandlerFunc
- func RequireScope(allowed ...string) gin.HandlerFunc
- func SessionAuth(secret string, adminCheck ...AdminChecker) gin.HandlerFunc
- func SessionOrAPIKey(secret string, db *store.Store, adminCheck AdminChecker) gin.HandlerFunc
- func SetRateLimitBackend(b RateLimitBackend)
- type AdminChecker
- type BruteForceProtection
- type Claims
- type RateLimitBackend
- type RedisClient
- type RedisResult
Constants ¶
This section is empty.
Variables ¶
var ( // Business metrics LicenseActivations = promauto.NewCounterVec( prometheus.CounterOpts{ Name: "keygate_license_activations_total", Help: "Total license activations", }, []string{"product_id", "status"}, ) LicenseVerifications = promauto.NewCounterVec( prometheus.CounterOpts{ Name: "keygate_license_verifications_total", Help: "Total license verifications", }, []string{"product_id", "result"}, ) WebhookDeliveries = promauto.NewCounterVec( prometheus.CounterOpts{ Name: "keygate_webhook_deliveries_total", Help: "Total webhook delivery attempts", }, []string{"status"}, ) EmailDeliveries = promauto.NewCounterVec( prometheus.CounterOpts{ Name: "keygate_email_deliveries_total", Help: "Total email delivery attempts", }, []string{"status"}, ) ActiveLicenses = promauto.NewGauge( prometheus.GaugeOpts{ Name: "keygate_active_licenses", Help: "Current number of active licenses", }, ) BruteForceBlocks = promauto.NewCounter( prometheus.CounterOpts{ Name: "keygate_brute_force_blocks_total", Help: "Total brute force lockouts triggered", }, ) )
Functions ¶
func APIKeyAuth ¶
func APIKeyAuth(s *store.Store) gin.HandlerFunc
APIKeyAuth validates the Bearer token as an API key and injects the product context.
func AdminOnly ¶
func AdminOnly() gin.HandlerFunc
func Idempotency ¶ added in v0.1.1
func Idempotency(s *store.Store) gin.HandlerFunc
Idempotency wraps a handler so retries with the same `Idempotency-Key` header return the original response without re-executing the handler. Stripe / Mailgun / GitHub conventions:
- Header is OPTIONAL. Without it, the handler runs normally — no dedup, no cache.
- Same key + same body → cached response (status + body).
- Same key + different body → 422 IDEMPOTENCY_KEY_CONFLICT (the client is reusing the key for a different operation, which is a programming error).
- Concurrent retries of the same key → 409 IDEMPOTENCY_IN_FLIGHT while the first one is still running. Client retries with backoff.
- 24h TTL — set by the table default.
Apply selectively to write endpoints where dedup matters (`/license/activate`, `/license/usage`, `/license/floating/checkout`). Read endpoints don't need it; brief caches like `/license/verify` don't either since they're already idempotent by definition.
Safety notes:
- The body is read into memory and re-injected so the handler can bind it again. Capped at 256 KiB; oversized requests skip the idempotency layer entirely (and the handler validates body limits itself).
- 5xx responses are NOT cached — they're transient, retries SHOULD re-execute. We delete the slot so the retry can claim it fresh.
- 4xx responses ARE cached — they represent a deterministic outcome (validation failure, license-not-found, etc.) so retrying just gets the same answer.
func LicenseBruteForceGuard ¶
func LicenseBruteForceGuard(bf *BruteForceProtection) gin.HandlerFunc
LicenseBruteForceGuard is a middleware that checks brute-force state before processing.
func PrometheusMetrics ¶
func PrometheusMetrics() gin.HandlerFunc
PrometheusMetrics is a Gin middleware that records HTTP metrics.
func RateLimit ¶
func RateLimit(rate int, window time.Duration) gin.HandlerFunc
RateLimit creates a rate limiting middleware using the configured backend.
func RateLimitByIP ¶
func RateLimitByIP(rate int, window time.Duration) gin.HandlerFunc
RateLimitByIP creates a rate limiter keyed by IP only.
func RequestID ¶
func RequestID() gin.HandlerFunc
RequestID adds a unique request ID to every request for tracing.
func RequireScope ¶
func RequireScope(allowed ...string) gin.HandlerFunc
RequireScope is the single source of truth for "may this caller touch this route?" Behavior:
Session-authenticated admin → always pass. An interactive admin login has all powers; restricting it via API-style scopes would break the dashboard.
API key with `admin` scope → always pass. admin is the wildcard.
API key with at least one of the listed `allowed` scopes → pass.
Anything else (no auth, unknown scope, expired session, etc.) → 403 INSUFFICIENT_SCOPE.
Callers list every scope that should reach this route. Passing only `model.ScopeAdmin` means "admin-only" (most routes). Passing `model.ScopeAdmin, model.ScopeLicensesWrite` means a license-write key is also allowed, in addition to admins.
func SessionAuth ¶
func SessionAuth(secret string, adminCheck ...AdminChecker) gin.HandlerFunc
SessionAuth validates a JWT from the Authorization header or session cookie. Admin status is checked at request time (from DB, not JWT claims) for security — this ensures role changes take effect immediately without waiting for JWT expiry.
func SessionOrAPIKey ¶ added in v0.1.1
func SessionOrAPIKey(secret string, db *store.Store, adminCheck AdminChecker) gin.HandlerFunc
SessionOrAPIKey accepts either:
`Authorization: Bearer kg_live_<token>` — programmatic credential (api_keys row). Whether the key may access a specific route is decided by the downstream RequireScope middleware, NOT here.
Session cookie OR `Authorization: Bearer <JWT>` — interactive admin login (same logic as SessionAuth).
We deliberately don't enforce `admin` scope at this layer anymore. Doing so blocks restricted-scope keys (e.g. licenses:write) from ever reaching the route that they DO have permission for. Scope enforcement is now per-route via RequireScope.
auth_type ("session" or "api_key") is set so RequireScope and audit code can tell the two paths apart without re-examining the Authorization header.
func SetRateLimitBackend ¶
func SetRateLimitBackend(b RateLimitBackend)
SetRateLimitBackend sets the global rate limit backend (call once at startup).
Types ¶
type AdminChecker ¶
AdminChecker checks if a user has admin privileges by user ID. Injected at startup — queries the database for the user's role.
type BruteForceProtection ¶
type BruteForceProtection struct {
// contains filtered or unexported fields
}
BruteForceProtection tracks failed authentication attempts and blocks IPs/keys that exceed the threshold. Uses exponential backoff.
func NewBruteForceProtection ¶
func NewBruteForceProtection(maxFails int, lockout, maxLockout, window time.Duration) *BruteForceProtection
func (*BruteForceProtection) IsBlocked ¶
func (bf *BruteForceProtection) IsBlocked(key string) (bool, time.Duration)
IsBlocked checks if a key is currently locked out.
func (*BruteForceProtection) RecordFailure ¶
func (bf *BruteForceProtection) RecordFailure(key string)
RecordFailure records a failed attempt for a key (IP or license key).
func (*BruteForceProtection) RecordSuccess ¶
func (bf *BruteForceProtection) RecordSuccess(key string)
RecordSuccess clears the failure record for a key.
type RateLimitBackend ¶
RateLimitBackend abstracts the rate limiting storage.
func NewMemoryBackend ¶
func NewMemoryBackend() RateLimitBackend
func NewRedisBackend ¶
func NewRedisBackend(client RedisClient) RateLimitBackend
NewRedisBackend creates a Redis-backed rate limiter.
type RedisClient ¶
type RedisClient interface {
Eval(ctx context.Context, script string, keys []string, args ...interface{}) RedisResult
}
RedisClient is a minimal interface for Redis operations needed by rate limiting. Compatible with github.com/redis/go-redis/v9.
type RedisResult ¶
RedisResult is the minimal result interface.