2016-02-22 22:07:02 +01:00
// Copyright 2016 Martin Hebnes Pedersen (LA5NTA). All rights reserved.
// Use of this source code is governed by the MIT-license that can be
// found in the LICENSE file.
2025-06-14 07:50:42 +02:00
package api
2015-05-22 20:01:56 +02:00
import (
2022-02-16 20:58:10 +01:00
"context"
2015-05-22 20:01:56 +02:00
"encoding/json"
"fmt"
"log"
2025-09-03 18:51:27 +02:00
"maps"
2019-09-29 17:35:18 +02:00
"net"
2015-05-22 20:01:56 +02:00
"net/http"
"os"
"sort"
2020-09-05 23:01:19 +02:00
"strconv"
2015-05-22 20:01:56 +02:00
"strings"
"time"
2025-06-14 07:50:42 +02:00
"github.com/la5nta/pat/app"
2025-05-20 08:59:40 +02:00
"github.com/la5nta/pat/cfg"
2021-08-12 22:26:39 -06:00
"github.com/la5nta/pat/internal/buildinfo"
"github.com/la5nta/pat/internal/gpsd"
2025-06-04 13:57:40 +02:00
"github.com/la5nta/pat/internal/patapi"
2025-12-29 20:34:40 +01:00
"github.com/la5nta/pat/web"
2021-08-12 22:26:39 -06:00
2015-05-22 20:01:56 +02:00
"github.com/gorilla/mux"
"github.com/gorilla/websocket"
2025-06-04 13:57:40 +02:00
"github.com/hashicorp/go-version"
2015-05-22 20:01:56 +02:00
"github.com/la5nta/wl2k-go/catalog"
2025-06-04 13:57:40 +02:00
"github.com/la5nta/wl2k-go/transport/ardop"
2021-09-02 22:11:59 -06:00
"github.com/n8jja/Pat-Vara/vara"
2025-08-20 19:01:31 +02:00
"github.com/pd0mz/go-maidenhead"
2015-05-22 20:01:56 +02:00
)
2021-06-15 18:06:11 -06:00
type HTTPError struct {
2021-06-15 13:41:36 -06:00
error
StatusCode int
}
2025-06-14 07:50:42 +02:00
func ListenAndServe ( ctx context . Context , a * app . App , addr string ) error {
2021-07-16 10:27:44 -06:00
log . Printf ( "Starting HTTP service (http://%s)..." , addr )
2015-05-22 20:01:56 +02:00
2025-06-14 07:50:42 +02:00
if host , _ , _ := net . SplitHostPort ( addr ) ; host == "" && a . Config ( ) . GPSd . EnableHTTP {
2019-09-29 17:35:18 +02:00
// TODO: maybe make a popup showing the warning ont the web UI?
2025-06-14 07:50:42 +02:00
fmt . Fprintf ( os . Stderr , "\nWARNING: You have enable GPSd HTTP endpoint (enable_http). You might expose" +
2019-12-08 23:37:14 +01:00
"\n your current position to anyone who has access to the Pat web interface!\n\n" )
2019-09-26 01:36:55 +02:00
}
2025-12-29 20:34:40 +01:00
handler := NewHandler ( a )
2025-06-14 07:50:42 +02:00
go handler . wsHub . WatchMBox ( ctx , a . Mailbox ( ) )
if err := a . EnableWebSocket ( ctx , handler . wsHub ) ; err != nil {
return err
}
2017-07-28 23:00:25 +02:00
2023-10-13 23:06:04 +02:00
srv := http . Server {
2023-10-21 13:10:19 +02:00
Addr : addr ,
2025-06-14 07:50:42 +02:00
Handler : handler ,
2023-10-13 23:06:04 +02:00
}
2022-02-16 20:58:10 +01:00
errs := make ( chan error , 1 )
go func ( ) {
errs <- srv . ListenAndServe ( )
} ( )
select {
case <- ctx . Done ( ) :
log . Println ( "Shutting down HTTP server..." )
2022-02-23 00:19:59 +01:00
ctx , cancel := context . WithTimeout ( context . Background ( ) , 10 * time . Second )
defer cancel ( )
2022-02-16 20:58:10 +01:00
srv . Shutdown ( ctx )
return nil
case err := <- errs :
return err
}
2015-05-22 20:01:56 +02:00
}
2025-06-14 07:50:42 +02:00
type Handler struct {
* app . App
wsHub * WSHub
r * mux . Router
}
2025-06-17 06:05:37 +02:00
func ( h Handler ) ServeHTTP ( w http . ResponseWriter , r * http . Request ) { h . r . ServeHTTP ( w , r ) }
2025-12-29 20:34:40 +01:00
func NewHandler ( app * app . App ) * Handler {
2025-06-14 07:50:42 +02:00
r := mux . NewRouter ( )
h := & Handler { app , NewWSHub ( app ) , r }
r . HandleFunc ( "/api/connect" , h . ConnectHandler )
r . HandleFunc ( "/api/disconnect" , h . DisconnectHandler )
2025-06-17 06:05:37 +02:00
2025-06-14 07:50:42 +02:00
r . HandleFunc ( "/api/mailbox/{box}" , h . mailboxHandler ) . Methods ( "GET" )
r . HandleFunc ( "/api/mailbox/{box}/{mid}" , h . messageHandler ) . Methods ( "GET" )
r . HandleFunc ( "/api/mailbox/{box}/{mid}" , h . messageDeleteHandler ) . Methods ( "DELETE" )
r . HandleFunc ( "/api/mailbox/{box}/{mid}/{attachment}" , h . attachmentHandler ) . Methods ( "GET" )
r . HandleFunc ( "/api/mailbox/{box}/{mid}/read" , h . readHandler ) . Methods ( "POST" )
r . HandleFunc ( "/api/mailbox/{box}" , h . postMessageHandler ) . Methods ( "POST" )
2025-06-17 06:05:37 +02:00
2025-06-14 07:50:42 +02:00
r . HandleFunc ( "/api/posreport" , h . postPositionHandler ) . Methods ( "POST" )
r . HandleFunc ( "/api/status" , h . statusHandler ) . Methods ( "GET" )
r . HandleFunc ( "/api/current_gps_position" , h . positionHandler ) . Methods ( "GET" )
2025-08-20 19:01:31 +02:00
r . HandleFunc ( "/api/coords_to_locator" , h . coordsToLocatorHandler ) . Methods ( "POST" )
2025-06-14 07:50:42 +02:00
r . HandleFunc ( "/api/qsy" , h . qsyHandler ) . Methods ( "POST" )
r . HandleFunc ( "/api/rmslist" , h . rmslistHandler ) . Methods ( "GET" )
2025-06-17 06:05:37 +02:00
2025-06-14 07:50:42 +02:00
r . HandleFunc ( "/api/config" , h . configHandler ) . Methods ( "GET" , "PUT" )
2025-08-21 09:09:18 +02:00
r . HandleFunc ( "/api/config/connect_aliases" , h . connectAliasesHandler ) . Methods ( "GET" )
r . HandleFunc ( "/api/config/connect_aliases/{alias}" , h . connectAliasHandler ) . Methods ( "GET" , "PUT" , "DELETE" )
2025-06-22 18:59:12 +02:00
r . HandleFunc ( "/api/reload" , h . reloadHandler ) . Methods ( "POST" )
2025-06-17 06:05:37 +02:00
r . HandleFunc ( "/api/bandwidths" , h . bandwidthsHandler ) . Methods ( "GET" )
2025-08-21 09:09:18 +02:00
r . HandleFunc ( "/api/connect_aliases" , h . connectAliasesHandler ) . Methods ( "GET" ) // DEPRECATED: Use /api/config/connect_aliases.
2025-06-14 07:50:42 +02:00
r . HandleFunc ( "/api/new-release-check" , h . newReleaseCheckHandler ) . Methods ( "GET" )
r . HandleFunc ( "/api/formcatalog" , h . FormsManager ( ) . GetFormsCatalogHandler ) . Methods ( "GET" )
r . HandleFunc ( "/api/form" , h . FormsManager ( ) . PostFormDataHandler ( h . Mailbox ( ) . MBoxPath ) ) . Methods ( "POST" )
r . HandleFunc ( "/api/template" , h . FormsManager ( ) . GetTemplateDataHandler ( h . Mailbox ( ) . MBoxPath ) ) . Methods ( "GET" )
r . HandleFunc ( "/api/form" , h . FormsManager ( ) . GetFormDataHandler ) . Methods ( "GET" )
r . HandleFunc ( "/api/forms" , h . FormsManager ( ) . GetFormTemplateHandler ) . Methods ( "GET" )
r . PathPrefix ( "/api/forms/" ) . Handler ( http . StripPrefix ( "/api/forms/" , http . HandlerFunc ( h . FormsManager ( ) . GetFormAssetHandler ) ) ) . Methods ( "GET" )
r . HandleFunc ( "/api/formsUpdate" , h . FormsManager ( ) . UpdateFormTemplatesHandler ) . Methods ( "POST" )
2025-06-27 13:20:44 +02:00
r . HandleFunc ( "/api/winlink-account/password-recovery-email" , h . winlinkPasswordRecoveryEmailHandler ) . Methods ( "GET" , "PUT" )
2025-06-30 06:54:51 +02:00
r . HandleFunc ( "/api/winlink-account/registration" , h . winlinkAccountRegistrationHandler ) . Methods ( "GET" , "POST" )
2025-06-27 13:20:44 +02:00
2025-06-14 07:50:42 +02:00
r . HandleFunc ( "/ws" , h . wsHandler )
2025-12-29 20:34:40 +01:00
r . PathPrefix ( "/ui" ) . Handler ( web . UIHandler ( h . Options ( ) . MyCall ) )
r . PathPrefix ( "/dist" ) . Handler ( web . DistHandler ( ) )
r . HandleFunc ( "/" , func ( w http . ResponseWriter , r * http . Request ) { http . Redirect ( w , r , "/ui" , http . StatusFound ) } )
2025-06-14 07:50:42 +02:00
return h
}
func ( h Handler ) connectAliasesHandler ( w http . ResponseWriter , _ * http . Request ) {
_ = json . NewEncoder ( w ) . Encode ( h . Config ( ) . ConnectAliases )
2015-11-06 18:32:25 +01:00
}
2025-08-21 09:09:18 +02:00
func ( h Handler ) connectAliasHandler ( w http . ResponseWriter , r * http . Request ) {
// Make a copy of the map to avoid concurrenct read/write of the "live" map
2025-09-03 18:51:27 +02:00
currentAliases := maps . Clone ( h . Config ( ) . ConnectAliases )
2025-08-21 09:09:18 +02:00
alias := mux . Vars ( r ) [ "alias" ]
switch r . Method {
case http . MethodGet :
v , ok := currentAliases [ alias ]
if ! ok {
http . NotFound ( w , r )
return
}
json . NewEncoder ( w ) . Encode ( v )
case http . MethodDelete :
delete ( currentAliases , alias )
if err := h . SetConnectAliases ( currentAliases ) ; err != nil {
http . Error ( w , err . Error ( ) , http . StatusInternalServerError )
2025-08-30 09:45:46 +02:00
return
2025-08-21 09:09:18 +02:00
}
w . WriteHeader ( http . StatusNoContent )
case http . MethodPut :
var v string
if err := json . NewDecoder ( r . Body ) . Decode ( & v ) ; err != nil {
http . Error ( w , err . Error ( ) , http . StatusBadRequest )
return
}
currentAliases [ alias ] = v
if err := h . SetConnectAliases ( currentAliases ) ; err != nil {
http . Error ( w , err . Error ( ) , http . StatusInternalServerError )
return
}
json . NewEncoder ( w ) . Encode ( v )
default :
w . WriteHeader ( http . StatusMethodNotAllowed )
}
}
2025-06-14 07:50:42 +02:00
func ( h Handler ) postPositionHandler ( w http . ResponseWriter , r * http . Request ) {
2015-05-22 20:01:56 +02:00
var pos catalog . PosReport
if err := json . NewDecoder ( r . Body ) . Decode ( & pos ) ; err != nil {
http . Error ( w , err . Error ( ) , http . StatusBadRequest )
return
}
2017-07-22 16:52:18 +02:00
if pos . Date . IsZero ( ) {
pos . Date = time . Now ( )
}
2025-06-17 09:12:28 +02:00
msg := pos . Message ( h . Options ( ) . MyCall )
2017-07-22 16:52:18 +02:00
2015-05-22 20:01:56 +02:00
// Post to outbox
2025-06-14 07:50:42 +02:00
if err := h . Mailbox ( ) . AddOut ( msg ) ; err != nil {
2015-05-22 20:01:56 +02:00
log . Println ( err )
http . Error ( w , err . Error ( ) , http . StatusInternalServerError )
2025-06-17 09:12:28 +02:00
return
2015-05-22 20:01:56 +02:00
}
2025-06-17 09:12:28 +02:00
fmt . Fprintln ( w , "Position update posted" )
2015-05-22 20:01:56 +02:00
}
2025-06-14 07:50:42 +02:00
func ( h Handler ) wsHandler ( w http . ResponseWriter , r * http . Request ) {
2015-05-22 20:01:56 +02:00
upgrader := websocket . Upgrader {
ReadBufferSize : 1024 ,
WriteBufferSize : 1024 ,
}
conn , err := upgrader . Upgrade ( w , r , nil )
if err != nil {
log . Println ( err )
return
}
2025-06-14 07:50:42 +02:00
_ = conn . WriteJSON ( struct { MyCall string } { h . Options ( ) . MyCall } )
h . wsHub . Handle ( conn )
2015-05-22 20:01:56 +02:00
}
2025-06-14 07:50:42 +02:00
func ( h Handler ) statusHandler ( w http . ResponseWriter , _ * http . Request ) {
_ = json . NewEncoder ( w ) . Encode ( h . GetStatus ( ) )
2021-07-16 10:58:58 -06:00
}
2016-08-09 12:32:08 +02:00
2025-06-14 07:50:42 +02:00
func ( h Handler ) bandwidthsHandler ( w http . ResponseWriter , req * http . Request ) {
2022-03-13 18:26:18 -06:00
type BandwidthResponse struct {
Mode string ` json:"mode" `
Bandwidths [ ] string ` json:"bandwidths" `
2022-04-16 19:03:05 +02:00
Default string ` json:"default,omitempty" `
2022-03-13 18:26:18 -06:00
}
mode := strings . ToLower ( req . FormValue ( "mode" ) )
2022-04-16 19:03:05 +02:00
resp := BandwidthResponse { Mode : mode , Bandwidths : [ ] string { } }
2024-12-30 22:10:05 +01:00
switch mode {
2025-06-14 07:50:42 +02:00
case app . MethodArdop :
2022-03-13 18:26:18 -06:00
for _ , bw := range ardop . Bandwidths ( ) {
resp . Bandwidths = append ( resp . Bandwidths , bw . String ( ) )
}
2025-06-14 07:50:42 +02:00
if bw := h . Config ( ) . Ardop . ARQBandwidth ; ! bw . IsZero ( ) {
2022-04-16 19:03:05 +02:00
resp . Default = bw . String ( )
}
2025-06-14 07:50:42 +02:00
case app . MethodVaraHF :
2021-09-02 22:11:59 -06:00
resp . Bandwidths = vara . Bandwidths ( )
2025-06-14 07:50:42 +02:00
if bw := h . Config ( ) . VaraHF . Bandwidth ; bw != 0 {
2021-09-02 22:11:59 -06:00
resp . Default = fmt . Sprintf ( "%d" , bw )
}
2022-03-13 18:26:18 -06:00
}
_ = json . NewEncoder ( w ) . Encode ( resp )
}
2025-06-14 07:50:42 +02:00
func ( h Handler ) rmslistHandler ( w http . ResponseWriter , req * http . Request ) {
2025-06-17 09:12:28 +02:00
var (
forceDownload , _ = strconv . ParseBool ( req . FormValue ( "force-download" ) )
band = req . FormValue ( "band" )
mode = strings . ToLower ( req . FormValue ( "mode" ) )
prefix = strings . ToUpper ( req . FormValue ( "prefix" ) )
)
2025-06-14 07:50:42 +02:00
list , err := h . ReadRMSList ( req . Context ( ) , forceDownload , func ( r app . RMS ) bool {
2020-09-05 23:01:19 +02:00
switch {
case r . URL == nil :
return false
case mode != "" && ! r . IsMode ( mode ) :
return false
case band != "" && ! r . IsBand ( band ) :
return false
case prefix != "" && ! strings . HasPrefix ( r . Callsign , prefix ) :
return false
default :
return true
}
} )
if err != nil {
log . Println ( err )
http . Error ( w , err . Error ( ) , http . StatusInternalServerError )
return
}
2025-06-17 09:12:28 +02:00
2025-08-13 15:03:56 +02:00
// Sort by predictions if we have more than 1/3 entries with predictions,
// otherwise sort by distance.
nPredictions := 0
for _ , rms := range list {
if rms . Prediction != nil {
nPredictions ++
}
}
if nPredictions > len ( list ) / 3 {
sort . Sort ( sort . Reverse ( app . ByLinkQuality ( list ) ) )
} else {
sort . Sort ( app . ByDist ( list ) )
}
2025-06-17 09:12:28 +02:00
json . NewEncoder ( w ) . Encode ( list )
2020-09-05 23:01:19 +02:00
}
2025-06-14 07:50:42 +02:00
func ( h Handler ) qsyHandler ( w http . ResponseWriter , req * http . Request ) {
2020-09-05 20:33:17 +02:00
type QSYPayload struct {
Transport string ` json:"transport" `
Freq json . Number ` json:"freq" `
}
var payload QSYPayload
if err := json . NewDecoder ( req . Body ) . Decode ( & payload ) ; err != nil {
http . Error ( w , err . Error ( ) , http . StatusBadRequest )
return
}
2020-09-05 23:01:19 +02:00
2025-06-14 07:50:42 +02:00
rig , rigName , ok , err := h . VFOForTransport ( payload . Transport )
2020-09-05 23:01:19 +02:00
switch {
case rigName == "" :
// Either unsupported mode or no rig configured for this transport
w . WriteHeader ( http . StatusServiceUnavailable )
2020-09-05 20:33:17 +02:00
return
2020-09-05 23:01:19 +02:00
case ! ok :
// A rig is configured, but not loaded properly
w . WriteHeader ( http . StatusInternalServerError )
log . Printf ( "QSY failed: Hamlib rig '%s' not loaded." , rigName )
case err != nil :
w . WriteHeader ( http . StatusInternalServerError )
log . Printf ( "QSY failed: %v" , err )
default :
2025-06-14 07:50:42 +02:00
if _ , _ , err := app . SetFreq ( rig , string ( payload . Freq ) ) ; err != nil {
2020-09-05 23:01:19 +02:00
w . WriteHeader ( http . StatusInternalServerError )
log . Printf ( "QSY failed: %v" , err )
return
}
2021-07-16 10:58:58 -06:00
_ = json . NewEncoder ( w ) . Encode ( payload )
2020-09-05 20:33:17 +02:00
}
}
2025-06-14 07:50:42 +02:00
func ( h Handler ) positionHandler ( w http . ResponseWriter , req * http . Request ) {
2019-09-26 01:36:55 +02:00
// Throw error if GPSd http endpoint is not enabled
2025-06-14 07:50:42 +02:00
if ! h . Config ( ) . GPSd . EnableHTTP || h . Config ( ) . GPSd . Addr == "" {
2019-09-30 18:54:44 +02:00
http . Error ( w , "GPSd not enabled or address not set in config file" , http . StatusInternalServerError )
return
}
2019-09-26 01:36:55 +02:00
2019-09-30 18:54:44 +02:00
host , _ , _ := net . SplitHostPort ( req . RemoteAddr )
log . Printf ( "Location data from GPSd served to %s" , host )
2019-09-26 01:36:55 +02:00
2025-06-14 07:50:42 +02:00
conn , err := gpsd . Dial ( h . Config ( ) . GPSd . Addr )
2019-09-30 18:54:44 +02:00
if err != nil {
// do not pass error message to response as GPSd address might be leaked
http . Error ( w , "GPSd Dial failed" , http . StatusInternalServerError )
return
}
defer conn . Close ( )
2019-09-26 01:36:55 +02:00
2019-09-30 18:54:44 +02:00
conn . Watch ( true )
2019-09-29 17:35:18 +02:00
2019-09-30 18:54:44 +02:00
pos , err := conn . NextPosTimeout ( 5 * time . Second )
if err != nil {
2019-12-08 23:37:14 +01:00
http . Error ( w , "GPSd get next position failed: " + err . Error ( ) , http . StatusInternalServerError )
2019-09-26 01:36:55 +02:00
return
}
2025-06-14 07:50:42 +02:00
if h . Config ( ) . GPSd . UseServerTime {
2019-09-30 18:54:44 +02:00
pos . Time = time . Now ( )
}
2021-07-16 10:58:58 -06:00
_ = json . NewEncoder ( w ) . Encode ( pos )
2019-09-26 01:36:55 +02:00
}
2025-06-14 07:50:42 +02:00
func ( h Handler ) DisconnectHandler ( w http . ResponseWriter , req * http . Request ) {
2020-09-17 01:00:07 +02:00
dirty , _ := strconv . ParseBool ( req . FormValue ( "dirty" ) )
2025-06-14 07:50:42 +02:00
if ok := h . AbortActiveConnection ( dirty ) ; ! ok {
2020-09-17 01:00:07 +02:00
w . WriteHeader ( http . StatusBadRequest )
}
2021-07-16 10:58:58 -06:00
_ = json . NewEncoder ( w ) . Encode ( struct { } { } )
2020-09-17 01:00:07 +02:00
}
2025-06-14 07:50:42 +02:00
func ( h Handler ) ConnectHandler ( w http . ResponseWriter , req * http . Request ) {
2015-11-06 18:32:25 +01:00
connectStr := req . FormValue ( "url" )
2015-05-22 20:01:56 +02:00
2025-06-14 07:50:42 +02:00
nMsgs := h . Mailbox ( ) . InboxCount ( )
2015-05-22 20:01:56 +02:00
2025-06-14 07:50:42 +02:00
if success := h . Connect ( connectStr ) ; ! success {
2015-05-22 20:01:56 +02:00
http . Error ( w , "Session failure" , http . StatusInternalServerError )
}
2025-06-14 07:50:42 +02:00
_ = json . NewEncoder ( w ) . Encode ( struct { NumReceived int } {
h . Mailbox ( ) . InboxCount ( ) - nMsgs ,
2015-05-22 20:01:56 +02:00
} )
}
2025-06-14 07:50:42 +02:00
func ( h Handler ) newReleaseCheckHandler ( w http . ResponseWriter , r * http . Request ) {
2025-06-04 13:57:40 +02:00
ctx , cancel := context . WithTimeout ( r . Context ( ) , 10 * time . Second )
defer cancel ( )
release , err := patapi . GetLatestVersion ( ctx )
if err != nil {
http . Error ( w , "Error getting latest version: " + err . Error ( ) , http . StatusInternalServerError )
return
}
currentVer , err := version . NewVersion ( buildinfo . Version )
if err != nil {
http . Error ( w , "Invalid current version format: " + err . Error ( ) , http . StatusInternalServerError )
return
}
latestVer , err := version . NewVersion ( release . Version )
if err != nil {
http . Error ( w , "Invalid latest version format: " + err . Error ( ) , http . StatusInternalServerError )
return
}
if currentVer . Compare ( latestVer ) >= 0 {
w . WriteHeader ( http . StatusNoContent )
return
}
w . Header ( ) . Set ( "Content-Type" , "application/json" )
json . NewEncoder ( w ) . Encode ( release )
}
2025-06-14 07:50:42 +02:00
func ( h Handler ) configHandler ( w http . ResponseWriter , r * http . Request ) {
2025-05-20 08:59:40 +02:00
const RedactedPassword = "[REDACTED]"
2025-06-14 07:50:42 +02:00
currentConfig , err := app . LoadConfig ( h . Options ( ) . ConfigPath , cfg . DefaultConfig )
2025-05-20 08:59:40 +02:00
if err != nil {
log . Println ( err )
http . Error ( w , err . Error ( ) , http . StatusInternalServerError )
return
}
if r . Method == "GET" {
if currentConfig . SecureLoginPassword != "" {
// Redact password before sending over unsafe channel.
currentConfig . SecureLoginPassword = RedactedPassword
}
json . NewEncoder ( w ) . Encode ( currentConfig )
return
}
var newConfig cfg . Config
if err := json . NewDecoder ( r . Body ) . Decode ( & newConfig ) ; err != nil {
http . Error ( w , err . Error ( ) , http . StatusBadRequest )
return
}
2025-08-20 19:21:13 +02:00
// Security: Prevent GPSd EnableHTTP from being changed via web interface
if newConfig . GPSd . EnableHTTP != currentConfig . GPSd . EnableHTTP {
http . Error ( w , "GPSd EnableHTTP setting cannot be changed via web interface for security reasons. Please edit the configuration file manually." , http . StatusForbidden )
return
}
2025-05-20 08:59:40 +02:00
// Reset redacted password if it was unmodified (to retain old value)
if newConfig . SecureLoginPassword == RedactedPassword {
newConfig . SecureLoginPassword = currentConfig . SecureLoginPassword
}
2025-06-14 07:50:42 +02:00
if err := app . WriteConfig ( newConfig , h . Options ( ) . ConfigPath ) ; err != nil {
2025-05-20 08:59:40 +02:00
http . Error ( w , err . Error ( ) , http . StatusInternalServerError )
}
_ = json . NewEncoder ( w ) . Encode ( "OK" )
}
2025-06-22 18:59:12 +02:00
func ( h Handler ) reloadHandler ( w http . ResponseWriter , r * http . Request ) {
if err := h . App . Reload ( ) ; err != nil {
http . Error ( w , err . Error ( ) , http . StatusInternalServerError )
return
}
w . WriteHeader ( http . StatusOK )
}
2025-08-20 19:01:31 +02:00
func ( h Handler ) coordsToLocatorHandler ( w http . ResponseWriter , r * http . Request ) {
var req struct {
Lat float64 ` json:"lat" `
Lon float64 ` json:"lon" `
}
if err := json . NewDecoder ( r . Body ) . Decode ( & req ) ; err != nil {
http . Error ( w , "Invalid JSON: " + err . Error ( ) , http . StatusBadRequest )
return
}
point := maidenhead . NewPoint ( req . Lat , req . Lon )
locator , err := point . GridSquare ( )
if err != nil {
http . Error ( w , "Failed to convert coordinates to locator: " + err . Error ( ) , http . StatusInternalServerError )
return
}
w . Header ( ) . Set ( "Content-Type" , "application/json" )
json . NewEncoder ( w ) . Encode ( struct {
Locator string ` json:"locator" `
} { Locator : locator } )
}