package handlers import ( "encoding/json" "net/http" "github.com/Silo-Server/silo-server/internal/cache" "github.com/Silo-Server/silo-server/internal/ratelimit" ) // RateLimitHandler handles rate limit config admin endpoints. type RateLimitHandler struct { store ratelimit.SettingsStore mw *ratelimit.Middleware eventBus cache.EventBus } // NewRateLimitHandler creates a new RateLimitHandler. func NewRateLimitHandler(store ratelimit.SettingsStore, mw *ratelimit.Middleware, eventBus cache.EventBus) *RateLimitHandler { return &RateLimitHandler{store: store, mw: mw, eventBus: eventBus} } type rateLimitConfigResponse struct { Enabled bool `json:"enabled"` Backend string `json:"backend"` GlobalReqPerSecond float64 `json:"global_requests_per_second"` Tiers map[string]tierConfigResponse `json:"tiers"` IPReqPerSecond float64 `json:"ip_requests_per_second"` IPReqPerMinute float64 `json:"ip_requests_per_minute"` IPBurst int `json:"ip_burst"` AuthEndpoints map[string]authEndpointConfigResponse `json:"auth_endpoints"` } type tierConfigResponse struct { RequestsPerSecond float64 `json:"requests_per_second"` RequestsPerMinute float64 `json:"requests_per_minute"` Burst int `json:"burst"` } type authEndpointConfigResponse struct { RequestsPerMinute float64 `json:"requests_per_minute"` Burst int `json:"burst"` } // HandleGetConfig handles GET /admin/rate-limits/config. func (h *RateLimitHandler) HandleGetConfig(w http.ResponseWriter, r *http.Request) { cfg, err := ratelimit.LoadConfig(r.Context(), h.store) if err != nil { writeError(w, http.StatusInternalServerError, "internal_error", "Failed to load rate limit config") return } backend, _ := h.store.Get(r.Context(), "ratelimit.backend") if backend == "" { backend = "memory" } resp := rateLimitConfigResponse{ Enabled: cfg.Enabled, Backend: backend, GlobalReqPerSecond: cfg.GlobalReqPerSecond, Tiers: make(map[string]tierConfigResponse), IPReqPerSecond: cfg.IPReqPerSecond, IPReqPerMinute: cfg.IPReqPerMinute, IPBurst: cfg.IPBurst, AuthEndpoints: make(map[string]authEndpointConfigResponse), } for name, tier := range cfg.Tiers { resp.Tiers[name] = tierConfigResponse{ RequestsPerSecond: tier.RequestsPerSecond, RequestsPerMinute: tier.RequestsPerMinute, Burst: tier.Burst, } } for name, ep := range cfg.AuthEndpoints { resp.AuthEndpoints[name] = authEndpointConfigResponse{ RequestsPerMinute: ep.RequestsPerMinute, Burst: ep.Burst, } } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(resp) } // HandleUpdateConfig handles PUT /admin/rate-limits/config. func (h *RateLimitHandler) HandleUpdateConfig(w http.ResponseWriter, r *http.Request) { var req rateLimitConfigResponse if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "bad_request", "Invalid request body") return } // Load existing config so we can preserve fields not included in the request. existing, err := ratelimit.LoadConfig(r.Context(), h.store) if err != nil { writeError(w, http.StatusInternalServerError, "internal_error", "Failed to load existing config") return } cfg := ratelimit.Config{ Enabled: req.Enabled, GlobalReqPerSecond: req.GlobalReqPerSecond, Tiers: make(map[string]ratelimit.TierConfig), IPReqPerSecond: req.IPReqPerSecond, IPReqPerMinute: req.IPReqPerMinute, IPBurst: req.IPBurst, AuthEndpoints: make(map[string]ratelimit.AuthEndpointConfig), } // Preserve IP settings if not provided (zero values mean omitted from request). if cfg.IPReqPerSecond == 0 { cfg.IPReqPerSecond = existing.IPReqPerSecond } if cfg.IPReqPerMinute == 0 { cfg.IPReqPerMinute = existing.IPReqPerMinute } if cfg.IPBurst == 0 { cfg.IPBurst = existing.IPBurst } for name, tier := range req.Tiers { cfg.Tiers[name] = ratelimit.TierConfig{ RequestsPerSecond: tier.RequestsPerSecond, RequestsPerMinute: tier.RequestsPerMinute, Burst: tier.Burst, } } // Preserve existing auth endpoint settings if not provided in request. for name, ep := range existing.AuthEndpoints { cfg.AuthEndpoints[name] = ep } for name, ep := range req.AuthEndpoints { cfg.AuthEndpoints[name] = ratelimit.AuthEndpointConfig{ RequestsPerMinute: ep.RequestsPerMinute, Burst: ep.Burst, } } if err := ratelimit.SaveConfig(r.Context(), h.store, cfg); err != nil { writeError(w, http.StatusInternalServerError, "internal_error", "Failed to save rate limit config") return } // Save backend setting (infrastructure-level, requires restart) if req.Backend == "memory" || req.Backend == "redis" { if err := h.store.Set(r.Context(), "ratelimit.backend", req.Backend); err != nil { writeError(w, http.StatusInternalServerError, "internal_error", "Failed to save backend setting") return } } // Hot-reload: apply new config immediately on this instance if err := h.mw.Reload(r.Context()); err != nil { writeError(w, http.StatusInternalServerError, "internal_error", "Config saved but reload failed") return } // Publish for multi-instance reload (if EventBus is available/backed by Redis) if h.eventBus != nil { _ = h.eventBus.Publish(r.Context(), cache.ChannelAdmin, cache.Event{ Type: cache.EventSettingsChanged, }) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"status": "ok"}) }