From db80f8a1591113fc3ba3063ae3a1540dd9db1331 Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Mon, 20 Mar 2023 20:32:10 -0800 Subject: [PATCH] chore: refactor api endpoints (#339) * move typegen code * update taskfile to fix code-gen caches and use 'dir' attribute * enable dumping stack traces for errors * log request start and stop * set zerolog stack handler * fix routes function * refactor context adapters to use requests directly * change some method signatures to support GID * start requiring validation tags * first pass on updating handlers to use adapters * add errs package * code gen * tidy * rework API to use external server package --- Taskfile.yml | 46 ++-- backend/app/api/app.go | 2 +- backend/app/api/handlers/v1/controller.go | 15 +- .../app/api/handlers/v1/v1_ctrl_actions.go | 13 +- backend/app/api/handlers/v1/v1_ctrl_assets.go | 11 +- backend/app/api/handlers/v1/v1_ctrl_auth.go | 24 +- backend/app/api/handlers/v1/v1_ctrl_group.go | 88 +++----- backend/app/api/handlers/v1/v1_ctrl_items.go | 146 +++++------- .../handlers/v1/v1_ctrl_items_attachments.go | 21 +- backend/app/api/handlers/v1/v1_ctrl_labels.go | 122 ++++------ .../app/api/handlers/v1/v1_ctrl_locations.go | 174 +++++---------- .../api/handlers/v1/v1_ctrl_maint_entry.go | 119 +++------- .../app/api/handlers/v1/v1_ctrl_notifiers.go | 41 ++-- backend/app/api/handlers/v1/v1_ctrl_qrcode.go | 25 +-- .../app/api/handlers/v1/v1_ctrl_reporting.go | 4 +- .../app/api/handlers/v1/v1_ctrl_statistics.go | 57 ++--- backend/app/api/handlers/v1/v1_ctrl_user.go | 27 +-- backend/app/api/main.go | 39 ++-- backend/app/api/middleware.go | 12 +- backend/app/api/routes.go | 139 ++++++------ backend/app/api/static/docs/docs.go | 104 +++++---- backend/app/api/static/docs/swagger.json | 104 +++++---- backend/app/api/static/docs/swagger.yaml | 73 +++--- .../app/tools/typegen}/main.go | 0 backend/go.mod | 3 +- backend/go.sum | 20 +- backend/internal/data/repo/repo_items.go | 4 +- backend/internal/data/repo/repo_labels.go | 8 +- backend/internal/data/repo/repo_locations.go | 6 +- .../internal/data/repo/repo_locations_test.go | 2 +- .../data/repo/repo_maintenance_entry.go | 15 +- .../data/repo/repo_maintenance_entry_test.go | 2 +- backend/internal/web/adapters/actions.go | 25 ++- backend/internal/web/adapters/adapters.go | 6 +- backend/internal/web/adapters/command.go | 36 +-- backend/internal/web/adapters/decoders.go | 18 +- backend/internal/web/adapters/query.go | 25 ++- backend/internal/web/mid/errors.go | 36 +-- backend/internal/web/mid/logger.go | 91 ++------ backend/internal/web/mid/panic.go | 33 --- backend/pkgs/server/constants.go | 8 - backend/pkgs/server/errors.go | 23 -- backend/pkgs/server/handler.go | 25 --- backend/pkgs/server/middleware.go | 37 --- backend/pkgs/server/mux.go | 102 --------- backend/pkgs/server/request.go | 48 ---- backend/pkgs/server/request_test.go | 210 ------------------ backend/pkgs/server/response.go | 39 ---- backend/pkgs/server/response_test.go | 40 ---- backend/pkgs/server/result.go | 19 -- backend/pkgs/server/server.go | 144 ------------ backend/pkgs/server/server_options.go | 54 ----- backend/pkgs/server/server_test.go | 101 --------- backend/pkgs/server/worker.go | 21 -- docs/docs/api/openapi-2.0.json | 104 +++++---- frontend/lib/api/types/data-contracts.ts | 42 ++-- 56 files changed, 806 insertions(+), 1947 deletions(-) rename {scripts/process-types => backend/app/tools/typegen}/main.go (100%) delete mode 100644 backend/internal/web/mid/panic.go delete mode 100644 backend/pkgs/server/constants.go delete mode 100644 backend/pkgs/server/errors.go delete mode 100644 backend/pkgs/server/handler.go delete mode 100644 backend/pkgs/server/middleware.go delete mode 100644 backend/pkgs/server/mux.go delete mode 100644 backend/pkgs/server/request.go delete mode 100644 backend/pkgs/server/request_test.go delete mode 100644 backend/pkgs/server/response.go delete mode 100644 backend/pkgs/server/response_test.go delete mode 100644 backend/pkgs/server/result.go delete mode 100644 backend/pkgs/server/server.go delete mode 100644 backend/pkgs/server/server_options.go delete mode 100644 backend/pkgs/server/server_test.go delete mode 100644 backend/pkgs/server/worker.go diff --git a/Taskfile.yml b/Taskfile.yml index a4e5fdc..5686f2c 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -27,47 +27,47 @@ tasks: --modular \ --path ./backend/app/api/static/docs/swagger.json \ --output ./frontend/lib/api/types - - go run ./scripts/process-types/*.go ./frontend/lib/api/types/data-contracts.ts + - go run ./backend/app/tools/typegen/main.go ./frontend/lib/api/types/data-contracts.ts - cp ./backend/app/api/static/docs/swagger.json docs/docs/api/openapi-2.0.json sources: - "./backend/app/api/**/*" - "./backend/internal/data/**" - - "./backend/internal/services/**/*" - - "./scripts/process-types.py" - generates: - - "./frontend/lib/api/types/data-contracts.ts" - - "./backend/internal/data/ent/schema" - - "./backend/app/api/static/docs/swagger.json" - - "./backend/app/api/static/docs/swagger.yaml" + - "./backend/internal/core/services/**/*" + - "./backend/app/tools/typegen/main.go" go:run: desc: Starts the backend api server (depends on generate task) + dir: backend deps: - generate cmds: - - cd backend && go run ./app/api/ {{ .CLI_ARGS }} + - go run ./app/api/ {{ .CLI_ARGS }} silent: false go:test: desc: Runs all go tests using gotestsum - supports passing gotestsum args + dir: backend cmds: - - cd backend && gotestsum {{ .CLI_ARGS }} ./... + - gotestsum {{ .CLI_ARGS }} ./... go:coverage: desc: Runs all go tests with -race flag and generates a coverage report + dir: backend cmds: - - cd backend && go test -race -coverprofile=coverage.out -covermode=atomic ./app/... ./internal/... ./pkgs/... -v -cover + - go test -race -coverprofile=coverage.out -covermode=atomic ./app/... ./internal/... ./pkgs/... -v -cover silent: true go:tidy: desc: Runs go mod tidy on the backend + dir: backend cmds: - - cd backend && go mod tidy + - go mod tidy go:lint: desc: Runs golangci-lint + dir: backend cmds: - - cd backend && golangci-lint run ./... + - golangci-lint run ./... go:all: desc: Runs all go test and lint related tasks @@ -78,19 +78,19 @@ tasks: go:build: desc: Builds the backend binary + dir: backend cmds: - - cd backend && go build -o ../build/backend ./app/api + - go build -o ../build/backend ./app/api db:generate: desc: Run Entgo.io Code Generation + dir: backend/internal/ cmds: - | - cd backend/internal/ && go generate ./... \ + go generate ./... \ --template=./data/ent/schema/templates/has_id.tmpl sources: - "./backend/internal/data/ent/schema/**/*" - generates: - - "./backend/internal/ent/" db:migration: desc: Runs the database diff engine to generate a SQL migration files @@ -101,23 +101,27 @@ tasks: ui:watch: desc: Starts the vitest test runner in watch mode + dir: frontend cmds: - - cd frontend && pnpm run test:watch + - pnpm run test:watch ui:dev: desc: Run frontend development server + dir: frontend cmds: - - cd frontend && pnpm dev + - pnpm dev ui:fix: desc: Runs prettier and eslint on the frontend + dir: frontend cmds: - - cd frontend && pnpm run lint:fix + - pnpm run lint:fix ui:check: desc: Runs type checking + dir: frontend cmds: - - cd frontend && pnpm run typecheck + - pnpm run typecheck test:ci: desc: Runs end-to-end test on a live server (only for use in CI) diff --git a/backend/app/api/app.go b/backend/app/api/app.go index 854c4e5..a84b9ae 100644 --- a/backend/app/api/app.go +++ b/backend/app/api/app.go @@ -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 { diff --git a/backend/app/api/handlers/v1/controller.go b/backend/app/api/handlers/v1/controller.go index 51fb02e..ff03bfe 100644 --- a/backend/app/api/handlers/v1/controller.go +++ b/backend/app/api/handlers/v1/controller.go @@ -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", diff --git a/backend/app/api/handlers/v1/v1_ctrl_actions.go b/backend/app/api/handlers/v1/v1_ctrl_actions.go index d131738..10c9f2e 100644 --- a/backend/app/api/handlers/v1/v1_ctrl_actions.go +++ b/backend/app/api/handlers/v1/v1_ctrl_actions.go @@ -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) } diff --git a/backend/app/api/handlers/v1/v1_ctrl_assets.go b/backend/app/api/handlers/v1/v1_ctrl_assets.go index 6bc596a..117ebe4 100644 --- a/backend/app/api/handlers/v1/v1_ctrl_assets.go +++ b/backend/app/api/handlers/v1/v1_ctrl_assets.go @@ -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) } } diff --git a/backend/app/api/handlers/v1/v1_ctrl_auth.go b/backend/app/api/handlers/v1/v1_ctrl_auth.go index bdfcdf6..f6b723b 100644 --- a/backend/app/api/handlers/v1/v1_ctrl_auth.go +++ b/backend/app/api/handlers/v1/v1_ctrl_auth.go @@ -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) } } diff --git a/backend/app/api/handlers/v1/v1_ctrl_group.go b/backend/app/api/handlers/v1/v1_ctrl_group.go index 3e42d95..bd9ff4c 100644 --- a/backend/app/api/handlers/v1/v1_ctrl_group.go +++ b/backend/app/api/handlers/v1/v1_ctrl_group.go @@ -6,14 +6,13 @@ 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/rs/zerolog/log" + "github.com/hay-kot/homebox/backend/internal/web/adapters" + "github.com/hay-kot/safeserve/errchain" ) type ( GroupInvitationCreate struct { - Uses int `json:"uses"` + Uses int `json:"uses" validate:"required,min=1,max=100"` ExpiresAt time.Time `json:"expiresAt"` } @@ -32,8 +31,13 @@ type ( // @Success 200 {object} repo.Group // @Router /v1/groups [Get] // @Security Bearer -func (ctrl *V1Controller) HandleGroupGet() server.HandlerFunc { - return ctrl.handleGroupGeneral() +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) + } + + return adapters.Command(fn, http.StatusOK) } // HandleGroupUpdate godoc @@ -45,41 +49,13 @@ func (ctrl *V1Controller) HandleGroupGet() server.HandlerFunc { // @Success 200 {object} repo.Group // @Router /v1/groups [Put] // @Security Bearer -func (ctrl *V1Controller) HandleGroupUpdate() server.HandlerFunc { - return ctrl.handleGroupGeneral() -} - -func (ctrl *V1Controller) handleGroupGeneral() server.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) error { - ctx := services.NewContext(r.Context()) - - switch r.Method { - case http.MethodGet: - group, err := ctrl.repo.Groups.GroupByID(ctx, ctx.GID) - if err != nil { - log.Err(err).Msg("failed to get group") - return validate.NewRequestError(err, http.StatusInternalServerError) - } - - return server.Respond(w, http.StatusOK, group) - - case http.MethodPut: - data := repo.GroupUpdate{} - if err := server.Decode(r, &data); err != nil { - return validate.NewRequestError(err, http.StatusBadRequest) - } - - group, err := ctrl.svc.Group.UpdateGroup(ctx, data) - if err != nil { - log.Err(err).Msg("failed to update group") - return validate.NewRequestError(err, http.StatusInternalServerError) - } - - return server.Respond(w, http.StatusOK, group) - } - - return nil +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) } + + return adapters.Action(fn, http.StatusOK) } // HandleGroupInvitationsCreate godoc @@ -91,30 +67,22 @@ func (ctrl *V1Controller) handleGroupGeneral() server.HandlerFunc { // @Success 200 {object} GroupInvitation // @Router /v1/groups/invitations [Post] // @Security Bearer -func (ctrl *V1Controller) HandleGroupInvitationsCreate() server.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) error { - data := GroupInvitationCreate{} - if err := server.Decode(r, &data); err != nil { - log.Err(err).Msg("failed to decode user registration data") - return validate.NewRequestError(err, http.StatusBadRequest) +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) } - if data.ExpiresAt.IsZero() { - data.ExpiresAt = time.Now().Add(time.Hour * 24) - } + auth := services.NewContext(r.Context()) - ctx := services.NewContext(r.Context()) + token, err := ctrl.svc.Group.NewInvitation(auth, body.Uses, body.ExpiresAt) - token, err := ctrl.svc.Group.NewInvitation(ctx, data.Uses, data.ExpiresAt) - if err != nil { - log.Err(err).Msg("failed to create new token") - return validate.NewRequestError(err, http.StatusInternalServerError) - } - - return server.Respond(w, http.StatusCreated, GroupInvitation{ + return GroupInvitation{ Token: token, - ExpiresAt: data.ExpiresAt, - Uses: data.Uses, - }) + ExpiresAt: body.ExpiresAt, + Uses: body.Uses, + }, err } + + return adapters.Action(fn, http.StatusCreated) } diff --git a/backend/app/api/handlers/v1/v1_ctrl_items.go b/backend/app/api/handlers/v1/v1_ctrl_items.go index dd6a6a3..d38c037 100644 --- a/backend/app/api/handlers/v1/v1_ctrl_items.go +++ b/backend/app/api/handlers/v1/v1_ctrl_items.go @@ -7,10 +7,13 @@ import ( "net/http" "strings" + "github.com/google/uuid" "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/homebox/backend/internal/web/adapters" + "github.com/hay-kot/safeserve/errchain" + "github.com/hay-kot/safeserve/server" "github.com/rs/zerolog/log" ) @@ -27,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() @@ -76,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) } } @@ -93,26 +96,15 @@ func (ctrl *V1Controller) HandleItemsGetAll() server.HandlerFunc { // @Tags Items // @Produce json // @Param payload body repo.ItemCreate true "Item Data" -// @Success 200 {object} repo.ItemSummary +// @Success 201 {object} repo.ItemSummary // @Router /v1/items [POST] // @Security Bearer -func (ctrl *V1Controller) HandleItemsCreate() server.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) error { - createData := repo.ItemCreate{} - if err := server.Decode(r, &createData); err != nil { - log.Err(err).Msg("failed to decode request body") - return validate.NewRequestError(err, http.StatusInternalServerError) - } - - ctx := services.NewContext(r.Context()) - item, err := ctrl.svc.Items.Create(ctx, createData) - if err != nil { - log.Err(err).Msg("failed to create item") - return validate.NewRequestError(err, http.StatusInternalServerError) - } - - return server.Respond(w, http.StatusCreated, item) +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) } + + return adapters.Action(fn, http.StatusCreated) } // HandleItemGet godocs @@ -124,8 +116,14 @@ func (ctrl *V1Controller) HandleItemsCreate() server.HandlerFunc { // @Success 200 {object} repo.ItemOut // @Router /v1/items/{id} [GET] // @Security Bearer -func (ctrl *V1Controller) HandleItemGet() server.HandlerFunc { - return ctrl.handleItemsGeneral() +func (ctrl *V1Controller) HandleItemGet() errchain.HandlerFunc { + fn := func(r *http.Request, ID uuid.UUID) (repo.ItemOut, error) { + auth := services.NewContext(r.Context()) + + return ctrl.repo.Items.GetOneByGroup(auth, auth.GID, ID) + } + + return adapters.CommandID("id", fn, http.StatusOK) } // HandleItemDelete godocs @@ -137,8 +135,14 @@ func (ctrl *V1Controller) HandleItemGet() server.HandlerFunc { // @Success 204 // @Router /v1/items/{id} [DELETE] // @Security Bearer -func (ctrl *V1Controller) HandleItemDelete() server.HandlerFunc { - return ctrl.handleItemsGeneral() +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) + return nil, err + } + + return adapters.CommandID("id", fn, http.StatusNoContent) } // HandleItemUpdate godocs @@ -151,50 +155,15 @@ func (ctrl *V1Controller) HandleItemDelete() server.HandlerFunc { // @Success 200 {object} repo.ItemOut // @Router /v1/items/{id} [PUT] // @Security Bearer -func (ctrl *V1Controller) HandleItemUpdate() server.HandlerFunc { - return ctrl.handleItemsGeneral() -} +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()) -func (ctrl *V1Controller) handleItemsGeneral() server.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) error { - ctx := services.NewContext(r.Context()) - ID, err := ctrl.routeID(r) - if err != nil { - return err - } - - switch r.Method { - case http.MethodGet: - items, err := ctrl.repo.Items.GetOneByGroup(r.Context(), ctx.GID, ID) - if err != nil { - log.Err(err).Msg("failed to get item") - return validate.NewRequestError(err, http.StatusInternalServerError) - } - return server.Respond(w, http.StatusOK, items) - case http.MethodDelete: - err = ctrl.repo.Items.DeleteByGroup(r.Context(), ctx.GID, ID) - if err != nil { - log.Err(err).Msg("failed to delete item") - return validate.NewRequestError(err, http.StatusInternalServerError) - } - return server.Respond(w, http.StatusNoContent, nil) - case http.MethodPut: - body := repo.ItemUpdate{} - if err := server.Decode(r, &body); err != nil { - log.Err(err).Msg("failed to decode request body") - return validate.NewRequestError(err, http.StatusInternalServerError) - } - body.ID = ID - result, err := ctrl.repo.Items.UpdateByGroup(r.Context(), ctx.GID, body) - if err != nil { - log.Err(err).Msg("failed to update item") - return validate.NewRequestError(err, http.StatusInternalServerError) - } - return server.Respond(w, http.StatusOK, result) - } - - return nil + body.ID = ID + return ctrl.repo.Items.UpdateByGroup(auth, auth.GID, body) } + + return adapters.ActionID("id", fn, http.StatusOK) } // HandleGetAllCustomFieldNames godocs @@ -206,17 +175,13 @@ func (ctrl *V1Controller) handleItemsGeneral() server.HandlerFunc { // @Router /v1/items/fields [GET] // @Success 200 {object} []string // @Security Bearer -func (ctrl *V1Controller) HandleGetAllCustomFieldNames() server.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) error { - ctx := services.NewContext(r.Context()) - - v, err := ctrl.repo.Items.GetAllCustomFieldNames(r.Context(), ctx.GID) - if err != nil { - return err - } - - return server.Respond(w, http.StatusOK, v) +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) } + + return adapters.Command(fn, http.StatusOK) } // HandleGetAllCustomFieldValues godocs @@ -228,17 +193,18 @@ func (ctrl *V1Controller) HandleGetAllCustomFieldNames() server.HandlerFunc { // @Router /v1/items/fields/values [GET] // @Success 200 {object} []string // @Security Bearer -func (ctrl *V1Controller) HandleGetAllCustomFieldValues() server.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) error { - ctx := services.NewContext(r.Context()) - - v, err := ctrl.repo.Items.GetAllCustomFieldValues(r.Context(), ctx.GID, r.URL.Query().Get("field")) - if err != nil { - return err - } - - return server.Respond(w, http.StatusOK, v) +func (ctrl *V1Controller) HandleGetAllCustomFieldValues() errchain.HandlerFunc { + type query struct { + Field string `schema:"field" validate:"required"` } + + fn := func(r *http.Request, q query) ([]string, error) { + auth := services.NewContext(r.Context()) + return ctrl.repo.Items.GetAllCustomFieldValues(auth, auth.GID, q.Field) + } + + return adapters.Action(fn, http.StatusOK) + } // HandleItemsImport godocs @@ -250,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 { @@ -272,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) } } @@ -283,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()) diff --git a/backend/app/api/handlers/v1/v1_ctrl_items_attachments.go b/backend/app/api/handlers/v1/v1_ctrl_items_attachments.go index b304bf5..9745538 100644 --- a/backend/app/api/handlers/v1/v1_ctrl_items_attachments.go +++ b/backend/app/api/handlers/v1/v1_ctrl_items_attachments.go @@ -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 diff --git a/backend/app/api/handlers/v1/v1_ctrl_labels.go b/backend/app/api/handlers/v1/v1_ctrl_labels.go index 8eaaff7..0dabe6f 100644 --- a/backend/app/api/handlers/v1/v1_ctrl_labels.go +++ b/backend/app/api/handlers/v1/v1_ctrl_labels.go @@ -3,12 +3,11 @@ package v1 import ( "net/http" + "github.com/google/uuid" "github.com/hay-kot/homebox/backend/internal/core/services" - "github.com/hay-kot/homebox/backend/internal/data/ent" "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/rs/zerolog/log" + "github.com/hay-kot/homebox/backend/internal/web/adapters" + "github.com/hay-kot/safeserve/errchain" ) // HandleLabelsGetAll godoc @@ -16,19 +15,16 @@ 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 { - return func(w http.ResponseWriter, r *http.Request) error { - user := services.UseUserCtx(r.Context()) - labels, err := ctrl.repo.Labels.GetAll(r.Context(), user.GroupID) - if err != nil { - log.Err(err).Msg("error getting labels") - return validate.NewRequestError(err, http.StatusInternalServerError) - } - return server.Respond(w, http.StatusOK, server.Results{Items: labels}) +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) } + + return adapters.Command(fn, http.StatusOK) } // HandleLabelsCreate godoc @@ -40,23 +36,13 @@ func (ctrl *V1Controller) HandleLabelsGetAll() server.HandlerFunc { // @Success 200 {object} repo.LabelSummary // @Router /v1/labels [POST] // @Security Bearer -func (ctrl *V1Controller) HandleLabelsCreate() server.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) error { - createData := repo.LabelCreate{} - if err := server.Decode(r, &createData); err != nil { - log.Err(err).Msg("error decoding label create data") - return validate.NewRequestError(err, http.StatusInternalServerError) - } - - user := services.UseUserCtx(r.Context()) - label, err := ctrl.repo.Labels.Create(r.Context(), user.GroupID, createData) - if err != nil { - log.Err(err).Msg("error creating label") - return validate.NewRequestError(err, http.StatusInternalServerError) - } - - return server.Respond(w, http.StatusCreated, label) +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) } + + return adapters.Action(fn, http.StatusCreated) } // HandleLabelDelete godocs @@ -68,8 +54,14 @@ func (ctrl *V1Controller) HandleLabelsCreate() server.HandlerFunc { // @Success 204 // @Router /v1/labels/{id} [DELETE] // @Security Bearer -func (ctrl *V1Controller) HandleLabelDelete() server.HandlerFunc { - return ctrl.handleLabelsGeneral() +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) + return nil, err + } + + return adapters.CommandID("id", fn, http.StatusNoContent) } // HandleLabelGet godocs @@ -81,8 +73,13 @@ func (ctrl *V1Controller) HandleLabelDelete() server.HandlerFunc { // @Success 200 {object} repo.LabelOut // @Router /v1/labels/{id} [GET] // @Security Bearer -func (ctrl *V1Controller) HandleLabelGet() server.HandlerFunc { - return ctrl.handleLabelsGeneral() +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) + } + + return adapters.CommandID("id", fn, http.StatusOK) } // HandleLabelUpdate godocs @@ -94,55 +91,12 @@ func (ctrl *V1Controller) HandleLabelGet() server.HandlerFunc { // @Success 200 {object} repo.LabelOut // @Router /v1/labels/{id} [PUT] // @Security Bearer -func (ctrl *V1Controller) HandleLabelUpdate() server.HandlerFunc { - return ctrl.handleLabelsGeneral() -} - -func (ctrl *V1Controller) handleLabelsGeneral() server.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) error { - ctx := services.NewContext(r.Context()) - ID, err := ctrl.routeID(r) - if err != nil { - return err - } - - switch r.Method { - case http.MethodGet: - labels, err := ctrl.repo.Labels.GetOneByGroup(r.Context(), ctx.GID, ID) - if err != nil { - if ent.IsNotFound(err) { - log.Err(err). - Str("id", ID.String()). - Msg("label not found") - return validate.NewRequestError(err, http.StatusNotFound) - } - log.Err(err).Msg("error getting label") - return validate.NewRequestError(err, http.StatusInternalServerError) - } - return server.Respond(w, http.StatusOK, labels) - - case http.MethodDelete: - err = ctrl.repo.Labels.DeleteByGroup(ctx, ctx.GID, ID) - if err != nil { - log.Err(err).Msg("error deleting label") - return validate.NewRequestError(err, http.StatusInternalServerError) - } - return server.Respond(w, http.StatusNoContent, nil) - - case http.MethodPut: - body := repo.LabelUpdate{} - if err := server.Decode(r, &body); err != nil { - return validate.NewRequestError(err, http.StatusInternalServerError) - } - - body.ID = ID - result, err := ctrl.repo.Labels.UpdateByGroup(ctx, ctx.GID, body) - if err != nil { - return validate.NewRequestError(err, http.StatusInternalServerError) - } - return server.Respond(w, http.StatusOK, result) - } - - return nil +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 + return ctrl.repo.Labels.UpdateByGroup(auth, auth.GID, data) } + + return adapters.ActionID("id", fn, http.StatusOK) } diff --git a/backend/app/api/handlers/v1/v1_ctrl_locations.go b/backend/app/api/handlers/v1/v1_ctrl_locations.go index c371b6e..b495c66 100644 --- a/backend/app/api/handlers/v1/v1_ctrl_locations.go +++ b/backend/app/api/handlers/v1/v1_ctrl_locations.go @@ -3,77 +3,50 @@ package v1 import ( "net/http" + "github.com/google/uuid" "github.com/hay-kot/homebox/backend/internal/core/services" - "github.com/hay-kot/homebox/backend/internal/data/ent" "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/rs/zerolog/log" + "github.com/hay-kot/homebox/backend/internal/web/adapters" + "github.com/hay-kot/safeserve/errchain" ) -// HandleLocationTreeQuery godoc +// HandleLocationTreeQuery // // @Summary Get Locations Tree // @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 { - return func(w http.ResponseWriter, r *http.Request) error { - user := services.UseUserCtx(r.Context()) - - q := r.URL.Query() - - withItems := queryBool(q.Get("withItems")) - - locTree, err := ctrl.repo.Locations.Tree( - r.Context(), - user.GroupID, - repo.TreeQuery{ - WithItems: withItems, - }, - ) - if err != nil { - log.Err(err).Msg("failed to get locations tree") - return validate.NewRequestError(err, http.StatusInternalServerError) - } - - return server.Respond(w, http.StatusOK, server.Results{Items: locTree}) +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) } + + return adapters.Query(fn, http.StatusOK) } -// HandleLocationGetAll godoc +// HandleLocationGetAll // // @Summary Get All Locations // @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 { - return func(w http.ResponseWriter, r *http.Request) error { - user := services.UseUserCtx(r.Context()) - - q := r.URL.Query() - - filter := repo.LocationQuery{ - FilterChildren: queryBool(q.Get("filterChildren")), - } - - locations, err := ctrl.repo.Locations.GetAll(r.Context(), user.GroupID, filter) - if err != nil { - log.Err(err).Msg("failed to get locations") - return validate.NewRequestError(err, http.StatusInternalServerError) - } - - return server.Respond(w, http.StatusOK, server.Results{Items: locations}) +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) } + + return adapters.Query(fn, http.StatusOK) } -// HandleLocationCreate godoc +// HandleLocationCreate // // @Summary Create Location // @Tags Locations @@ -82,26 +55,16 @@ func (ctrl *V1Controller) HandleLocationGetAll() server.HandlerFunc { // @Success 200 {object} repo.LocationSummary // @Router /v1/locations [POST] // @Security Bearer -func (ctrl *V1Controller) HandleLocationCreate() server.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) error { - createData := repo.LocationCreate{} - if err := server.Decode(r, &createData); err != nil { - log.Err(err).Msg("failed to decode location create data") - return validate.NewRequestError(err, http.StatusInternalServerError) - } - - user := services.UseUserCtx(r.Context()) - location, err := ctrl.repo.Locations.Create(r.Context(), user.GroupID, createData) - if err != nil { - log.Err(err).Msg("failed to create location") - return validate.NewRequestError(err, http.StatusInternalServerError) - } - - return server.Respond(w, http.StatusCreated, location) +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) } + + return adapters.Action(fn, http.StatusCreated) } -// HandleLocationDelete godocs +// HandleLocationDelete // // @Summary Delete Location // @Tags Locations @@ -110,11 +73,17 @@ func (ctrl *V1Controller) HandleLocationCreate() server.HandlerFunc { // @Success 204 // @Router /v1/locations/{id} [DELETE] // @Security Bearer -func (ctrl *V1Controller) HandleLocationDelete() server.HandlerFunc { - return ctrl.handleLocationGeneral() +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) + return nil, err + } + + return adapters.CommandID("id", fn, http.StatusNoContent) } -// HandleLocationGet godocs +// HandleLocationGet // // @Summary Get Location // @Tags Locations @@ -123,11 +92,16 @@ func (ctrl *V1Controller) HandleLocationDelete() server.HandlerFunc { // @Success 200 {object} repo.LocationOut // @Router /v1/locations/{id} [GET] // @Security Bearer -func (ctrl *V1Controller) HandleLocationGet() server.HandlerFunc { - return ctrl.handleLocationGeneral() +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) + } + + return adapters.CommandID("id", fn, http.StatusOK) } -// HandleLocationUpdate godocs +// HandleLocationUpdate // // @Summary Update Location // @Tags Locations @@ -137,58 +111,12 @@ func (ctrl *V1Controller) HandleLocationGet() server.HandlerFunc { // @Success 200 {object} repo.LocationOut // @Router /v1/locations/{id} [PUT] // @Security Bearer -func (ctrl *V1Controller) HandleLocationUpdate() server.HandlerFunc { - return ctrl.handleLocationGeneral() -} - -func (ctrl *V1Controller) handleLocationGeneral() server.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) error { - ctx := services.NewContext(r.Context()) - ID, err := ctrl.routeID(r) - if err != nil { - return err - } - - switch r.Method { - case http.MethodGet: - location, err := ctrl.repo.Locations.GetOneByGroup(r.Context(), ctx.GID, ID) - if err != nil { - l := log.Err(err). - Str("ID", ID.String()). - Str("GID", ctx.GID.String()) - - if ent.IsNotFound(err) { - l.Msg("location not found") - return validate.NewRequestError(err, http.StatusNotFound) - } - - l.Msg("failed to get location") - return validate.NewRequestError(err, http.StatusInternalServerError) - } - return server.Respond(w, http.StatusOK, location) - case http.MethodPut: - body := repo.LocationUpdate{} - if err := server.Decode(r, &body); err != nil { - log.Err(err).Msg("failed to decode location update data") - return validate.NewRequestError(err, http.StatusInternalServerError) - } - - body.ID = ID - - result, err := ctrl.repo.Locations.UpdateOneByGroup(r.Context(), ctx.GID, ID, body) - if err != nil { - log.Err(err).Msg("failed to update location") - return validate.NewRequestError(err, http.StatusInternalServerError) - } - return server.Respond(w, http.StatusOK, result) - case http.MethodDelete: - err = ctrl.repo.Locations.DeleteByGroup(r.Context(), ctx.GID, ID) - if err != nil { - log.Err(err).Msg("failed to delete location") - return validate.NewRequestError(err, http.StatusInternalServerError) - } - return server.Respond(w, http.StatusNoContent, nil) - } - return nil +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 + return ctrl.repo.Locations.UpdateByGroup(auth, auth.GID, ID, body) } + + return adapters.ActionID("id", fn, http.StatusOK) } diff --git a/backend/app/api/handlers/v1/v1_ctrl_maint_entry.go b/backend/app/api/handlers/v1/v1_ctrl_maint_entry.go index d7036f0..eeed717 100644 --- a/backend/app/api/handlers/v1/v1_ctrl_maint_entry.go +++ b/backend/app/api/handlers/v1/v1_ctrl_maint_entry.go @@ -2,13 +2,12 @@ package v1 import ( "net/http" - "strconv" + "github.com/google/uuid" "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/rs/zerolog/log" + "github.com/hay-kot/homebox/backend/internal/web/adapters" + "github.com/hay-kot/safeserve/errchain" ) // HandleMaintenanceGetLog godoc @@ -19,8 +18,13 @@ import ( // @Success 200 {object} repo.MaintenanceLog // @Router /v1/items/{id}/maintenance [GET] // @Security Bearer -func (ctrl *V1Controller) HandleMaintenanceLogGet() server.HandlerFunc { - return ctrl.handleMaintenanceLog() +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) + } + + return adapters.QueryID("id", fn, http.StatusOK) } // HandleMaintenanceEntryCreate godoc @@ -29,11 +33,16 @@ func (ctrl *V1Controller) HandleMaintenanceLogGet() server.HandlerFunc { // @Tags Maintenance // @Produce json // @Param payload body repo.MaintenanceEntryCreate true "Entry Data" -// @Success 200 {object} repo.MaintenanceEntry +// @Success 201 {object} repo.MaintenanceEntry // @Router /v1/items/{id}/maintenance [POST] // @Security Bearer -func (ctrl *V1Controller) HandleMaintenanceEntryCreate() server.HandlerFunc { - return ctrl.handleMaintenanceLog() +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) + } + + return adapters.ActionID("id", fn, http.StatusCreated) } // HandleMaintenanceEntryDelete godoc @@ -44,8 +53,14 @@ func (ctrl *V1Controller) HandleMaintenanceEntryCreate() server.HandlerFunc { // @Success 204 // @Router /v1/items/{id}/maintenance/{entry_id} [DELETE] // @Security Bearer -func (ctrl *V1Controller) HandleMaintenanceEntryDelete() server.HandlerFunc { - return ctrl.handleMaintenanceLog() +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) + return nil, err + } + + return adapters.CommandID("entry_id", fn, http.StatusNoContent) } // HandleMaintenanceEntryUpdate godoc @@ -57,81 +72,11 @@ 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 { - return ctrl.handleMaintenanceLog() -} - -func (ctrl *V1Controller) handleMaintenanceLog() server.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) error { - ctx := services.NewContext(r.Context()) - itemID, err := ctrl.routeID(r) - if err != nil { - return err - } - - switch r.Method { - case http.MethodGet: - completed, _ := strconv.ParseBool(r.URL.Query().Get("completed")) - scheduled, _ := strconv.ParseBool(r.URL.Query().Get("scheduled")) - query := repo.MaintenanceLogQuery{ - Completed: completed, - Scheduled: scheduled, - } - - mlog, err := ctrl.repo.MaintEntry.GetLog(ctx, itemID, query) - if err != nil { - log.Err(err).Msg("failed to get items") - return validate.NewRequestError(err, http.StatusInternalServerError) - } - return server.Respond(w, http.StatusOK, mlog) - case http.MethodPost: - var create repo.MaintenanceEntryCreate - err := server.Decode(r, &create) - if err != nil { - return validate.NewRequestError(err, http.StatusBadRequest) - } - - entry, err := ctrl.repo.MaintEntry.Create(ctx, itemID, create) - if err != nil { - log.Err(err).Msg("failed to create item") - return validate.NewRequestError(err, http.StatusInternalServerError) - } - - return server.Respond(w, http.StatusCreated, entry) - case http.MethodPut: - entryID, err := ctrl.routeUUID(r, "entry_id") - if err != nil { - return err - } - - var update repo.MaintenanceEntryUpdate - err = server.Decode(r, &update) - if err != nil { - return validate.NewRequestError(err, http.StatusBadRequest) - } - - entry, err := ctrl.repo.MaintEntry.Update(ctx, entryID, update) - if err != nil { - log.Err(err).Msg("failed to update item") - return validate.NewRequestError(err, http.StatusInternalServerError) - } - - return server.Respond(w, http.StatusOK, entry) - case http.MethodDelete: - entryID, err := ctrl.routeUUID(r, "entry_id") - if err != nil { - return err - } - - err = ctrl.repo.MaintEntry.Delete(ctx, entryID) - if err != nil { - log.Err(err).Msg("failed to delete item") - return validate.NewRequestError(err, http.StatusInternalServerError) - } - - return server.Respond(w, http.StatusNoContent, nil) - } - - return nil +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) } + + return adapters.ActionID("entry_id", fn, http.StatusOK) } diff --git a/backend/app/api/handlers/v1/v1_ctrl_notifiers.go b/backend/app/api/handlers/v1/v1_ctrl_notifiers.go index a226249..51da4d7 100644 --- a/backend/app/api/handlers/v1/v1_ctrl_notifiers.go +++ b/backend/app/api/handlers/v1/v1_ctrl_notifiers.go @@ -1,7 +1,6 @@ package v1 import ( - "context" "net/http" "github.com/containrrr/shoutrrr" @@ -9,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 @@ -17,13 +16,13 @@ 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 { - fn := func(ctx context.Context, _ struct{}) ([]repo.NotifierOut, error) { - user := services.UseUserCtx(ctx) - return ctrl.repo.Notifiers.GetByUser(ctx, user.ID) +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) } return adapters.Query(fn, http.StatusOK) @@ -38,10 +37,10 @@ func (ctrl *V1Controller) HandleGetUserNotifiers() server.HandlerFunc { // @Success 200 {object} repo.NotifierOut // @Router /v1/notifiers [POST] // @Security Bearer -func (ctrl *V1Controller) HandleCreateNotifier() server.HandlerFunc { - fn := func(ctx context.Context, in repo.NotifierCreate) (repo.NotifierOut, error) { - auth := services.NewContext(ctx) - return ctrl.repo.Notifiers.Create(ctx, auth.GID, auth.UID, in) +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) } return adapters.Action(fn, http.StatusCreated) @@ -55,10 +54,10 @@ func (ctrl *V1Controller) HandleCreateNotifier() server.HandlerFunc { // @Success 204 // @Router /v1/notifiers/{id} [DELETE] // @Security Bearer -func (ctrl *V1Controller) HandleDeleteNotifier() server.HandlerFunc { - fn := func(ctx context.Context, ID uuid.UUID) (any, error) { - auth := services.NewContext(ctx) - return nil, ctrl.repo.Notifiers.Delete(ctx, auth.UID, ID) +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) } return adapters.CommandID("id", fn, http.StatusNoContent) @@ -73,10 +72,10 @@ func (ctrl *V1Controller) HandleDeleteNotifier() server.HandlerFunc { // @Success 200 {object} repo.NotifierOut // @Router /v1/notifiers/{id} [PUT] // @Security Bearer -func (ctrl *V1Controller) HandleUpdateNotifier() server.HandlerFunc { - fn := func(ctx context.Context, ID uuid.UUID, in repo.NotifierUpdate) (repo.NotifierOut, error) { - auth := services.NewContext(ctx) - return ctrl.repo.Notifiers.Update(ctx, auth.UID, ID, in) +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) } return adapters.ActionID("id", fn, http.StatusOK) @@ -92,12 +91,12 @@ 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"` } - fn := func(ctx context.Context, q body) (any, error) { + fn := func(r *http.Request, q body) (any, error) { err := shoutrrr.Send(q.URL, "Test message from Homebox") return nil, err } diff --git a/backend/app/api/handlers/v1/v1_ctrl_qrcode.go b/backend/app/api/handlers/v1/v1_ctrl_qrcode.go index 5084cb8..1f06e2f 100644 --- a/backend/app/api/handlers/v1/v1_ctrl_qrcode.go +++ b/backend/app/api/handlers/v1/v1_ctrl_qrcode.go @@ -6,8 +6,8 @@ import ( "io" "net/http" - "github.com/hay-kot/homebox/backend/internal/sys/validate" - "github.com/hay-kot/homebox/backend/pkgs/server" + "github.com/hay-kot/homebox/backend/internal/web/adapters" + "github.com/hay-kot/safeserve/errchain" "github.com/yeqown/go-qrcode/v2" "github.com/yeqown/go-qrcode/writer/standard" @@ -26,25 +26,24 @@ var qrcodeLogo []byte // @Success 200 {string} string "image/jpeg" // @Router /v1/qrcode [GET] // @Security Bearer -func (ctrl *V1Controller) HandleGenerateQRCode() server.HandlerFunc { - const MaxLength = 4_296 // assume alphanumeric characters only +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"` + } return func(w http.ResponseWriter, r *http.Request) error { - data := r.URL.Query().Get("data") + q, err := adapters.DecodeQuery[query](r) + if err != nil { + return err + } image, err := png.Decode(bytes.NewReader(qrcodeLogo)) if err != nil { panic(err) } - if len(data) > MaxLength { - return validate.NewFieldErrors(validate.FieldError{ - Field: "data", - Error: "max length is 4,296 characters exceeded", - }) - } - - qrc, err := qrcode.New(data) + qrc, err := qrcode.New(q.Data) if err != nil { return err } diff --git a/backend/app/api/handlers/v1/v1_ctrl_reporting.go b/backend/app/api/handlers/v1/v1_ctrl_reporting.go index b665edd..030a1ff 100644 --- a/backend/app/api/handlers/v1/v1_ctrl_reporting.go +++ b/backend/app/api/handlers/v1/v1_ctrl_reporting.go @@ -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()) diff --git a/backend/app/api/handlers/v1/v1_ctrl_statistics.go b/backend/app/api/handlers/v1/v1_ctrl_statistics.go index 223f70a..55a8dcb 100644 --- a/backend/app/api/handlers/v1/v1_ctrl_statistics.go +++ b/backend/app/api/handlers/v1/v1_ctrl_statistics.go @@ -5,8 +5,11 @@ import ( "time" "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/homebox/backend/internal/web/adapters" + "github.com/hay-kot/safeserve/errchain" + "github.com/hay-kot/safeserve/server" ) // HandleGroupGet godoc @@ -17,17 +20,13 @@ import ( // @Success 200 {object} []repo.TotalsByOrganizer // @Router /v1/groups/statistics/locations [GET] // @Security Bearer -func (ctrl *V1Controller) HandleGroupStatisticsLocations() server.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) error { - ctx := services.NewContext(r.Context()) - - stats, err := ctrl.repo.Groups.StatsLocationsByPurchasePrice(ctx, ctx.GID) - if err != nil { - return validate.NewRequestError(err, http.StatusInternalServerError) - } - - return server.Respond(w, http.StatusOK, stats) +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) } + + return adapters.Command(fn, http.StatusOK) } // HandleGroupStatisticsLabels godoc @@ -38,17 +37,13 @@ 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 { - return func(w http.ResponseWriter, r *http.Request) error { - ctx := services.NewContext(r.Context()) - - stats, err := ctrl.repo.Groups.StatsLabelsByPurchasePrice(ctx, ctx.GID) - if err != nil { - return validate.NewRequestError(err, http.StatusInternalServerError) - } - - return server.Respond(w, http.StatusOK, stats) +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) } + + return adapters.Command(fn, http.StatusOK) } // HandleGroupStatistics godoc @@ -59,17 +54,13 @@ func (ctrl *V1Controller) HandleGroupStatisticsLabels() server.HandlerFunc { // @Success 200 {object} repo.GroupStatistics // @Router /v1/groups/statistics [GET] // @Security Bearer -func (ctrl *V1Controller) HandleGroupStatistics() server.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) error { - ctx := services.NewContext(r.Context()) - - stats, err := ctrl.repo.Groups.StatsGroup(ctx, ctx.GID) - if err != nil { - return validate.NewRequestError(err, http.StatusInternalServerError) - } - - return server.Respond(w, http.StatusOK, stats) +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) } + + return adapters.Command(fn, http.StatusOK) } // HandleGroupStatisticsPriceOverTime godoc @@ -82,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 @@ -108,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) } } diff --git a/backend/app/api/handlers/v1/v1_ctrl_user.go b/backend/app/api/handlers/v1/v1_ctrl_user.go index 8331496..cc57305 100644 --- a/backend/app/api/handlers/v1/v1_ctrl_user.go +++ b/backend/app/api/handlers/v1/v1_ctrl_user.go @@ -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) } } diff --git a/backend/app/api/main.go b/backend/app/api/main.go index e380dfa..c92572a 100644 --- a/backend/app/api/main.go +++ b/backend/app/api/main.go @@ -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,9 +19,12 @@ 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" + "github.com/rs/zerolog/pkgerrors" ) var ( @@ -38,6 +44,8 @@ var ( // @name Authorization // @description "Type 'Bearer TOKEN' to correctly set the API Key" func main() { + zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack + cfg, err := config.New() if err != nil { panic(err) @@ -118,26 +126,27 @@ func run(cfg *config.Config) error { ) // ========================================================================= - // Start Server\ + // 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) // ========================================================================= @@ -175,5 +184,5 @@ func run(cfg *config.Config) error { }() } - return app.server.Start() + return app.server.Start(router) } diff --git a/backend/app/api/middleware.go b/backend/app/api/middleware.go index c694618..3617c09 100644 --- a/backend/app/api/middleware.go +++ b/backend/app/api/middleware.go @@ -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, diff --git a/backend/app/api/routes.go b/backend/app/api/routes.go index e638fbe..e36ee02 100644 --- a/backend/app/api/routes.go +++ b/backend/app/api/routes.go @@ -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.HandleMaintenanceEntryCreate(), 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 { diff --git a/backend/app/api/static/docs/docs.go b/backend/app/api/static/docs/docs.go index c68703b..5ad614f 100644 --- a/backend/app/api/static/docs/docs.go +++ b/backend/app/api/static/docs/docs.go @@ -425,8 +425,8 @@ const docTemplate = `{ } ], "responses": { - "200": { - "description": "OK", + "201": { + "description": "Created", "schema": { "$ref": "#/definitions/repo.ItemSummary" } @@ -694,7 +694,7 @@ const docTemplate = `{ "422": { "description": "Unprocessable Entity", "schema": { - "$ref": "#/definitions/server.ErrorResponse" + "$ref": "#/definitions/mid.ErrorResponse" } } } @@ -864,8 +864,8 @@ const docTemplate = `{ } ], "responses": { - "200": { - "description": "OK", + "201": { + "description": "Created", "schema": { "$ref": "#/definitions/repo.MaintenanceEntry" } @@ -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": { @@ -1902,9 +1916,15 @@ const docTemplate = `{ }, "repo.ItemCreate": { "type": "object", + "required": [ + "description", + "name" + ], "properties": { "description": { - "type": "string" + "type": "string", + "maxLength": 1000, + "minLength": 1 }, "labelIds": { "type": "array", @@ -1917,7 +1937,9 @@ const docTemplate = `{ "type": "string" }, "name": { - "type": "string" + "type": "string", + "maxLength": 255, + "minLength": 1 }, "parentId": { "type": "string", @@ -2208,15 +2230,21 @@ const docTemplate = `{ }, "repo.LabelCreate": { "type": "object", + "required": [ + "name" + ], "properties": { "color": { "type": "string" }, "description": { - "type": "string" + "type": "string", + "maxLength": 255 }, "name": { - "type": "string" + "type": "string", + "maxLength": 255, + "minLength": 1 } } }, @@ -2663,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": { @@ -2791,12 +2786,17 @@ const docTemplate = `{ }, "v1.GroupInvitationCreate": { "type": "object", + "required": [ + "uses" + ], "properties": { "expiresAt": { "type": "string" }, "uses": { - "type": "integer" + "type": "integer", + "maximum": 100, + "minimum": 1 } } }, @@ -2821,6 +2821,12 @@ const docTemplate = `{ "type": "string" } } + }, + "v1.Wrapped": { + "type": "object", + "properties": { + "item": {} + } } }, "securityDefinitions": { diff --git a/backend/app/api/static/docs/swagger.json b/backend/app/api/static/docs/swagger.json index c46f5a5..c12bd42 100644 --- a/backend/app/api/static/docs/swagger.json +++ b/backend/app/api/static/docs/swagger.json @@ -417,8 +417,8 @@ } ], "responses": { - "200": { - "description": "OK", + "201": { + "description": "Created", "schema": { "$ref": "#/definitions/repo.ItemSummary" } @@ -686,7 +686,7 @@ "422": { "description": "Unprocessable Entity", "schema": { - "$ref": "#/definitions/server.ErrorResponse" + "$ref": "#/definitions/mid.ErrorResponse" } } } @@ -856,8 +856,8 @@ } ], "responses": { - "200": { - "description": "OK", + "201": { + "description": "Created", "schema": { "$ref": "#/definitions/repo.MaintenanceEntry" } @@ -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": { @@ -1894,9 +1908,15 @@ }, "repo.ItemCreate": { "type": "object", + "required": [ + "description", + "name" + ], "properties": { "description": { - "type": "string" + "type": "string", + "maxLength": 1000, + "minLength": 1 }, "labelIds": { "type": "array", @@ -1909,7 +1929,9 @@ "type": "string" }, "name": { - "type": "string" + "type": "string", + "maxLength": 255, + "minLength": 1 }, "parentId": { "type": "string", @@ -2200,15 +2222,21 @@ }, "repo.LabelCreate": { "type": "object", + "required": [ + "name" + ], "properties": { "color": { "type": "string" }, "description": { - "type": "string" + "type": "string", + "maxLength": 255 }, "name": { - "type": "string" + "type": "string", + "maxLength": 255, + "minLength": 1 } } }, @@ -2655,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": { @@ -2783,12 +2778,17 @@ }, "v1.GroupInvitationCreate": { "type": "object", + "required": [ + "uses" + ], "properties": { "expiresAt": { "type": "string" }, "uses": { - "type": "integer" + "type": "integer", + "maximum": 100, + "minimum": 1 } } }, @@ -2813,6 +2813,12 @@ "type": "string" } } + }, + "v1.Wrapped": { + "type": "object", + "properties": { + "item": {} + } } }, "securityDefinitions": { diff --git a/backend/app/api/static/docs/swagger.yaml b/backend/app/api/static/docs/swagger.yaml index 658d4b0..8a8b7f7 100644 --- a/backend/app/api/static/docs/swagger.yaml +++ b/backend/app/api/static/docs/swagger.yaml @@ -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: @@ -67,6 +76,8 @@ definitions: repo.ItemCreate: properties: description: + maxLength: 1000 + minLength: 1 type: string labelIds: items: @@ -76,10 +87,15 @@ definitions: description: Edges type: string name: + maxLength: 255 + minLength: 1 type: string parentId: type: string x-nullable: true + required: + - description + - name type: object repo.ItemField: properties: @@ -281,9 +297,14 @@ definitions: color: type: string description: + maxLength: 255 type: string name: + maxLength: 255 + minLength: 1 type: string + required: + - name type: object repo.LabelOut: properties: @@ -579,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: @@ -666,7 +665,11 @@ definitions: expiresAt: type: string uses: + maximum: 100 + minimum: 1 type: integer + required: + - uses type: object v1.ItemAttachmentToken: properties: @@ -682,6 +685,10 @@ definitions: token: type: string type: object + v1.Wrapped: + properties: + item: {} + type: object info: contact: name: Don't @@ -932,8 +939,8 @@ paths: produces: - application/json responses: - "200": - description: OK + "201": + description: Created schema: $ref: '#/definitions/repo.ItemSummary' security: @@ -1036,7 +1043,7 @@ paths: "422": description: Unprocessable Entity schema: - $ref: '#/definitions/server.ErrorResponse' + $ref: '#/definitions/mid.ErrorResponse' security: - Bearer: [] summary: Create Item Attachment @@ -1140,8 +1147,8 @@ paths: produces: - application/json responses: - "200": - description: OK + "201": + description: Created schema: $ref: '#/definitions/repo.MaintenanceEntry' security: @@ -1252,7 +1259,7 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/server.Results' + - $ref: '#/definitions/v1.Wrapped' - properties: items: items: @@ -1354,7 +1361,7 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/server.Results' + - $ref: '#/definitions/v1.Wrapped' - properties: items: items: @@ -1462,7 +1469,7 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/server.Results' + - $ref: '#/definitions/v1.Wrapped' - properties: items: items: @@ -1483,7 +1490,7 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/server.Results' + - $ref: '#/definitions/v1.Wrapped' - properties: items: items: @@ -1725,7 +1732,7 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/server.Result' + - $ref: '#/definitions/v1.Wrapped' - properties: item: $ref: '#/definitions/repo.UserOut' @@ -1750,7 +1757,7 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/server.Result' + - $ref: '#/definitions/v1.Wrapped' - properties: item: $ref: '#/definitions/repo.UserUpdate' diff --git a/scripts/process-types/main.go b/backend/app/tools/typegen/main.go similarity index 100% rename from scripts/process-types/main.go rename to backend/app/tools/typegen/main.go diff --git a/backend/go.mod b/backend/go.mod index 8a63556..f5be84f 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -12,7 +12,9 @@ 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 github.com/stretchr/testify v1.8.2 github.com/swaggo/http-swagger v1.3.4 @@ -45,7 +47,6 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.17 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect - github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/swaggo/files v1.0.0 // indirect github.com/yeqown/reedsolomon v1.0.0 // indirect diff --git a/backend/go.sum b/backend/go.sum index 054a9c0..469f9e2 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -1,5 +1,3 @@ -ariga.io/atlas v0.9.1-0.20230119145809-92243f7c55cb h1:mbsFtavDqGdYwdDpP50LGOOZ2hgyGoJcZeOpbgKMyu4= -ariga.io/atlas v0.9.1-0.20230119145809-92243f7c55cb/go.mod h1:T230JFcENj4ZZzMkZrXFDSkv+2kXkUgpJ5FQQ5hMcKU= ariga.io/atlas v0.10.0 h1:B1aCP6gSDQET6j8ybn7m6MArjQuoLH5d4DQBT+NP5k8= ariga.io/atlas v0.10.0/go.mod h1:+TR129FJZ5Lvzms6dvCeGWh1yR6hMvmXBhug4hrNIGk= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= @@ -204,8 +202,6 @@ github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk5 github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= -github.com/ardanlabs/conf/v3 v3.1.4 h1:c0jJYbqHJcrR/uYImbGC1q7quH3DYxH49zGCT7WLJH4= -github.com/ardanlabs/conf/v3 v3.1.4/go.mod h1:bIacyuGeZjkTdtszdbvOcuq49VhHpV3+IPZ2ewOAK4I= github.com/ardanlabs/conf/v3 v3.1.5 h1:G6df2AxKnGHAK+ur2p50Ys8Vo1HnKcsvqSj9lxVeczk= github.com/ardanlabs/conf/v3 v3.1.5/go.mod h1:zclexWKe0NVj6LHQ8NgDDZ7bQ1spE0KeKPFficdtAjU= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= @@ -300,15 +296,11 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU= -github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s= github.com/go-playground/validator/v10 v10.12.0 h1:E4gtWgxWxp8YSxExrQFv5BpCahla0PVF2oTTEYaWQGI= github.com/go-playground/validator/v10 v10.12.0/go.mod h1:hCAPuzYvKdP33pxWa+2+6AIKXEKqjIUyqsNCtbsSJrA= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= -github.com/gocarina/gocsv v0.0.0-20230219202803-bcce7dc8d0bb h1:WZ3ADdZNC1i7uJsarVzPSSh0B27+XlmmCerFmU28T/4= -github.com/gocarina/gocsv v0.0.0-20230219202803-bcce7dc8d0bb/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI= github.com/gocarina/gocsv v0.0.0-20230226133904-70c27cb2918a h1:/5o1ejt5M0fNAN2lU1NBLtPzUSZru689EWJq01ptr+E= github.com/gocarina/gocsv v0.0.0-20230226133904-70c27cb2918a/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -447,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= @@ -481,8 +475,6 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4= -github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= -github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/leodido/go-urn v1.2.2 h1:7z68G0FCGvDk646jz1AelTYNYWrTNm0bEcFAo147wt4= github.com/leodido/go-urn v1.2.2/go.mod h1:kUaIbLZWttglzwNuG0pgsh5vuV6u2YcGBYz1hIPjtOQ= github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= @@ -507,7 +499,6 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= @@ -533,7 +524,6 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRW github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= @@ -585,8 +575,8 @@ github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w= github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= @@ -603,10 +593,8 @@ github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrf github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= -github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.14.0/go.mod h1:WT//axPky3FdvXHzGw33dNdXXXfFQqmEalje+egj8As= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -628,8 +616,6 @@ github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/swaggo/files v1.0.0 h1:1gGXVIeUFCS/dta17rnP0iOpr6CXFwKD7EO5ID233e4= github.com/swaggo/files v1.0.0/go.mod h1:N59U6URJLyU1PQgFqPM7wXLMhJx7QAolnvfQkqO13kc= -github.com/swaggo/http-swagger v1.3.3 h1:Hu5Z0L9ssyBLofaama21iYaF2VbWyA8jdohaaCGpHsc= -github.com/swaggo/http-swagger v1.3.3/go.mod h1:sE+4PjD89IxMPm77FnkDz0sdO+p5lbXzrVWT6OTVVGo= github.com/swaggo/http-swagger v1.3.4 h1:q7t/XLx0n15H1Q9/tk3Y9L4n210XzJF5WtnDX64a5ww= github.com/swaggo/http-swagger v1.3.4/go.mod h1:9dAh0unqMBAlbp1uE2Uc2mQTxNMU/ha4UbucIg1MFkQ= github.com/swaggo/swag v1.8.10 h1:eExW4bFa52WOjqRzRD58bgWsWfdFJso50lpbeTcmTfo= diff --git a/backend/internal/data/repo/repo_items.go b/backend/internal/data/repo/repo_items.go index 69434b8..04c203b 100644 --- a/backend/internal/data/repo/repo_items.go +++ b/backend/internal/data/repo/repo_items.go @@ -51,8 +51,8 @@ type ( ItemCreate struct { ImportRef string `json:"-"` ParentID uuid.UUID `json:"parentId" extensions:"x-nullable"` - Name string `json:"name"` - Description string `json:"description"` + Name string `json:"name" validate:"required,min=1,max=255"` + Description string `json:"description" validate:"required,min=1,max=1000"` AssetID AssetID `json:"-"` // Edges diff --git a/backend/internal/data/repo/repo_labels.go b/backend/internal/data/repo/repo_labels.go index a761ef8..ee62fd8 100644 --- a/backend/internal/data/repo/repo_labels.go +++ b/backend/internal/data/repo/repo_labels.go @@ -17,15 +17,15 @@ type LabelRepository struct { } type ( LabelCreate struct { - Name string `json:"name"` - Description string `json:"description"` + Name string `json:"name" validate:"required,min=1,max=255"` + Description string `json:"description" validate:"max=255"` Color string `json:"color"` } LabelUpdate struct { ID uuid.UUID `json:"id"` - Name string `json:"name"` - Description string `json:"description"` + Name string `json:"name" validate:"required,min=1,max=255"` + Description string `json:"description" validate:"max=255"` Color string `json:"color"` } diff --git a/backend/internal/data/repo/repo_locations.go b/backend/internal/data/repo/repo_locations.go index 542095e..e65a983 100644 --- a/backend/internal/data/repo/repo_locations.go +++ b/backend/internal/data/repo/repo_locations.go @@ -91,7 +91,7 @@ func mapLocationOut(location *ent.Location) LocationOut { } type LocationQuery struct { - FilterChildren bool `json:"filterChildren"` + FilterChildren bool `json:"filterChildren" schema:"filterChildren"` } // GetALlWithCount returns all locations with item count field populated @@ -217,7 +217,7 @@ func (r *LocationRepository) Update(ctx context.Context, data LocationUpdate) (L return r.update(ctx, data, location.ID(data.ID)) } -func (r *LocationRepository) UpdateOneByGroup(ctx context.Context, GID, ID uuid.UUID, data LocationUpdate) (LocationOut, error) { +func (r *LocationRepository) UpdateByGroup(ctx context.Context, GID, ID uuid.UUID, data LocationUpdate) (LocationOut, error) { return r.update(ctx, data, location.ID(ID), location.HasGroupWith(group.ID(GID))) } @@ -246,7 +246,7 @@ type FlatTreeItem struct { } type TreeQuery struct { - WithItems bool `json:"withItems"` + WithItems bool `json:"withItems" schema:"withItems"` } func (lr *LocationRepository) Tree(ctx context.Context, GID uuid.UUID, tq TreeQuery) ([]TreeItem, error) { diff --git a/backend/internal/data/repo/repo_locations_test.go b/backend/internal/data/repo/repo_locations_test.go index 0334a42..644828d 100644 --- a/backend/internal/data/repo/repo_locations_test.go +++ b/backend/internal/data/repo/repo_locations_test.go @@ -124,7 +124,7 @@ func TestItemRepository_TreeQuery(t *testing.T) { locs := useLocations(t, 3) // Set relations - _, err := tRepos.Locations.UpdateOneByGroup(context.Background(), tGroup.ID, locs[0].ID, LocationUpdate{ + _, err := tRepos.Locations.UpdateByGroup(context.Background(), tGroup.ID, locs[0].ID, LocationUpdate{ ID: locs[0].ID, ParentID: locs[1].ID, Name: locs[0].Name, diff --git a/backend/internal/data/repo/repo_maintenance_entry.go b/backend/internal/data/repo/repo_maintenance_entry.go index 96eae75..7ef282b 100644 --- a/backend/internal/data/repo/repo_maintenance_entry.go +++ b/backend/internal/data/repo/repo_maintenance_entry.go @@ -6,6 +6,8 @@ import ( "github.com/google/uuid" "github.com/hay-kot/homebox/backend/internal/data/ent" + "github.com/hay-kot/homebox/backend/internal/data/ent/group" + "github.com/hay-kot/homebox/backend/internal/data/ent/item" "github.com/hay-kot/homebox/backend/internal/data/ent/maintenanceentry" "github.com/hay-kot/homebox/backend/internal/data/types" ) @@ -92,16 +94,21 @@ func (r *MaintenanceEntryRepository) Update(ctx context.Context, ID uuid.UUID, i } type MaintenanceLogQuery struct { - Completed bool - Scheduled bool + Completed bool `json:"completed" schema:"completed"` + Scheduled bool `json:"scheduled" schema:"scheduled"` } -func (r *MaintenanceEntryRepository) GetLog(ctx context.Context, itemID uuid.UUID, query MaintenanceLogQuery) (MaintenanceLog, error) { +func (r *MaintenanceEntryRepository) GetLog(ctx context.Context, groupID, itemID uuid.UUID, query MaintenanceLogQuery) (MaintenanceLog, error) { log := MaintenanceLog{ ItemID: itemID, } - q := r.db.MaintenanceEntry.Query().Where(maintenanceentry.ItemID(itemID)) + q := r.db.MaintenanceEntry.Query().Where( + maintenanceentry.ItemID(itemID), + maintenanceentry.HasItemWith( + item.HasGroupWith(group.IDEQ(groupID)), + ), + ) if query.Completed { q = q.Where(maintenanceentry.And( diff --git a/backend/internal/data/repo/repo_maintenance_entry_test.go b/backend/internal/data/repo/repo_maintenance_entry_test.go index e9763e7..a82768c 100644 --- a/backend/internal/data/repo/repo_maintenance_entry_test.go +++ b/backend/internal/data/repo/repo_maintenance_entry_test.go @@ -59,7 +59,7 @@ func TestMaintenanceEntryRepository_GetLog(t *testing.T) { } // Get the log for the item - log, err := tRepos.MaintEntry.GetLog(context.Background(), item.ID, MaintenanceLogQuery{ + log, err := tRepos.MaintEntry.GetLog(context.Background(), tGroup.ID, item.ID, MaintenanceLogQuery{ Completed: true, }) if err != nil { diff --git a/backend/internal/web/adapters/actions.go b/backend/internal/web/adapters/actions.go index 697b357..db9a410 100644 --- a/backend/internal/web/adapters/actions.go +++ b/backend/internal/web/adapters/actions.go @@ -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. @@ -16,25 +17,25 @@ import ( // Foo string `json:"foo"` // } // -// fn := func(ctx context.Context, b Body) (any, error) { +// fn := func(r *http.Request, b Body) (any, error) { // // do something with b // return nil, nil // } // // 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 := decode[T](r) + v, err := DecodeBody[T](r) if err != nil { return err } - res, err := f(r.Context(), v) + res, err := f(r, v) if err != nil { return err } - return server.Respond(w, ok, res) + return server.JSON(w, ok, res) } } @@ -46,29 +47,29 @@ func Action[T any, Y any](f AdapterFunc[T, Y], ok int) server.HandlerFunc { // Foo string `json:"foo"` // } // -// fn := func(ctx context.Context, ID uuid.UUID, b Body) (any, error) { +// fn := func(r *http.Request, ID uuid.UUID, b Body) (any, error) { // // do something with ID and b // return nil, nil // } // // 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) + ID, err := RouteUUID(r, param) if err != nil { return err } - v, err := decode[T](r) + v, err := DecodeBody[T](r) if err != nil { return err } - res, err := f(r.Context(), ID, v) + res, err := f(r, ID, v) if err != nil { return err } - return server.Respond(w, ok, res) + return server.JSON(w, ok, res) } } diff --git a/backend/internal/web/adapters/adapters.go b/backend/internal/web/adapters/adapters.go index 444dc86..8372a60 100644 --- a/backend/internal/web/adapters/adapters.go +++ b/backend/internal/web/adapters/adapters.go @@ -1,10 +1,10 @@ package adapters import ( - "context" + "net/http" "github.com/google/uuid" ) -type AdapterFunc[T any, Y any] func(context.Context, T) (Y, error) -type IDFunc[T any, Y any] func(context.Context, uuid.UUID, T) (Y, error) +type AdapterFunc[T any, Y any] func(*http.Request, T) (Y, error) +type IDFunc[T any, Y any] func(*http.Request, uuid.UUID, T) (Y, error) diff --git a/backend/internal/web/adapters/command.go b/backend/internal/web/adapters/command.go index eaa32ca..3d7eb06 100644 --- a/backend/internal/web/adapters/command.go +++ b/backend/internal/web/adapters/command.go @@ -1,36 +1,36 @@ package adapters import ( - "context" "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(context.Context) (T, error) -type CommandIDFunc[T any] func(context.Context, uuid.UUID) (T, error) +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. // // Example: // -// fn := func(ctx context.Context) (interface{}, error) { -// // do something -// return nil, nil -// } +// fn := func(r *http.Request) (interface{}, error) { +// // do something +// return nil, nil +// } // -// r.Get("/foo", adapters.Command(fn, http.NoContent)) -func Command[T any](f CommandFunc[T], ok int) server.HandlerFunc { +// r.Get("/foo", adapters.Command(fn, http.NoContent)) +func Command[T any](f CommandFunc[T], ok int) errchain.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) error { - res, err := f(r.Context()) + res, err := f(r) if err != nil { return err } - return server.Respond(w, ok, res) + return server.JSON(w, ok, res) } } @@ -39,24 +39,24 @@ func Command[T any](f CommandFunc[T], ok int) server.HandlerFunc { // // Example: // -// fn := func(ctx context.Context, id uuid.UUID) (interface{}, error) { +// fn := func(r *http.Request, id uuid.UUID) (interface{}, error) { // // do something // return nil, nil // } // // 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) + ID, err := RouteUUID(r, param) if err != nil { return err } - res, err := f(r.Context(), ID) + res, err := f(r, ID) if err != nil { return err } - return server.Respond(w, ok, res) + return server.JSON(w, ok, res) } } diff --git a/backend/internal/web/adapters/decoders.go b/backend/internal/web/adapters/decoders.go index c88fc21..d1444ef 100644 --- a/backend/internal/web/adapters/decoders.go +++ b/backend/internal/web/adapters/decoders.go @@ -3,47 +3,49 @@ package adapters import ( "net/http" + "github.com/pkg/errors" + "github.com/go-chi/chi/v5" "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() -func decodeQuery[T any](r *http.Request) (T, error) { +func DecodeQuery[T any](r *http.Request) (T, error) { var v T err := queryDecoder.Decode(&v, r.URL.Query()) if err != nil { - return v, err + return v, errors.Wrap(err, "decoding error") } err = validate.Check(v) if err != nil { - return v, err + return v, errors.Wrap(err, "validation error") } return v, nil } -func decode[T any](r *http.Request) (T, error) { +func DecodeBody[T any](r *http.Request) (T, error) { var v T err := server.Decode(r, &v) if err != nil { - return v, err + return v, errors.Wrap(err, "body decoding error") } err = validate.Check(v) if err != nil { - return v, err + return v, errors.Wrap(err, "validation error") } return v, nil } -func routeUUID(r *http.Request, key string) (uuid.UUID, error) { +func RouteUUID(r *http.Request, key string) (uuid.UUID, error) { ID, err := uuid.Parse(chi.URLParam(r, key)) if err != nil { return uuid.Nil, validate.NewRouteKeyError(key) diff --git a/backend/internal/web/adapters/query.go b/backend/internal/web/adapters/query.go index 19d7e0a..8f669f0 100644 --- a/backend/internal/web/adapters/query.go +++ b/backend/internal/web/adapters/query.go @@ -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. @@ -14,25 +15,25 @@ import ( // Foo string `schema:"foo"` // } // -// fn := func(ctx context.Context, q Query) (any, error) { +// fn := func(r *http.Request, q Query) (any, error) { // // do something with q // return nil, nil // } // // 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) + q, err := DecodeQuery[T](r) if err != nil { return err } - res, err := f(r.Context(), q) + res, err := f(r, q) if err != nil { return err } - return server.Respond(w, ok, res) + return server.JSON(w, ok, res) } } @@ -44,29 +45,29 @@ func Query[T any, Y any](f AdapterFunc[T, Y], ok int) server.HandlerFunc { // Foo string `schema:"foo"` // } // -// fn := func(ctx context.Context, ID uuid.UUID, q Query) (any, error) { +// fn := func(r *http.Request, ID uuid.UUID, q Query) (any, error) { // // do something with ID and q // return nil, nil // } // // 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) + ID, err := RouteUUID(r, param) if err != nil { return err } - q, err := decodeQuery[T](r) + q, err := DecodeQuery[T](r) if err != nil { return err } - res, err := f(r.Context(), ID, q) + res, err := f(r, ID, q) if err != nil { return err } - return server.Respond(w, ok, res) + return server.JSON(w, ok, res) } } diff --git a/backend/internal/web/mid/errors.go b/backend/internal/web/mid/errors.go index d55394f..269387c 100644 --- a/backend/internal/web/mid/errors.go +++ b/backend/internal/web/mid/errors.go @@ -3,33 +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). - Str("trace_id", server.GetTraceID(r.Context())). + Stack(). + 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): @@ -59,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 }) } } diff --git a/backend/internal/web/mid/logger.go b/backend/internal/web/mid/logger.go index fb39c67..d087c68 100644 --- a/backend/internal/web/mid/logger.go +++ b/backend/internal/web/mid/logger.go @@ -1,96 +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} - - err := next.ServeHTTP(record, r) // Blocks until the next handler returns. - - url := fmt.Sprintf("%s %s", r.RequestURI, r.Proto) - - log.Info(). - Str("trace_id", server.GetTraceID(r.Context())). - 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") }) } } diff --git a/backend/internal/web/mid/panic.go b/backend/internal/web/mid/panic.go deleted file mode 100644 index 9879bb8..0000000 --- a/backend/internal/web/mid/panic.go +++ /dev/null @@ -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) - }) - } -} diff --git a/backend/pkgs/server/constants.go b/backend/pkgs/server/constants.go deleted file mode 100644 index e083a57..0000000 --- a/backend/pkgs/server/constants.go +++ /dev/null @@ -1,8 +0,0 @@ -package server - -const ( - ContentType = "Content-Type" - ContentJSON = "application/json" - ContentXML = "application/xml" - ContentFormUrlEncoded = "application/x-www-form-urlencoded" -) diff --git a/backend/pkgs/server/errors.go b/backend/pkgs/server/errors.go deleted file mode 100644 index 5b1d60b..0000000 --- a/backend/pkgs/server/errors.go +++ /dev/null @@ -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) -} diff --git a/backend/pkgs/server/handler.go b/backend/pkgs/server/handler.go deleted file mode 100644 index 76ae131..0000000 --- a/backend/pkgs/server/handler.go +++ /dev/null @@ -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 - }) -} diff --git a/backend/pkgs/server/middleware.go b/backend/pkgs/server/middleware.go deleted file mode 100644 index 8e3bb23..0000000 --- a/backend/pkgs/server/middleware.go +++ /dev/null @@ -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) - }) - } -} diff --git a/backend/pkgs/server/mux.go b/backend/pkgs/server/mux.go deleted file mode 100644 index 7f62ab7..0000000 --- a/backend/pkgs/server/mux.go +++ /dev/null @@ -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)) -} diff --git a/backend/pkgs/server/request.go b/backend/pkgs/server/request.go deleted file mode 100644 index 38c3189..0000000 --- a/backend/pkgs/server/request.go +++ /dev/null @@ -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) -} diff --git a/backend/pkgs/server/request_test.go b/backend/pkgs/server/request_test.go deleted file mode 100644 index 05dc8c5..0000000 --- a/backend/pkgs/server/request_test.go +++ /dev/null @@ -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) - } - }) - } -} diff --git a/backend/pkgs/server/response.go b/backend/pkgs/server/response.go deleted file mode 100644 index 7d5880e..0000000 --- a/backend/pkgs/server/response.go +++ /dev/null @@ -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 -} diff --git a/backend/pkgs/server/response_test.go b/backend/pkgs/server/response_test.go deleted file mode 100644 index 14e7a37..0000000 --- a/backend/pkgs/server/response_test.go +++ /dev/null @@ -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")) -} diff --git a/backend/pkgs/server/result.go b/backend/pkgs/server/result.go deleted file mode 100644 index 69dcf81..0000000 --- a/backend/pkgs/server/result.go +++ /dev/null @@ -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, - } -} diff --git a/backend/pkgs/server/server.go b/backend/pkgs/server/server.go deleted file mode 100644 index d021b31..0000000 --- a/backend/pkgs/server/server.go +++ /dev/null @@ -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() - }) -} diff --git a/backend/pkgs/server/server_options.go b/backend/pkgs/server/server_options.go deleted file mode 100644 index 93b7781..0000000 --- a/backend/pkgs/server/server_options.go +++ /dev/null @@ -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 - } -} diff --git a/backend/pkgs/server/server_test.go b/backend/pkgs/server/server_test.go deleted file mode 100644 index a5cb218..0000000 --- a/backend/pkgs/server/server_test.go +++ /dev/null @@ -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()) -} diff --git a/backend/pkgs/server/worker.go b/backend/pkgs/server/worker.go deleted file mode 100644 index 9cf1cc7..0000000 --- a/backend/pkgs/server/worker.go +++ /dev/null @@ -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() -} diff --git a/docs/docs/api/openapi-2.0.json b/docs/docs/api/openapi-2.0.json index c46f5a5..c12bd42 100644 --- a/docs/docs/api/openapi-2.0.json +++ b/docs/docs/api/openapi-2.0.json @@ -417,8 +417,8 @@ } ], "responses": { - "200": { - "description": "OK", + "201": { + "description": "Created", "schema": { "$ref": "#/definitions/repo.ItemSummary" } @@ -686,7 +686,7 @@ "422": { "description": "Unprocessable Entity", "schema": { - "$ref": "#/definitions/server.ErrorResponse" + "$ref": "#/definitions/mid.ErrorResponse" } } } @@ -856,8 +856,8 @@ } ], "responses": { - "200": { - "description": "OK", + "201": { + "description": "Created", "schema": { "$ref": "#/definitions/repo.MaintenanceEntry" } @@ -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": { @@ -1894,9 +1908,15 @@ }, "repo.ItemCreate": { "type": "object", + "required": [ + "description", + "name" + ], "properties": { "description": { - "type": "string" + "type": "string", + "maxLength": 1000, + "minLength": 1 }, "labelIds": { "type": "array", @@ -1909,7 +1929,9 @@ "type": "string" }, "name": { - "type": "string" + "type": "string", + "maxLength": 255, + "minLength": 1 }, "parentId": { "type": "string", @@ -2200,15 +2222,21 @@ }, "repo.LabelCreate": { "type": "object", + "required": [ + "name" + ], "properties": { "color": { "type": "string" }, "description": { - "type": "string" + "type": "string", + "maxLength": 255 }, "name": { - "type": "string" + "type": "string", + "maxLength": 255, + "minLength": 1 } } }, @@ -2655,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": { @@ -2783,12 +2778,17 @@ }, "v1.GroupInvitationCreate": { "type": "object", + "required": [ + "uses" + ], "properties": { "expiresAt": { "type": "string" }, "uses": { - "type": "integer" + "type": "integer", + "maximum": 100, + "minimum": 1 } } }, @@ -2813,6 +2813,12 @@ "type": "string" } } + }, + "v1.Wrapped": { + "type": "object", + "properties": { + "item": {} + } } }, "securityDefinitions": { diff --git a/frontend/lib/api/types/data-contracts.ts b/frontend/lib/api/types/data-contracts.ts index adee10d..b9be3ef 100644 --- a/frontend/lib/api/types/data-contracts.ts +++ b/frontend/lib/api/types/data-contracts.ts @@ -10,6 +10,11 @@ * --------------------------------------------------------------- */ +export interface MidErrorResponse { + error: string; + fields: Record; +} + export interface DocumentOut { id: string; path: string; @@ -52,10 +57,18 @@ export interface ItemAttachmentUpdate { } export interface ItemCreate { + /** + * @minLength 1 + * @maxLength 1000 + */ description: string; labelIds: string[]; /** Edges */ locationId: string; + /** + * @minLength 1 + * @maxLength 255 + */ name: string; parentId: string | null; } @@ -164,7 +177,12 @@ export interface ItemUpdate { export interface LabelCreate { color: string; + /** @maxLength 255 */ description: string; + /** + * @minLength 1 + * @maxLength 255 + */ name: string; } @@ -346,22 +364,6 @@ export interface ValueOverTimeEntry { value: number; } -export interface ServerErrorResponse { - error: string; - fields: Record; -} - -export interface ServerResult { - details: any; - error: boolean; - item: any; - message: string; -} - -export interface ServerResults { - items: any; -} - export interface UserRegistration { email: string; name: string; @@ -402,6 +404,10 @@ export interface GroupInvitation { export interface GroupInvitationCreate { expiresAt: Date | string; + /** + * @min 1 + * @max 100 + */ uses: number; } @@ -414,3 +420,7 @@ export interface TokenResponse { expiresAt: Date | string; token: string; } + +export interface Wrapped { + item: any; +}