aboutsummaryrefslogtreecommitdiff
path: root/internal/server/server.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/server/server.go')
-rw-r--r--internal/server/server.go105
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")