diff options
| author | Fuwn <[email protected]> | 2026-01-20 17:30:15 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-01-20 17:30:15 -0800 |
| commit | 8b0f24948b3bfb68bf0eb284a5a984f55c9693d4 (patch) | |
| tree | 23d60be9cb58f41f2c3c1f794567617818576328 /internal | |
| parent | feat: Add dockerx command for multi-platform builds (diff) | |
| download | kaze-8b0f24948b3bfb68bf0eb284a5a984f55c9693d4.tar.xz kaze-8b0f24948b3bfb68bf0eb284a5a984f55c9693d4.zip | |
feat: Add POST /api/reload endpoint for config reload
- Add /api/reload endpoint that triggers configuration reload
- Always requires API key authentication (even if api.access is public)
- Returns error if no API keys are configured
- Update config.example.yaml with new endpoint and {group}/{name} format docs
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/server/server.go | 105 |
1 files changed, 79 insertions, 26 deletions
diff --git a/internal/server/server.go b/internal/server/server.go index b47bf79..ab9ff24 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -28,14 +28,18 @@ var templatesFS embed.FS //go:embed static/* var staticFS embed.FS +// ReloadFunc is a callback function for reloading configuration +type ReloadFunc func() error + // Server handles HTTP requests for the status page type Server struct { - config *config.Config - storage *storage.Storage - scheduler *monitor.Scheduler - logger *slog.Logger - server *http.Server - templates *template.Template + config *config.Config + storage *storage.Storage + scheduler *monitor.Scheduler + logger *slog.Logger + server *http.Server + templates *template.Template + reloadConfig ReloadFunc } // New creates a new HTTP server @@ -89,6 +93,9 @@ func New(cfg *config.Config, store *storage.Storage, sched *monitor.Scheduler, l mux.HandleFunc("GET /api/page", s.withAPIAuth(s.handleAPIPage)) } + // Config reload endpoint - always requires authentication + mux.HandleFunc("POST /api/reload", s.withStrictAuth(s.handleAPIReload)) + // Create HTTP server s.server = &http.Server{ Addr: fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port), @@ -145,39 +152,59 @@ func (s *Server) withAPIAuth(handler http.HandlerFunc) http.HandlerFunc { return case "authenticated": - // Check for API key in header or query param - apiKey := r.Header.Get("X-API-Key") - if apiKey == "" { - apiKey = r.URL.Query().Get("api_key") - } - - if apiKey == "" { + if !s.checkAPIKey(r) { w.Header().Set("WWW-Authenticate", "API-Key") s.jsonError(w, "API key required", http.StatusUnauthorized) return } - // Check if key is valid - valid := false - for _, key := range s.config.API.Keys { - if key == apiKey { - valid = true - break - } - } + // case "public" or default: allow access + } - if !valid { - s.jsonError(w, "Invalid API key", http.StatusUnauthorized) - return - } + handler(w, r) + } +} - // case "public" or default: allow access +// withStrictAuth wraps an API handler that always requires authentication, +// regardless of the api.access setting. Used for sensitive operations like config reload. +func (s *Server) withStrictAuth(handler http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Always require API key, even if api.access is "public" + if len(s.config.API.Keys) == 0 { + s.jsonError(w, "No API keys configured. Add keys to api.keys in config to use this endpoint.", http.StatusForbidden) + return + } + + if !s.checkAPIKey(r) { + w.Header().Set("WWW-Authenticate", "API-Key") + s.jsonError(w, "API key required", http.StatusUnauthorized) + return } handler(w, r) } } +// checkAPIKey validates the API key from request header or query parameter +func (s *Server) checkAPIKey(r *http.Request) bool { + apiKey := r.Header.Get("X-API-Key") + if apiKey == "" { + apiKey = r.URL.Query().Get("api_key") + } + + if apiKey == "" { + return false + } + + for _, key := range s.config.API.Keys { + if key == apiKey { + return true + } + } + + return false +} + // PageData contains data for rendering the status page type PageData struct { Site config.SiteConfig @@ -991,6 +1018,32 @@ func (s *Server) handleAPIIncidents(w http.ResponseWriter, r *http.Request) { }) } +// SetReloadFunc sets the callback function for reloading configuration +func (s *Server) SetReloadFunc(fn ReloadFunc) { + s.reloadConfig = fn +} + +// handleAPIReload triggers a configuration reload (always requires authentication) +func (s *Server) handleAPIReload(w http.ResponseWriter, r *http.Request) { + if s.reloadConfig == nil { + s.jsonError(w, "Reload function not configured", http.StatusServiceUnavailable) + return + } + + s.logger.Info("config reload triggered via API", "remote_addr", r.RemoteAddr) + + if err := s.reloadConfig(); err != nil { + s.logger.Error("config reload failed", "error", err) + s.jsonError(w, fmt.Sprintf("Reload failed: %v", err), http.StatusInternalServerError) + return + } + + s.jsonResponse(w, map[string]string{ + "status": "ok", + "message": "Configuration reloaded successfully", + }) +} + // jsonResponse writes a JSON response func (s *Server) jsonResponse(w http.ResponseWriter, data interface{}) { w.Header().Set("Content-Type", "application/json") |