Files
beacon-server/internal/api/handlers/admin.go
T

100 lines
3.5 KiB
Go

// Copyright 2026 Beacon Contributors
// SPDX-License-Identifier: AGPL-3.0-or-later
package handlers
import (
"encoding/json"
"errors"
"io"
"mime"
"net/http"
"github.com/MeshCore-Beacon/beacon-server/internal/api"
mw "github.com/MeshCore-Beacon/beacon-server/internal/api/middleware"
"github.com/go-chi/chi/v5"
)
// AdminRouter mounts operator endpoints. Its caller must wrap the
// entire subrouter with BearerAuth, including unknown paths and methods.
func AdminRouter(runtime *mw.RuntimeConfig, routes map[string]http.Handler) http.Handler {
r := chi.NewRouter()
r.Get("/config", getAdminConfig(runtime))
r.Put("/config", updateAdminConfig(runtime))
for path, handler := range routes {
r.Mount(path, handler)
}
return r
}
// getAdminConfig godoc
//
// @Summary Inspect selected running configuration
// @Description Returns current CORS options, auth configuration status and configured broker count. Credential fields and other configuration are excluded. Runtime origin updates are lost on restart.
// @Tags Admin
// @Produce json
// @Security AdminKey
// @Success 200 {object} api.AdminConfig
// @Failure 401 {object} map[string]APIError
// @Failure 503 {object} map[string]APIError
// @Router /admin/config [get]
func getAdminConfig(runtime *mw.RuntimeConfig) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
respond(w, http.StatusOK, runtime.Snapshot())
}
}
// updateAdminConfig godoc
// @Summary Update runtime CORS origins
// @Description Replaces only cors.allowed_origins immediately. Updates are serialized; concurrent valid updates are applied one at a time. Already-running requests may finish with the previous policy. Nothing is persisted; restart reloads saved configuration.
// @Tags Admin
// @Accept json
// @Produce json
// @Security AdminKey
// @Param config body api.UpdateAdminConfigRequest true "1-32 ASCII HTTP(S) origins, at most 512 bytes each; one hostname wildcard is supported, or a sole *. Empty/null lists and unsupported fields are rejected. Body at most 16 KiB."
// @Success 200 {object} api.UpdateAdminConfigResponse
// @Failure 400,401,413,415,503 {object} map[string]APIError
// @Router /admin/config [put]
func updateAdminConfig(runtime *mw.RuntimeConfig) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
media, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil || media != "application/json" {
respondError(w, 415, "Content-Type must be application/json")
return
}
r.Body = http.MaxBytesReader(w, r.Body, 16*1024)
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields()
var input api.UpdateAdminConfigRequest
err = decoder.Decode(&input)
if err == nil {
var extra any
err = decoder.Decode(&extra)
if errors.Is(err, io.EOF) {
err = nil
} else if err == nil {
err = errors.New("multiple JSON values")
}
}
if err != nil {
var sizeError *http.MaxBytesError
if errors.As(err, &sizeError) {
respondError(w, 413, "request body exceeds 16 KiB")
} else {
respondError(w, 400, "invalid configuration JSON")
}
return
}
if input.CORS == nil || input.CORS.AllowedOrigins == nil {
respondError(w, 400, "cors.allowed_origins is required")
return
}
config, err := runtime.UpdateOrigins(*input.CORS.AllowedOrigins)
if err != nil {
respondError(w, 400, mw.ErrInvalidOrigins.Error())
return
}
respond(w, 200, api.UpdateAdminConfigResponse{Config: config, Persisted: false, RequiresRestart: false})
}
}