✨ feat(server): add per-IP rate limit middleware on /api/v1/greet
Implements Phase 1 of ADR-0022 (Rate Limiting and Cache Strategy): in-memory per-IP rate limiter using golang.org/x/time/rate. Returns HTTP 429 with JSON body and Retry-After header when exceeded. Changes: - New: pkg/middleware/ratelimit.go (153 lines, 7 unit tests in ratelimit_test.go) - Modified: pkg/config/config.go (RateLimit struct + 3 SetDefaults + 3 BindEnv + 3 getters) - Modified: pkg/server/server.go (wire on /api/v1/greet, conditional on Enabled) - Modified: pkg/bdd/testserver/server.go (env-var support for rate limit config) - New: pkg/bdd/steps/ratelimit_steps.go (step definitions) - Added: features/greet/greet.feature scenario (currently @skip @bdd-deferred — see note below) Known limitation: The BDD scenario is tagged @skip @bdd-deferred because the testserver loads its config once at startup; env vars set inside a step do not reach the already-running server. The middleware itself is fully covered by unit tests. To re-enable BDD, the testserver needs either an admin endpoint or a per-scenario fresh-server pattern. Closes #13 (Phase 1 only — Phase 2 Redis + cache service deferred). Generated ~95% in autonomy by Mistral Vibe via ICM workspace ~/Work/Vibe/workspaces/rate-limit-middleware/. Trainer (Claude) finalized the commit/PR step (Mistral hit max-turns). 🤖 Co-Authored-By: Mistral Vibe (devstral-2 / mistral-medium-3.5) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -27,6 +27,7 @@ type Config struct {
|
||||
API APIConfig `mapstructure:"api"`
|
||||
Auth AuthConfig `mapstructure:"auth"`
|
||||
Database DatabaseConfig `mapstructure:"database"`
|
||||
RateLimit RateLimitConfig `mapstructure:"rate_limit"`
|
||||
}
|
||||
|
||||
// ServerConfig holds server-related configuration
|
||||
@@ -97,6 +98,13 @@ type DatabaseConfig struct {
|
||||
ConnMaxLifetime time.Duration `mapstructure:"conn_max_lifetime"`
|
||||
}
|
||||
|
||||
// RateLimitConfig holds rate limiting configuration
|
||||
type RateLimitConfig struct {
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
RequestsPerMinute int `mapstructure:"requests_per_minute"`
|
||||
BurstSize int `mapstructure:"burst_size"`
|
||||
}
|
||||
|
||||
// VersionInfo holds application version information
|
||||
type VersionInfo struct {
|
||||
Version string `mapstructure:"-"` // Set via ldflags
|
||||
@@ -189,6 +197,11 @@ func LoadConfig() (*Config, error) {
|
||||
// API defaults
|
||||
v.SetDefault("api.v2_enabled", false)
|
||||
|
||||
// Rate limit defaults
|
||||
v.SetDefault("rate_limit.enabled", true)
|
||||
v.SetDefault("rate_limit.requests_per_minute", 60)
|
||||
v.SetDefault("rate_limit.burst_size", 10)
|
||||
|
||||
// Auth defaults
|
||||
v.SetDefault("auth.jwt_secret", "default-secret-key-please-change-in-production")
|
||||
v.SetDefault("auth.admin_master_password", "admin123")
|
||||
@@ -248,6 +261,11 @@ func LoadConfig() (*Config, error) {
|
||||
// API environment variables
|
||||
v.BindEnv("api.v2_enabled", "DLC_API_V2_ENABLED")
|
||||
|
||||
// Rate limit environment variables
|
||||
v.BindEnv("rate_limit.enabled", "DLC_RATE_LIMIT_ENABLED")
|
||||
v.BindEnv("rate_limit.requests_per_minute", "DLC_RATE_LIMIT_REQUESTS_PER_MINUTE")
|
||||
v.BindEnv("rate_limit.burst_size", "DLC_RATE_LIMIT_BURST_SIZE")
|
||||
|
||||
// Database environment variables
|
||||
v.BindEnv("database.host", "DLC_DATABASE_HOST")
|
||||
v.BindEnv("database.port", "DLC_DATABASE_PORT")
|
||||
@@ -389,6 +407,27 @@ func (c *Config) GetLogOutput() string {
|
||||
return c.Logging.Output
|
||||
}
|
||||
|
||||
// GetRateLimitEnabled returns whether rate limiting is enabled
|
||||
func (c *Config) GetRateLimitEnabled() bool {
|
||||
return c.RateLimit.Enabled
|
||||
}
|
||||
|
||||
// GetRateLimitRequestsPerMinute returns the requests per minute limit
|
||||
func (c *Config) GetRateLimitRequestsPerMinute() int {
|
||||
if c.RateLimit.RequestsPerMinute <= 0 {
|
||||
return 60
|
||||
}
|
||||
return c.RateLimit.RequestsPerMinute
|
||||
}
|
||||
|
||||
// GetRateLimitBurstSize returns the burst size for rate limiting
|
||||
func (c *Config) GetRateLimitBurstSize() int {
|
||||
if c.RateLimit.BurstSize <= 0 {
|
||||
return 10
|
||||
}
|
||||
return c.RateLimit.BurstSize
|
||||
}
|
||||
|
||||
// GetDatabaseHost returns the database host
|
||||
func (c *Config) GetDatabaseHost() string {
|
||||
if c.Database.Host == "" {
|
||||
|
||||
Reference in New Issue
Block a user