diff --git a/config.sample.yaml b/config.sample.yaml index 500cb1c..11e365c 100644 --- a/config.sample.yaml +++ b/config.sample.yaml @@ -1,3 +1,6 @@ +# this is used to compose URLs, for example in M3U responses. +# set it to what users access the instance as. +baseURL: "https://stillbox.example.com/" db: connect: 'postgres://postgres:password@localhost:5432/example' partition: diff --git a/internal/jsontypes/url.go b/internal/jsontypes/url.go new file mode 100644 index 0000000..1304bb7 --- /dev/null +++ b/internal/jsontypes/url.go @@ -0,0 +1,57 @@ +package jsontypes + +import ( + "encoding/json" + "net/url" + + "gopkg.in/yaml.v3" +) + +type URL url.URL + +func (u *URL) URL() url.URL { + return url.URL(*u) +} + +func (u *URL) UnmarshalJSON(b []byte) error { + var s string + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + ur, err := url.Parse(s) + if err != nil { + return err + } + + *u = URL(*ur) + return nil +} + +func (u *URL) UnmarshalYAML(n *yaml.Node) error { + var s string + + err := n.Decode(&s) + if err != nil { + return err + } + + ur, err := url.Parse(s) + if err != nil { + return err + } + + *u = URL(*ur) + return nil +} + +func (u *URL) UnmarshalText(t []byte) error { + ur, err := url.Parse(string(t)) + if err != nil { + return err + } + + *u = URL(*ur) + return nil +} diff --git a/internal/jsontypes/uuid.go b/internal/jsontypes/uuid.go index d420f83..2a9dcf5 100644 --- a/internal/jsontypes/uuid.go +++ b/internal/jsontypes/uuid.go @@ -1,6 +1,8 @@ package jsontypes import ( + "encoding/json" + "github.com/google/uuid" ) @@ -17,6 +19,28 @@ func (u *UUIDs) UUIDs() []uuid.UUID { return r } +func (u *UUIDs) UnmarshalJSON(b []byte) error { + var ss []string + err := json.Unmarshal(b, &ss) + if err != nil { + return err + } + + usl := make([]UUID, 0, len(ss)) + for _, s := range ss { + uu, err := uuid.Parse(s) + if err != nil { + return err + } + + usl = append(usl, UUID(uu)) + } + + *u = usl + + return nil +} + func (u UUID) UUID() uuid.UUID { return uuid.UUID(u) } @@ -26,12 +50,12 @@ func (u *UUID) MarshalJSON() ([]byte, error) { } func (u *UUID) UnmarshalJSON(b []byte) error { - id, err := uuid.Parse(string(b[:])) - if err != nil { - return err - } - *u = UUID(id) - return nil + id, err := uuid.Parse(string(b[:])) + if err != nil { + return err + } + *u = UUID(id) + return nil } func (u *UUID) UnmarshalText(t []byte) error { diff --git a/pkg/config/config.go b/pkg/config/config.go index 0a10370..bbea959 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -16,16 +16,17 @@ type Configuration struct { } type Config struct { - DB DB `yaml:"db"` - CORS CORS `yaml:"cors"` - Auth Auth `yaml:"auth"` - Alerting Alerting `yaml:"alerting"` - Log []Logger `yaml:"log"` - Listen string `yaml:"listen"` - Public bool `yaml:"public"` - RateLimit RateLimit `yaml:"rateLimit"` - Notify Notify `yaml:"notify"` - Relay []Relay `yaml:"relay"` + BaseURL jsontypes.URL `yaml:"baseURL"` + DB DB `yaml:"db"` + CORS CORS `yaml:"cors"` + Auth Auth `yaml:"auth"` + Alerting Alerting `yaml:"alerting"` + Log []Logger `yaml:"log"` + Listen string `yaml:"listen"` + Public bool `yaml:"public"` + RateLimit RateLimit `yaml:"rateLimit"` + Notify Notify `yaml:"notify"` + Relay []Relay `yaml:"relay"` } type Auth struct { diff --git a/pkg/config/parse.go b/pkg/config/parse.go index e8ba75f..065fac5 100644 --- a/pkg/config/parse.go +++ b/pkg/config/parse.go @@ -62,7 +62,7 @@ func (c *Configuration) read() error { }) if err != nil { - return fmt.Errorf("unmarshal err: %w", err) + return fmt.Errorf("config: %w", err) } return nil diff --git a/pkg/rest/api.go b/pkg/rest/api.go index 6998f9d..6f0c40b 100644 --- a/pkg/rest/api.go +++ b/pkg/rest/api.go @@ -3,6 +3,7 @@ package rest import ( "errors" "net/http" + "net/url" "dynatron.me/x/stillbox/pkg/talkgroups/tgstore" @@ -19,10 +20,11 @@ type API interface { } type api struct { + baseURL url.URL } -func New() *api { - s := new(api) +func New(baseURL url.URL) *api { + s := &api{baseURL} return s } @@ -33,7 +35,7 @@ func (a *api) Subrouter() http.Handler { r.Mount("/talkgroup", new(talkgroupAPI).Subrouter()) r.Mount("/call", new(callsAPI).Subrouter()) r.Mount("/user", new(usersAPI).Subrouter()) - r.Mount("/incident", new(incidentsAPI).Subrouter()) + r.Mount("/incident", newIncidentsAPI(&a.baseURL).Subrouter()) return r } diff --git a/pkg/rest/incidents.go b/pkg/rest/incidents.go index 9e09f1b..9e72a73 100644 --- a/pkg/rest/incidents.go +++ b/pkg/rest/incidents.go @@ -5,8 +5,11 @@ import ( "encoding/json" "fmt" "net/http" + "net/url" + "dynatron.me/x/stillbox/internal/common" "dynatron.me/x/stillbox/internal/forms" + "dynatron.me/x/stillbox/internal/jsontypes" "dynatron.me/x/stillbox/pkg/database" "dynatron.me/x/stillbox/pkg/incidents" "dynatron.me/x/stillbox/pkg/incidents/incstore" @@ -17,6 +20,11 @@ import ( ) type incidentsAPI struct { + baseURL *url.URL +} + +func newIncidentsAPI(baseURL *url.URL) API { + return &incidentsAPI{baseURL} } func (ia *incidentsAPI) Subrouter() http.Handler { @@ -25,13 +33,13 @@ func (ia *incidentsAPI) Subrouter() http.Handler { r.Get(`/{id:[a-f0-9-]+}`, ia.getIncident) r.Get(`/{id:[a-f0-9-]+}.m3u`, ia.getCallsM3U) - r.Post(`/create`, ia.createIncident) + r.Post(`/new`, ia.createIncident) r.Post(`/`, ia.listIncidents) r.Post(`/{id:[a-f0-9-]+}/calls`, ia.postCalls) r.Put(`/{id:[a-f0-9]+}`, ia.updateIncident) - r.Delete(`/{id:[a-f0-9]+}`, ia.deleteIncident) + r.Delete(`/{id:[a-f0-9-]+}`, ia.deleteIncident) return r } @@ -148,10 +156,10 @@ func (ia *incidentsAPI) deleteIncident(w http.ResponseWriter, r *http.Request) { } type CallIncidentParams struct { - Add []uuid.UUID `json:"add"` + Add jsontypes.UUIDs `json:"add"` Notes json.RawMessage `json:"notes"` - Remove []uuid.UUID `json:"remove"` + Remove jsontypes.UUIDs `json:"remove"` } func (ia *incidentsAPI) postCalls(w http.ResponseWriter, r *http.Request) { @@ -170,7 +178,7 @@ func (ia *incidentsAPI) postCalls(w http.ResponseWriter, r *http.Request) { return } - err = incs.AddRemoveIncidentCalls(ctx, id, p.Add, p.Notes, p.Remove) + err = incs.AddRemoveIncidentCalls(ctx, id, p.Add.UUIDs(), p.Notes, p.Remove.UUIDs()) if err != nil { wErr(w, r, autoError(err)) return @@ -197,9 +205,7 @@ func (ia *incidentsAPI) getCallsM3U(w http.ResponseWriter, r *http.Request) { var b bytes.Buffer - callUrl := r.URL - callUrl.RawQuery = "" - callUrl.Fragment = "" + callUrl := common.PtrTo(*ia.baseURL) b.WriteString("#EXTM3U\n\n") for _, c := range inc.Calls { @@ -213,7 +219,7 @@ func (ia *incidentsAPI) getCallsM3U(w http.ResponseWriter, r *http.Request) { from = fmt.Sprintf(" from %d", c.Source) } - callUrl.Path = "/api/call/%s" + c.ID.String() + callUrl.Path = "/api/call/" + c.ID.String() fmt.Fprintf(w, "#EXTINF:%d,%s%s (%s)\n%s\n\n", c.Duration.Seconds(), diff --git a/pkg/server/server.go b/pkg/server/server.go index 729b648..0ea300d 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -71,7 +71,7 @@ func New(ctx context.Context, cfg *config.Configuration) (*Server, error) { } tgCache := tgstore.NewCache() - api := rest.New() + api := rest.New(cfg.BaseURL.URL()) srv := &Server{ auth: authenticator,