rework API to use external server package

This commit is contained in:
Hayden 2023-03-20 20:27:50 -08:00
parent 2926fa27ca
commit c95e3f2f96
No known key found for this signature in database
GPG key ID: 17CF79474E257545
47 changed files with 387 additions and 1377 deletions

View file

@ -8,7 +8,7 @@ import (
"github.com/hay-kot/homebox/backend/internal/data/repo"
"github.com/hay-kot/homebox/backend/internal/sys/config"
"github.com/hay-kot/homebox/backend/pkgs/mailer"
"github.com/hay-kot/homebox/backend/pkgs/server"
"github.com/hay-kot/safeserve/server"
)
type app struct {

View file

@ -5,9 +5,18 @@ import (
"github.com/hay-kot/homebox/backend/internal/core/services"
"github.com/hay-kot/homebox/backend/internal/data/repo"
"github.com/hay-kot/homebox/backend/pkgs/server"
"github.com/hay-kot/safeserve/errchain"
"github.com/hay-kot/safeserve/server"
)
type Wrapped struct {
Item interface{} `json:"item"`
}
func Wrap(v any) Wrapped {
return Wrapped{Item: v}
}
func WithMaxUploadSize(maxUploadSize int64) func(*V1Controller) {
return func(ctrl *V1Controller) {
ctrl.maxUploadSize = maxUploadSize
@ -81,9 +90,9 @@ func NewControllerV1(svc *services.AllServices, repos *repo.AllRepos, options ..
// @Produce json
// @Success 200 {object} ApiSummary
// @Router /v1/status [GET]
func (ctrl *V1Controller) HandleBase(ready ReadyFunc, build Build) server.HandlerFunc {
func (ctrl *V1Controller) HandleBase(ready ReadyFunc, build Build) errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
return server.Respond(w, http.StatusOK, ApiSummary{
return server.JSON(w, http.StatusOK, ApiSummary{
Healthy: ready(),
Title: "Homebox",
Message: "Track, Manage, and Organize your shit",

View file

@ -7,7 +7,8 @@ import (
"github.com/google/uuid"
"github.com/hay-kot/homebox/backend/internal/core/services"
"github.com/hay-kot/homebox/backend/internal/sys/validate"
"github.com/hay-kot/homebox/backend/pkgs/server"
"github.com/hay-kot/safeserve/errchain"
"github.com/hay-kot/safeserve/server"
"github.com/rs/zerolog/log"
)
@ -15,7 +16,7 @@ type ActionAmountResult struct {
Completed int `json:"completed"`
}
func actionHandlerFactory(ref string, fn func(context.Context, uuid.UUID) (int, error)) server.HandlerFunc {
func actionHandlerFactory(ref string, fn func(context.Context, uuid.UUID) (int, error)) errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
ctx := services.NewContext(r.Context())
@ -25,7 +26,7 @@ func actionHandlerFactory(ref string, fn func(context.Context, uuid.UUID) (int,
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusOK, ActionAmountResult{Completed: totalCompleted})
return server.JSON(w, http.StatusOK, ActionAmountResult{Completed: totalCompleted})
}
}
@ -38,7 +39,7 @@ func actionHandlerFactory(ref string, fn func(context.Context, uuid.UUID) (int,
// @Success 200 {object} ActionAmountResult
// @Router /v1/actions/ensure-asset-ids [Post]
// @Security Bearer
func (ctrl *V1Controller) HandleEnsureAssetID() server.HandlerFunc {
func (ctrl *V1Controller) HandleEnsureAssetID() errchain.HandlerFunc {
return actionHandlerFactory("ensure asset IDs", ctrl.svc.Items.EnsureAssetID)
}
@ -51,7 +52,7 @@ func (ctrl *V1Controller) HandleEnsureAssetID() server.HandlerFunc {
// @Success 200 {object} ActionAmountResult
// @Router /v1/actions/ensure-import-refs [Post]
// @Security Bearer
func (ctrl *V1Controller) HandleEnsureImportRefs() server.HandlerFunc {
func (ctrl *V1Controller) HandleEnsureImportRefs() errchain.HandlerFunc {
return actionHandlerFactory("ensure import refs", ctrl.svc.Items.EnsureImportRef)
}
@ -64,6 +65,6 @@ func (ctrl *V1Controller) HandleEnsureImportRefs() server.HandlerFunc {
// @Success 200 {object} ActionAmountResult
// @Router /v1/actions/zero-item-time-fields [Post]
// @Security Bearer
func (ctrl *V1Controller) HandleItemDateZeroOut() server.HandlerFunc {
func (ctrl *V1Controller) HandleItemDateZeroOut() errchain.HandlerFunc {
return actionHandlerFactory("zero out date time", ctrl.repo.Items.ZeroOutTimeFields)
}

View file

@ -9,7 +9,8 @@ import (
"github.com/hay-kot/homebox/backend/internal/core/services"
"github.com/hay-kot/homebox/backend/internal/data/repo"
"github.com/hay-kot/homebox/backend/internal/sys/validate"
"github.com/hay-kot/homebox/backend/pkgs/server"
"github.com/hay-kot/safeserve/errchain"
"github.com/hay-kot/safeserve/server"
"github.com/rs/zerolog/log"
)
@ -23,7 +24,7 @@ import (
// @Success 200 {object} repo.PaginationResult[repo.ItemSummary]{}
// @Router /v1/assets/{id} [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleAssetGet() server.HandlerFunc {
func (ctrl *V1Controller) HandleAssetGet() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
ctx := services.NewContext(r.Context())
assetIdParam := chi.URLParam(r, "id")
@ -38,7 +39,7 @@ func (ctrl *V1Controller) HandleAssetGet() server.HandlerFunc {
if pageParam != "" {
page, err = strconv.ParseInt(pageParam, 10, 64)
if err != nil {
return server.Respond(w, http.StatusBadRequest, "Invalid page number")
return server.JSON(w, http.StatusBadRequest, "Invalid page number")
}
}
@ -47,7 +48,7 @@ func (ctrl *V1Controller) HandleAssetGet() server.HandlerFunc {
if pageSizeParam != "" {
pageSize, err = strconv.ParseInt(pageSizeParam, 10, 64)
if err != nil {
return server.Respond(w, http.StatusBadRequest, "Invalid page size")
return server.JSON(w, http.StatusBadRequest, "Invalid page size")
}
}
@ -56,6 +57,6 @@ func (ctrl *V1Controller) HandleAssetGet() server.HandlerFunc {
log.Err(err).Msg("failed to get item")
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusOK, items)
return server.JSON(w, http.StatusOK, items)
}
}

View file

@ -8,7 +8,8 @@ import (
"github.com/hay-kot/homebox/backend/internal/core/services"
"github.com/hay-kot/homebox/backend/internal/sys/validate"
"github.com/hay-kot/homebox/backend/pkgs/server"
"github.com/hay-kot/safeserve/errchain"
"github.com/hay-kot/safeserve/server"
"github.com/rs/zerolog/log"
)
@ -36,26 +37,27 @@ type (
// @Produce json
// @Success 200 {object} TokenResponse
// @Router /v1/users/login [POST]
func (ctrl *V1Controller) HandleAuthLogin() server.HandlerFunc {
func (ctrl *V1Controller) HandleAuthLogin() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
loginForm := &LoginForm{}
switch r.Header.Get("Content-Type") {
case server.ContentFormUrlEncoded:
case "application/x-www-form-urlencoded":
err := r.ParseForm()
if err != nil {
return server.Respond(w, http.StatusBadRequest, server.Wrap(err))
return errors.New("failed to parse form")
}
loginForm.Username = r.PostFormValue("username")
loginForm.Password = r.PostFormValue("password")
case server.ContentJSON:
case "application/json":
err := server.Decode(r, loginForm)
if err != nil {
log.Err(err).Msg("failed to decode login form")
return errors.New("failed to decode login form")
}
default:
return server.Respond(w, http.StatusBadRequest, errors.New("invalid content type"))
return server.JSON(w, http.StatusBadRequest, errors.New("invalid content type"))
}
if loginForm.Username == "" || loginForm.Password == "" {
@ -76,7 +78,7 @@ func (ctrl *V1Controller) HandleAuthLogin() server.HandlerFunc {
return validate.NewRequestError(errors.New("authentication failed"), http.StatusInternalServerError)
}
return server.Respond(w, http.StatusOK, TokenResponse{
return server.JSON(w, http.StatusOK, TokenResponse{
Token: "Bearer " + newToken.Raw,
ExpiresAt: newToken.ExpiresAt,
AttachmentToken: newToken.AttachmentToken,
@ -91,7 +93,7 @@ func (ctrl *V1Controller) HandleAuthLogin() server.HandlerFunc {
// @Success 204
// @Router /v1/users/logout [POST]
// @Security Bearer
func (ctrl *V1Controller) HandleAuthLogout() server.HandlerFunc {
func (ctrl *V1Controller) HandleAuthLogout() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
token := services.UseTokenCtx(r.Context())
if token == "" {
@ -103,7 +105,7 @@ func (ctrl *V1Controller) HandleAuthLogout() server.HandlerFunc {
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusNoContent, nil)
return server.JSON(w, http.StatusNoContent, nil)
}
}
@ -116,7 +118,7 @@ func (ctrl *V1Controller) HandleAuthLogout() server.HandlerFunc {
// @Success 200
// @Router /v1/users/refresh [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleAuthRefresh() server.HandlerFunc {
func (ctrl *V1Controller) HandleAuthRefresh() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
requestToken := services.UseTokenCtx(r.Context())
if requestToken == "" {
@ -128,6 +130,6 @@ func (ctrl *V1Controller) HandleAuthRefresh() server.HandlerFunc {
return validate.NewUnauthorizedError()
}
return server.Respond(w, http.StatusOK, newToken)
return server.JSON(w, http.StatusOK, newToken)
}
}

View file

@ -7,7 +7,7 @@ import (
"github.com/hay-kot/homebox/backend/internal/core/services"
"github.com/hay-kot/homebox/backend/internal/data/repo"
"github.com/hay-kot/homebox/backend/internal/web/adapters"
"github.com/hay-kot/homebox/backend/pkgs/server"
"github.com/hay-kot/safeserve/errchain"
)
type (
@ -31,7 +31,7 @@ type (
// @Success 200 {object} repo.Group
// @Router /v1/groups [Get]
// @Security Bearer
func (ctrl *V1Controller) HandleGroupGet() server.HandlerFunc {
func (ctrl *V1Controller) HandleGroupGet() errchain.HandlerFunc {
fn := func(r *http.Request) (repo.Group, error) {
auth := services.NewContext(r.Context())
return ctrl.repo.Groups.GroupByID(auth, auth.GID)
@ -49,7 +49,7 @@ func (ctrl *V1Controller) HandleGroupGet() server.HandlerFunc {
// @Success 200 {object} repo.Group
// @Router /v1/groups [Put]
// @Security Bearer
func (ctrl *V1Controller) HandleGroupUpdate() server.HandlerFunc {
func (ctrl *V1Controller) HandleGroupUpdate() errchain.HandlerFunc {
fn := func(r *http.Request, body repo.GroupUpdate) (repo.Group, error) {
auth := services.NewContext(r.Context())
return ctrl.svc.Group.UpdateGroup(auth, body)
@ -67,7 +67,7 @@ func (ctrl *V1Controller) HandleGroupUpdate() server.HandlerFunc {
// @Success 200 {object} GroupInvitation
// @Router /v1/groups/invitations [Post]
// @Security Bearer
func (ctrl *V1Controller) HandleGroupInvitationsCreate() server.HandlerFunc {
func (ctrl *V1Controller) HandleGroupInvitationsCreate() errchain.HandlerFunc {
fn := func(r *http.Request, body GroupInvitationCreate) (GroupInvitation, error) {
if body.ExpiresAt.IsZero() {
body.ExpiresAt = time.Now().Add(time.Hour * 24)

View file

@ -12,7 +12,8 @@ import (
"github.com/hay-kot/homebox/backend/internal/data/repo"
"github.com/hay-kot/homebox/backend/internal/sys/validate"
"github.com/hay-kot/homebox/backend/internal/web/adapters"
"github.com/hay-kot/homebox/backend/pkgs/server"
"github.com/hay-kot/safeserve/errchain"
"github.com/hay-kot/safeserve/server"
"github.com/rs/zerolog/log"
)
@ -29,7 +30,7 @@ import (
// @Success 200 {object} repo.PaginationResult[repo.ItemSummary]{}
// @Router /v1/items [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleItemsGetAll() server.HandlerFunc {
func (ctrl *V1Controller) HandleItemsGetAll() errchain.HandlerFunc {
extractQuery := func(r *http.Request) repo.ItemQuery {
params := r.URL.Query()
@ -78,14 +79,14 @@ func (ctrl *V1Controller) HandleItemsGetAll() server.HandlerFunc {
items, err := ctrl.repo.Items.QueryByGroup(ctx, ctx.GID, extractQuery(r))
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return server.Respond(w, http.StatusOK, repo.PaginationResult[repo.ItemSummary]{
return server.JSON(w, http.StatusOK, repo.PaginationResult[repo.ItemSummary]{
Items: []repo.ItemSummary{},
})
}
log.Err(err).Msg("failed to get items")
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusOK, items)
return server.JSON(w, http.StatusOK, items)
}
}
@ -98,7 +99,7 @@ func (ctrl *V1Controller) HandleItemsGetAll() server.HandlerFunc {
// @Success 201 {object} repo.ItemSummary
// @Router /v1/items [POST]
// @Security Bearer
func (ctrl *V1Controller) HandleItemsCreate() server.HandlerFunc {
func (ctrl *V1Controller) HandleItemsCreate() errchain.HandlerFunc {
fn := func(r *http.Request, body repo.ItemCreate) (repo.ItemOut, error) {
return ctrl.svc.Items.Create(services.NewContext(r.Context()), body)
}
@ -115,7 +116,7 @@ func (ctrl *V1Controller) HandleItemsCreate() server.HandlerFunc {
// @Success 200 {object} repo.ItemOut
// @Router /v1/items/{id} [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleItemGet() server.HandlerFunc {
func (ctrl *V1Controller) HandleItemGet() errchain.HandlerFunc {
fn := func(r *http.Request, ID uuid.UUID) (repo.ItemOut, error) {
auth := services.NewContext(r.Context())
@ -134,7 +135,7 @@ func (ctrl *V1Controller) HandleItemGet() server.HandlerFunc {
// @Success 204
// @Router /v1/items/{id} [DELETE]
// @Security Bearer
func (ctrl *V1Controller) HandleItemDelete() server.HandlerFunc {
func (ctrl *V1Controller) HandleItemDelete() errchain.HandlerFunc {
fn := func(r *http.Request, ID uuid.UUID) (any, error) {
auth := services.NewContext(r.Context())
err := ctrl.repo.Items.DeleteByGroup(auth, auth.GID, ID)
@ -154,7 +155,7 @@ func (ctrl *V1Controller) HandleItemDelete() server.HandlerFunc {
// @Success 200 {object} repo.ItemOut
// @Router /v1/items/{id} [PUT]
// @Security Bearer
func (ctrl *V1Controller) HandleItemUpdate() server.HandlerFunc {
func (ctrl *V1Controller) HandleItemUpdate() errchain.HandlerFunc {
fn := func(r *http.Request, ID uuid.UUID, body repo.ItemUpdate) (repo.ItemOut, error) {
auth := services.NewContext(r.Context())
@ -174,7 +175,7 @@ func (ctrl *V1Controller) HandleItemUpdate() server.HandlerFunc {
// @Router /v1/items/fields [GET]
// @Success 200 {object} []string
// @Security Bearer
func (ctrl *V1Controller) HandleGetAllCustomFieldNames() server.HandlerFunc {
func (ctrl *V1Controller) HandleGetAllCustomFieldNames() errchain.HandlerFunc {
fn := func(r *http.Request) ([]string, error) {
auth := services.NewContext(r.Context())
return ctrl.repo.Items.GetAllCustomFieldNames(auth, auth.GID)
@ -192,7 +193,7 @@ func (ctrl *V1Controller) HandleGetAllCustomFieldNames() server.HandlerFunc {
// @Router /v1/items/fields/values [GET]
// @Success 200 {object} []string
// @Security Bearer
func (ctrl *V1Controller) HandleGetAllCustomFieldValues() server.HandlerFunc {
func (ctrl *V1Controller) HandleGetAllCustomFieldValues() errchain.HandlerFunc {
type query struct {
Field string `schema:"field" validate:"required"`
}
@ -215,7 +216,7 @@ func (ctrl *V1Controller) HandleGetAllCustomFieldValues() server.HandlerFunc {
// @Param csv formData file true "Image to upload"
// @Router /v1/items/import [Post]
// @Security Bearer
func (ctrl *V1Controller) HandleItemsImport() server.HandlerFunc {
func (ctrl *V1Controller) HandleItemsImport() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
err := r.ParseMultipartForm(ctrl.maxUploadSize << 20)
if err != nil {
@ -237,7 +238,7 @@ func (ctrl *V1Controller) HandleItemsImport() server.HandlerFunc {
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusNoContent, nil)
return server.JSON(w, http.StatusNoContent, nil)
}
}
@ -248,7 +249,7 @@ func (ctrl *V1Controller) HandleItemsImport() server.HandlerFunc {
// @Success 200 {string} string "text/csv"
// @Router /v1/items/export [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleItemsExport() server.HandlerFunc {
func (ctrl *V1Controller) HandleItemsExport() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
ctx := services.NewContext(r.Context())

View file

@ -8,7 +8,8 @@ import (
"github.com/hay-kot/homebox/backend/internal/data/ent/attachment"
"github.com/hay-kot/homebox/backend/internal/data/repo"
"github.com/hay-kot/homebox/backend/internal/sys/validate"
"github.com/hay-kot/homebox/backend/pkgs/server"
"github.com/hay-kot/safeserve/errchain"
"github.com/hay-kot/safeserve/server"
"github.com/rs/zerolog/log"
)
@ -28,10 +29,10 @@ type (
// @Param type formData string true "Type of file"
// @Param name formData string true "name of the file including extension"
// @Success 200 {object} repo.ItemOut
// @Failure 422 {object} server.ErrorResponse
// @Failure 422 {object} mid.ErrorResponse
// @Router /v1/items/{id}/attachments [POST]
// @Security Bearer
func (ctrl *V1Controller) HandleItemAttachmentCreate() server.HandlerFunc {
func (ctrl *V1Controller) HandleItemAttachmentCreate() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
err := r.ParseMultipartForm(ctrl.maxUploadSize << 20)
if err != nil {
@ -61,7 +62,7 @@ func (ctrl *V1Controller) HandleItemAttachmentCreate() server.HandlerFunc {
}
if !errs.Nil() {
return server.Respond(w, http.StatusUnprocessableEntity, errs)
return server.JSON(w, http.StatusUnprocessableEntity, errs)
}
attachmentType := r.FormValue("type")
@ -88,7 +89,7 @@ func (ctrl *V1Controller) HandleItemAttachmentCreate() server.HandlerFunc {
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusCreated, item)
return server.JSON(w, http.StatusCreated, item)
}
}
@ -102,7 +103,7 @@ func (ctrl *V1Controller) HandleItemAttachmentCreate() server.HandlerFunc {
// @Success 200 {object} ItemAttachmentToken
// @Router /v1/items/{id}/attachments/{attachment_id} [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleItemAttachmentGet() server.HandlerFunc {
func (ctrl *V1Controller) HandleItemAttachmentGet() errchain.HandlerFunc {
return ctrl.handleItemAttachmentsHandler
}
@ -115,7 +116,7 @@ func (ctrl *V1Controller) HandleItemAttachmentGet() server.HandlerFunc {
// @Success 204
// @Router /v1/items/{id}/attachments/{attachment_id} [DELETE]
// @Security Bearer
func (ctrl *V1Controller) HandleItemAttachmentDelete() server.HandlerFunc {
func (ctrl *V1Controller) HandleItemAttachmentDelete() errchain.HandlerFunc {
return ctrl.handleItemAttachmentsHandler
}
@ -129,7 +130,7 @@ func (ctrl *V1Controller) HandleItemAttachmentDelete() server.HandlerFunc {
// @Success 200 {object} repo.ItemOut
// @Router /v1/items/{id}/attachments/{attachment_id} [PUT]
// @Security Bearer
func (ctrl *V1Controller) HandleItemAttachmentUpdate() server.HandlerFunc {
func (ctrl *V1Controller) HandleItemAttachmentUpdate() errchain.HandlerFunc {
return ctrl.handleItemAttachmentsHandler
}
@ -164,7 +165,7 @@ func (ctrl *V1Controller) handleItemAttachmentsHandler(w http.ResponseWriter, r
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusNoContent, nil)
return server.JSON(w, http.StatusNoContent, nil)
// Update Attachment Handler
case http.MethodPut:
@ -182,7 +183,7 @@ func (ctrl *V1Controller) handleItemAttachmentsHandler(w http.ResponseWriter, r
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusOK, val)
return server.JSON(w, http.StatusOK, val)
}
return nil

View file

@ -7,7 +7,7 @@ import (
"github.com/hay-kot/homebox/backend/internal/core/services"
"github.com/hay-kot/homebox/backend/internal/data/repo"
"github.com/hay-kot/homebox/backend/internal/web/adapters"
"github.com/hay-kot/homebox/backend/pkgs/server"
"github.com/hay-kot/safeserve/errchain"
)
// HandleLabelsGetAll godoc
@ -15,10 +15,10 @@ import (
// @Summary Get All Labels
// @Tags Labels
// @Produce json
// @Success 200 {object} server.Results{items=[]repo.LabelOut}
// @Success 200 {object} Wrapped{items=[]repo.LabelOut}
// @Router /v1/labels [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleLabelsGetAll() server.HandlerFunc {
func (ctrl *V1Controller) HandleLabelsGetAll() errchain.HandlerFunc {
fn := func(r *http.Request) ([]repo.LabelSummary, error) {
auth := services.NewContext(r.Context())
return ctrl.repo.Labels.GetAll(auth, auth.GID)
@ -36,7 +36,7 @@ func (ctrl *V1Controller) HandleLabelsGetAll() server.HandlerFunc {
// @Success 200 {object} repo.LabelSummary
// @Router /v1/labels [POST]
// @Security Bearer
func (ctrl *V1Controller) HandleLabelsCreate() server.HandlerFunc {
func (ctrl *V1Controller) HandleLabelsCreate() errchain.HandlerFunc {
fn := func(r *http.Request, data repo.LabelCreate) (repo.LabelOut, error) {
auth := services.NewContext(r.Context())
return ctrl.repo.Labels.Create(auth, auth.GID, data)
@ -54,7 +54,7 @@ func (ctrl *V1Controller) HandleLabelsCreate() server.HandlerFunc {
// @Success 204
// @Router /v1/labels/{id} [DELETE]
// @Security Bearer
func (ctrl *V1Controller) HandleLabelDelete() server.HandlerFunc {
func (ctrl *V1Controller) HandleLabelDelete() errchain.HandlerFunc {
fn := func(r *http.Request, ID uuid.UUID) (any, error) {
auth := services.NewContext(r.Context())
err := ctrl.repo.Labels.DeleteByGroup(auth, auth.GID, ID)
@ -73,7 +73,7 @@ func (ctrl *V1Controller) HandleLabelDelete() server.HandlerFunc {
// @Success 200 {object} repo.LabelOut
// @Router /v1/labels/{id} [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleLabelGet() server.HandlerFunc {
func (ctrl *V1Controller) HandleLabelGet() errchain.HandlerFunc {
fn := func(r *http.Request, ID uuid.UUID) (repo.LabelOut, error) {
auth := services.NewContext(r.Context())
return ctrl.repo.Labels.GetOneByGroup(auth, auth.GID, ID)
@ -91,7 +91,7 @@ func (ctrl *V1Controller) HandleLabelGet() server.HandlerFunc {
// @Success 200 {object} repo.LabelOut
// @Router /v1/labels/{id} [PUT]
// @Security Bearer
func (ctrl *V1Controller) HandleLabelUpdate() server.HandlerFunc {
func (ctrl *V1Controller) HandleLabelUpdate() errchain.HandlerFunc {
fn := func(r *http.Request, ID uuid.UUID, data repo.LabelUpdate) (repo.LabelOut, error) {
auth := services.NewContext(r.Context())
data.ID = ID

View file

@ -7,7 +7,7 @@ import (
"github.com/hay-kot/homebox/backend/internal/core/services"
"github.com/hay-kot/homebox/backend/internal/data/repo"
"github.com/hay-kot/homebox/backend/internal/web/adapters"
"github.com/hay-kot/homebox/backend/pkgs/server"
"github.com/hay-kot/safeserve/errchain"
)
// HandleLocationTreeQuery
@ -16,10 +16,10 @@ import (
// @Tags Locations
// @Produce json
// @Param withItems query bool false "include items in response tree"
// @Success 200 {object} server.Results{items=[]repo.TreeItem}
// @Success 200 {object} Wrapped{items=[]repo.TreeItem}
// @Router /v1/locations/tree [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleLocationTreeQuery() server.HandlerFunc {
func (ctrl *V1Controller) HandleLocationTreeQuery() errchain.HandlerFunc {
fn := func(r *http.Request, query repo.TreeQuery) ([]repo.TreeItem, error) {
auth := services.NewContext(r.Context())
return ctrl.repo.Locations.Tree(auth, auth.GID, query)
@ -34,10 +34,10 @@ func (ctrl *V1Controller) HandleLocationTreeQuery() server.HandlerFunc {
// @Tags Locations
// @Produce json
// @Param filterChildren query bool false "Filter locations with parents"
// @Success 200 {object} server.Results{items=[]repo.LocationOutCount}
// @Success 200 {object} Wrapped{items=[]repo.LocationOutCount}
// @Router /v1/locations [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleLocationGetAll() server.HandlerFunc {
func (ctrl *V1Controller) HandleLocationGetAll() errchain.HandlerFunc {
fn := func(r *http.Request, q repo.LocationQuery) ([]repo.LocationOutCount, error) {
auth := services.NewContext(r.Context())
return ctrl.repo.Locations.GetAll(auth, auth.GID, q)
@ -55,7 +55,7 @@ func (ctrl *V1Controller) HandleLocationGetAll() server.HandlerFunc {
// @Success 200 {object} repo.LocationSummary
// @Router /v1/locations [POST]
// @Security Bearer
func (ctrl *V1Controller) HandleLocationCreate() server.HandlerFunc {
func (ctrl *V1Controller) HandleLocationCreate() errchain.HandlerFunc {
fn := func(r *http.Request, createData repo.LocationCreate) (repo.LocationOut, error) {
auth := services.NewContext(r.Context())
return ctrl.repo.Locations.Create(auth, auth.GID, createData)
@ -73,7 +73,7 @@ func (ctrl *V1Controller) HandleLocationCreate() server.HandlerFunc {
// @Success 204
// @Router /v1/locations/{id} [DELETE]
// @Security Bearer
func (ctrl *V1Controller) HandleLocationDelete() server.HandlerFunc {
func (ctrl *V1Controller) HandleLocationDelete() errchain.HandlerFunc {
fn := func(r *http.Request, ID uuid.UUID) (any, error) {
auth := services.NewContext(r.Context())
err := ctrl.repo.Locations.DeleteByGroup(auth, auth.GID, ID)
@ -92,7 +92,7 @@ func (ctrl *V1Controller) HandleLocationDelete() server.HandlerFunc {
// @Success 200 {object} repo.LocationOut
// @Router /v1/locations/{id} [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleLocationGet() server.HandlerFunc {
func (ctrl *V1Controller) HandleLocationGet() errchain.HandlerFunc {
fn := func(r *http.Request, ID uuid.UUID) (repo.LocationOut, error) {
auth := services.NewContext(r.Context())
return ctrl.repo.Locations.GetOneByGroup(auth, auth.GID, ID)
@ -111,7 +111,7 @@ func (ctrl *V1Controller) HandleLocationGet() server.HandlerFunc {
// @Success 200 {object} repo.LocationOut
// @Router /v1/locations/{id} [PUT]
// @Security Bearer
func (ctrl *V1Controller) HandleLocationUpdate() server.HandlerFunc {
func (ctrl *V1Controller) HandleLocationUpdate() errchain.HandlerFunc {
fn := func(r *http.Request, ID uuid.UUID, body repo.LocationUpdate) (repo.LocationOut, error) {
auth := services.NewContext(r.Context())
body.ID = ID

View file

@ -7,7 +7,7 @@ import (
"github.com/hay-kot/homebox/backend/internal/core/services"
"github.com/hay-kot/homebox/backend/internal/data/repo"
"github.com/hay-kot/homebox/backend/internal/web/adapters"
"github.com/hay-kot/homebox/backend/pkgs/server"
"github.com/hay-kot/safeserve/errchain"
)
// HandleMaintenanceGetLog godoc
@ -18,7 +18,7 @@ import (
// @Success 200 {object} repo.MaintenanceLog
// @Router /v1/items/{id}/maintenance [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleMaintenanceLogGet() server.HandlerFunc {
func (ctrl *V1Controller) HandleMaintenanceLogGet() errchain.HandlerFunc {
fn := func(r *http.Request, ID uuid.UUID, q repo.MaintenanceLogQuery) (repo.MaintenanceLog, error) {
auth := services.NewContext(r.Context())
return ctrl.repo.MaintEntry.GetLog(auth, auth.GID, ID, q)
@ -36,7 +36,7 @@ func (ctrl *V1Controller) HandleMaintenanceLogGet() server.HandlerFunc {
// @Success 201 {object} repo.MaintenanceEntry
// @Router /v1/items/{id}/maintenance [POST]
// @Security Bearer
func (ctrl *V1Controller) HandleMaintenanceEntryCreate() server.HandlerFunc {
func (ctrl *V1Controller) HandleMaintenanceEntryCreate() errchain.HandlerFunc {
fn := func(r *http.Request, itemID uuid.UUID, body repo.MaintenanceEntryCreate) (repo.MaintenanceEntry, error) {
auth := services.NewContext(r.Context())
return ctrl.repo.MaintEntry.Create(auth, itemID, body)
@ -53,7 +53,7 @@ func (ctrl *V1Controller) HandleMaintenanceEntryCreate() server.HandlerFunc {
// @Success 204
// @Router /v1/items/{id}/maintenance/{entry_id} [DELETE]
// @Security Bearer
func (ctrl *V1Controller) HandleMaintenanceEntryDelete() server.HandlerFunc {
func (ctrl *V1Controller) HandleMaintenanceEntryDelete() errchain.HandlerFunc {
fn := func(r *http.Request, entryID uuid.UUID) (any, error) {
auth := services.NewContext(r.Context())
err := ctrl.repo.MaintEntry.Delete(auth, entryID)
@ -72,7 +72,7 @@ func (ctrl *V1Controller) HandleMaintenanceEntryDelete() server.HandlerFunc {
// @Success 200 {object} repo.MaintenanceEntry
// @Router /v1/items/{id}/maintenance/{entry_id} [PUT]
// @Security Bearer
func (ctrl *V1Controller) HandleMaintenanceEntryUpdate() server.HandlerFunc {
func (ctrl *V1Controller) HandleMaintenanceEntryUpdate() errchain.HandlerFunc {
fn := func(r *http.Request, entryID uuid.UUID, body repo.MaintenanceEntryUpdate) (repo.MaintenanceEntry, error) {
auth := services.NewContext(r.Context())
return ctrl.repo.MaintEntry.Update(auth, entryID, body)

View file

@ -8,7 +8,7 @@ import (
"github.com/hay-kot/homebox/backend/internal/core/services"
"github.com/hay-kot/homebox/backend/internal/data/repo"
"github.com/hay-kot/homebox/backend/internal/web/adapters"
"github.com/hay-kot/homebox/backend/pkgs/server"
"github.com/hay-kot/safeserve/errchain"
)
// HandleGetUserNotifiers godoc
@ -16,10 +16,10 @@ import (
// @Summary Get Notifiers
// @Tags Notifiers
// @Produce json
// @Success 200 {object} server.Results{items=[]repo.NotifierOut}
// @Success 200 {object} Wrapped{items=[]repo.NotifierOut}
// @Router /v1/notifiers [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleGetUserNotifiers() server.HandlerFunc {
func (ctrl *V1Controller) HandleGetUserNotifiers() errchain.HandlerFunc {
fn := func(r *http.Request, _ struct{}) ([]repo.NotifierOut, error) {
user := services.UseUserCtx(r.Context())
return ctrl.repo.Notifiers.GetByUser(r.Context(), user.ID)
@ -37,7 +37,7 @@ func (ctrl *V1Controller) HandleGetUserNotifiers() server.HandlerFunc {
// @Success 200 {object} repo.NotifierOut
// @Router /v1/notifiers [POST]
// @Security Bearer
func (ctrl *V1Controller) HandleCreateNotifier() server.HandlerFunc {
func (ctrl *V1Controller) HandleCreateNotifier() errchain.HandlerFunc {
fn := func(r *http.Request, in repo.NotifierCreate) (repo.NotifierOut, error) {
auth := services.NewContext(r.Context())
return ctrl.repo.Notifiers.Create(auth, auth.GID, auth.UID, in)
@ -54,7 +54,7 @@ func (ctrl *V1Controller) HandleCreateNotifier() server.HandlerFunc {
// @Success 204
// @Router /v1/notifiers/{id} [DELETE]
// @Security Bearer
func (ctrl *V1Controller) HandleDeleteNotifier() server.HandlerFunc {
func (ctrl *V1Controller) HandleDeleteNotifier() errchain.HandlerFunc {
fn := func(r *http.Request, ID uuid.UUID) (any, error) {
auth := services.NewContext(r.Context())
return nil, ctrl.repo.Notifiers.Delete(auth, auth.UID, ID)
@ -72,7 +72,7 @@ func (ctrl *V1Controller) HandleDeleteNotifier() server.HandlerFunc {
// @Success 200 {object} repo.NotifierOut
// @Router /v1/notifiers/{id} [PUT]
// @Security Bearer
func (ctrl *V1Controller) HandleUpdateNotifier() server.HandlerFunc {
func (ctrl *V1Controller) HandleUpdateNotifier() errchain.HandlerFunc {
fn := func(r *http.Request, ID uuid.UUID, in repo.NotifierUpdate) (repo.NotifierOut, error) {
auth := services.NewContext(r.Context())
return ctrl.repo.Notifiers.Update(auth, auth.UID, ID, in)
@ -91,7 +91,7 @@ func (ctrl *V1Controller) HandleUpdateNotifier() server.HandlerFunc {
// @Success 204
// @Router /v1/notifiers/test [POST]
// @Security Bearer
func (ctrl *V1Controller) HandlerNotifierTest() server.HandlerFunc {
func (ctrl *V1Controller) HandlerNotifierTest() errchain.HandlerFunc {
type body struct {
URL string `json:"url" validate:"required"`
}

View file

@ -7,7 +7,7 @@ import (
"net/http"
"github.com/hay-kot/homebox/backend/internal/web/adapters"
"github.com/hay-kot/homebox/backend/pkgs/server"
"github.com/hay-kot/safeserve/errchain"
"github.com/yeqown/go-qrcode/v2"
"github.com/yeqown/go-qrcode/writer/standard"
@ -26,7 +26,7 @@ var qrcodeLogo []byte
// @Success 200 {string} string "image/jpeg"
// @Router /v1/qrcode [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleGenerateQRCode() server.HandlerFunc {
func (ctrl *V1Controller) HandleGenerateQRCode() errchain.HandlerFunc {
type query struct {
// 4,296 characters is the maximum length of a QR code
Data string `schema:"data" validate:"required,max=4296"`

View file

@ -4,7 +4,7 @@ import (
"net/http"
"github.com/hay-kot/homebox/backend/internal/core/services"
"github.com/hay-kot/homebox/backend/pkgs/server"
"github.com/hay-kot/safeserve/errchain"
)
// HandleBillOfMaterialsExport godoc
@ -15,7 +15,7 @@ import (
// @Success 200 {string} string "text/csv"
// @Router /v1/reporting/bill-of-materials [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleBillOfMaterialsExport() server.HandlerFunc {
func (ctrl *V1Controller) HandleBillOfMaterialsExport() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
actor := services.UseUserCtx(r.Context())

View file

@ -8,7 +8,8 @@ import (
"github.com/hay-kot/homebox/backend/internal/data/repo"
"github.com/hay-kot/homebox/backend/internal/sys/validate"
"github.com/hay-kot/homebox/backend/internal/web/adapters"
"github.com/hay-kot/homebox/backend/pkgs/server"
"github.com/hay-kot/safeserve/errchain"
"github.com/hay-kot/safeserve/server"
)
// HandleGroupGet godoc
@ -19,7 +20,7 @@ import (
// @Success 200 {object} []repo.TotalsByOrganizer
// @Router /v1/groups/statistics/locations [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleGroupStatisticsLocations() server.HandlerFunc {
func (ctrl *V1Controller) HandleGroupStatisticsLocations() errchain.HandlerFunc {
fn := func(r *http.Request) ([]repo.TotalsByOrganizer, error) {
auth := services.NewContext(r.Context())
return ctrl.repo.Groups.StatsLocationsByPurchasePrice(auth, auth.GID)
@ -36,7 +37,7 @@ func (ctrl *V1Controller) HandleGroupStatisticsLocations() server.HandlerFunc {
// @Success 200 {object} []repo.TotalsByOrganizer
// @Router /v1/groups/statistics/labels [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleGroupStatisticsLabels() server.HandlerFunc {
func (ctrl *V1Controller) HandleGroupStatisticsLabels() errchain.HandlerFunc {
fn := func(r *http.Request) ([]repo.TotalsByOrganizer, error) {
auth := services.NewContext(r.Context())
return ctrl.repo.Groups.StatsLabelsByPurchasePrice(auth, auth.GID)
@ -53,7 +54,7 @@ func (ctrl *V1Controller) HandleGroupStatisticsLabels() server.HandlerFunc {
// @Success 200 {object} repo.GroupStatistics
// @Router /v1/groups/statistics [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleGroupStatistics() server.HandlerFunc {
func (ctrl *V1Controller) HandleGroupStatistics() errchain.HandlerFunc {
fn := func(r *http.Request) (repo.GroupStatistics, error) {
auth := services.NewContext(r.Context())
return ctrl.repo.Groups.StatsGroup(auth, auth.GID)
@ -72,7 +73,7 @@ func (ctrl *V1Controller) HandleGroupStatistics() server.HandlerFunc {
// @Param end query string false "end date"
// @Router /v1/groups/statistics/purchase-price [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleGroupStatisticsPriceOverTime() server.HandlerFunc {
func (ctrl *V1Controller) HandleGroupStatisticsPriceOverTime() errchain.HandlerFunc {
parseDate := func(datestr string, defaultDate time.Time) (time.Time, error) {
if datestr == "" {
return defaultDate, nil
@ -98,6 +99,6 @@ func (ctrl *V1Controller) HandleGroupStatisticsPriceOverTime() server.HandlerFun
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusOK, stats)
return server.JSON(w, http.StatusOK, stats)
}
}

View file

@ -8,7 +8,8 @@ import (
"github.com/hay-kot/homebox/backend/internal/core/services"
"github.com/hay-kot/homebox/backend/internal/data/repo"
"github.com/hay-kot/homebox/backend/internal/sys/validate"
"github.com/hay-kot/homebox/backend/pkgs/server"
"github.com/hay-kot/safeserve/errchain"
"github.com/hay-kot/safeserve/server"
"github.com/rs/zerolog/log"
)
@ -20,7 +21,7 @@ import (
// @Param payload body services.UserRegistration true "User Data"
// @Success 204
// @Router /v1/users/register [Post]
func (ctrl *V1Controller) HandleUserRegistration() server.HandlerFunc {
func (ctrl *V1Controller) HandleUserRegistration() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
regData := services.UserRegistration{}
@ -39,7 +40,7 @@ func (ctrl *V1Controller) HandleUserRegistration() server.HandlerFunc {
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusNoContent, nil)
return server.JSON(w, http.StatusNoContent, nil)
}
}
@ -48,10 +49,10 @@ func (ctrl *V1Controller) HandleUserRegistration() server.HandlerFunc {
// @Summary Get User Self
// @Tags User
// @Produce json
// @Success 200 {object} server.Result{item=repo.UserOut}
// @Success 200 {object} Wrapped{item=repo.UserOut}
// @Router /v1/users/self [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleUserSelf() server.HandlerFunc {
func (ctrl *V1Controller) HandleUserSelf() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
token := services.UseTokenCtx(r.Context())
usr, err := ctrl.svc.User.GetSelf(r.Context(), token)
@ -60,7 +61,7 @@ func (ctrl *V1Controller) HandleUserSelf() server.HandlerFunc {
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusOK, server.Wrap(usr))
return server.JSON(w, http.StatusOK, Wrap(usr))
}
}
@ -70,10 +71,10 @@ func (ctrl *V1Controller) HandleUserSelf() server.HandlerFunc {
// @Tags User
// @Produce json
// @Param payload body repo.UserUpdate true "User Data"
// @Success 200 {object} server.Result{item=repo.UserUpdate}
// @Success 200 {object} Wrapped{item=repo.UserUpdate}
// @Router /v1/users/self [PUT]
// @Security Bearer
func (ctrl *V1Controller) HandleUserSelfUpdate() server.HandlerFunc {
func (ctrl *V1Controller) HandleUserSelfUpdate() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
updateData := repo.UserUpdate{}
if err := server.Decode(r, &updateData); err != nil {
@ -87,7 +88,7 @@ func (ctrl *V1Controller) HandleUserSelfUpdate() server.HandlerFunc {
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusOK, server.Wrap(newData))
return server.JSON(w, http.StatusOK, Wrap(newData))
}
}
@ -99,7 +100,7 @@ func (ctrl *V1Controller) HandleUserSelfUpdate() server.HandlerFunc {
// @Success 204
// @Router /v1/users/self [DELETE]
// @Security Bearer
func (ctrl *V1Controller) HandleUserSelfDelete() server.HandlerFunc {
func (ctrl *V1Controller) HandleUserSelfDelete() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
if ctrl.isDemo {
return validate.NewRequestError(nil, http.StatusForbidden)
@ -110,7 +111,7 @@ func (ctrl *V1Controller) HandleUserSelfDelete() server.HandlerFunc {
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusNoContent, nil)
return server.JSON(w, http.StatusNoContent, nil)
}
}
@ -129,7 +130,7 @@ type (
// @Param payload body ChangePassword true "Password Payload"
// @Router /v1/users/change-password [PUT]
// @Security Bearer
func (ctrl *V1Controller) HandleUserSelfChangePassword() server.HandlerFunc {
func (ctrl *V1Controller) HandleUserSelfChangePassword() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
if ctrl.isDemo {
return validate.NewRequestError(nil, http.StatusForbidden)
@ -148,6 +149,6 @@ func (ctrl *V1Controller) HandleUserSelfChangePassword() server.HandlerFunc {
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusNoContent, nil)
return server.JSON(w, http.StatusNoContent, nil)
}
}

View file

@ -9,6 +9,9 @@ import (
atlas "ariga.io/atlas/sql/migrate"
"entgo.io/ent/dialect/sql/schema"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/hay-kot/homebox/backend/app/api/static/docs"
"github.com/hay-kot/homebox/backend/internal/core/services"
"github.com/hay-kot/homebox/backend/internal/data/ent"
@ -16,7 +19,8 @@ import (
"github.com/hay-kot/homebox/backend/internal/data/repo"
"github.com/hay-kot/homebox/backend/internal/sys/config"
"github.com/hay-kot/homebox/backend/internal/web/mid"
"github.com/hay-kot/homebox/backend/pkgs/server"
"github.com/hay-kot/safeserve/errchain"
"github.com/hay-kot/safeserve/server"
_ "github.com/mattn/go-sqlite3"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
@ -123,25 +127,26 @@ func run(cfg *config.Config) error {
// =========================================================================
// Start Server
logger := log.With().Caller().Logger()
mwLogger := mid.Logger(logger)
if app.conf.Mode == config.ModeDevelopment {
mwLogger = mid.SugarLogger(logger)
}
router := chi.NewMux()
router.Use(
middleware.RequestID,
middleware.RealIP,
mid.Logger(logger),
middleware.Recoverer,
middleware.StripSlashes,
)
chain := errchain.New(mid.Errors(app.server, logger))
app.mountRoutes(router, chain, app.repos)
app.server = server.NewServer(
server.WithHost(app.conf.Web.Host),
server.WithPort(app.conf.Web.Port),
server.WithMiddleware(
mwLogger,
mid.Errors(logger),
mid.Panic(app.conf.Mode == config.ModeDevelopment),
),
)
app.mountRoutes(app.repos)
log.Info().Msgf("Starting HTTP Server on %s:%s", app.server.Host, app.server.Port)
// =========================================================================
@ -179,5 +184,5 @@ func run(cfg *config.Config) error {
}()
}
return app.server.Start()
return app.server.Start(router)
}

View file

@ -9,7 +9,7 @@ import (
"github.com/hay-kot/homebox/backend/internal/core/services"
"github.com/hay-kot/homebox/backend/internal/sys/validate"
"github.com/hay-kot/homebox/backend/pkgs/server"
"github.com/hay-kot/safeserve/errchain"
)
type tokenHasKey struct {
@ -30,9 +30,9 @@ const (
// the required roles, a 403 Forbidden will be returned.
//
// WARNING: This middleware _MUST_ be called after mwAuthToken or else it will panic
func (a *app) mwRoles(rm RoleMode, required ...string) server.Middleware {
return func(next server.Handler) server.Handler {
return server.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
func (a *app) mwRoles(rm RoleMode, required ...string) errchain.Middleware {
return func(next errchain.Handler) errchain.Handler {
return errchain.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
ctx := r.Context()
maybeToken := ctx.Value(hashedToken)
@ -116,8 +116,8 @@ func getCookie(r *http.Request) (string, error) {
// - header = "Bearer 1234567890"
// - query = "?access_token=1234567890"
// - cookie = hb.auth.token = 1234567890
func (a *app) mwAuthToken(next server.Handler) server.Handler {
return server.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
func (a *app) mwAuthToken(next errchain.Handler) errchain.Handler {
return errchain.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
keyFuncs := [...]KeyFunc{
getBearer,
getCookie,

View file

@ -10,12 +10,13 @@ import (
"path"
"path/filepath"
"github.com/go-chi/chi/v5"
"github.com/hay-kot/homebox/backend/app/api/handlers/debughandlers"
v1 "github.com/hay-kot/homebox/backend/app/api/handlers/v1"
_ "github.com/hay-kot/homebox/backend/app/api/static/docs"
"github.com/hay-kot/homebox/backend/internal/data/ent/authroles"
"github.com/hay-kot/homebox/backend/internal/data/repo"
"github.com/hay-kot/homebox/backend/pkgs/server"
"github.com/hay-kot/safeserve/errchain"
httpSwagger "github.com/swaggo/http-swagger" // http-swagger middleware
)
@ -36,12 +37,12 @@ func (a *app) debugRouter() *http.ServeMux {
}
// registerRoutes registers all the routes for the API
func (a *app) mountRoutes(repos *repo.AllRepos) {
func (a *app) mountRoutes(r *chi.Mux, chain *errchain.ErrChain, repos *repo.AllRepos) {
registerMimes()
a.server.Get("/swagger/*", server.ToHandler(httpSwagger.Handler(
r.Get("/swagger/*", httpSwagger.Handler(
httpSwagger.URL(fmt.Sprintf("%s://%s/swagger/doc.json", a.conf.Swagger.Scheme, a.conf.Swagger.Host)),
)))
))
// =========================================================================
// API Version 1
@ -56,99 +57,103 @@ func (a *app) mountRoutes(repos *repo.AllRepos) {
v1.WithDemoStatus(a.conf.Demo), // Disable Password Change in Demo Mode
)
a.server.Get(v1Base("/status"), v1Ctrl.HandleBase(func() bool { return true }, v1.Build{
r.Get(v1Base("/status"), chain.ToHandlerFunc(v1Ctrl.HandleBase(func() bool { return true }, v1.Build{
Version: version,
Commit: commit,
BuildTime: buildTime,
}))
})))
a.server.Post(v1Base("/users/register"), v1Ctrl.HandleUserRegistration())
a.server.Post(v1Base("/users/login"), v1Ctrl.HandleAuthLogin())
r.Post(v1Base("/users/register"), chain.ToHandlerFunc(v1Ctrl.HandleUserRegistration()))
r.Post(v1Base("/users/login"), chain.ToHandlerFunc(v1Ctrl.HandleAuthLogin()))
userMW := []server.Middleware{
userMW := []errchain.Middleware{
a.mwAuthToken,
a.mwRoles(RoleModeOr, authroles.RoleUser.String()),
}
a.server.Get(v1Base("/users/self"), v1Ctrl.HandleUserSelf(), userMW...)
a.server.Put(v1Base("/users/self"), v1Ctrl.HandleUserSelfUpdate(), userMW...)
a.server.Delete(v1Base("/users/self"), v1Ctrl.HandleUserSelfDelete(), userMW...)
a.server.Post(v1Base("/users/logout"), v1Ctrl.HandleAuthLogout(), userMW...)
a.server.Get(v1Base("/users/refresh"), v1Ctrl.HandleAuthRefresh(), userMW...)
a.server.Put(v1Base("/users/self/change-password"), v1Ctrl.HandleUserSelfChangePassword(), userMW...)
r.Get(v1Base("/users/self"), chain.ToHandlerFunc(v1Ctrl.HandleUserSelf(), userMW...))
r.Put(v1Base("/users/self"), chain.ToHandlerFunc(v1Ctrl.HandleUserSelfUpdate(), userMW...))
r.Delete(v1Base("/users/self"), chain.ToHandlerFunc(v1Ctrl.HandleUserSelfDelete(), userMW...))
r.Post(v1Base("/users/logout"), chain.ToHandlerFunc(v1Ctrl.HandleAuthLogout(), userMW...))
r.Get(v1Base("/users/refresh"), chain.ToHandlerFunc(v1Ctrl.HandleAuthRefresh(), userMW...))
r.Put(v1Base("/users/self/change-password"), chain.ToHandlerFunc(v1Ctrl.HandleUserSelfChangePassword(), userMW...))
a.server.Post(v1Base("/groups/invitations"), v1Ctrl.HandleGroupInvitationsCreate(), userMW...)
a.server.Get(v1Base("/groups/statistics"), v1Ctrl.HandleGroupStatistics(), userMW...)
a.server.Get(v1Base("/groups/statistics/purchase-price"), v1Ctrl.HandleGroupStatisticsPriceOverTime(), userMW...)
a.server.Get(v1Base("/groups/statistics/locations"), v1Ctrl.HandleGroupStatisticsLocations(), userMW...)
a.server.Get(v1Base("/groups/statistics/labels"), v1Ctrl.HandleGroupStatisticsLabels(), userMW...)
r.Post(v1Base("/groups/invitations"), chain.ToHandlerFunc(v1Ctrl.HandleGroupInvitationsCreate(), userMW...))
r.Get(v1Base("/groups/statistics"), chain.ToHandlerFunc(v1Ctrl.HandleGroupStatistics(), userMW...))
r.Get(v1Base("/groups/statistics/purchase-price"), chain.ToHandlerFunc(v1Ctrl.HandleGroupStatisticsPriceOverTime(), userMW...))
r.Get(v1Base("/groups/statistics/locations"), chain.ToHandlerFunc(v1Ctrl.HandleGroupStatisticsLocations(), userMW...))
r.Get(v1Base("/groups/statistics/labels"), chain.ToHandlerFunc(v1Ctrl.HandleGroupStatisticsLabels(), userMW...))
// TODO: I don't like /groups being the URL for users
a.server.Get(v1Base("/groups"), v1Ctrl.HandleGroupGet(), userMW...)
a.server.Put(v1Base("/groups"), v1Ctrl.HandleGroupUpdate(), userMW...)
r.Get(v1Base("/groups"), chain.ToHandlerFunc(v1Ctrl.HandleGroupGet(), userMW...))
r.Put(v1Base("/groups"), chain.ToHandlerFunc(v1Ctrl.HandleGroupUpdate(), userMW...))
a.server.Post(v1Base("/actions/ensure-asset-ids"), v1Ctrl.HandleEnsureAssetID(), userMW...)
a.server.Post(v1Base("/actions/zero-item-time-fields"), v1Ctrl.HandleItemDateZeroOut(), userMW...)
a.server.Post(v1Base("/actions/ensure-import-refs"), v1Ctrl.HandleEnsureImportRefs(), userMW...)
r.Post(v1Base("/actions/ensure-asset-ids"), chain.ToHandlerFunc(v1Ctrl.HandleEnsureAssetID(), userMW...))
r.Post(v1Base("/actions/zero-item-time-fields"), chain.ToHandlerFunc(v1Ctrl.HandleItemDateZeroOut(), userMW...))
r.Post(v1Base("/actions/ensure-import-refs"), chain.ToHandlerFunc(v1Ctrl.HandleEnsureImportRefs(), userMW...))
a.server.Get(v1Base("/locations"), v1Ctrl.HandleLocationGetAll(), userMW...)
a.server.Post(v1Base("/locations"), v1Ctrl.HandleLocationCreate(), userMW...)
a.server.Get(v1Base("/locations/tree"), v1Ctrl.HandleLocationTreeQuery(), userMW...)
a.server.Get(v1Base("/locations/{id}"), v1Ctrl.HandleLocationGet(), userMW...)
a.server.Put(v1Base("/locations/{id}"), v1Ctrl.HandleLocationUpdate(), userMW...)
a.server.Delete(v1Base("/locations/{id}"), v1Ctrl.HandleLocationDelete(), userMW...)
r.Get(v1Base("/locations"), chain.ToHandlerFunc(v1Ctrl.HandleLocationGetAll(), userMW...))
r.Post(v1Base("/locations"), chain.ToHandlerFunc(v1Ctrl.HandleLocationCreate(), userMW...))
r.Get(v1Base("/locations/tree"), chain.ToHandlerFunc(v1Ctrl.HandleLocationTreeQuery(), userMW...))
r.Get(v1Base("/locations/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleLocationGet(), userMW...))
r.Put(v1Base("/locations/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleLocationUpdate(), userMW...))
r.Delete(v1Base("/locations/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleLocationDelete(), userMW...))
a.server.Get(v1Base("/labels"), v1Ctrl.HandleLabelsGetAll(), userMW...)
a.server.Post(v1Base("/labels"), v1Ctrl.HandleLabelsCreate(), userMW...)
a.server.Get(v1Base("/labels/{id}"), v1Ctrl.HandleLabelGet(), userMW...)
a.server.Put(v1Base("/labels/{id}"), v1Ctrl.HandleLabelUpdate(), userMW...)
a.server.Delete(v1Base("/labels/{id}"), v1Ctrl.HandleLabelDelete(), userMW...)
r.Get(v1Base("/labels"), chain.ToHandlerFunc(v1Ctrl.HandleLabelsGetAll(), userMW...))
r.Post(v1Base("/labels"), chain.ToHandlerFunc(v1Ctrl.HandleLabelsCreate(), userMW...))
r.Get(v1Base("/labels/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleLabelGet(), userMW...))
r.Put(v1Base("/labels/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleLabelUpdate(), userMW...))
r.Delete(v1Base("/labels/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleLabelDelete(), userMW...))
a.server.Get(v1Base("/items"), v1Ctrl.HandleItemsGetAll(), userMW...)
a.server.Post(v1Base("/items"), v1Ctrl.HandleItemsCreate(), userMW...)
a.server.Post(v1Base("/items/import"), v1Ctrl.HandleItemsImport(), userMW...)
a.server.Get(v1Base("/items/export"), v1Ctrl.HandleItemsExport(), userMW...)
a.server.Get(v1Base("/items/fields"), v1Ctrl.HandleGetAllCustomFieldNames(), userMW...)
a.server.Get(v1Base("/items/fields/values"), v1Ctrl.HandleGetAllCustomFieldValues(), userMW...)
r.Get(v1Base("/items"), chain.ToHandlerFunc(v1Ctrl.HandleItemsGetAll(), userMW...))
r.Post(v1Base("/items"), chain.ToHandlerFunc(v1Ctrl.HandleItemsCreate(), userMW...))
r.Post(v1Base("/items/import"), chain.ToHandlerFunc(v1Ctrl.HandleItemsImport(), userMW...))
r.Get(v1Base("/items/export"), chain.ToHandlerFunc(v1Ctrl.HandleItemsExport(), userMW...))
r.Get(v1Base("/items/fields"), chain.ToHandlerFunc(v1Ctrl.HandleGetAllCustomFieldNames(), userMW...))
r.Get(v1Base("/items/fields/values"), chain.ToHandlerFunc(v1Ctrl.HandleGetAllCustomFieldValues(), userMW...))
a.server.Get(v1Base("/items/{id}"), v1Ctrl.HandleItemGet(), userMW...)
a.server.Put(v1Base("/items/{id}"), v1Ctrl.HandleItemUpdate(), userMW...)
a.server.Delete(v1Base("/items/{id}"), v1Ctrl.HandleItemDelete(), userMW...)
r.Get(v1Base("/items/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleItemGet(), userMW...))
r.Put(v1Base("/items/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleItemUpdate(), userMW...))
r.Delete(v1Base("/items/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleItemDelete(), userMW...))
a.server.Post(v1Base("/items/{id}/attachments"), v1Ctrl.HandleItemAttachmentCreate(), userMW...)
a.server.Put(v1Base("/items/{id}/attachments/{attachment_id}"), v1Ctrl.HandleItemAttachmentUpdate(), userMW...)
a.server.Delete(v1Base("/items/{id}/attachments/{attachment_id}"), v1Ctrl.HandleItemAttachmentDelete(), userMW...)
r.Post(v1Base("/items/{id}/attachments"), chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentCreate(), userMW...))
r.Put(v1Base("/items/{id}/attachments/{attachment_id}"), chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentUpdate(), userMW...))
r.Delete(v1Base("/items/{id}/attachments/{attachment_id}"), chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentDelete(), userMW...))
a.server.Get(v1Base("/items/{id}/maintenance"), v1Ctrl.HandleMaintenanceLogGet(), userMW...)
a.server.Post(v1Base("/items/{id}/maintenance"), v1Ctrl.HandleMaintenanceEntryCreate(), userMW...)
a.server.Put(v1Base("/items/{id}/maintenance/{entry_id}"), v1Ctrl.HandleMaintenanceEntryUpdate(), userMW...)
a.server.Delete(v1Base("/items/{id}/maintenance/{entry_id}"), v1Ctrl.HandleMaintenanceEntryDelete(), userMW...)
r.Get(v1Base("/items/{id}/maintenance"), chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceLogGet(), userMW...))
r.Post(v1Base("/items/{id}/maintenance"), chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceEntryCreate(), userMW...))
r.Put(v1Base("/items/{id}/maintenance/{entry_id}"), chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceEntryUpdate(), userMW...))
r.Delete(v1Base("/items/{id}/maintenance/{entry_id}"), chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceEntryDelete(), userMW...))
a.server.Get(v1Base("/asset/{id}"), v1Ctrl.HandleAssetGet(), userMW...)
r.Get(v1Base("/asset/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleAssetGet(), userMW...))
// Notifiers
a.server.Get(v1Base("/notifiers"), v1Ctrl.HandleGetUserNotifiers(), userMW...)
a.server.Post(v1Base("/notifiers"), v1Ctrl.HandleCreateNotifier(), userMW...)
a.server.Put(v1Base("/notifiers/{id}"), v1Ctrl.HandleUpdateNotifier(), userMW...)
a.server.Delete(v1Base("/notifiers/{id}"), v1Ctrl.HandleDeleteNotifier(), userMW...)
a.server.Post(v1Base("/notifiers/test"), v1Ctrl.HandlerNotifierTest(), userMW...)
r.Get(v1Base("/notifiers"), chain.ToHandlerFunc(v1Ctrl.HandleGetUserNotifiers(), userMW...))
r.Post(v1Base("/notifiers"), chain.ToHandlerFunc(v1Ctrl.HandleCreateNotifier(), userMW...))
r.Put(v1Base("/notifiers/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleUpdateNotifier(), userMW...))
r.Delete(v1Base("/notifiers/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleDeleteNotifier(), userMW...))
r.Post(v1Base("/notifiers/test"), chain.ToHandlerFunc(v1Ctrl.HandlerNotifierTest(), userMW...))
// Asset-Like endpoints
a.server.Get(
assetMW := []errchain.Middleware{
a.mwAuthToken,
a.mwRoles(RoleModeOr, authroles.RoleUser.String(), authroles.RoleAttachments.String()),
}
r.Get(
v1Base("/qrcode"),
v1Ctrl.HandleGenerateQRCode(),
a.mwAuthToken, a.mwRoles(RoleModeOr, authroles.RoleUser.String(), authroles.RoleAttachments.String()),
chain.ToHandlerFunc(v1Ctrl.HandleGenerateQRCode(), assetMW...),
)
a.server.Get(
r.Get(
v1Base("/items/{id}/attachments/{attachment_id}"),
v1Ctrl.HandleItemAttachmentGet(),
a.mwAuthToken, a.mwRoles(RoleModeOr, authroles.RoleUser.String(), authroles.RoleAttachments.String()),
chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentGet(), assetMW...),
)
// Reporting Services
a.server.Get(v1Base("/reporting/bill-of-materials"), v1Ctrl.HandleBillOfMaterialsExport(), userMW...)
r.Get(v1Base("/reporting/bill-of-materials"), chain.ToHandlerFunc(v1Ctrl.HandleBillOfMaterialsExport(), userMW...))
r.NotFound(chain.ToHandlerFunc(notFoundHandler()))
a.server.NotFound(notFoundHandler())
}
func registerMimes() {
@ -165,7 +170,7 @@ func registerMimes() {
// notFoundHandler perform the main logic around handling the internal SPA embed and ensuring that
// the client side routing is handled correctly.
func notFoundHandler() server.HandlerFunc {
func notFoundHandler() errchain.HandlerFunc {
tryRead := func(fs embed.FS, prefix, requestedPath string, w http.ResponseWriter) error {
f, err := fs.Open(path.Join(prefix, requestedPath))
if err != nil {

View file

@ -694,7 +694,7 @@ const docTemplate = `{
"422": {
"description": "Unprocessable Entity",
"schema": {
"$ref": "#/definitions/server.ErrorResponse"
"$ref": "#/definitions/mid.ErrorResponse"
}
}
}
@ -947,7 +947,7 @@ const docTemplate = `{
"schema": {
"allOf": [
{
"$ref": "#/definitions/server.Results"
"$ref": "#/definitions/v1.Wrapped"
},
{
"type": "object",
@ -1119,7 +1119,7 @@ const docTemplate = `{
"schema": {
"allOf": [
{
"$ref": "#/definitions/server.Results"
"$ref": "#/definitions/v1.Wrapped"
},
{
"type": "object",
@ -1199,7 +1199,7 @@ const docTemplate = `{
"schema": {
"allOf": [
{
"$ref": "#/definitions/server.Results"
"$ref": "#/definitions/v1.Wrapped"
},
{
"type": "object",
@ -1339,7 +1339,7 @@ const docTemplate = `{
"schema": {
"allOf": [
{
"$ref": "#/definitions/server.Results"
"$ref": "#/definitions/v1.Wrapped"
},
{
"type": "object",
@ -1719,7 +1719,7 @@ const docTemplate = `{
"schema": {
"allOf": [
{
"$ref": "#/definitions/server.Result"
"$ref": "#/definitions/v1.Wrapped"
},
{
"type": "object",
@ -1764,7 +1764,7 @@ const docTemplate = `{
"schema": {
"allOf": [
{
"$ref": "#/definitions/server.Result"
"$ref": "#/definitions/v1.Wrapped"
},
{
"type": "object",
@ -1801,6 +1801,20 @@ const docTemplate = `{
}
},
"definitions": {
"mid.ErrorResponse": {
"type": "object",
"properties": {
"error": {
"type": "string"
},
"fields": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
},
"repo.DocumentOut": {
"type": "object",
"properties": {
@ -2677,39 +2691,6 @@ const docTemplate = `{
}
}
},
"server.ErrorResponse": {
"type": "object",
"properties": {
"error": {
"type": "string"
},
"fields": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
},
"server.Result": {
"type": "object",
"properties": {
"details": {},
"error": {
"type": "boolean"
},
"item": {},
"message": {
"type": "string"
}
}
},
"server.Results": {
"type": "object",
"properties": {
"items": {}
}
},
"services.UserRegistration": {
"type": "object",
"properties": {
@ -2840,6 +2821,12 @@ const docTemplate = `{
"type": "string"
}
}
},
"v1.Wrapped": {
"type": "object",
"properties": {
"item": {}
}
}
},
"securityDefinitions": {

View file

@ -686,7 +686,7 @@
"422": {
"description": "Unprocessable Entity",
"schema": {
"$ref": "#/definitions/server.ErrorResponse"
"$ref": "#/definitions/mid.ErrorResponse"
}
}
}
@ -939,7 +939,7 @@
"schema": {
"allOf": [
{
"$ref": "#/definitions/server.Results"
"$ref": "#/definitions/v1.Wrapped"
},
{
"type": "object",
@ -1111,7 +1111,7 @@
"schema": {
"allOf": [
{
"$ref": "#/definitions/server.Results"
"$ref": "#/definitions/v1.Wrapped"
},
{
"type": "object",
@ -1191,7 +1191,7 @@
"schema": {
"allOf": [
{
"$ref": "#/definitions/server.Results"
"$ref": "#/definitions/v1.Wrapped"
},
{
"type": "object",
@ -1331,7 +1331,7 @@
"schema": {
"allOf": [
{
"$ref": "#/definitions/server.Results"
"$ref": "#/definitions/v1.Wrapped"
},
{
"type": "object",
@ -1711,7 +1711,7 @@
"schema": {
"allOf": [
{
"$ref": "#/definitions/server.Result"
"$ref": "#/definitions/v1.Wrapped"
},
{
"type": "object",
@ -1756,7 +1756,7 @@
"schema": {
"allOf": [
{
"$ref": "#/definitions/server.Result"
"$ref": "#/definitions/v1.Wrapped"
},
{
"type": "object",
@ -1793,6 +1793,20 @@
}
},
"definitions": {
"mid.ErrorResponse": {
"type": "object",
"properties": {
"error": {
"type": "string"
},
"fields": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
},
"repo.DocumentOut": {
"type": "object",
"properties": {
@ -2669,39 +2683,6 @@
}
}
},
"server.ErrorResponse": {
"type": "object",
"properties": {
"error": {
"type": "string"
},
"fields": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
},
"server.Result": {
"type": "object",
"properties": {
"details": {},
"error": {
"type": "boolean"
},
"item": {},
"message": {
"type": "string"
}
}
},
"server.Results": {
"type": "object",
"properties": {
"items": {}
}
},
"services.UserRegistration": {
"type": "object",
"properties": {
@ -2832,6 +2813,12 @@
"type": "string"
}
}
},
"v1.Wrapped": {
"type": "object",
"properties": {
"item": {}
}
}
},
"securityDefinitions": {

View file

@ -1,5 +1,14 @@
basePath: /api
definitions:
mid.ErrorResponse:
properties:
error:
type: string
fields:
additionalProperties:
type: string
type: object
type: object
repo.DocumentOut:
properties:
id:
@ -591,28 +600,6 @@ definitions:
value:
type: number
type: object
server.ErrorResponse:
properties:
error:
type: string
fields:
additionalProperties:
type: string
type: object
type: object
server.Result:
properties:
details: {}
error:
type: boolean
item: {}
message:
type: string
type: object
server.Results:
properties:
items: {}
type: object
services.UserRegistration:
properties:
email:
@ -698,6 +685,10 @@ definitions:
token:
type: string
type: object
v1.Wrapped:
properties:
item: {}
type: object
info:
contact:
name: Don't
@ -1052,7 +1043,7 @@ paths:
"422":
description: Unprocessable Entity
schema:
$ref: '#/definitions/server.ErrorResponse'
$ref: '#/definitions/mid.ErrorResponse'
security:
- Bearer: []
summary: Create Item Attachment
@ -1268,7 +1259,7 @@ paths:
description: OK
schema:
allOf:
- $ref: '#/definitions/server.Results'
- $ref: '#/definitions/v1.Wrapped'
- properties:
items:
items:
@ -1370,7 +1361,7 @@ paths:
description: OK
schema:
allOf:
- $ref: '#/definitions/server.Results'
- $ref: '#/definitions/v1.Wrapped'
- properties:
items:
items:
@ -1478,7 +1469,7 @@ paths:
description: OK
schema:
allOf:
- $ref: '#/definitions/server.Results'
- $ref: '#/definitions/v1.Wrapped'
- properties:
items:
items:
@ -1499,7 +1490,7 @@ paths:
description: OK
schema:
allOf:
- $ref: '#/definitions/server.Results'
- $ref: '#/definitions/v1.Wrapped'
- properties:
items:
items:
@ -1741,7 +1732,7 @@ paths:
description: OK
schema:
allOf:
- $ref: '#/definitions/server.Result'
- $ref: '#/definitions/v1.Wrapped'
- properties:
item:
$ref: '#/definitions/repo.UserOut'
@ -1766,7 +1757,7 @@ paths:
description: OK
schema:
allOf:
- $ref: '#/definitions/server.Result'
- $ref: '#/definitions/v1.Wrapped'
- properties:
item:
$ref: '#/definitions/repo.UserUpdate'

View file

@ -12,6 +12,7 @@ require (
github.com/gocarina/gocsv v0.0.0-20230226133904-70c27cb2918a
github.com/google/uuid v1.3.0
github.com/gorilla/schema v1.2.0
github.com/hay-kot/safeserve v0.0.1
github.com/mattn/go-sqlite3 v1.14.16
github.com/pkg/errors v0.9.1
github.com/rs/zerolog v1.29.0

View file

@ -439,6 +439,8 @@ github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOn
github.com/hashicorp/memberlist v0.3.1/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
github.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=
github.com/hashicorp/serf v0.9.8/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=
github.com/hay-kot/safeserve v0.0.1 h1:9u8Ooyk8NNkqgxrqkLMWtMqauWEl/VZVtEUTLbHuAU8=
github.com/hay-kot/safeserve v0.0.1/go.mod h1:RUvwyfQTmbNgm5sHt+tQOqtdcpWadXWMhLty74Vedzw=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=

View file

@ -3,7 +3,8 @@ package adapters
import (
"net/http"
"github.com/hay-kot/homebox/backend/pkgs/server"
"github.com/hay-kot/safeserve/errchain"
"github.com/hay-kot/safeserve/server"
)
// Action is a function that adapts a function to the server.Handler interface.
@ -22,7 +23,7 @@ import (
// }
//
// r.Post("/foo", adapters.Action(fn, http.StatusCreated))
func Action[T any, Y any](f AdapterFunc[T, Y], ok int) server.HandlerFunc {
func Action[T any, Y any](f AdapterFunc[T, Y], ok int) errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
v, err := DecodeBody[T](r)
if err != nil {
@ -34,7 +35,7 @@ func Action[T any, Y any](f AdapterFunc[T, Y], ok int) server.HandlerFunc {
return err
}
return server.Respond(w, ok, res)
return server.JSON(w, ok, res)
}
}
@ -52,7 +53,7 @@ func Action[T any, Y any](f AdapterFunc[T, Y], ok int) server.HandlerFunc {
// }
//
// r.Post("/foo/{id}", adapters.ActionID(fn, http.StatusCreated))
func ActionID[T any, Y any](param string, f IDFunc[T, Y], ok int) server.HandlerFunc {
func ActionID[T any, Y any](param string, f IDFunc[T, Y], ok int) errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
ID, err := RouteUUID(r, param)
if err != nil {
@ -69,6 +70,6 @@ func ActionID[T any, Y any](param string, f IDFunc[T, Y], ok int) server.Handler
return err
}
return server.Respond(w, ok, res)
return server.JSON(w, ok, res)
}
}

View file

@ -4,13 +4,14 @@ import (
"net/http"
"github.com/google/uuid"
"github.com/hay-kot/homebox/backend/pkgs/server"
"github.com/hay-kot/safeserve/errchain"
"github.com/hay-kot/safeserve/server"
)
type CommandFunc[T any] func(*http.Request) (T, error)
type CommandIDFunc[T any] func(*http.Request, uuid.UUID) (T, error)
// Command is an HandlerAdapter that returns a server.HandlerFunc that
// Command is an HandlerAdapter that returns a errchain.HandlerFunc that
// The command adapters are used to handle commands that do not accept a body
// or a query. You can think of them as a way to handle RPC style Rest Endpoints.
//
@ -22,14 +23,14 @@ type CommandIDFunc[T any] func(*http.Request, uuid.UUID) (T, error)
// }
//
// r.Get("/foo", adapters.Command(fn, http.NoContent))
func Command[T any](f CommandFunc[T], ok int) server.HandlerFunc {
func Command[T any](f CommandFunc[T], ok int) errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
res, err := f(r)
if err != nil {
return err
}
return server.Respond(w, ok, res)
return server.JSON(w, ok, res)
}
}
@ -44,7 +45,7 @@ func Command[T any](f CommandFunc[T], ok int) server.HandlerFunc {
// }
//
// r.Get("/foo/{id}", adapters.CommandID("id", fn, http.NoContent))
func CommandID[T any](param string, f CommandIDFunc[T], ok int) server.HandlerFunc {
func CommandID[T any](param string, f CommandIDFunc[T], ok int) errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
ID, err := RouteUUID(r, param)
if err != nil {
@ -56,6 +57,6 @@ func CommandID[T any](param string, f CommandIDFunc[T], ok int) server.HandlerFu
return err
}
return server.Respond(w, ok, res)
return server.JSON(w, ok, res)
}
}

View file

@ -9,7 +9,7 @@ import (
"github.com/google/uuid"
"github.com/gorilla/schema"
"github.com/hay-kot/homebox/backend/internal/sys/validate"
"github.com/hay-kot/homebox/backend/pkgs/server"
"github.com/hay-kot/safeserve/server"
)
var queryDecoder = schema.NewDecoder()

View file

@ -3,7 +3,8 @@ package adapters
import (
"net/http"
"github.com/hay-kot/homebox/backend/pkgs/server"
"github.com/hay-kot/safeserve/errchain"
"github.com/hay-kot/safeserve/server"
)
// Query is a server.Handler that decodes a query from the request and calls the provided function.
@ -20,7 +21,7 @@ import (
// }
//
// r.Get("/foo", adapters.Query(fn, http.StatusOK))
func Query[T any, Y any](f AdapterFunc[T, Y], ok int) server.HandlerFunc {
func Query[T any, Y any](f AdapterFunc[T, Y], ok int) errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
q, err := DecodeQuery[T](r)
if err != nil {
@ -32,7 +33,7 @@ func Query[T any, Y any](f AdapterFunc[T, Y], ok int) server.HandlerFunc {
return err
}
return server.Respond(w, ok, res)
return server.JSON(w, ok, res)
}
}
@ -50,7 +51,7 @@ func Query[T any, Y any](f AdapterFunc[T, Y], ok int) server.HandlerFunc {
// }
//
// r.Get("/foo/{id}", adapters.QueryID(fn, http.StatusOK))
func QueryID[T any, Y any](param string, f IDFunc[T, Y], ok int) server.HandlerFunc {
func QueryID[T any, Y any](param string, f IDFunc[T, Y], ok int) errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
ID, err := RouteUUID(r, param)
if err != nil {
@ -67,6 +68,6 @@ func QueryID[T any, Y any](param string, f IDFunc[T, Y], ok int) server.HandlerF
return err
}
return server.Respond(w, ok, res)
return server.JSON(w, ok, res)
}
}

View file

@ -3,34 +3,42 @@ package mid
import (
"net/http"
"github.com/go-chi/chi/v5/middleware"
"github.com/hay-kot/homebox/backend/internal/data/ent"
"github.com/hay-kot/homebox/backend/internal/sys/validate"
"github.com/hay-kot/homebox/backend/pkgs/server"
"github.com/hay-kot/safeserve/errchain"
"github.com/hay-kot/safeserve/server"
"github.com/rs/zerolog"
)
func Errors(log zerolog.Logger) server.Middleware {
return func(h server.Handler) server.Handler {
return server.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
type ErrorResponse struct {
Error string `json:"error"`
Fields map[string]string `json:"fields,omitempty"`
}
func Errors(svr *server.Server, log zerolog.Logger) errchain.ErrorHandler {
return func(h errchain.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
err := h.ServeHTTP(w, r)
if err != nil {
var resp server.ErrorResponse
var resp ErrorResponse
var code int
traceID := r.Context().Value(middleware.RequestIDKey).(string)
log.Err(err).
Stack().
Str("trace_id", server.GetTraceID(r.Context())).
Str("req_id", traceID).
Msg("ERROR occurred")
switch {
case validate.IsUnauthorizedError(err):
code = http.StatusUnauthorized
resp = server.ErrorResponse{
resp = ErrorResponse{
Error: "unauthorized",
}
case validate.IsInvalidRouteKeyError(err):
code = http.StatusBadRequest
resp = server.ErrorResponse{
resp = ErrorResponse{
Error: err.Error(),
}
case validate.IsFieldError(err):
@ -60,17 +68,18 @@ func Errors(log zerolog.Logger) server.Middleware {
code = http.StatusInternalServerError
}
if err := server.Respond(w, code, resp); err != nil {
return err
if err := server.JSON(w, code, resp); err != nil {
log.Err(err).Msg("failed to write response")
}
// If Showdown error, return error
if server.IsShutdownError(err) {
return err
err := svr.Shutdown(err.Error())
if err != nil {
log.Err(err).Msg("failed to shutdown server")
}
}
}
return nil
})
}
}

View file

@ -1,106 +1,33 @@
package mid
import (
"fmt"
"net/http"
"github.com/hay-kot/homebox/backend/pkgs/server"
"github.com/go-chi/chi/v5/middleware"
"github.com/rs/zerolog"
)
type statusRecorder struct {
type spy struct {
http.ResponseWriter
Status int
status int
}
func (r *statusRecorder) WriteHeader(status int) {
r.Status = status
r.ResponseWriter.WriteHeader(status)
func (s *spy) WriteHeader(status int) {
s.status = status
s.ResponseWriter.WriteHeader(status)
}
func Logger(log zerolog.Logger) server.Middleware {
return func(next server.Handler) server.Handler {
return server.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
traceId := server.GetTraceID(r.Context())
func Logger(l zerolog.Logger) func(http.Handler) http.Handler {
return func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
reqID := r.Context().Value(middleware.RequestIDKey).(string)
log.Info().
Str("trace_id", traceId).
Str("method", r.Method).
Str("path", r.URL.Path).
Str("remove_address", r.RemoteAddr).
Msg("request started")
l.Info().Str("method", r.Method).Str("path", r.URL.Path).Str("rid", reqID).Msg("request received")
record := &statusRecorder{ResponseWriter: w, Status: http.StatusOK}
s := &spy{ResponseWriter: w}
h.ServeHTTP(s, r)
err := next.ServeHTTP(record, r)
log.Info().
Str("trace_id", traceId).
Str("method", r.Method).
Str("url", r.URL.Path).
Str("remote_address", r.RemoteAddr).
Int("status_code", record.Status).
Msg("request completed")
return err
})
}
}
func SugarLogger(log zerolog.Logger) server.Middleware {
orange := func(s string) string { return "\033[33m" + s + "\033[0m" }
aqua := func(s string) string { return "\033[36m" + s + "\033[0m" }
red := func(s string) string { return "\033[31m" + s + "\033[0m" }
green := func(s string) string { return "\033[32m" + s + "\033[0m" }
fmtCode := func(code int) string {
switch {
case code >= 500:
return red(fmt.Sprintf("%d", code))
case code >= 400:
return orange(fmt.Sprintf("%d", code))
case code >= 300:
return aqua(fmt.Sprintf("%d", code))
default:
return green(fmt.Sprintf("%d", code))
}
}
bold := func(s string) string { return "\033[1m" + s + "\033[0m" }
atLeast6 := func(s string) string {
for len(s) <= 6 {
s += " "
}
return s
}
return func(next server.Handler) server.Handler {
return server.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
record := &statusRecorder{ResponseWriter: w, Status: http.StatusOK}
url := fmt.Sprintf("%s %s", r.RequestURI, r.Proto)
log.Info().
Str("trace_id", server.GetTraceID(r.Context())).
Str("status", "started").
Msgf("%s %s %s",
bold(fmtCode(record.Status)),
bold(orange(atLeast6(r.Method))),
aqua(url),
)
err := next.ServeHTTP(record, r) // Blocks until the next handler returns.
log.Info().
Str("trace_id", server.GetTraceID(r.Context())).
Str("status", "completed").
Msgf("%s %s %s",
bold(fmtCode(record.Status)),
bold(orange(atLeast6(r.Method))),
aqua(url),
)
return err
l.Info().Str("method", r.Method).Str("path", r.URL.Path).Int("status", s.status).Str("rid", reqID).Msg("request finished")
})
}
}

View file

@ -1,33 +0,0 @@
package mid
import (
"fmt"
"net/http"
"runtime/debug"
"github.com/hay-kot/homebox/backend/pkgs/server"
)
// Panic is a middleware that recovers from panics anywhere in the chain and wraps the error.
// and returns it up the middleware chain.
func Panic(develop bool) server.Middleware {
return func(h server.Handler) server.Handler {
return server.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (err error) {
defer func() {
if rec := recover(); rec != nil {
trace := debug.Stack()
if develop {
err = fmt.Errorf("PANIC [%v]", rec)
fmt.Printf("%s", string(trace))
} else {
err = fmt.Errorf("PANIC [%v] TRACE[%s]", rec, string(trace))
}
}
}()
return h.ServeHTTP(w, r)
})
}
}

View file

@ -1,8 +0,0 @@
package server
const (
ContentType = "Content-Type"
ContentJSON = "application/json"
ContentXML = "application/xml"
ContentFormUrlEncoded = "application/x-www-form-urlencoded"
)

View file

@ -1,23 +0,0 @@
package server
import "errors"
type shutdownError struct {
message string
}
func (e *shutdownError) Error() string {
return e.message
}
// ShutdownError returns an error that indicates that the server has lost
// integrity and should be shut down.
func ShutdownError(message string) error {
return &shutdownError{message}
}
// IsShutdownError returns true if the error is a shutdown error.
func IsShutdownError(err error) bool {
var e *shutdownError
return errors.As(err, &e)
}

View file

@ -1,25 +0,0 @@
package server
import (
"net/http"
)
type HandlerFunc func(w http.ResponseWriter, r *http.Request) error
func (f HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
return f(w, r)
}
type Handler interface {
ServeHTTP(http.ResponseWriter, *http.Request) error
}
// ToHandler converts a function to a customer implementation of the Handler interface.
// that returns an error. This wrapper around the handler function and simply
// returns the nil in all cases
func ToHandler(handler http.Handler) Handler {
return HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
handler.ServeHTTP(w, r)
return nil
})
}

View file

@ -1,37 +0,0 @@
package server
import (
"net/http"
"strings"
)
type Middleware func(Handler) Handler
// wrapMiddleware creates a new handler by wrapping middleware around a final
// handler. The middlewares' Handlers will be executed by requests in the order
// they are provided.
func wrapMiddleware(mw []Middleware, handler Handler) Handler {
// Loop backwards through the middleware invoking each one. Replace the
// handler with the new wrapped handler. Looping backwards ensures that the
// first middleware of the slice is the first to be executed by requests.
for i := len(mw) - 1; i >= 0; i-- {
h := mw[i]
if h != nil {
handler = h(handler)
}
}
return handler
}
// StripTrailingSlash is a middleware that will strip trailing slashes from the request path.
//
// Example: /api/v1/ -> /api/v1
func StripTrailingSlash() Middleware {
return func(h Handler) Handler {
return HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
r.URL.Path = strings.TrimSuffix(r.URL.Path, "/")
return h.ServeHTTP(w, r)
})
}
}

View file

@ -1,102 +0,0 @@
package server
import (
"context"
"net/http"
"github.com/google/uuid"
)
type vkey int
const (
// Key is the key for the server in the request context.
key vkey = 1
)
type Values struct {
TraceID string
}
func GetTraceID(ctx context.Context) string {
v, ok := ctx.Value(key).(Values)
if !ok {
return ""
}
return v.TraceID
}
func (s *Server) toHttpHandler(handler Handler, mw ...Middleware) http.HandlerFunc {
handler = wrapMiddleware(mw, handler)
handler = wrapMiddleware(s.mw, handler)
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// Add the trace ID to the context
ctx = context.WithValue(ctx, key, Values{
TraceID: uuid.NewString(),
})
err := handler.ServeHTTP(w, r.WithContext(ctx))
if err != nil {
if IsShutdownError(err) {
_ = s.Shutdown("SIGTERM")
}
}
}
}
func (s *Server) handle(method, pattern string, handler Handler, mw ...Middleware) {
h := s.toHttpHandler(handler, mw...)
switch method {
case http.MethodGet:
s.mux.Get(pattern, h)
case http.MethodPost:
s.mux.Post(pattern, h)
case http.MethodPut:
s.mux.Put(pattern, h)
case http.MethodDelete:
s.mux.Delete(pattern, h)
case http.MethodPatch:
s.mux.Patch(pattern, h)
case http.MethodHead:
s.mux.Head(pattern, h)
case http.MethodOptions:
s.mux.Options(pattern, h)
}
}
func (s *Server) Get(pattern string, handler Handler, mw ...Middleware) {
s.handle(http.MethodGet, pattern, handler, mw...)
}
func (s *Server) Post(pattern string, handler Handler, mw ...Middleware) {
s.handle(http.MethodPost, pattern, handler, mw...)
}
func (s *Server) Put(pattern string, handler Handler, mw ...Middleware) {
s.handle(http.MethodPut, pattern, handler, mw...)
}
func (s *Server) Delete(pattern string, handler Handler, mw ...Middleware) {
s.handle(http.MethodDelete, pattern, handler, mw...)
}
func (s *Server) Patch(pattern string, handler Handler, mw ...Middleware) {
s.handle(http.MethodPatch, pattern, handler, mw...)
}
func (s *Server) Head(pattern string, handler Handler, mw ...Middleware) {
s.handle(http.MethodHead, pattern, handler, mw...)
}
func (s *Server) Options(pattern string, handler Handler, mw ...Middleware) {
s.handle(http.MethodOptions, pattern, handler, mw...)
}
func (s *Server) NotFound(handler Handler) {
s.mux.NotFound(s.toHttpHandler(handler))
}

View file

@ -1,48 +0,0 @@
package server
import (
"encoding/json"
"net/http"
)
// Decode reads the body of an HTTP request looking for a JSON document. The
// body is decoded into the provided value.
func Decode(r *http.Request, val interface{}) error {
decoder := json.NewDecoder(r.Body)
// decoder.DisallowUnknownFields()
if err := decoder.Decode(val); err != nil {
return err
}
return nil
}
// GetId is a shortcut to get the id from the request URL or return a default value
func GetParam(r *http.Request, key, d string) string {
val := r.URL.Query().Get(key)
if val == "" {
return d
}
return val
}
// GetSkip is a shortcut to get the skip from the request URL parameters
func GetSkip(r *http.Request, d string) string {
return GetParam(r, "skip", d)
}
// GetSkip is a shortcut to get the skip from the request URL parameters
func GetId(r *http.Request, d string) string {
return GetParam(r, "id", d)
}
// GetLimit is a shortcut to get the limit from the request URL parameters
func GetLimit(r *http.Request, d string) string {
return GetParam(r, "limit", d)
}
// GetQuery is a shortcut to get the sort from the request URL parameters
func GetQuery(r *http.Request, d string) string {
return GetParam(r, "query", d)
}

View file

@ -1,210 +0,0 @@
package server
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
)
type TestStruct struct {
Name string `json:"name"`
Data string `json:"data"`
}
func TestDecode(t *testing.T) {
type args struct {
r *http.Request
val interface{}
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "check_error",
args: args{
r: &http.Request{
Body: http.NoBody,
},
val: make(map[string]interface{}),
},
wantErr: true,
},
{
name: "check_success",
args: args{
r: httptest.NewRequest("POST", "/", strings.NewReader(`{"name":"test","data":"test"}`)),
val: TestStruct{
Name: "test",
Data: "test",
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := Decode(tt.args.r, &tt.args.val); (err != nil) != tt.wantErr {
t.Errorf("Decode() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestGetParam(t *testing.T) {
type args struct {
r *http.Request
key string
d string
}
tests := []struct {
name string
args args
want string
}{
{
name: "check_default",
args: args{
r: httptest.NewRequest("POST", "/", strings.NewReader(`{"name":"test","data":"test"}`)),
key: "id",
d: "default",
},
want: "default",
},
{
name: "check_id",
args: args{
r: httptest.NewRequest("POST", "/item?id=123", strings.NewReader(`{"name":"test","data":"test"}`)),
key: "id",
d: "",
},
want: "123",
},
{
name: "check_query",
args: args{
r: httptest.NewRequest("POST", "/item?query=hello-world", strings.NewReader(`{"name":"test","data":"test"}`)),
key: "query",
d: "",
},
want: "hello-world",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := GetParam(tt.args.r, tt.args.key, tt.args.d); got != tt.want {
t.Errorf("GetParam() = %v, want %v", got, tt.want)
}
})
}
}
func TestGetSkip(t *testing.T) {
type args struct {
r *http.Request
d string
}
tests := []struct {
name string
args args
want string
}{
{
name: "check_default",
args: args{
r: httptest.NewRequest("POST", "/", strings.NewReader(`{"name":"test","data":"test"}`)),
d: "0",
},
want: "0",
},
{
name: "check_skip",
args: args{
r: httptest.NewRequest("POST", "/item?skip=107", strings.NewReader(`{"name":"test","data":"test"}`)),
d: "0",
},
want: "107",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := GetSkip(tt.args.r, tt.args.d); got != tt.want {
t.Errorf("GetSkip() = %v, want %v", got, tt.want)
}
})
}
}
func TestGetLimit(t *testing.T) {
type args struct {
r *http.Request
d string
}
tests := []struct {
name string
args args
want string
}{
{
name: "check_default",
args: args{
r: httptest.NewRequest("POST", "/", strings.NewReader(`{"name":"test","data":"test"}`)),
d: "0",
},
want: "0",
},
{
name: "check_limit",
args: args{
r: httptest.NewRequest("POST", "/item?limit=107", strings.NewReader(`{"name":"test","data":"test"}`)),
d: "0",
},
want: "107",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := GetLimit(tt.args.r, tt.args.d); got != tt.want {
t.Errorf("GetLimit() = %v, want %v", got, tt.want)
}
})
}
}
func TestGetQuery(t *testing.T) {
type args struct {
r *http.Request
d string
}
tests := []struct {
name string
args args
want string
}{
{
name: "check_default",
args: args{
r: httptest.NewRequest("POST", "/", strings.NewReader(`{"name":"test","data":"test"}`)),
d: "0",
},
want: "0",
},
{
name: "check_query",
args: args{
r: httptest.NewRequest("POST", "/item?query=hello-query", strings.NewReader(`{"name":"test","data":"test"}`)),
d: "0",
},
want: "hello-query",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := GetQuery(tt.args.r, tt.args.d); got != tt.want {
t.Errorf("GetQuery() = %v, want %v", got, tt.want)
}
})
}
}

View file

@ -1,39 +0,0 @@
package server
import (
"encoding/json"
"net/http"
)
type ErrorResponse struct {
Error string `json:"error"`
Fields map[string]string `json:"fields,omitempty"`
}
// Respond converts a Go value to JSON and sends it to the client.
// Adapted from https://github.com/ardanlabs/service/tree/master/foundation/web
func Respond(w http.ResponseWriter, statusCode int, data interface{}) error {
if statusCode == http.StatusNoContent {
w.WriteHeader(statusCode)
return nil
}
// Convert the response value to JSON.
jsonData, err := json.Marshal(data)
if err != nil {
panic(err)
}
// Set the content type and headers once we know marshaling has succeeded.
w.Header().Set("Content-Type", ContentJSON)
// Write the status code to the response.
w.WriteHeader(statusCode)
// Send the result back to the client.
if _, err := w.Write(jsonData); err != nil {
return err
}
return nil
}

View file

@ -1,40 +0,0 @@
package server
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_Respond_NoContent(t *testing.T) {
recorder := httptest.NewRecorder()
dummystruct := struct {
Name string
}{
Name: "dummy",
}
err := Respond(recorder, http.StatusNoContent, dummystruct)
assert.NoError(t, err)
assert.Equal(t, http.StatusNoContent, recorder.Code)
assert.Empty(t, recorder.Body.String())
}
func Test_Respond_JSON(t *testing.T) {
recorder := httptest.NewRecorder()
dummystruct := struct {
Name string `json:"name"`
}{
Name: "dummy",
}
err := Respond(recorder, http.StatusCreated, dummystruct)
assert.NoError(t, err)
assert.Equal(t, http.StatusCreated, recorder.Code)
assert.JSONEq(t, recorder.Body.String(), `{"name":"dummy"}`)
assert.Equal(t, "application/json", recorder.Header().Get("Content-Type"))
}

View file

@ -1,19 +0,0 @@
package server
type Result struct {
Error bool `json:"error,omitempty"`
Details interface{} `json:"details,omitempty"`
Message string `json:"message,omitempty"`
Item interface{} `json:"item,omitempty"`
}
type Results struct {
Items any `json:"items"`
}
// Wrap creates a Wrapper instance and adds the initial namespace and data to be returned.
func Wrap(data interface{}) Result {
return Result{
Item: data,
}
}

View file

@ -1,144 +0,0 @@
package server
import (
"context"
"errors"
"fmt"
"net/http"
"os"
"os/signal"
"sync"
"syscall"
"time"
"github.com/go-chi/chi/v5"
)
var (
ErrServerNotStarted = errors.New("server not started")
ErrServerAlreadyStarted = errors.New("server already started")
)
type Server struct {
Host string
Port string
Worker Worker
wg sync.WaitGroup
mux *chi.Mux
// mw is the global middleware chain for the server.
mw []Middleware
started bool
activeServer *http.Server
idleTimeout time.Duration
readTimeout time.Duration
writeTimeout time.Duration
}
func NewServer(opts ...Option) *Server {
s := &Server{
Host: "localhost",
Port: "8080",
mux: chi.NewRouter(),
Worker: NewSimpleWorker(),
idleTimeout: 30 * time.Second,
readTimeout: 10 * time.Second,
writeTimeout: 10 * time.Second,
}
for _, opt := range opts {
err := opt(s)
if err != nil {
panic(err)
}
}
return s
}
func (s *Server) Shutdown(sig string) error {
if !s.started {
return ErrServerNotStarted
}
fmt.Printf("Received %s signal, shutting down\n", sig)
// Create a context with a 5-second timeout.
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err := s.activeServer.Shutdown(ctx)
s.started = false
if err != nil {
return err
}
fmt.Println("Http server shutdown, waiting for all tasks to finish")
s.wg.Wait()
return nil
}
func (s *Server) Start() error {
if s.started {
return ErrServerAlreadyStarted
}
s.activeServer = &http.Server{
Addr: s.Host + ":" + s.Port,
Handler: s.mux,
IdleTimeout: s.idleTimeout,
ReadTimeout: s.readTimeout,
WriteTimeout: s.writeTimeout,
}
shutdownError := make(chan error)
go func() {
// Create a quit channel which carries os.Signal values.
quit := make(chan os.Signal, 1)
// Use signal.Notify() to listen for incoming SIGINT and SIGTERM signals and
// relay them to the quit channel.
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
// Read the signal from the quit channel. block until received
sig := <-quit
err := s.Shutdown(sig.String())
if err != nil {
shutdownError <- err
}
// Exit the application with a 0 (success) status code.
os.Exit(0)
}()
s.started = true
err := s.activeServer.ListenAndServe()
if !errors.Is(err, http.ErrServerClosed) {
return err
}
err = <-shutdownError
if err != nil {
return err
}
fmt.Println("Server shutdown successfully")
return nil
}
// Background starts a go routine that runs on the servers pool. In the event of a shutdown
// request, the server will wait until all open goroutines have finished before shutting down.
func (svr *Server) Background(task func()) {
svr.wg.Add(1)
svr.Worker.Add(func() {
defer svr.wg.Done()
task()
})
}

View file

@ -1,54 +0,0 @@
package server
import "time"
type Option = func(s *Server) error
func WithMiddleware(mw ...Middleware) Option {
return func(s *Server) error {
s.mw = append(s.mw, mw...)
return nil
}
}
func WithWorker(w Worker) Option {
return func(s *Server) error {
s.Worker = w
return nil
}
}
func WithHost(host string) Option {
return func(s *Server) error {
s.Host = host
return nil
}
}
func WithPort(port string) Option {
return func(s *Server) error {
s.Port = port
return nil
}
}
func WithReadTimeout(seconds int) Option {
return func(s *Server) error {
s.readTimeout = time.Duration(seconds) * time.Second
return nil
}
}
func WithWriteTimeout(seconds int) Option {
return func(s *Server) error {
s.writeTimeout = time.Duration(seconds) * time.Second
return nil
}
}
func WithIdleTimeout(seconds int) Option {
return func(s *Server) error {
s.idleTimeout = time.Duration(seconds) * time.Second
return nil
}
}

View file

@ -1,101 +0,0 @@
package server
import (
"net/http"
"sync/atomic"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func testServer(t *testing.T, r http.Handler) *Server {
svr := NewServer(WithHost("127.0.0.1"), WithPort("19245"))
if r != nil {
svr.mux.Mount("/", r)
}
go func() {
err := svr.Start()
assert.NoError(t, err)
}()
ping := func() error {
_, err := http.Get("http://127.0.0.1:19245")
return err
}
for {
if err := ping(); err == nil {
break
}
time.Sleep(time.Millisecond * 100)
}
return svr
}
func Test_ServerShutdown_Error(t *testing.T) {
svr := NewServer(WithHost("127.0.0.1"), WithPort("19245"))
err := svr.Shutdown("test")
assert.ErrorIs(t, err, ErrServerNotStarted)
}
func Test_ServerStarts_Error(t *testing.T) {
svr := testServer(t, nil)
err := svr.Start()
assert.ErrorIs(t, err, ErrServerAlreadyStarted)
err = svr.Shutdown("test")
assert.NoError(t, err)
}
func Test_ServerStarts(t *testing.T) {
svr := testServer(t, nil)
err := svr.Shutdown("test")
assert.NoError(t, err)
}
func Test_GracefulServerShutdownWithWorkers(t *testing.T) {
isFinished := false
svr := testServer(t, nil)
svr.Background(func() {
time.Sleep(time.Second * 4)
isFinished = true
})
err := svr.Shutdown("test")
assert.NoError(t, err)
assert.True(t, isFinished)
}
func Test_GracefulServerShutdownWithRequests(t *testing.T) {
var isFinished atomic.Bool
router := http.NewServeMux()
// add long running handler func
router.HandleFunc("/test", func(rw http.ResponseWriter, r *http.Request) {
time.Sleep(time.Second * 3)
isFinished.Store(true)
})
svr := testServer(t, router)
// Make request to "/test"
go func() {
_, _ = http.Get("http://127.0.0.1:19245/test") // This is probably bad?
}()
time.Sleep(time.Second) // Hack to wait for the request to be made
err := svr.Shutdown("test")
assert.NoError(t, err)
assert.True(t, isFinished.Load())
}

View file

@ -1,21 +0,0 @@
package server
// TODO: #2 Implement Go routine pool/job queue
type Worker interface {
Add(func())
}
// SimpleWorker is a simple background worker that implements
// the Worker interface and runs all tasks in a go routine without
// a pool or que or limits. It's useful for simple or small applications
// with minimal/short background tasks
type SimpleWorker struct{}
func NewSimpleWorker() *SimpleWorker {
return &SimpleWorker{}
}
func (sw *SimpleWorker) Add(task func()) {
go task()
}

View file

@ -686,7 +686,7 @@
"422": {
"description": "Unprocessable Entity",
"schema": {
"$ref": "#/definitions/server.ErrorResponse"
"$ref": "#/definitions/mid.ErrorResponse"
}
}
}
@ -939,7 +939,7 @@
"schema": {
"allOf": [
{
"$ref": "#/definitions/server.Results"
"$ref": "#/definitions/v1.Wrapped"
},
{
"type": "object",
@ -1111,7 +1111,7 @@
"schema": {
"allOf": [
{
"$ref": "#/definitions/server.Results"
"$ref": "#/definitions/v1.Wrapped"
},
{
"type": "object",
@ -1191,7 +1191,7 @@
"schema": {
"allOf": [
{
"$ref": "#/definitions/server.Results"
"$ref": "#/definitions/v1.Wrapped"
},
{
"type": "object",
@ -1331,7 +1331,7 @@
"schema": {
"allOf": [
{
"$ref": "#/definitions/server.Results"
"$ref": "#/definitions/v1.Wrapped"
},
{
"type": "object",
@ -1711,7 +1711,7 @@
"schema": {
"allOf": [
{
"$ref": "#/definitions/server.Result"
"$ref": "#/definitions/v1.Wrapped"
},
{
"type": "object",
@ -1756,7 +1756,7 @@
"schema": {
"allOf": [
{
"$ref": "#/definitions/server.Result"
"$ref": "#/definitions/v1.Wrapped"
},
{
"type": "object",
@ -1793,6 +1793,20 @@
}
},
"definitions": {
"mid.ErrorResponse": {
"type": "object",
"properties": {
"error": {
"type": "string"
},
"fields": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
},
"repo.DocumentOut": {
"type": "object",
"properties": {
@ -2669,39 +2683,6 @@
}
}
},
"server.ErrorResponse": {
"type": "object",
"properties": {
"error": {
"type": "string"
},
"fields": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
},
"server.Result": {
"type": "object",
"properties": {
"details": {},
"error": {
"type": "boolean"
},
"item": {},
"message": {
"type": "string"
}
}
},
"server.Results": {
"type": "object",
"properties": {
"items": {}
}
},
"services.UserRegistration": {
"type": "object",
"properties": {
@ -2832,6 +2813,12 @@
"type": "string"
}
}
},
"v1.Wrapped": {
"type": "object",
"properties": {
"item": {}
}
}
},
"securityDefinitions": {

View file

@ -10,6 +10,11 @@
* ---------------------------------------------------------------
*/
export interface MidErrorResponse {
error: string;
fields: Record<string, string>;
}
export interface DocumentOut {
id: string;
path: string;
@ -359,22 +364,6 @@ export interface ValueOverTimeEntry {
value: number;
}
export interface ServerErrorResponse {
error: string;
fields: Record<string, string>;
}
export interface ServerResult {
details: any;
error: boolean;
item: any;
message: string;
}
export interface ServerResults {
items: any;
}
export interface UserRegistration {
email: string;
name: string;
@ -431,3 +420,7 @@ export interface TokenResponse {
expiresAt: Date | string;
token: string;
}
export interface Wrapped {
item: any;
}