Files
beacon-server/internal/api/handlers/scopes.go
T
MrAlders0n 5ccb1a2ce3 fix(api): warn on untrusted proxies and tighten config validation
Warn at startup when limits are on without trusted_proxies, and once when a
request carries forwarding headers Beacon ignores. Reject short or whitespace
admin keys and negative durations. Key default-window stats by hour, return
500 for scope lookup failures, and validate IATA path params.
2026-10-02 16:53:32 -04:00

101 lines
3.4 KiB
Go

// Copyright 2026 Beacon Contributors
// SPDX-License-Identifier: AGPL-3.0-or-later
package handlers
import (
"errors"
"net/http"
"github.com/MeshCore-Beacon/beacon-server/internal/api"
"github.com/go-chi/chi/v5"
"github.com/jackc/pgx/v5"
)
// ScopeMembership lists scopes by configured region and imported catalogue membership.
type ScopeMembership interface {
NamesForIATAs(iatas []string) []string // sorted
}
func scopeNamesFor(scopes ScopeMembership, iatas []string) []string {
if scopes == nil {
return []string{}
}
return scopes.NamesForIATAs(iatas)
}
// ScopesRouter mounts all /scopes routes onto a subrouter.
//
// GET /scopes → listScopes
// GET /scopes/{name} → getScope
func ScopesRouter(reader api.Reader, scopes ScopeMembership) http.Handler {
r := chi.NewRouter()
r.Get("/", listScopes(reader, scopes))
r.Get("/{name}", getScope(reader))
return r
}
// listScopes godoc
//
// @Summary List transport scopes
// @Description Without filters, lists every stored scope name, including imported names retained after an importer is disabled or a source is removed. With IATA or region filters, lists only manual scopes configured for a matching region and imported scopes whose current MeshMapper catalogue includes a matching IATA; observed traffic does not add scopes.
// @Tags Scopes
// @Produce json
// @Param iatas query string false "Filter by IATA code(s), comma-separated"
// @Param region query string false "Filter by region slug"
// @Param regionId query int false "Filter by region ID"
// @Success 200 {array} string
// @Failure 400 {object} handlers.APIError
// @Failure 500 {object} handlers.APIError
// @Router /scopes [get]
func listScopes(reader api.Reader, scopes ScopeMembership) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
iatas := parseIATAs(r)
regionID, region := r.URL.Query().Get("regionId"), r.URL.Query().Get("region")
if regionID != "" || region != "" {
regionIATAs, err := resolveRegionIATAs(r.Context(), regionID, region, reader)
if err != nil {
respondError(w, http.StatusBadRequest, err.Error())
return
}
iatas = append(iatas, regionIATAs...)
} else if len(iatas) == 0 {
names, err := reader.GetScopeNames(r.Context())
if err != nil {
respondError(w, http.StatusInternalServerError, "internal server error")
return
}
respond(w, http.StatusOK, names)
return
}
respond(w, http.StatusOK, scopeNamesFor(scopes, iatas))
}
}
// getScope godoc
//
// @Summary Get scope detail by name
// @Description packetCount sums packets per hour across every retained rollup hour; iatas are the IATAs that heard the scope's packets. Observer and node counts are current memberships.
// @Tags Scopes
// @Produce json
// @Param name path string true "Scope name e.g. %23bc (URL-encoded #bc)"
// @Success 200 {object} api.ScopeDetail
// @Failure 404 {object} handlers.APIError
// @Failure 500 {object} handlers.APIError
// @Router /scopes/{name} [get]
func getScope(reader api.Reader) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
name := chi.URLParam(r, "name")
scope, err := reader.GetScopeByName(r.Context(), name)
switch {
case errors.Is(err, pgx.ErrNoRows), err == nil && scope == nil:
respondError(w, http.StatusNotFound, "scope not found")
return
case err != nil:
respondError(w, http.StatusInternalServerError, "failed to load scope")
return
}
respond(w, http.StatusOK, scope)
}
}