middleware

package
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 11, 2026 License: AGPL-3.0 Imports: 20 Imported by: 0

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

Constants

This section is empty.

Variables

View Source
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 IssueJWT

func IssueJWT(secret, userID, email, name string, isAdmin bool, ttl time.Duration) (string, error)

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

type AdminChecker func(ctx context.Context, userID string) bool

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 Claims

type Claims struct {
	UserID  string `json:"uid"`
	Email   string `json:"email"`
	Name    string `json:"name"`
	IsAdmin bool   `json:"adm,omitempty"`
	jwt.RegisteredClaims
}

type RateLimitBackend

type RateLimitBackend interface {
	Allow(key string, rate int, window time.Duration) bool
}

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

type RedisResult interface {
	Int64() (int64, error)
}

RedisResult is the minimal result interface.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL