170 lines
5.7 KiB
Go
170 lines
5.7 KiB
Go
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"})
|
|
}
|