stillbox/pkg/rest/api.go

239 lines
5.1 KiB
Go
Raw Normal View History

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"
"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-19 21:51:39 -05:00
type PublicAPI interface {
API
PublicRoutes(r chi.Router)
}
2024-11-04 11:15:24 -05:00
type api struct {
2024-12-29 15:08:06 -05:00
baseURL url.URL
2025-01-19 21:51:39 -05:00
share publicAPI
}
type publicAPI interface {
API
PublicRouter() http.Handler
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{
baseURL: baseURL,
}
2024-11-04 11:15:24 -05:00
return s
}
2025-01-19 21:51:39 -05:00
func (a *api) PublicRoutes(r chi.Router) {
r.Mount("/share", a.share.PublicRouter())
}
2024-11-04 11:15:24 -05:00
func (a *api) Subrouter() http.Handler {
r := chi.NewMux()
2024-11-10 14:40:50 -05:00
r.Mount("/talkgroup", new(talkgroupAPI).Subrouter())
r.Mount("/user", new(usersAPI).Subrouter())
2025-01-19 21:51:39 -05:00
r.Mount("/call", new(callsAPI).Subrouter())
2024-12-29 15:08:06 -05:00
r.Mount("/incident", newIncidentsAPI(&a.baseURL).Subrouter())
2025-01-19 21:51:39 -05:00
a.share = newShareAPI(&a.baseURL, r)
r.Mount("/share", a.share.Subrouter())
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(),
}
}
func unauthErrText(err error) render.Renderer {
return &errResponse{
Err: err,
Code: http.StatusUnauthorized,
Error: "Unauthorized: " + err.Error(),
}
}
func forbiddenErrText(err error) render.Renderer {
return &errResponse{
Err: err,
Code: http.StatusForbidden,
Error: "Forbidden: " + err.Error(),
}
}
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,
tgstore.ErrBadDirection: badRequestErrText,
tgstore.ErrBadOrder: badRequestErrText,
2024-11-24 00:06:43 -05:00
pgx.ErrNoRows: recordNotFound,
ErrMissingTGSys: badRequestErrText,
ErrTGIDMismatch: badRequestErrText,
ErrSysMismatch: badRequestErrText,
tgstore.ErrReference: constraintErrText,
rbac.ErrBadSubject: unauthErrText,
ErrBadAppName: unauthErrText,
2024-12-29 15:24:40 -05:00
common.ErrPageOutOfRange: badRequestErrText,
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
}
}
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-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,
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(&params, 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
}