2024-11-10 14:44:52 -05:00
|
|
|
package rest
|
2024-11-04 11:15:24 -05:00
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
|
|
|
"net/http"
|
2024-12-29 15:08:06 -05:00
|
|
|
"net/url"
|
2024-11-04 11:15:24 -05:00
|
|
|
|
2024-12-29 15:24:40 -05:00
|
|
|
"dynatron.me/x/stillbox/internal/common"
|
2025-01-18 17:22:08 -05:00
|
|
|
"dynatron.me/x/stillbox/pkg/rbac"
|
2025-01-19 21:51:39 -05:00
|
|
|
"dynatron.me/x/stillbox/pkg/shares"
|
2024-11-20 19:59:24 -05:00
|
|
|
"dynatron.me/x/stillbox/pkg/talkgroups/tgstore"
|
2024-11-04 11:15:24 -05:00
|
|
|
|
|
|
|
"github.com/go-chi/chi/v5"
|
2024-11-10 14:40:50 -05:00
|
|
|
"github.com/go-chi/render"
|
2024-11-04 11:15:24 -05:00
|
|
|
"github.com/go-viper/mapstructure/v2"
|
2024-12-29 09:30:24 -05:00
|
|
|
"github.com/google/uuid"
|
2024-11-04 11:15:24 -05:00
|
|
|
"github.com/jackc/pgx/v5"
|
|
|
|
"github.com/rs/zerolog/log"
|
|
|
|
)
|
|
|
|
|
|
|
|
type API interface {
|
|
|
|
Subrouter() http.Handler
|
|
|
|
}
|
|
|
|
|
2025-01-20 20:28:25 -05:00
|
|
|
type APIRoot interface {
|
2025-01-19 21:51:39 -05:00
|
|
|
API
|
2025-01-20 20:28:25 -05:00
|
|
|
ShareRouter() http.Handler
|
2025-01-19 21:51:39 -05:00
|
|
|
}
|
|
|
|
|
2024-11-04 11:15:24 -05:00
|
|
|
type api struct {
|
2025-01-20 20:28:25 -05:00
|
|
|
baseURL *url.URL
|
|
|
|
shares *shareAPI
|
|
|
|
tgs *talkgroupAPI
|
|
|
|
calls *callsAPI
|
|
|
|
users *usersAPI
|
|
|
|
incidents *incidentsAPI
|
2025-01-19 21:51:39 -05:00
|
|
|
}
|
|
|
|
|
2025-01-20 20:28:25 -05:00
|
|
|
func (a *api) ShareRouter() http.Handler {
|
|
|
|
return a.shares.RootRouter()
|
2024-11-04 11:15:24 -05:00
|
|
|
}
|
|
|
|
|
2024-12-29 15:08:06 -05:00
|
|
|
func New(baseURL url.URL) *api {
|
2025-01-19 21:51:39 -05:00
|
|
|
s := &api{
|
2025-01-20 20:28:25 -05:00
|
|
|
baseURL: &baseURL,
|
|
|
|
tgs: new(talkgroupAPI),
|
|
|
|
calls: new(callsAPI),
|
2025-01-20 16:31:43 -05:00
|
|
|
incidents: newIncidentsAPI(&baseURL),
|
2025-01-20 20:28:25 -05:00
|
|
|
users: new(usersAPI),
|
2025-01-19 21:51:39 -05:00
|
|
|
}
|
2025-01-20 20:28:25 -05:00
|
|
|
s.shares = newShareAPI(&baseURL,
|
|
|
|
ShareHandlers{
|
|
|
|
ShareRequestCall: s.calls.shareCallRoute,
|
|
|
|
ShareRequestCallDL: s.calls.shareCallDLRoute,
|
|
|
|
ShareRequestIncident: s.incidents.getIncident,
|
|
|
|
ShareRequestIncidentM3U: s.incidents.getCallsM3U,
|
2025-01-22 14:15:53 -05:00
|
|
|
ShareRequestTalkgroups: s.tgs.getTGsShareRoute,
|
2025-01-20 20:28:25 -05:00
|
|
|
},
|
|
|
|
)
|
2024-11-04 11:15:24 -05:00
|
|
|
|
|
|
|
return s
|
|
|
|
}
|
|
|
|
|
|
|
|
func (a *api) Subrouter() http.Handler {
|
|
|
|
r := chi.NewMux()
|
|
|
|
|
2025-01-20 16:31:43 -05:00
|
|
|
r.Mount("/talkgroup", a.tgs.Subrouter())
|
|
|
|
r.Mount("/user", a.users.Subrouter())
|
|
|
|
r.Mount("/call", a.calls.Subrouter())
|
|
|
|
r.Mount("/incident", a.incidents.Subrouter())
|
|
|
|
r.Mount("/share", a.shares.Subrouter())
|
2024-11-09 13:14:31 -05:00
|
|
|
|
2024-11-04 11:15:24 -05:00
|
|
|
return r
|
|
|
|
}
|
|
|
|
|
2024-11-10 10:13:38 -05:00
|
|
|
type errResponse struct {
|
2024-11-10 14:40:50 -05:00
|
|
|
Err error `json:"-"`
|
|
|
|
Code int `json:"-"`
|
|
|
|
Error string `json:"error"`
|
2024-11-04 11:15:24 -05:00
|
|
|
}
|
|
|
|
|
2024-11-10 14:40:50 -05:00
|
|
|
func (e *errResponse) Render(w http.ResponseWriter, r *http.Request) error {
|
|
|
|
switch e.Code {
|
|
|
|
default:
|
|
|
|
log.Error().Str("path", r.URL.Path).Err(e.Err).Int("code", e.Code).Str("msg", e.Error).Msg("request failed")
|
|
|
|
}
|
|
|
|
|
|
|
|
render.Status(r, e.Code)
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func badRequest(err error) render.Renderer {
|
|
|
|
return &errResponse{
|
|
|
|
Err: err,
|
|
|
|
Code: http.StatusBadRequest,
|
|
|
|
Error: "Bad request",
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-11-24 00:06:43 -05:00
|
|
|
func badRequestErrText(err error) render.Renderer {
|
|
|
|
return &errResponse{
|
|
|
|
Err: err,
|
|
|
|
Code: http.StatusBadRequest,
|
|
|
|
Error: "Bad request: " + err.Error(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-12-16 13:34:07 -05:00
|
|
|
func unauthErrText(err error) render.Renderer {
|
|
|
|
return &errResponse{
|
|
|
|
Err: err,
|
|
|
|
Code: http.StatusUnauthorized,
|
|
|
|
Error: "Unauthorized: " + err.Error(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-01-18 17:22:08 -05:00
|
|
|
func forbiddenErrText(err error) render.Renderer {
|
|
|
|
return &errResponse{
|
|
|
|
Err: err,
|
|
|
|
Code: http.StatusForbidden,
|
|
|
|
Error: "Forbidden: " + err.Error(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-12-10 19:10:25 -05:00
|
|
|
func constraintErrText(err error) render.Renderer {
|
|
|
|
return &errResponse{
|
|
|
|
Err: err,
|
|
|
|
Code: http.StatusConflict,
|
|
|
|
Error: "Constraint violation: " + err.Error(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-11-10 14:40:50 -05:00
|
|
|
func recordNotFound(err error) render.Renderer {
|
|
|
|
return &errResponse{
|
|
|
|
Err: err,
|
|
|
|
Code: http.StatusNotFound,
|
|
|
|
Error: "Record not found",
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-11-24 00:06:43 -05:00
|
|
|
func notFoundErrText(err error) render.Renderer {
|
2024-11-20 16:51:19 -05:00
|
|
|
return &errResponse{
|
|
|
|
Err: err,
|
|
|
|
Code: http.StatusNotFound,
|
|
|
|
Error: "Record not found: " + err.Error(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-11-10 14:40:50 -05:00
|
|
|
func internalError(err error) render.Renderer {
|
|
|
|
return &errResponse{
|
|
|
|
Err: err,
|
2024-11-20 07:26:59 -05:00
|
|
|
Code: http.StatusInternalServerError,
|
2024-11-10 14:40:50 -05:00
|
|
|
Error: "Internal server error",
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
type errResponder func(error) render.Renderer
|
|
|
|
|
|
|
|
var statusMapping = map[error]errResponder{
|
2024-11-24 00:06:43 -05:00
|
|
|
tgstore.ErrNoSuchSystem: notFoundErrText,
|
|
|
|
tgstore.ErrNotFound: notFoundErrText,
|
|
|
|
tgstore.ErrInvalidOrderBy: badRequestErrText,
|
2024-12-17 21:17:10 -05:00
|
|
|
tgstore.ErrBadDirection: badRequestErrText,
|
|
|
|
tgstore.ErrBadOrder: badRequestErrText,
|
2024-11-24 00:06:43 -05:00
|
|
|
pgx.ErrNoRows: recordNotFound,
|
2024-12-10 19:10:25 -05:00
|
|
|
ErrMissingTGSys: badRequestErrText,
|
|
|
|
ErrTGIDMismatch: badRequestErrText,
|
|
|
|
ErrSysMismatch: badRequestErrText,
|
|
|
|
tgstore.ErrReference: constraintErrText,
|
2025-01-18 17:22:08 -05:00
|
|
|
rbac.ErrBadSubject: unauthErrText,
|
2024-12-16 13:34:07 -05:00
|
|
|
ErrBadAppName: unauthErrText,
|
2024-12-29 15:24:40 -05:00
|
|
|
common.ErrPageOutOfRange: badRequestErrText,
|
2025-01-18 17:22:08 -05:00
|
|
|
rbac.ErrNotAuthorized: unauthErrText,
|
2025-01-19 21:51:39 -05:00
|
|
|
shares.ErrNoShare: notFoundErrText,
|
|
|
|
ErrBadShare: notFoundErrText,
|
|
|
|
shares.ErrBadType: badRequestErrText,
|
2024-11-10 10:13:38 -05:00
|
|
|
}
|
|
|
|
|
2024-11-10 14:40:50 -05:00
|
|
|
func autoError(err error) render.Renderer {
|
2024-11-04 11:15:24 -05:00
|
|
|
c, ok := statusMapping[err]
|
|
|
|
if ok {
|
2024-11-10 14:40:50 -05:00
|
|
|
c(err)
|
2024-11-04 11:15:24 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
for e, c := range statusMapping { // check if err wraps an error we know about
|
|
|
|
if errors.Is(err, e) {
|
2024-11-10 14:40:50 -05:00
|
|
|
return c(err)
|
2024-11-04 11:15:24 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-01-18 17:22:08 -05:00
|
|
|
if rbac.ErrAccessDenied(err) != nil {
|
|
|
|
return forbiddenErrText(err)
|
|
|
|
}
|
|
|
|
|
2024-11-10 14:40:50 -05:00
|
|
|
return internalError(err)
|
2024-11-04 11:15:24 -05:00
|
|
|
}
|
|
|
|
|
2024-11-10 14:40:50 -05:00
|
|
|
func wErr(w http.ResponseWriter, r *http.Request, v render.Renderer) {
|
|
|
|
err := render.Render(w, r, v)
|
2024-11-04 11:15:24 -05:00
|
|
|
if err != nil {
|
2024-11-10 14:40:50 -05:00
|
|
|
log.Error().Err(err).Msg("wErr render error")
|
2024-11-04 11:15:24 -05:00
|
|
|
}
|
2024-11-09 21:04:16 -05:00
|
|
|
}
|
|
|
|
|
2024-11-04 11:15:24 -05:00
|
|
|
func decodeParams(d interface{}, r *http.Request) error {
|
|
|
|
params := chi.RouteContext(r.Context()).URLParams
|
|
|
|
m := make(map[string]string, len(params.Keys))
|
|
|
|
|
|
|
|
for i, k := range params.Keys {
|
|
|
|
m[k] = params.Values[i]
|
|
|
|
}
|
|
|
|
|
|
|
|
dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
|
2024-11-04 23:41:52 -05:00
|
|
|
Metadata: nil,
|
|
|
|
Result: d,
|
|
|
|
TagName: "param",
|
2024-11-04 11:15:24 -05:00
|
|
|
WeaklyTypedInput: true,
|
2024-12-02 17:53:43 -05:00
|
|
|
DecodeHook: mapstructure.ComposeDecodeHookFunc(
|
|
|
|
mapstructure.StringToTimeDurationHookFunc(),
|
|
|
|
mapstructure.TextUnmarshallerHookFunc(),
|
|
|
|
),
|
2024-11-04 11:15:24 -05:00
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return dec.Decode(m)
|
|
|
|
}
|
|
|
|
|
2024-12-29 09:30:24 -05:00
|
|
|
// idOnlyParam checks for a sole URL parameter, id, and writes an errorif this fails.
|
|
|
|
func idOnlyParam(w http.ResponseWriter, r *http.Request) (uuid.UUID, error) {
|
|
|
|
params := struct {
|
|
|
|
ID uuid.UUID `param:"id"`
|
|
|
|
}{}
|
|
|
|
|
|
|
|
err := decodeParams(¶ms, r)
|
|
|
|
if err != nil {
|
|
|
|
wErr(w, r, badRequest(err))
|
|
|
|
return uuid.UUID{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return params.ID, nil
|
|
|
|
}
|
|
|
|
|
2024-11-10 14:40:50 -05:00
|
|
|
func respond(w http.ResponseWriter, r *http.Request, v interface{}) {
|
|
|
|
render.DefaultResponder(w, r, v)
|
2024-11-04 11:15:24 -05:00
|
|
|
}
|