mirror of
https://github.com/MeshCore-Beacon/beacon-server.git
synced 2026-09-09 05:33:54 +00:00
172 lines
4.7 KiB
Go
172 lines
4.7 KiB
Go
// Copyright 2026 Beacon Contributors
|
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/MeshCore-Beacon/beacon-server/internal/api"
|
|
"github.com/go-chi/chi/v5"
|
|
)
|
|
|
|
// RegionsRouter mounts all /regions routes onto a subrouter.
|
|
//
|
|
// GET /regions → listRegions
|
|
// GET /regions/{regionId} → getRegion
|
|
//
|
|
// Note: region creation and IATA assignment are managed via the server config
|
|
// file, not the API (v1). These endpoints are read-only.
|
|
func RegionsRouter(reader api.Reader) http.Handler {
|
|
r := chi.NewRouter()
|
|
r.Get("/", listRegions(reader))
|
|
r.Get("/{regionId}", getRegion(reader))
|
|
return r
|
|
}
|
|
|
|
// listRegions godoc
|
|
//
|
|
// @Summary List all regions
|
|
// @Tags Regions
|
|
// @Produce json
|
|
// @Success 200 {array} api.RegionSummary
|
|
// @Failure 404 {object} handlers.APIError
|
|
// @Router /regions [get]
|
|
func listRegions(reader api.Reader) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
regions, err := reader.ListRegions(r.Context())
|
|
if err != nil {
|
|
respondError(w, http.StatusNotFound, "no regions found")
|
|
return
|
|
}
|
|
respond(w, http.StatusOK, regions)
|
|
}
|
|
}
|
|
|
|
// getRegion godoc
|
|
//
|
|
// @Summary Get a single region
|
|
// @Tags Regions
|
|
// @Produce json
|
|
// @Param regionId path int true "Region ID"
|
|
// @Success 200 {object} api.Region
|
|
// @Failure 400 {object} handlers.APIError
|
|
// @Failure 404 {object} handlers.APIError
|
|
// @Router /regions/{regionId} [get]
|
|
func getRegion(reader api.Reader) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
regionID := chi.URLParam(r, "regionId")
|
|
regionInt, err := strconv.ParseInt(regionID, 10, 32)
|
|
if err != nil {
|
|
respondError(w, http.StatusBadRequest, "invalid region ID")
|
|
return
|
|
}
|
|
region, err := reader.GetRegion(r.Context(), int32(regionInt))
|
|
if err != nil {
|
|
respondError(w, http.StatusNotFound, "region not found")
|
|
return
|
|
}
|
|
respond(w, http.StatusOK, region)
|
|
}
|
|
}
|
|
|
|
// parseIATAs splits a comma-separated iatas query param and uppercases each value.
|
|
func parseIATAs(r *http.Request) []string {
|
|
raw := r.URL.Query().Get("iatas")
|
|
if raw == "" {
|
|
// fall back to single iata param for backwards compatibility
|
|
if single := r.URL.Query().Get("iata"); single != "" {
|
|
return []string{strings.ToUpper(strings.TrimSpace(single))}
|
|
}
|
|
return nil
|
|
}
|
|
parts := strings.Split(raw, ",")
|
|
iatas := make([]string, 0, len(parts))
|
|
for _, p := range parts {
|
|
p = strings.TrimSpace(p)
|
|
if p != "" {
|
|
iatas = append(iatas, strings.ToUpper(p))
|
|
}
|
|
}
|
|
return iatas
|
|
}
|
|
|
|
// parseInt16CSVOrSingle reads a comma-separated list from pluralParam, falling back to a
|
|
// single value from singularParam if pluralParam is absent -- same precedence as parseIATAs.
|
|
func parseInt16CSVOrSingle(r *http.Request, pluralParam, singularParam string) ([]int16, error) {
|
|
raw := r.URL.Query().Get(pluralParam)
|
|
if raw == "" {
|
|
single := r.URL.Query().Get(singularParam)
|
|
if single == "" {
|
|
return nil, nil
|
|
}
|
|
v, err := strconv.ParseInt(single, 10, 16)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return []int16{int16(v)}, nil
|
|
}
|
|
parts := strings.Split(raw, ",")
|
|
out := make([]int16, 0, len(parts))
|
|
for _, p := range parts {
|
|
p = strings.TrimSpace(p)
|
|
if p == "" {
|
|
continue
|
|
}
|
|
v, err := strconv.ParseInt(p, 10, 16)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, int16(v))
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// parseCSVOrSingle reads a comma-separated list from pluralParam, falling back to a single
|
|
// value from singularParam if pluralParam is absent -- same precedence as parseIATAs, but
|
|
// without the uppercasing (used for scope names, which are case-sensitive, e.g. "#bc").
|
|
func parseCSVOrSingle(r *http.Request, pluralParam, singularParam string) []string {
|
|
raw := r.URL.Query().Get(pluralParam)
|
|
if raw == "" {
|
|
if single := r.URL.Query().Get(singularParam); single != "" {
|
|
return []string{single}
|
|
}
|
|
return nil
|
|
}
|
|
parts := strings.Split(raw, ",")
|
|
out := make([]string, 0, len(parts))
|
|
for _, p := range parts {
|
|
p = strings.TrimSpace(p)
|
|
if p != "" {
|
|
out = append(out, p)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// resolveRegionIATAs expands a regionId or region slug to a slice of IATA codes.
|
|
func resolveRegionIATAs(ctx context.Context, regionID, regionSlug string, reader api.Reader) ([]string, error) {
|
|
var region *api.Region
|
|
var err error
|
|
switch {
|
|
case regionID != "":
|
|
rid, e := strconv.ParseInt(regionID, 10, 32)
|
|
if e != nil {
|
|
return nil, fmt.Errorf("invalid regionId: %w", e)
|
|
}
|
|
region, err = reader.GetRegion(ctx, int32(rid))
|
|
case regionSlug != "":
|
|
region, err = reader.GetRegionBySlug(ctx, regionSlug)
|
|
default:
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("region not found: %w", err)
|
|
}
|
|
return region.IATAs, nil
|
|
}
|