This commit is contained in:
Daniel Ponte 2025-01-20 16:31:43 -05:00
parent 3116874247
commit a9f64f74fb
6 changed files with 104 additions and 51 deletions

View file

@ -18,52 +18,64 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
type APIRoot interface {
API
Shares() ShareAPI
ShareSubroutes(chi.Router)
}
type API interface { type API interface {
Subrouter() http.Handler Subrouter() http.Handler
} }
type PublicAPI interface { type ShareableAPI interface {
API API
PublicRoutes(r chi.Router) GETSubroutes(chi.Router)
} }
type api struct { type api struct {
baseURL url.URL baseURL *url.URL
share publicAPI shares ShareAPI
tgs API
calls ShareableAPI
users API
incidents ShareableAPI
} }
type publicAPI interface { func (a *api) Shares() ShareAPI {
API return a.shares
PublicRouter() http.Handler
} }
func New(baseURL url.URL) *api { func New(baseURL url.URL) *api {
s := &api{ s := &api{
baseURL: baseURL, baseURL: &baseURL,
shares: newShareAPI(&baseURL),
tgs: new(talkgroupAPI),
calls: new(callsAPI),
incidents: newIncidentsAPI(&baseURL),
users: new(usersAPI),
} }
return s return s
} }
func (a *api) PublicRoutes(r chi.Router) {
r.Mount("/share", a.share.PublicRouter())
}
func (a *api) Subrouter() http.Handler { func (a *api) Subrouter() http.Handler {
r := chi.NewMux() r := chi.NewMux()
r.Mount("/talkgroup", new(talkgroupAPI).Subrouter()) r.Mount("/talkgroup", a.tgs.Subrouter())
r.Mount("/user", new(usersAPI).Subrouter()) r.Mount("/user", a.users.Subrouter())
r.Mount("/call", new(callsAPI).Subrouter()) r.Mount("/call", a.calls.Subrouter())
r.Mount("/incident", newIncidentsAPI(&a.baseURL).Subrouter()) r.Mount("/incident", a.incidents.Subrouter())
r.Mount("/share", a.shares.Subrouter())
a.share = newShareAPI(&a.baseURL, r)
r.Mount("/share", a.share.Subrouter())
return r return r
} }
func (a *api) ShareSubroutes(r chi.Router) {
r.Route("/calls", a.calls.GETSubroutes)
r.Route("/incidents", a.incidents.GETSubroutes)
}
type errResponse struct { type errResponse struct {
Err error `json:"-"` Err error `json:"-"`
Code int `json:"-"` Code int `json:"-"`

View file

@ -30,14 +30,17 @@ type callsAPI struct {
func (ca *callsAPI) Subrouter() http.Handler { func (ca *callsAPI) Subrouter() http.Handler {
r := chi.NewMux() r := chi.NewMux()
r.Get(`/{call:[a-f0-9-]+}`, ca.getAudio) ca.GETSubroutes(r)
r.Get(`/{call:[a-f0-9-]+}/{download:download}`, ca.getAudio)
r.Post(`/`, ca.listCalls) r.Post(`/`, ca.listCalls)
return r return r
} }
func (ca *callsAPI) GETSubroutes(r chi.Router) {
r.Get(`/{call:[a-f0-9-]+}`, ca.getAudio)
r.Get(`/{call:[a-f0-9-]+}/{download:download}`, ca.getAudio)
}
func (ca *callsAPI) getAudio(w http.ResponseWriter, r *http.Request) { func (ca *callsAPI) getAudio(w http.ResponseWriter, r *http.Request) {
p := struct { p := struct {
CallID *uuid.UUID `param:"call"` CallID *uuid.UUID `param:"call"`

View file

@ -22,16 +22,14 @@ type incidentsAPI struct {
baseURL *url.URL baseURL *url.URL
} }
func newIncidentsAPI(baseURL *url.URL) API { func newIncidentsAPI(baseURL *url.URL) ShareableAPI {
return &incidentsAPI{baseURL} return &incidentsAPI{baseURL}
} }
func (ia *incidentsAPI) Subrouter() http.Handler { func (ia *incidentsAPI) Subrouter() http.Handler {
r := chi.NewMux() r := chi.NewMux()
r.Get(`/{id:[a-f0-9-]+}`, ia.getIncident) ia.GETSubroutes(r)
r.Get(`/{id:[a-f0-9-]+}.m3u`, ia.getCallsM3U)
r.Post(`/new`, ia.createIncident) r.Post(`/new`, ia.createIncident)
r.Post(`/`, ia.listIncidents) r.Post(`/`, ia.listIncidents)
r.Post(`/{id:[a-f0-9-]+}/calls`, ia.postCalls) r.Post(`/{id:[a-f0-9-]+}/calls`, ia.postCalls)
@ -43,6 +41,11 @@ func (ia *incidentsAPI) Subrouter() http.Handler {
return r return r
} }
func (ia *incidentsAPI) GETSubroutes(r chi.Router) {
r.Get(`/{id:[a-f0-9-]+}`, ia.getIncident)
r.Get(`/{id:[a-f0-9-]+}.m3u`, ia.getCallsM3U)
}
func (ia *incidentsAPI) listIncidents(w http.ResponseWriter, r *http.Request) { func (ia *incidentsAPI) listIncidents(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
incs := incstore.FromCtx(ctx) incs := incstore.FromCtx(ctx)

View file

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
"strings"
"time" "time"
"dynatron.me/x/stillbox/internal/forms" "dynatron.me/x/stillbox/internal/forms"
@ -38,14 +39,36 @@ func (rt ShareRequestType) IsValid() bool {
type ShareHandlers map[shares.EntityType]http.Handler type ShareHandlers map[shares.EntityType]http.Handler
type shareAPI struct { type shareAPI struct {
baseURL *url.URL baseURL *url.URL
router http.Handler
} }
func newShareAPI(baseURL *url.URL, hand http.Handler) publicAPI { type ShareAPI interface {
API
ShareMiddleware() func(http.Handler) http.Handler
}
func newShareAPI(baseURL *url.URL) ShareAPI {
return &shareAPI{ return &shareAPI{
baseURL: baseURL, baseURL: baseURL,
router: hand, }
}
func (a *shareAPI) ShareMiddleware() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
if !strings.HasPrefix(r.URL.Path, "/share/") {
next.ServeHTTP(w, r)
return
}
nr, err := a.getShare(r)
if err != nil {
wErr(w, r, autoError(err))
return
}
next.ServeHTTP(w, nr)
}
return http.HandlerFunc(fn)
} }
} }
@ -58,14 +81,10 @@ func (sa *shareAPI) Subrouter() http.Handler {
return r return r
} }
func (sa *shareAPI) PublicRouter() http.Handler { //func (sa *shareAPI) PublicRoutes(r chi.Router) http.Handler {
r := chi.NewMux() // r.Get(`/{type}/{id:[A-Za-z0-9_-]{20,}}`, sa.getShare)
// r.Get(`/{type}/{id:[A-Za-z0-9_-]{20,}}*`, sa.getShare)
r.Get(`/{type}/{id:[A-Za-z0-9_-]{20,}}`, sa.getShare) //}
r.Get(`/{type}/{id:[A-Za-z0-9_-]{20,}}*`, sa.getShare)
return r
}
func (sa *shareAPI) createShare(w http.ResponseWriter, r *http.Request) { func (sa *shareAPI) createShare(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
@ -91,28 +110,34 @@ func (sa *shareAPI) createShare(w http.ResponseWriter, r *http.Request) {
func (sa *shareAPI) deleteShare(w http.ResponseWriter, r *http.Request) { func (sa *shareAPI) deleteShare(w http.ResponseWriter, r *http.Request) {
} }
func (sa *shareAPI) getShare(w http.ResponseWriter, r *http.Request) { func (sa *shareAPI) getShare(r *http.Request) (*http.Request, error) {
ctx := r.Context() ctx := r.Context()
shs := shares.FromCtx(ctx) shs := shares.FromCtx(ctx)
rType, id, err := shareParams(w, r) params := struct {
Type string `param:"type"`
ID string `param:"shareId"`
}{}
err := decodeParams(&params, r)
if err != nil { if err != nil {
return return nil, err
} }
rType := ShareRequestType(params.Type)
id := params.ID
if !rType.IsValid() { if !rType.IsValid() {
wErr(w, r, autoError(ErrBadShare)) return nil, ErrBadShare
} }
sh, err := shs.GetShare(ctx, id) sh, err := shs.GetShare(ctx, id)
if err != nil { if err != nil {
wErr(w, r, autoError(err)) return nil, err
return
} }
if sh.Expiration != nil && sh.Expiration.Time().Before(time.Now()) { if sh.Expiration != nil && sh.Expiration.Time().Before(time.Now()) {
wErr(w, r, autoError(shares.ErrNoShare)) return nil, shares.ErrNoShare
return
} }
ctx = rbac.CtxWithSubject(ctx, sh) ctx = rbac.CtxWithSubject(ctx, sh)
@ -120,12 +145,12 @@ func (sa *shareAPI) getShare(w http.ResponseWriter, r *http.Request) {
switch rType { switch rType {
case ShareRequestCall, ShareRequestIncident: case ShareRequestCall, ShareRequestIncident:
r.URL.Path = fmt.Sprintf("/%s/%s", rType, sh.EntityID.String()) r.URL.Path += fmt.Sprintf("/%s/%s", rType, sh.EntityID.String())
case ShareRequestIncidentM3U: case ShareRequestIncidentM3U:
r.URL.Path = fmt.Sprintf("/incident/%s.m3u", sh.EntityID.String()) r.URL.Path = fmt.Sprintf("/incident/%s.m3u", sh.EntityID.String())
} }
sa.router.ServeHTTP(w, r) return r, nil
} }
// idOnlyParam checks for a sole URL parameter, id, and writes an errorif this fails. // idOnlyParam checks for a sole URL parameter, id, and writes an errorif this fails.

View file

@ -2,6 +2,7 @@ package server
import ( import (
"errors" "errors"
"fmt"
"io/fs" "io/fs"
"net/http" "net/http"
"path" "path"
@ -30,6 +31,7 @@ func (s *Server) setupRoutes() {
s.installPprof() s.installPprof()
r.Group(func(r chi.Router) { r.Group(func(r chi.Router) {
// authenticated routes // authenticated routes
r.Use(s.auth.VerifyMiddleware(), s.auth.AuthMiddleware()) r.Use(s.auth.VerifyMiddleware(), s.auth.AuthMiddleware())
@ -39,6 +41,11 @@ func (s *Server) setupRoutes() {
r.Mount("/api", s.rest.Subrouter()) r.Mount("/api", s.rest.Subrouter())
}) })
r.Route("/share/{type}/{shareId:[A-Za-z0-9_-]{20,}}", func(r chi.Router) {
r.Use(s.rest.Shares().ShareMiddleware())
s.rest.ShareSubroutes(r)
})
r.Group(func(r chi.Router) { r.Group(func(r chi.Router) {
s.rateLimit(r) s.rateLimit(r)
r.Use(render.SetContentType(render.ContentTypeJSON)) r.Use(render.SetContentType(render.ContentTypeJSON))
@ -51,7 +58,6 @@ func (s *Server) setupRoutes() {
s.rateLimit(r) s.rateLimit(r)
r.Use(render.SetContentType(render.ContentTypeJSON)) r.Use(render.SetContentType(render.ContentTypeJSON))
s.auth.PublicRoutes(r) s.auth.PublicRoutes(r)
s.rest.PublicRoutes(r)
}) })
r.Group(func(r chi.Router) { r.Group(func(r chi.Router) {
@ -62,6 +68,10 @@ func (s *Server) setupRoutes() {
s.clientRoute(r, clientRoot) s.clientRoute(r, clientRoot)
}) })
chi.Walk(r, func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error {
fmt.Printf("[%s]: '%s' has %d middlewares\n", method, route, len(middlewares))
return nil
})
} }
// WithCtxStores is a middleware that installs all stores in the request context. // WithCtxStores is a middleware that installs all stores in the request context.

View file

@ -45,7 +45,7 @@ type Server struct {
notifier notify.Notifier notifier notify.Notifier
hup chan os.Signal hup chan os.Signal
tgs tgstore.Store tgs tgstore.Store
rest rest.PublicAPI rest rest.APIRoot
partman partman.PartitionManager partman partman.PartitionManager
users users.Store users users.Store
calls callstore.Store calls callstore.Store