mirror of
https://github.com/MeshTender/MeshTender.git
synced 2026-09-02 01:38:17 +00:00
180 lines
6.1 KiB
Go
180 lines
6.1 KiB
Go
package core
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/jleight/meshtender/internal/store"
|
|
"github.com/jleight/meshtender/internal/web"
|
|
)
|
|
|
|
// consoleConfig is the JSON payload behind the console's "Apply organization
|
|
// configuration" modal: the orgs whose config applies to this repeater, the
|
|
// selected org/profile, the full recommended-command list (profile base settings
|
|
// + region commands), and the location state that drives the region commands.
|
|
type consoleConfig struct {
|
|
Orgs []store.RepeaterConfigOrg `json:"orgs"`
|
|
SelectedOrg int64 `json:"selectedOrg"`
|
|
SelectedProfile string `json:"selectedProfile"`
|
|
Commands []consoleConfigCommand `json:"commands"`
|
|
Location consoleConfigLocation `json:"location"`
|
|
}
|
|
|
|
// consoleConfigCommand is one line of the recommended configuration. Every line
|
|
// is shown; Runnable=false marks a line the user can't send (a note, or a command
|
|
// they lack permission for), with Reason explaining why.
|
|
type consoleConfigCommand struct {
|
|
Line string `json:"line"` // CLI text; empty for a comment-only note
|
|
Comment string `json:"comment,omitempty"` // optional note text
|
|
Runnable bool `json:"runnable"`
|
|
Reason string `json:"reason,omitempty"` // why it isn't runnable
|
|
}
|
|
|
|
// consoleConfigLocation reports what we know about the repeater's location, which
|
|
// the region commands depend on.
|
|
type consoleConfigLocation struct {
|
|
Known bool `json:"known"`
|
|
Lat *float64 `json:"lat,omitempty"`
|
|
Lon *float64 `json:"lon,omitempty"`
|
|
NeedsLocation bool `json:"needsLocation"` // the org has regions but we have no coords
|
|
RegionsCover bool `json:"regionsCover"` // the location falls inside some org region
|
|
}
|
|
|
|
// consoleConfigJSON serves the recommended configuration for a repeater under a
|
|
// chosen org/profile. Location defaults to the repeater's stored coordinates; a
|
|
// ?lat=&lon= override lets the client preview a picked location before saving it.
|
|
func (s *Handlers) consoleConfigJSON(w http.ResponseWriter, r *http.Request) {
|
|
rep, id, ok := s.requireRepeaterAccess(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
ctx := r.Context()
|
|
uid := s.Auth.CurrentUserID(ctx)
|
|
|
|
orgs, err := s.Store.ListRepeaterConfigOrgs(ctx, id)
|
|
if err != nil {
|
|
s.ServerError(w, r, "could not load organizations", err)
|
|
return
|
|
}
|
|
resp := consoleConfig{Orgs: orgs}
|
|
if len(orgs) == 0 {
|
|
writeConfigJSON(w, resp) // no config-bearing orgs for this repeater
|
|
return
|
|
}
|
|
|
|
// Selected org: ?org= when it's one this repeater participates in, else the first.
|
|
resp.SelectedOrg = orgs[0].OrgID
|
|
if v := r.URL.Query().Get("org"); v != "" {
|
|
if oid, perr := strconv.ParseInt(v, 10, 64); perr == nil {
|
|
for _, o := range orgs {
|
|
if o.OrgID == oid {
|
|
resp.SelectedOrg = oid
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Location: a ?lat=&lon= preview wins, otherwise the repeater's stored coords.
|
|
lat, lon := rep.Latitude, rep.Longitude
|
|
if qLat, qLon, okq := web.PreviewLatLon(r); okq {
|
|
lat, lon = &qLat, &qLon
|
|
}
|
|
|
|
cv, err := web.BuildConfigView(ctx, s.Store, resp.SelectedOrg, r.URL.Query().Get("profile"), lat, lon)
|
|
if err != nil {
|
|
s.ServerError(w, r, "could not load configuration", err)
|
|
return
|
|
}
|
|
resp.SelectedProfile = cv.Selected
|
|
|
|
// Resolve each recommended line against the catalog + the user's sendable set so
|
|
// the UI can mark which lines they may actually run.
|
|
catalog, err := s.Store.ListCommands(ctx)
|
|
if err != nil {
|
|
s.ServerError(w, r, "could not load commands", err)
|
|
return
|
|
}
|
|
sendable, err := s.Store.ListSendableCommandIDs(ctx, uid, id)
|
|
if err != nil {
|
|
s.ServerError(w, r, "could not load permissions", err)
|
|
return
|
|
}
|
|
allowed := make(map[int64]bool, len(sendable))
|
|
for _, cid := range sendable {
|
|
allowed[cid] = true
|
|
}
|
|
runnable := func(line string) (bool, string) {
|
|
cmd := resolveCommand(line, catalog)
|
|
if cmd == nil {
|
|
return false, "not a recognized command"
|
|
}
|
|
if !allowed[cmd.ID] {
|
|
return false, "you don't have permission to run this"
|
|
}
|
|
return true, ""
|
|
}
|
|
|
|
// Profile base settings (verbatim; comment-only steps are notes, not commands),
|
|
// with the location-derived region commands spliced at the profile's
|
|
// {{ region }} marker or appended after all steps when it has none. The marker
|
|
// itself is dropped by SplitAtRegionMarker.
|
|
emitStep := func(step store.ConfigStep) {
|
|
c := consoleConfigCommand{Line: step.CommandLine, Comment: step.Comment}
|
|
if step.IsComment() {
|
|
c.Reason = "note"
|
|
} else {
|
|
c.Runnable, c.Reason = runnable(step.CommandLine)
|
|
}
|
|
resp.Commands = append(resp.Commands, c)
|
|
}
|
|
before, after := store.SplitAtRegionMarker(cv.SelectedSteps)
|
|
for _, step := range before {
|
|
emitStep(step)
|
|
}
|
|
for _, line := range cv.RegionDef {
|
|
c := consoleConfigCommand{Line: line}
|
|
c.Runnable, c.Reason = runnable(line)
|
|
resp.Commands = append(resp.Commands, c)
|
|
}
|
|
for _, step := range after {
|
|
emitStep(step)
|
|
}
|
|
|
|
resp.Location = consoleConfigLocation{
|
|
Known: lat != nil && lon != nil,
|
|
Lat: lat,
|
|
Lon: lon,
|
|
NeedsLocation: cv.HasRegions && (lat == nil || lon == nil),
|
|
RegionsCover: len(cv.RegionDef) > 0,
|
|
}
|
|
writeConfigJSON(w, resp)
|
|
}
|
|
|
|
func writeConfigJSON(w http.ResponseWriter, v any) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(v)
|
|
}
|
|
|
|
// handleSetRepeaterLocation persists a location picked on the console map when the
|
|
// repeater has none (or the user is correcting it). Access-gated the same way as
|
|
// confirming — anyone who can operate the repeater can set its location.
|
|
func (s *Handlers) handleSetRepeaterLocation(w http.ResponseWriter, r *http.Request) {
|
|
_, id, ok := s.requireRepeaterAccess(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
lat, errLat := strconv.ParseFloat(r.FormValue("lat"), 64)
|
|
lon, errLon := strconv.ParseFloat(r.FormValue("lon"), 64)
|
|
if errLat != nil || errLon != nil || lat < -90 || lat > 90 || lon < -180 || lon > 180 {
|
|
http.Error(w, "invalid coordinates", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if err := s.Store.SetRepeaterLocation(r.Context(), id, lat, lon); err != nil {
|
|
s.ServerError(w, r, "could not save location", err)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|