mirror of
https://github.com/hay-kot/homebox.git
synced 2024-11-16 05:38:42 +00:00
refactor: http interfaces (#114)
* implement custom http handler interface * implement trace_id * normalize http method spacing for consistent logs * fix failing test * fix linter errors * cleanup old dead code * more route cleanup * cleanup some inconsistent errors * update and generate code * make taskfile more consistent * update task calls * run tidy * drop `@` tag for version * use relative paths * tidy * fix auto-setting variables * update build paths * add contributing guide * tidy
This commit is contained in:
parent
e2d93f8523
commit
6529549289
40 changed files with 984 additions and 808 deletions
4
.github/workflows/partial-backend.yaml
vendored
4
.github/workflows/partial-backend.yaml
vendored
|
@ -28,7 +28,7 @@ jobs:
|
||||||
args: --timeout=6m
|
args: --timeout=6m
|
||||||
|
|
||||||
- name: Build API
|
- name: Build API
|
||||||
run: task api:build
|
run: task go:build
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: task api:coverage
|
run: task go:coverage
|
||||||
|
|
4
.github/workflows/pull-requests.yaml
vendored
4
.github/workflows/pull-requests.yaml
vendored
|
@ -8,8 +8,8 @@ on:
|
||||||
jobs:
|
jobs:
|
||||||
backend-tests:
|
backend-tests:
|
||||||
name: "Backend Server Tests"
|
name: "Backend Server Tests"
|
||||||
uses: hay-kot/homebox/.github/workflows/partial-backend.yaml@main
|
uses: ./.github/workflows/partial-backend.yaml
|
||||||
|
|
||||||
frontend-tests:
|
frontend-tests:
|
||||||
name: "Frontend and End-to-End Tests"
|
name: "Frontend and End-to-End Tests"
|
||||||
uses: hay-kot/homebox/.github/workflows/partial-frontend.yaml@main
|
uses: ./.github/workflows/partial-frontend.yaml
|
||||||
|
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -3,7 +3,6 @@ backend/.data/*
|
||||||
config.yml
|
config.yml
|
||||||
homebox.db
|
homebox.db
|
||||||
.idea
|
.idea
|
||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
test-mailer.json
|
test-mailer.json
|
||||||
node_modules
|
node_modules
|
||||||
|
@ -32,6 +31,7 @@ node_modules
|
||||||
go.work
|
go.work
|
||||||
.task/
|
.task/
|
||||||
backend/.env
|
backend/.env
|
||||||
|
build/*
|
||||||
|
|
||||||
# Output Directory for Nuxt/Frontend during build step
|
# Output Directory for Nuxt/Frontend during build step
|
||||||
backend/app/api/public/*
|
backend/app/api/public/*
|
||||||
|
|
51
CONTRIBUTING.md
Normal file
51
CONTRIBUTING.md
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
# Contributing
|
||||||
|
|
||||||
|
## We Develop with Github
|
||||||
|
|
||||||
|
We use github to host code, to track issues and feature requests, as well as accept pull requests.
|
||||||
|
|
||||||
|
## Branch Flow
|
||||||
|
|
||||||
|
We use the `main` branch as the development branch. All PRs should be made to the `main` branch from a feature branch. To create a pull request you can use the following steps:
|
||||||
|
|
||||||
|
1. Fork the repository and create a new branch from `main`.
|
||||||
|
2. If you've added code that should be tested, add tests.
|
||||||
|
3. If you've changed API's, update the documentation.
|
||||||
|
4. Ensure that the test suite and linters pass
|
||||||
|
5. Issue your pull request
|
||||||
|
|
||||||
|
## How To Get Started
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
There is a devcontainer available for this project. If you are using VSCode, you can use the devcontainer to get started. If you are not using VSCode, you can need to ensure that you have the following tools installed:
|
||||||
|
|
||||||
|
- [Go 1.19+](https://golang.org/doc/install)
|
||||||
|
- [Swaggo](https://github.com/swaggo/swag)
|
||||||
|
- [Node.js 16+](https://nodejs.org/en/download/)
|
||||||
|
- [pnpm](https://pnpm.io/installation)
|
||||||
|
- [Taskfile](https://taskfile.dev/#/installation) (Optional but recommended)
|
||||||
|
- For code generation, you'll need to have `python3` available on your path. In most cases, this is already installed and available.
|
||||||
|
|
||||||
|
If you're using `taskfile` you can run `task --list-all` for a list of all commands and their descriptions.
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
|
If you're using the taskfile you can use the `task setup` command to run the required setup commands. Otherwise you can review the commands required in the `Taskfile.yml` file.
|
||||||
|
|
||||||
|
Note that when installing dependencies with pnpm you must use the `--shamefully-hoist` flag. If you don't use this flag you will get an error when running the the frontend server.
|
||||||
|
|
||||||
|
### API Development Notes
|
||||||
|
|
||||||
|
start command `task go:run`
|
||||||
|
|
||||||
|
1. API Server does not auto reload. You'll need to restart the server after making changes.
|
||||||
|
2. Unit tests should be written in Go, however end-to-end or user story tests should be written in TypeScript using the client library in the frontend directory.
|
||||||
|
|
||||||
|
### Frontend Development Notes
|
||||||
|
|
||||||
|
start command `task: ui:dev`
|
||||||
|
|
||||||
|
1. The frontend is a Vue 3 app with Nuxt.js that uses Tailwind and DaisyUI for styling.
|
||||||
|
2. We're using Vitest for our automated testing. you can run these with `task ui:watch`.
|
||||||
|
3. Tests require the API server to be running and in some cases the first run will fail due to a race condition. If this happens just run the tests again and they should pass.
|
70
Taskfile.yml
70
Taskfile.yml
|
@ -5,11 +5,12 @@ env:
|
||||||
UNSAFE_DISABLE_PASSWORD_PROJECTION: "yes_i_am_sure"
|
UNSAFE_DISABLE_PASSWORD_PROJECTION: "yes_i_am_sure"
|
||||||
tasks:
|
tasks:
|
||||||
setup:
|
setup:
|
||||||
desc: Install dependencies
|
desc: Install development dependencies
|
||||||
cmds:
|
cmds:
|
||||||
- go install github.com/swaggo/swag/cmd/swag@latest
|
- go install github.com/swaggo/swag/cmd/swag@latest
|
||||||
- cd backend && go mod tidy
|
- cd backend && go mod tidy
|
||||||
- cd frontend && pnpm install --shamefully-hoist
|
- cd frontend && pnpm install --shamefully-hoist
|
||||||
|
|
||||||
generate:
|
generate:
|
||||||
desc: |
|
desc: |
|
||||||
Generates collateral files from the backend project
|
Generates collateral files from the backend project
|
||||||
|
@ -37,7 +38,7 @@ tasks:
|
||||||
- "./backend/app/api/static/docs/swagger.json"
|
- "./backend/app/api/static/docs/swagger.json"
|
||||||
- "./backend/app/api/static/docs/swagger.yaml"
|
- "./backend/app/api/static/docs/swagger.yaml"
|
||||||
|
|
||||||
api:
|
go:run:
|
||||||
desc: Starts the backend api server (depends on generate task)
|
desc: Starts the backend api server (depends on generate task)
|
||||||
deps:
|
deps:
|
||||||
- generate
|
- generate
|
||||||
|
@ -45,42 +46,38 @@ tasks:
|
||||||
- cd backend && go run ./app/api/ {{ .CLI_ARGS }}
|
- cd backend && go run ./app/api/ {{ .CLI_ARGS }}
|
||||||
silent: false
|
silent: false
|
||||||
|
|
||||||
api:build:
|
go:test:
|
||||||
|
desc: Runs all go tests using gotestsum - supports passing gotestsum args
|
||||||
cmds:
|
cmds:
|
||||||
- cd backend && go build ./app/api/
|
- cd backend && gotestsum {{ .CLI_ARGS }} ./...
|
||||||
silent: true
|
|
||||||
|
|
||||||
api:test:
|
go:coverage:
|
||||||
cmds:
|
desc: Runs all go tests with -race flag and generates a coverage report
|
||||||
- cd backend && go test ./app/api/
|
|
||||||
silent: true
|
|
||||||
|
|
||||||
api:watch:
|
|
||||||
cmds:
|
|
||||||
- cd backend && gotestsum --watch ./...
|
|
||||||
|
|
||||||
api:coverage:
|
|
||||||
cmds:
|
cmds:
|
||||||
- cd backend && go test -race -coverprofile=coverage.out -covermode=atomic ./app/... ./internal/... ./pkgs/... -v -cover
|
- cd backend && go test -race -coverprofile=coverage.out -covermode=atomic ./app/... ./internal/... ./pkgs/... -v -cover
|
||||||
silent: true
|
silent: true
|
||||||
|
|
||||||
test:ci:
|
go:tidy:
|
||||||
|
desc: Runs go mod tidy on the backend
|
||||||
cmds:
|
cmds:
|
||||||
- cd backend && go build ./app/api
|
- cd backend && go mod tidy
|
||||||
- backend/api &
|
|
||||||
- sleep 5
|
|
||||||
- cd frontend && pnpm run test:ci
|
|
||||||
silent: true
|
|
||||||
|
|
||||||
frontend:watch:
|
go:lint:
|
||||||
desc: Starts the vitest test runner in watch mode
|
desc: Runs golangci-lint
|
||||||
cmds:
|
cmds:
|
||||||
- cd frontend && pnpm vitest --watch
|
- cd backend && golangci-lint run ./...
|
||||||
|
|
||||||
frontend:
|
go:all:
|
||||||
desc: Run frontend development server
|
desc: Runs all go test and lint related tasks
|
||||||
cmds:
|
cmds:
|
||||||
- cd frontend && pnpm dev
|
- task: go:tidy
|
||||||
|
- task: go:lint
|
||||||
|
- task: go:test
|
||||||
|
|
||||||
|
go:build:
|
||||||
|
desc: Builds the backend binary
|
||||||
|
cmds:
|
||||||
|
- cd backend && go build -o ../build/backend ./app/api
|
||||||
|
|
||||||
db:generate:
|
db:generate:
|
||||||
desc: Run Entgo.io Code Generation
|
desc: Run Entgo.io Code Generation
|
||||||
|
@ -99,3 +96,22 @@ tasks:
|
||||||
- db:generate
|
- db:generate
|
||||||
cmds:
|
cmds:
|
||||||
- cd backend && go run app/migrations/main.go {{ .CLI_ARGS }}
|
- cd backend && go run app/migrations/main.go {{ .CLI_ARGS }}
|
||||||
|
|
||||||
|
ui:watch:
|
||||||
|
desc: Starts the vitest test runner in watch mode
|
||||||
|
cmds:
|
||||||
|
- cd frontend && pnpm vitest --watch
|
||||||
|
|
||||||
|
ui:dev:
|
||||||
|
desc: Run frontend development server
|
||||||
|
cmds:
|
||||||
|
- cd frontend && pnpm dev
|
||||||
|
|
||||||
|
test:ci:
|
||||||
|
desc: Runs end-to-end test on a live server (only for use in CI)
|
||||||
|
cmds:
|
||||||
|
- cd backend && go build ./app/api
|
||||||
|
- backend/api &
|
||||||
|
- sleep 5
|
||||||
|
- cd frontend && pnpm run test:ci
|
||||||
|
silent: true
|
||||||
|
|
|
@ -76,9 +76,9 @@ func NewControllerV1(svc *services.AllServices, options ...func(*V1Controller))
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} ApiSummary
|
// @Success 200 {object} ApiSummary
|
||||||
// @Router /v1/status [GET]
|
// @Router /v1/status [GET]
|
||||||
func (ctrl *V1Controller) HandleBase(ready ReadyFunc, build Build) http.HandlerFunc {
|
func (ctrl *V1Controller) HandleBase(ready ReadyFunc, build Build) server.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) error {
|
||||||
server.Respond(w, http.StatusOK, ApiSummary{
|
return server.Respond(w, http.StatusOK, ApiSummary{
|
||||||
Healthy: ready(),
|
Healthy: ready(),
|
||||||
Title: "Go API Template",
|
Title: "Go API Template",
|
||||||
Message: "Welcome to the Go API Template Application!",
|
Message: "Welcome to the Go API Template Application!",
|
||||||
|
|
|
@ -5,17 +5,23 @@ import (
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/hay-kot/homebox/backend/pkgs/server"
|
"github.com/hay-kot/homebox/backend/internal/sys/validate"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (ctrl *V1Controller) routeID(w http.ResponseWriter, r *http.Request) (uuid.UUID, error) {
|
// routeID extracts the ID from the request URL. If the ID is not in a valid
|
||||||
ID, err := uuid.Parse(chi.URLParam(r, "id"))
|
// format, an error is returned. If a error is returned, it can be directly returned
|
||||||
if err != nil {
|
// from the handler. the validate.ErrInvalidID error is known by the error middleware
|
||||||
log.Err(err).Msg("failed to parse id")
|
// and will be handled accordingly.
|
||||||
server.RespondError(w, http.StatusBadRequest, err)
|
//
|
||||||
return uuid.Nil, err
|
// Example: /api/v1/ac614db5-d8b8-4659-9b14-6e913a6eb18a -> uuid.UUID{ac614db5-d8b8-4659-9b14-6e913a6eb18a}
|
||||||
}
|
func (ctrl *V1Controller) routeID(r *http.Request) (uuid.UUID, error) {
|
||||||
|
return ctrl.routeUUID(r, "id")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctrl *V1Controller) routeUUID(r *http.Request, key string) (uuid.UUID, error) {
|
||||||
|
ID, err := uuid.Parse(chi.URLParam(r, key))
|
||||||
|
if err != nil {
|
||||||
|
return uuid.Nil, validate.NewInvalidRouteKeyError(key)
|
||||||
|
}
|
||||||
return ID, nil
|
return ID, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/hay-kot/homebox/backend/internal/services"
|
"github.com/hay-kot/homebox/backend/internal/services"
|
||||||
|
"github.com/hay-kot/homebox/backend/internal/sys/validate"
|
||||||
"github.com/hay-kot/homebox/backend/pkgs/server"
|
"github.com/hay-kot/homebox/backend/pkgs/server"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
@ -32,17 +33,15 @@ type (
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} TokenResponse
|
// @Success 200 {object} TokenResponse
|
||||||
// @Router /v1/users/login [POST]
|
// @Router /v1/users/login [POST]
|
||||||
func (ctrl *V1Controller) HandleAuthLogin() http.HandlerFunc {
|
func (ctrl *V1Controller) HandleAuthLogin() server.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) error {
|
||||||
loginForm := &LoginForm{}
|
loginForm := &LoginForm{}
|
||||||
|
|
||||||
switch r.Header.Get("Content-Type") {
|
switch r.Header.Get("Content-Type") {
|
||||||
case server.ContentFormUrlEncoded:
|
case server.ContentFormUrlEncoded:
|
||||||
err := r.ParseForm()
|
err := r.ParseForm()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
server.Respond(w, http.StatusBadRequest, server.Wrap(err))
|
return server.Respond(w, http.StatusBadRequest, server.Wrap(err))
|
||||||
log.Error().Err(err).Msg("failed to parse form")
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
loginForm.Username = r.PostFormValue("username")
|
loginForm.Username = r.PostFormValue("username")
|
||||||
|
@ -52,27 +51,31 @@ func (ctrl *V1Controller) HandleAuthLogin() http.HandlerFunc {
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Err(err).Msg("failed to decode login form")
|
log.Err(err).Msg("failed to decode login form")
|
||||||
server.Respond(w, http.StatusBadRequest, server.Wrap(err))
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
server.Respond(w, http.StatusBadRequest, errors.New("invalid content type"))
|
return server.Respond(w, http.StatusBadRequest, errors.New("invalid content type"))
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if loginForm.Username == "" || loginForm.Password == "" {
|
if loginForm.Username == "" || loginForm.Password == "" {
|
||||||
server.RespondError(w, http.StatusBadRequest, errors.New("username and password are required"))
|
return validate.NewFieldErrors(
|
||||||
return
|
validate.FieldError{
|
||||||
|
Field: "username",
|
||||||
|
Error: "username or password is empty",
|
||||||
|
},
|
||||||
|
validate.FieldError{
|
||||||
|
Field: "password",
|
||||||
|
Error: "username or password is empty",
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
newToken, err := ctrl.svc.User.Login(r.Context(), loginForm.Username, loginForm.Password)
|
newToken, err := ctrl.svc.User.Login(r.Context(), loginForm.Username, loginForm.Password)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
server.RespondError(w, http.StatusInternalServerError, err)
|
return validate.NewRequestError(errors.New("authentication failed"), http.StatusInternalServerError)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
server.Respond(w, http.StatusOK, TokenResponse{
|
return server.Respond(w, http.StatusOK, TokenResponse{
|
||||||
Token: "Bearer " + newToken.Raw,
|
Token: "Bearer " + newToken.Raw,
|
||||||
ExpiresAt: newToken.ExpiresAt,
|
ExpiresAt: newToken.ExpiresAt,
|
||||||
})
|
})
|
||||||
|
@ -85,23 +88,19 @@ func (ctrl *V1Controller) HandleAuthLogin() http.HandlerFunc {
|
||||||
// @Success 204
|
// @Success 204
|
||||||
// @Router /v1/users/logout [POST]
|
// @Router /v1/users/logout [POST]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleAuthLogout() http.HandlerFunc {
|
func (ctrl *V1Controller) HandleAuthLogout() server.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) error {
|
||||||
token := services.UseTokenCtx(r.Context())
|
token := services.UseTokenCtx(r.Context())
|
||||||
|
|
||||||
if token == "" {
|
if token == "" {
|
||||||
server.RespondError(w, http.StatusUnauthorized, errors.New("no token within request context"))
|
return validate.NewRequestError(errors.New("no token within request context"), http.StatusUnauthorized)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err := ctrl.svc.User.Logout(r.Context(), token)
|
err := ctrl.svc.User.Logout(r.Context(), token)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
server.RespondError(w, http.StatusInternalServerError, err)
|
return validate.NewRequestError(err, http.StatusInternalServerError)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
server.Respond(w, http.StatusNoContent, nil)
|
return server.Respond(w, http.StatusNoContent, nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -113,22 +112,18 @@ func (ctrl *V1Controller) HandleAuthLogout() http.HandlerFunc {
|
||||||
// @Success 200
|
// @Success 200
|
||||||
// @Router /v1/users/refresh [GET]
|
// @Router /v1/users/refresh [GET]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleAuthRefresh() http.HandlerFunc {
|
func (ctrl *V1Controller) HandleAuthRefresh() server.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) error {
|
||||||
requestToken := services.UseTokenCtx(r.Context())
|
requestToken := services.UseTokenCtx(r.Context())
|
||||||
|
|
||||||
if requestToken == "" {
|
if requestToken == "" {
|
||||||
server.RespondError(w, http.StatusUnauthorized, errors.New("no user token found"))
|
return validate.NewRequestError(errors.New("no token within request context"), http.StatusUnauthorized)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
newToken, err := ctrl.svc.User.RenewToken(r.Context(), requestToken)
|
newToken, err := ctrl.svc.User.RenewToken(r.Context(), requestToken)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
server.RespondUnauthorized(w)
|
return validate.NewUnauthorizedError()
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
server.Respond(w, http.StatusOK, newToken)
|
return server.Respond(w, http.StatusOK, newToken)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
|
|
||||||
"github.com/hay-kot/homebox/backend/internal/repo"
|
"github.com/hay-kot/homebox/backend/internal/repo"
|
||||||
"github.com/hay-kot/homebox/backend/internal/services"
|
"github.com/hay-kot/homebox/backend/internal/services"
|
||||||
|
"github.com/hay-kot/homebox/backend/internal/sys/validate"
|
||||||
"github.com/hay-kot/homebox/backend/pkgs/server"
|
"github.com/hay-kot/homebox/backend/pkgs/server"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
@ -31,7 +32,7 @@ type (
|
||||||
// @Success 200 {object} repo.Group
|
// @Success 200 {object} repo.Group
|
||||||
// @Router /v1/groups [Get]
|
// @Router /v1/groups [Get]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleGroupGet() http.HandlerFunc {
|
func (ctrl *V1Controller) HandleGroupGet() server.HandlerFunc {
|
||||||
return ctrl.handleGroupGeneral()
|
return ctrl.handleGroupGeneral()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,12 +44,12 @@ func (ctrl *V1Controller) HandleGroupGet() http.HandlerFunc {
|
||||||
// @Success 200 {object} repo.Group
|
// @Success 200 {object} repo.Group
|
||||||
// @Router /v1/groups [Put]
|
// @Router /v1/groups [Put]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleGroupUpdate() http.HandlerFunc {
|
func (ctrl *V1Controller) HandleGroupUpdate() server.HandlerFunc {
|
||||||
return ctrl.handleGroupGeneral()
|
return ctrl.handleGroupGeneral()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctrl *V1Controller) handleGroupGeneral() http.HandlerFunc {
|
func (ctrl *V1Controller) handleGroupGeneral() server.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) error {
|
||||||
ctx := services.NewContext(r.Context())
|
ctx := services.NewContext(r.Context())
|
||||||
|
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
|
@ -56,29 +57,28 @@ func (ctrl *V1Controller) handleGroupGeneral() http.HandlerFunc {
|
||||||
group, err := ctrl.svc.Group.Get(ctx)
|
group, err := ctrl.svc.Group.Get(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Err(err).Msg("failed to get group")
|
log.Err(err).Msg("failed to get group")
|
||||||
server.RespondError(w, http.StatusInternalServerError, err)
|
return validate.NewRequestError(err, http.StatusInternalServerError)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
group.Currency = strings.ToUpper(group.Currency) // TODO: Hack to fix the currency enums being lower caseÍ
|
group.Currency = strings.ToUpper(group.Currency) // TODO: Hack to fix the currency enums being lower caseÍ
|
||||||
server.Respond(w, http.StatusOK, group)
|
return server.Respond(w, http.StatusOK, group)
|
||||||
|
|
||||||
case http.MethodPut:
|
case http.MethodPut:
|
||||||
data := repo.GroupUpdate{}
|
data := repo.GroupUpdate{}
|
||||||
if err := server.Decode(r, &data); err != nil {
|
if err := server.Decode(r, &data); err != nil {
|
||||||
server.RespondError(w, http.StatusBadRequest, err)
|
return validate.NewRequestError(err, http.StatusBadRequest)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
group, err := ctrl.svc.Group.UpdateGroup(ctx, data)
|
group, err := ctrl.svc.Group.UpdateGroup(ctx, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Err(err).Msg("failed to update group")
|
log.Err(err).Msg("failed to update group")
|
||||||
server.RespondError(w, http.StatusInternalServerError, err)
|
return validate.NewRequestError(err, http.StatusInternalServerError)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
group.Currency = strings.ToUpper(group.Currency) // TODO: Hack to fix the currency enums being lower case
|
group.Currency = strings.ToUpper(group.Currency) // TODO: Hack to fix the currency enums being lower case
|
||||||
server.Respond(w, http.StatusOK, group)
|
return server.Respond(w, http.StatusOK, group)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,13 +90,12 @@ func (ctrl *V1Controller) handleGroupGeneral() http.HandlerFunc {
|
||||||
// @Success 200 {object} GroupInvitation
|
// @Success 200 {object} GroupInvitation
|
||||||
// @Router /v1/groups/invitations [Post]
|
// @Router /v1/groups/invitations [Post]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleGroupInvitationsCreate() http.HandlerFunc {
|
func (ctrl *V1Controller) HandleGroupInvitationsCreate() server.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) error {
|
||||||
data := GroupInvitationCreate{}
|
data := GroupInvitationCreate{}
|
||||||
if err := server.Decode(r, &data); err != nil {
|
if err := server.Decode(r, &data); err != nil {
|
||||||
log.Err(err).Msg("failed to decode user registration data")
|
log.Err(err).Msg("failed to decode user registration data")
|
||||||
server.RespondError(w, http.StatusBadRequest, err)
|
return validate.NewRequestError(err, http.StatusBadRequest)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if data.ExpiresAt.IsZero() {
|
if data.ExpiresAt.IsZero() {
|
||||||
|
@ -108,11 +107,10 @@ func (ctrl *V1Controller) HandleGroupInvitationsCreate() http.HandlerFunc {
|
||||||
token, err := ctrl.svc.Group.NewInvitation(ctx, data.Uses, data.ExpiresAt)
|
token, err := ctrl.svc.Group.NewInvitation(ctx, data.Uses, data.ExpiresAt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Err(err).Msg("failed to create new token")
|
log.Err(err).Msg("failed to create new token")
|
||||||
server.RespondError(w, http.StatusInternalServerError, err)
|
return validate.NewRequestError(err, http.StatusInternalServerError)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
server.Respond(w, http.StatusCreated, GroupInvitation{
|
return server.Respond(w, http.StatusCreated, GroupInvitation{
|
||||||
Token: token,
|
Token: token,
|
||||||
ExpiresAt: data.ExpiresAt,
|
ExpiresAt: data.ExpiresAt,
|
||||||
Uses: data.Uses,
|
Uses: data.Uses,
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/hay-kot/homebox/backend/internal/repo"
|
"github.com/hay-kot/homebox/backend/internal/repo"
|
||||||
"github.com/hay-kot/homebox/backend/internal/services"
|
"github.com/hay-kot/homebox/backend/internal/services"
|
||||||
|
"github.com/hay-kot/homebox/backend/internal/sys/validate"
|
||||||
"github.com/hay-kot/homebox/backend/pkgs/server"
|
"github.com/hay-kot/homebox/backend/pkgs/server"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
@ -25,7 +26,7 @@ import (
|
||||||
// @Success 200 {object} repo.PaginationResult[repo.ItemSummary]{}
|
// @Success 200 {object} repo.PaginationResult[repo.ItemSummary]{}
|
||||||
// @Router /v1/items [GET]
|
// @Router /v1/items [GET]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleItemsGetAll() http.HandlerFunc {
|
func (ctrl *V1Controller) HandleItemsGetAll() server.HandlerFunc {
|
||||||
uuidList := func(params url.Values, key string) []uuid.UUID {
|
uuidList := func(params url.Values, key string) []uuid.UUID {
|
||||||
var ids []uuid.UUID
|
var ids []uuid.UUID
|
||||||
for _, id := range params[key] {
|
for _, id := range params[key] {
|
||||||
|
@ -58,15 +59,14 @@ func (ctrl *V1Controller) HandleItemsGetAll() http.HandlerFunc {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) error {
|
||||||
ctx := services.NewContext(r.Context())
|
ctx := services.NewContext(r.Context())
|
||||||
items, err := ctrl.svc.Items.Query(ctx, extractQuery(r))
|
items, err := ctrl.svc.Items.Query(ctx, extractQuery(r))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Err(err).Msg("failed to get items")
|
log.Err(err).Msg("failed to get items")
|
||||||
server.RespondServerError(w)
|
return validate.NewRequestError(err, http.StatusInternalServerError)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
server.Respond(w, http.StatusOK, items)
|
return server.Respond(w, http.StatusOK, items)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,24 +78,22 @@ func (ctrl *V1Controller) HandleItemsGetAll() http.HandlerFunc {
|
||||||
// @Success 200 {object} repo.ItemSummary
|
// @Success 200 {object} repo.ItemSummary
|
||||||
// @Router /v1/items [POST]
|
// @Router /v1/items [POST]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleItemsCreate() http.HandlerFunc {
|
func (ctrl *V1Controller) HandleItemsCreate() server.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) error {
|
||||||
createData := repo.ItemCreate{}
|
createData := repo.ItemCreate{}
|
||||||
if err := server.Decode(r, &createData); err != nil {
|
if err := server.Decode(r, &createData); err != nil {
|
||||||
log.Err(err).Msg("failed to decode request body")
|
log.Err(err).Msg("failed to decode request body")
|
||||||
server.RespondError(w, http.StatusInternalServerError, err)
|
return validate.NewRequestError(err, http.StatusInternalServerError)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
user := services.UseUserCtx(r.Context())
|
user := services.UseUserCtx(r.Context())
|
||||||
item, err := ctrl.svc.Items.Create(r.Context(), user.GroupID, createData)
|
item, err := ctrl.svc.Items.Create(r.Context(), user.GroupID, createData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Err(err).Msg("failed to create item")
|
log.Err(err).Msg("failed to create item")
|
||||||
server.RespondServerError(w)
|
return validate.NewRequestError(err, http.StatusInternalServerError)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
server.Respond(w, http.StatusCreated, item)
|
return server.Respond(w, http.StatusCreated, item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,7 +105,7 @@ func (ctrl *V1Controller) HandleItemsCreate() http.HandlerFunc {
|
||||||
// @Success 200 {object} repo.ItemOut
|
// @Success 200 {object} repo.ItemOut
|
||||||
// @Router /v1/items/{id} [GET]
|
// @Router /v1/items/{id} [GET]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleItemGet() http.HandlerFunc {
|
func (ctrl *V1Controller) HandleItemGet() server.HandlerFunc {
|
||||||
return ctrl.handleItemsGeneral()
|
return ctrl.handleItemsGeneral()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,7 +117,7 @@ func (ctrl *V1Controller) HandleItemGet() http.HandlerFunc {
|
||||||
// @Success 204
|
// @Success 204
|
||||||
// @Router /v1/items/{id} [DELETE]
|
// @Router /v1/items/{id} [DELETE]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleItemDelete() http.HandlerFunc {
|
func (ctrl *V1Controller) HandleItemDelete() server.HandlerFunc {
|
||||||
return ctrl.handleItemsGeneral()
|
return ctrl.handleItemsGeneral()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,16 +130,16 @@ func (ctrl *V1Controller) HandleItemDelete() http.HandlerFunc {
|
||||||
// @Success 200 {object} repo.ItemOut
|
// @Success 200 {object} repo.ItemOut
|
||||||
// @Router /v1/items/{id} [PUT]
|
// @Router /v1/items/{id} [PUT]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleItemUpdate() http.HandlerFunc {
|
func (ctrl *V1Controller) HandleItemUpdate() server.HandlerFunc {
|
||||||
return ctrl.handleItemsGeneral()
|
return ctrl.handleItemsGeneral()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctrl *V1Controller) handleItemsGeneral() http.HandlerFunc {
|
func (ctrl *V1Controller) handleItemsGeneral() server.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) error {
|
||||||
ctx := services.NewContext(r.Context())
|
ctx := services.NewContext(r.Context())
|
||||||
ID, err := ctrl.routeID(w, r)
|
ID, err := ctrl.routeID(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
|
@ -149,37 +147,32 @@ func (ctrl *V1Controller) handleItemsGeneral() http.HandlerFunc {
|
||||||
items, err := ctrl.svc.Items.GetOne(r.Context(), ctx.GID, ID)
|
items, err := ctrl.svc.Items.GetOne(r.Context(), ctx.GID, ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Err(err).Msg("failed to get item")
|
log.Err(err).Msg("failed to get item")
|
||||||
server.RespondServerError(w)
|
return validate.NewRequestError(err, http.StatusInternalServerError)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
server.Respond(w, http.StatusOK, items)
|
return server.Respond(w, http.StatusOK, items)
|
||||||
return
|
|
||||||
case http.MethodDelete:
|
case http.MethodDelete:
|
||||||
err = ctrl.svc.Items.Delete(r.Context(), ctx.GID, ID)
|
err = ctrl.svc.Items.Delete(r.Context(), ctx.GID, ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Err(err).Msg("failed to delete item")
|
log.Err(err).Msg("failed to delete item")
|
||||||
server.RespondServerError(w)
|
return validate.NewRequestError(err, http.StatusInternalServerError)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
server.Respond(w, http.StatusNoContent, nil)
|
return server.Respond(w, http.StatusNoContent, nil)
|
||||||
return
|
|
||||||
case http.MethodPut:
|
case http.MethodPut:
|
||||||
body := repo.ItemUpdate{}
|
body := repo.ItemUpdate{}
|
||||||
if err := server.Decode(r, &body); err != nil {
|
if err := server.Decode(r, &body); err != nil {
|
||||||
log.Err(err).Msg("failed to decode request body")
|
log.Err(err).Msg("failed to decode request body")
|
||||||
server.RespondError(w, http.StatusInternalServerError, err)
|
return validate.NewRequestError(err, http.StatusInternalServerError)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
body.ID = ID
|
body.ID = ID
|
||||||
result, err := ctrl.svc.Items.Update(r.Context(), ctx.GID, body)
|
result, err := ctrl.svc.Items.Update(r.Context(), ctx.GID, body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Err(err).Msg("failed to update item")
|
log.Err(err).Msg("failed to update item")
|
||||||
server.RespondServerError(w)
|
return validate.NewRequestError(err, http.StatusInternalServerError)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
server.Respond(w, http.StatusOK, result)
|
return server.Respond(w, http.StatusOK, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -191,29 +184,26 @@ func (ctrl *V1Controller) handleItemsGeneral() http.HandlerFunc {
|
||||||
// @Param csv formData file true "Image to upload"
|
// @Param csv formData file true "Image to upload"
|
||||||
// @Router /v1/items/import [Post]
|
// @Router /v1/items/import [Post]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleItemsImport() http.HandlerFunc {
|
func (ctrl *V1Controller) HandleItemsImport() server.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
|
||||||
err := r.ParseMultipartForm(ctrl.maxUploadSize << 20)
|
err := r.ParseMultipartForm(ctrl.maxUploadSize << 20)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Err(err).Msg("failed to parse multipart form")
|
log.Err(err).Msg("failed to parse multipart form")
|
||||||
server.RespondServerError(w)
|
return validate.NewRequestError(err, http.StatusInternalServerError)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
file, _, err := r.FormFile("csv")
|
file, _, err := r.FormFile("csv")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Err(err).Msg("failed to get file from form")
|
log.Err(err).Msg("failed to get file from form")
|
||||||
server.RespondServerError(w)
|
return validate.NewRequestError(err, http.StatusInternalServerError)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
reader := csv.NewReader(file)
|
reader := csv.NewReader(file)
|
||||||
data, err := reader.ReadAll()
|
data, err := reader.ReadAll()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Err(err).Msg("failed to read csv")
|
log.Err(err).Msg("failed to read csv")
|
||||||
server.RespondServerError(w)
|
return validate.NewRequestError(err, http.StatusInternalServerError)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
user := services.UseUserCtx(r.Context())
|
user := services.UseUserCtx(r.Context())
|
||||||
|
@ -221,10 +211,9 @@ func (ctrl *V1Controller) HandleItemsImport() http.HandlerFunc {
|
||||||
_, err = ctrl.svc.Items.CsvImport(r.Context(), user.GroupID, data)
|
_, err = ctrl.svc.Items.CsvImport(r.Context(), user.GroupID, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Err(err).Msg("failed to import items")
|
log.Err(err).Msg("failed to import items")
|
||||||
server.RespondServerError(w)
|
return validate.NewRequestError(err, http.StatusInternalServerError)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
server.Respond(w, http.StatusNoContent, nil)
|
return server.Respond(w, http.StatusNoContent, nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,11 +5,10 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/hay-kot/homebox/backend/ent/attachment"
|
"github.com/hay-kot/homebox/backend/ent/attachment"
|
||||||
"github.com/hay-kot/homebox/backend/internal/repo"
|
"github.com/hay-kot/homebox/backend/internal/repo"
|
||||||
"github.com/hay-kot/homebox/backend/internal/services"
|
"github.com/hay-kot/homebox/backend/internal/services"
|
||||||
|
"github.com/hay-kot/homebox/backend/internal/sys/validate"
|
||||||
"github.com/hay-kot/homebox/backend/pkgs/server"
|
"github.com/hay-kot/homebox/backend/pkgs/server"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
@ -29,19 +28,19 @@ type (
|
||||||
// @Param type formData string true "Type of file"
|
// @Param type formData string true "Type of file"
|
||||||
// @Param name formData string true "name of the file including extension"
|
// @Param name formData string true "name of the file including extension"
|
||||||
// @Success 200 {object} repo.ItemOut
|
// @Success 200 {object} repo.ItemOut
|
||||||
// @Failure 422 {object} []server.ValidationError
|
// @Failure 422 {object} server.ErrorResponse
|
||||||
// @Router /v1/items/{id}/attachments [POST]
|
// @Router /v1/items/{id}/attachments [POST]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleItemAttachmentCreate() http.HandlerFunc {
|
func (ctrl *V1Controller) HandleItemAttachmentCreate() server.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) error {
|
||||||
err := r.ParseMultipartForm(ctrl.maxUploadSize << 20)
|
err := r.ParseMultipartForm(ctrl.maxUploadSize << 20)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Err(err).Msg("failed to parse multipart form")
|
log.Err(err).Msg("failed to parse multipart form")
|
||||||
server.RespondError(w, http.StatusBadRequest, errors.New("failed to parse multipart form"))
|
return validate.NewRequestError(errors.New("failed to parse multipart form"), http.StatusBadRequest)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
errs := make(server.ValidationErrors, 0)
|
errs := validate.NewFieldErrors()
|
||||||
|
|
||||||
file, _, err := r.FormFile("file")
|
file, _, err := r.FormFile("file")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -51,8 +50,7 @@ func (ctrl *V1Controller) HandleItemAttachmentCreate() http.HandlerFunc {
|
||||||
errs = errs.Append("file", "file is required")
|
errs = errs.Append("file", "file is required")
|
||||||
default:
|
default:
|
||||||
log.Err(err).Msg("failed to get file from form")
|
log.Err(err).Msg("failed to get file from form")
|
||||||
server.RespondServerError(w)
|
return validate.NewRequestError(err, http.StatusInternalServerError)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,9 +60,8 @@ func (ctrl *V1Controller) HandleItemAttachmentCreate() http.HandlerFunc {
|
||||||
errs = errs.Append("name", "name is required")
|
errs = errs.Append("name", "name is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
if errs.HasErrors() {
|
if !errs.Nil() {
|
||||||
server.Respond(w, http.StatusUnprocessableEntity, errs)
|
return server.Respond(w, http.StatusUnprocessableEntity, errs)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
attachmentType := r.FormValue("type")
|
attachmentType := r.FormValue("type")
|
||||||
|
@ -72,9 +69,9 @@ func (ctrl *V1Controller) HandleItemAttachmentCreate() http.HandlerFunc {
|
||||||
attachmentType = attachment.TypeAttachment.String()
|
attachmentType = attachment.TypeAttachment.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
id, err := ctrl.routeID(w, r)
|
id, err := ctrl.routeID(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := services.NewContext(r.Context())
|
ctx := services.NewContext(r.Context())
|
||||||
|
@ -89,11 +86,10 @@ func (ctrl *V1Controller) HandleItemAttachmentCreate() http.HandlerFunc {
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Err(err).Msg("failed to add attachment")
|
log.Err(err).Msg("failed to add attachment")
|
||||||
server.RespondServerError(w)
|
return validate.NewRequestError(err, http.StatusInternalServerError)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
server.Respond(w, http.StatusCreated, item)
|
return server.Respond(w, http.StatusCreated, item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,21 +102,21 @@ func (ctrl *V1Controller) HandleItemAttachmentCreate() http.HandlerFunc {
|
||||||
// @Success 200
|
// @Success 200
|
||||||
// @Router /v1/items/{id}/attachments/download [GET]
|
// @Router /v1/items/{id}/attachments/download [GET]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleItemAttachmentDownload() http.HandlerFunc {
|
func (ctrl *V1Controller) HandleItemAttachmentDownload() server.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) error {
|
||||||
token := server.GetParam(r, "token", "")
|
token := server.GetParam(r, "token", "")
|
||||||
|
|
||||||
doc, err := ctrl.svc.Items.AttachmentPath(r.Context(), token)
|
doc, err := ctrl.svc.Items.AttachmentPath(r.Context(), token)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Err(err).Msg("failed to get attachment")
|
log.Err(err).Msg("failed to get attachment")
|
||||||
server.RespondServerError(w)
|
return validate.NewRequestError(err, http.StatusInternalServerError)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", doc.Title))
|
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", doc.Title))
|
||||||
w.Header().Set("Content-Type", "application/octet-stream")
|
w.Header().Set("Content-Type", "application/octet-stream")
|
||||||
http.ServeFile(w, r, doc.Path)
|
http.ServeFile(w, r, doc.Path)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -133,7 +129,7 @@ func (ctrl *V1Controller) HandleItemAttachmentDownload() http.HandlerFunc {
|
||||||
// @Success 200 {object} ItemAttachmentToken
|
// @Success 200 {object} ItemAttachmentToken
|
||||||
// @Router /v1/items/{id}/attachments/{attachment_id} [GET]
|
// @Router /v1/items/{id}/attachments/{attachment_id} [GET]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleItemAttachmentToken() http.HandlerFunc {
|
func (ctrl *V1Controller) HandleItemAttachmentToken() server.HandlerFunc {
|
||||||
return ctrl.handleItemAttachmentsHandler
|
return ctrl.handleItemAttachmentsHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,7 +141,7 @@ func (ctrl *V1Controller) HandleItemAttachmentToken() http.HandlerFunc {
|
||||||
// @Success 204
|
// @Success 204
|
||||||
// @Router /v1/items/{id}/attachments/{attachment_id} [DELETE]
|
// @Router /v1/items/{id}/attachments/{attachment_id} [DELETE]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleItemAttachmentDelete() http.HandlerFunc {
|
func (ctrl *V1Controller) HandleItemAttachmentDelete() server.HandlerFunc {
|
||||||
return ctrl.handleItemAttachmentsHandler
|
return ctrl.handleItemAttachmentsHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -158,66 +154,60 @@ func (ctrl *V1Controller) HandleItemAttachmentDelete() http.HandlerFunc {
|
||||||
// @Success 200 {object} repo.ItemOut
|
// @Success 200 {object} repo.ItemOut
|
||||||
// @Router /v1/items/{id}/attachments/{attachment_id} [PUT]
|
// @Router /v1/items/{id}/attachments/{attachment_id} [PUT]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleItemAttachmentUpdate() http.HandlerFunc {
|
func (ctrl *V1Controller) HandleItemAttachmentUpdate() server.HandlerFunc {
|
||||||
return ctrl.handleItemAttachmentsHandler
|
return ctrl.handleItemAttachmentsHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctrl *V1Controller) handleItemAttachmentsHandler(w http.ResponseWriter, r *http.Request) {
|
func (ctrl *V1Controller) handleItemAttachmentsHandler(w http.ResponseWriter, r *http.Request) error {
|
||||||
ID, err := ctrl.routeID(w, r)
|
ID, err := ctrl.routeID(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
attachmentId, err := uuid.Parse(chi.URLParam(r, "attachment_id"))
|
attachmentID, err := ctrl.routeUUID(r, "attachment_id")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Err(err).Msg("failed to parse attachment_id param")
|
return err
|
||||||
server.RespondError(w, http.StatusBadRequest, err)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := services.NewContext(r.Context())
|
ctx := services.NewContext(r.Context())
|
||||||
|
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
|
|
||||||
// Token Handler
|
// Token Handler
|
||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
token, err := ctrl.svc.Items.AttachmentToken(ctx, ID, attachmentId)
|
token, err := ctrl.svc.Items.AttachmentToken(ctx, ID, attachmentID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch err {
|
switch err {
|
||||||
case services.ErrNotFound:
|
case services.ErrNotFound:
|
||||||
log.Err(err).
|
log.Err(err).
|
||||||
Str("id", attachmentId.String()).
|
Str("id", attachmentID.String()).
|
||||||
Msg("failed to find attachment with id")
|
Msg("failed to find attachment with id")
|
||||||
|
|
||||||
server.RespondError(w, http.StatusNotFound, err)
|
return validate.NewRequestError(err, http.StatusNotFound)
|
||||||
|
|
||||||
case services.ErrFileNotFound:
|
case services.ErrFileNotFound:
|
||||||
log.Err(err).
|
log.Err(err).
|
||||||
Str("id", attachmentId.String()).
|
Str("id", attachmentID.String()).
|
||||||
Msg("failed to find file path for attachment with id")
|
Msg("failed to find file path for attachment with id")
|
||||||
log.Warn().Msg("attachment with no file path removed from database")
|
log.Warn().Msg("attachment with no file path removed from database")
|
||||||
|
|
||||||
server.RespondError(w, http.StatusNotFound, err)
|
return validate.NewRequestError(err, http.StatusNotFound)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
log.Err(err).Msg("failed to get attachment")
|
log.Err(err).Msg("failed to get attachment")
|
||||||
server.RespondServerError(w)
|
return validate.NewRequestError(err, http.StatusInternalServerError)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
server.Respond(w, http.StatusOK, ItemAttachmentToken{Token: token})
|
return server.Respond(w, http.StatusOK, ItemAttachmentToken{Token: token})
|
||||||
|
|
||||||
// Delete Attachment Handler
|
// Delete Attachment Handler
|
||||||
case http.MethodDelete:
|
case http.MethodDelete:
|
||||||
err = ctrl.svc.Items.AttachmentDelete(r.Context(), ctx.GID, ID, attachmentId)
|
err = ctrl.svc.Items.AttachmentDelete(r.Context(), ctx.GID, ID, attachmentID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Err(err).Msg("failed to delete attachment")
|
log.Err(err).Msg("failed to delete attachment")
|
||||||
server.RespondServerError(w)
|
return validate.NewRequestError(err, http.StatusInternalServerError)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
server.Respond(w, http.StatusNoContent, nil)
|
return server.Respond(w, http.StatusNoContent, nil)
|
||||||
|
|
||||||
// Update Attachment Handler
|
// Update Attachment Handler
|
||||||
case http.MethodPut:
|
case http.MethodPut:
|
||||||
|
@ -225,18 +215,18 @@ func (ctrl *V1Controller) handleItemAttachmentsHandler(w http.ResponseWriter, r
|
||||||
err = server.Decode(r, &attachment)
|
err = server.Decode(r, &attachment)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Err(err).Msg("failed to decode attachment")
|
log.Err(err).Msg("failed to decode attachment")
|
||||||
server.RespondError(w, http.StatusBadRequest, err)
|
return validate.NewRequestError(err, http.StatusBadRequest)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
attachment.ID = attachmentId
|
attachment.ID = attachmentID
|
||||||
val, err := ctrl.svc.Items.AttachmentUpdate(ctx, ID, &attachment)
|
val, err := ctrl.svc.Items.AttachmentUpdate(ctx, ID, &attachment)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Err(err).Msg("failed to delete attachment")
|
log.Err(err).Msg("failed to delete attachment")
|
||||||
server.RespondServerError(w)
|
return validate.NewRequestError(err, http.StatusInternalServerError)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
server.Respond(w, http.StatusOK, val)
|
return server.Respond(w, http.StatusOK, val)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"github.com/hay-kot/homebox/backend/ent"
|
"github.com/hay-kot/homebox/backend/ent"
|
||||||
"github.com/hay-kot/homebox/backend/internal/repo"
|
"github.com/hay-kot/homebox/backend/internal/repo"
|
||||||
"github.com/hay-kot/homebox/backend/internal/services"
|
"github.com/hay-kot/homebox/backend/internal/services"
|
||||||
|
"github.com/hay-kot/homebox/backend/internal/sys/validate"
|
||||||
"github.com/hay-kot/homebox/backend/pkgs/server"
|
"github.com/hay-kot/homebox/backend/pkgs/server"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
@ -17,16 +18,15 @@ import (
|
||||||
// @Success 200 {object} server.Results{items=[]repo.LabelOut}
|
// @Success 200 {object} server.Results{items=[]repo.LabelOut}
|
||||||
// @Router /v1/labels [GET]
|
// @Router /v1/labels [GET]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleLabelsGetAll() http.HandlerFunc {
|
func (ctrl *V1Controller) HandleLabelsGetAll() server.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) error {
|
||||||
user := services.UseUserCtx(r.Context())
|
user := services.UseUserCtx(r.Context())
|
||||||
labels, err := ctrl.svc.Labels.GetAll(r.Context(), user.GroupID)
|
labels, err := ctrl.svc.Labels.GetAll(r.Context(), user.GroupID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Err(err).Msg("error getting labels")
|
log.Err(err).Msg("error getting labels")
|
||||||
server.RespondServerError(w)
|
return validate.NewRequestError(err, http.StatusInternalServerError)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
server.Respond(w, http.StatusOK, server.Results{Items: labels})
|
return server.Respond(w, http.StatusOK, server.Results{Items: labels})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,24 +38,22 @@ func (ctrl *V1Controller) HandleLabelsGetAll() http.HandlerFunc {
|
||||||
// @Success 200 {object} repo.LabelSummary
|
// @Success 200 {object} repo.LabelSummary
|
||||||
// @Router /v1/labels [POST]
|
// @Router /v1/labels [POST]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleLabelsCreate() http.HandlerFunc {
|
func (ctrl *V1Controller) HandleLabelsCreate() server.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) error {
|
||||||
createData := repo.LabelCreate{}
|
createData := repo.LabelCreate{}
|
||||||
if err := server.Decode(r, &createData); err != nil {
|
if err := server.Decode(r, &createData); err != nil {
|
||||||
log.Err(err).Msg("error decoding label create data")
|
log.Err(err).Msg("error decoding label create data")
|
||||||
server.RespondError(w, http.StatusInternalServerError, err)
|
return validate.NewRequestError(err, http.StatusInternalServerError)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
user := services.UseUserCtx(r.Context())
|
user := services.UseUserCtx(r.Context())
|
||||||
label, err := ctrl.svc.Labels.Create(r.Context(), user.GroupID, createData)
|
label, err := ctrl.svc.Labels.Create(r.Context(), user.GroupID, createData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Err(err).Msg("error creating label")
|
log.Err(err).Msg("error creating label")
|
||||||
server.RespondServerError(w)
|
return validate.NewRequestError(err, http.StatusInternalServerError)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
server.Respond(w, http.StatusCreated, label)
|
return server.Respond(w, http.StatusCreated, label)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,7 +65,7 @@ func (ctrl *V1Controller) HandleLabelsCreate() http.HandlerFunc {
|
||||||
// @Success 204
|
// @Success 204
|
||||||
// @Router /v1/labels/{id} [DELETE]
|
// @Router /v1/labels/{id} [DELETE]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleLabelDelete() http.HandlerFunc {
|
func (ctrl *V1Controller) HandleLabelDelete() server.HandlerFunc {
|
||||||
return ctrl.handleLabelsGeneral()
|
return ctrl.handleLabelsGeneral()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,7 +77,7 @@ func (ctrl *V1Controller) HandleLabelDelete() http.HandlerFunc {
|
||||||
// @Success 200 {object} repo.LabelOut
|
// @Success 200 {object} repo.LabelOut
|
||||||
// @Router /v1/labels/{id} [GET]
|
// @Router /v1/labels/{id} [GET]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleLabelGet() http.HandlerFunc {
|
func (ctrl *V1Controller) HandleLabelGet() server.HandlerFunc {
|
||||||
return ctrl.handleLabelsGeneral()
|
return ctrl.handleLabelsGeneral()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,16 +89,16 @@ func (ctrl *V1Controller) HandleLabelGet() http.HandlerFunc {
|
||||||
// @Success 200 {object} repo.LabelOut
|
// @Success 200 {object} repo.LabelOut
|
||||||
// @Router /v1/labels/{id} [PUT]
|
// @Router /v1/labels/{id} [PUT]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleLabelUpdate() http.HandlerFunc {
|
func (ctrl *V1Controller) HandleLabelUpdate() server.HandlerFunc {
|
||||||
return ctrl.handleLabelsGeneral()
|
return ctrl.handleLabelsGeneral()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctrl *V1Controller) handleLabelsGeneral() http.HandlerFunc {
|
func (ctrl *V1Controller) handleLabelsGeneral() server.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) error {
|
||||||
ctx := services.NewContext(r.Context())
|
ctx := services.NewContext(r.Context())
|
||||||
ID, err := ctrl.routeID(w, r)
|
ID, err := ctrl.routeID(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
|
@ -111,40 +109,37 @@ func (ctrl *V1Controller) handleLabelsGeneral() http.HandlerFunc {
|
||||||
log.Err(err).
|
log.Err(err).
|
||||||
Str("id", ID.String()).
|
Str("id", ID.String()).
|
||||||
Msg("label not found")
|
Msg("label not found")
|
||||||
server.RespondError(w, http.StatusNotFound, err)
|
return validate.NewRequestError(err, http.StatusNotFound)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
log.Err(err).Msg("error getting label")
|
log.Err(err).Msg("error getting label")
|
||||||
server.RespondServerError(w)
|
return validate.NewRequestError(err, http.StatusInternalServerError)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
server.Respond(w, http.StatusOK, labels)
|
return server.Respond(w, http.StatusOK, labels)
|
||||||
|
|
||||||
case http.MethodDelete:
|
case http.MethodDelete:
|
||||||
err = ctrl.svc.Labels.Delete(r.Context(), ctx.GID, ID)
|
err = ctrl.svc.Labels.Delete(r.Context(), ctx.GID, ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Err(err).Msg("error deleting label")
|
log.Err(err).Msg("error deleting label")
|
||||||
server.RespondServerError(w)
|
return validate.NewRequestError(err, http.StatusInternalServerError)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
server.Respond(w, http.StatusNoContent, nil)
|
return server.Respond(w, http.StatusNoContent, nil)
|
||||||
|
|
||||||
case http.MethodPut:
|
case http.MethodPut:
|
||||||
body := repo.LabelUpdate{}
|
body := repo.LabelUpdate{}
|
||||||
if err := server.Decode(r, &body); err != nil {
|
if err := server.Decode(r, &body); err != nil {
|
||||||
log.Err(err).Msg("error decoding label update data")
|
log.Err(err).Msg("error decoding label update data")
|
||||||
server.RespondError(w, http.StatusInternalServerError, err)
|
return validate.NewRequestError(err, http.StatusInternalServerError)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body.ID = ID
|
body.ID = ID
|
||||||
result, err := ctrl.svc.Labels.Update(r.Context(), ctx.GID, body)
|
result, err := ctrl.svc.Labels.Update(r.Context(), ctx.GID, body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Err(err).Msg("error updating label")
|
log.Err(err).Msg("error updating label")
|
||||||
server.RespondServerError(w)
|
return validate.NewRequestError(err, http.StatusInternalServerError)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
server.Respond(w, http.StatusOK, result)
|
return server.Respond(w, http.StatusOK, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"github.com/hay-kot/homebox/backend/ent"
|
"github.com/hay-kot/homebox/backend/ent"
|
||||||
"github.com/hay-kot/homebox/backend/internal/repo"
|
"github.com/hay-kot/homebox/backend/internal/repo"
|
||||||
"github.com/hay-kot/homebox/backend/internal/services"
|
"github.com/hay-kot/homebox/backend/internal/services"
|
||||||
|
"github.com/hay-kot/homebox/backend/internal/sys/validate"
|
||||||
"github.com/hay-kot/homebox/backend/pkgs/server"
|
"github.com/hay-kot/homebox/backend/pkgs/server"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
@ -17,17 +18,16 @@ import (
|
||||||
// @Success 200 {object} server.Results{items=[]repo.LocationOutCount}
|
// @Success 200 {object} server.Results{items=[]repo.LocationOutCount}
|
||||||
// @Router /v1/locations [GET]
|
// @Router /v1/locations [GET]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleLocationGetAll() http.HandlerFunc {
|
func (ctrl *V1Controller) HandleLocationGetAll() server.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) error {
|
||||||
user := services.UseUserCtx(r.Context())
|
user := services.UseUserCtx(r.Context())
|
||||||
locations, err := ctrl.svc.Location.GetAll(r.Context(), user.GroupID)
|
locations, err := ctrl.svc.Location.GetAll(r.Context(), user.GroupID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Err(err).Msg("failed to get locations")
|
log.Err(err).Msg("failed to get locations")
|
||||||
server.RespondServerError(w)
|
return validate.NewRequestError(err, http.StatusInternalServerError)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
server.Respond(w, http.StatusOK, server.Results{Items: locations})
|
return server.Respond(w, http.StatusOK, server.Results{Items: locations})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,24 +39,22 @@ func (ctrl *V1Controller) HandleLocationGetAll() http.HandlerFunc {
|
||||||
// @Success 200 {object} repo.LocationSummary
|
// @Success 200 {object} repo.LocationSummary
|
||||||
// @Router /v1/locations [POST]
|
// @Router /v1/locations [POST]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleLocationCreate() http.HandlerFunc {
|
func (ctrl *V1Controller) HandleLocationCreate() server.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) error {
|
||||||
createData := repo.LocationCreate{}
|
createData := repo.LocationCreate{}
|
||||||
if err := server.Decode(r, &createData); err != nil {
|
if err := server.Decode(r, &createData); err != nil {
|
||||||
log.Err(err).Msg("failed to decode location create data")
|
log.Err(err).Msg("failed to decode location create data")
|
||||||
server.RespondError(w, http.StatusInternalServerError, err)
|
return validate.NewRequestError(err, http.StatusInternalServerError)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
user := services.UseUserCtx(r.Context())
|
user := services.UseUserCtx(r.Context())
|
||||||
location, err := ctrl.svc.Location.Create(r.Context(), user.GroupID, createData)
|
location, err := ctrl.svc.Location.Create(r.Context(), user.GroupID, createData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Err(err).Msg("failed to create location")
|
log.Err(err).Msg("failed to create location")
|
||||||
server.RespondServerError(w)
|
return validate.NewRequestError(err, http.StatusInternalServerError)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
server.Respond(w, http.StatusCreated, location)
|
return server.Respond(w, http.StatusCreated, location)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,7 +66,7 @@ func (ctrl *V1Controller) HandleLocationCreate() http.HandlerFunc {
|
||||||
// @Success 204
|
// @Success 204
|
||||||
// @Router /v1/locations/{id} [DELETE]
|
// @Router /v1/locations/{id} [DELETE]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleLocationDelete() http.HandlerFunc {
|
func (ctrl *V1Controller) HandleLocationDelete() server.HandlerFunc {
|
||||||
return ctrl.handleLocationGeneral()
|
return ctrl.handleLocationGeneral()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,7 +78,7 @@ func (ctrl *V1Controller) HandleLocationDelete() http.HandlerFunc {
|
||||||
// @Success 200 {object} repo.LocationOut
|
// @Success 200 {object} repo.LocationOut
|
||||||
// @Router /v1/locations/{id} [GET]
|
// @Router /v1/locations/{id} [GET]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleLocationGet() http.HandlerFunc {
|
func (ctrl *V1Controller) HandleLocationGet() server.HandlerFunc {
|
||||||
return ctrl.handleLocationGeneral()
|
return ctrl.handleLocationGeneral()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,16 +91,16 @@ func (ctrl *V1Controller) HandleLocationGet() http.HandlerFunc {
|
||||||
// @Success 200 {object} repo.LocationOut
|
// @Success 200 {object} repo.LocationOut
|
||||||
// @Router /v1/locations/{id} [PUT]
|
// @Router /v1/locations/{id} [PUT]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleLocationUpdate() http.HandlerFunc {
|
func (ctrl *V1Controller) HandleLocationUpdate() server.HandlerFunc {
|
||||||
return ctrl.handleLocationGeneral()
|
return ctrl.handleLocationGeneral()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctrl *V1Controller) handleLocationGeneral() http.HandlerFunc {
|
func (ctrl *V1Controller) handleLocationGeneral() server.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) error {
|
||||||
ctx := services.NewContext(r.Context())
|
ctx := services.NewContext(r.Context())
|
||||||
ID, err := ctrl.routeID(w, r)
|
ID, err := ctrl.routeID(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
|
@ -115,21 +113,18 @@ func (ctrl *V1Controller) handleLocationGeneral() http.HandlerFunc {
|
||||||
|
|
||||||
if ent.IsNotFound(err) {
|
if ent.IsNotFound(err) {
|
||||||
l.Msg("location not found")
|
l.Msg("location not found")
|
||||||
server.RespondError(w, http.StatusNotFound, err)
|
return validate.NewRequestError(err, http.StatusNotFound)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
l.Msg("failed to get location")
|
l.Msg("failed to get location")
|
||||||
server.RespondServerError(w)
|
return validate.NewRequestError(err, http.StatusInternalServerError)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
server.Respond(w, http.StatusOK, location)
|
return server.Respond(w, http.StatusOK, location)
|
||||||
case http.MethodPut:
|
case http.MethodPut:
|
||||||
body := repo.LocationUpdate{}
|
body := repo.LocationUpdate{}
|
||||||
if err := server.Decode(r, &body); err != nil {
|
if err := server.Decode(r, &body); err != nil {
|
||||||
log.Err(err).Msg("failed to decode location update data")
|
log.Err(err).Msg("failed to decode location update data")
|
||||||
server.RespondError(w, http.StatusInternalServerError, err)
|
return validate.NewRequestError(err, http.StatusInternalServerError)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body.ID = ID
|
body.ID = ID
|
||||||
|
@ -137,18 +132,17 @@ func (ctrl *V1Controller) handleLocationGeneral() http.HandlerFunc {
|
||||||
result, err := ctrl.svc.Location.Update(r.Context(), ctx.GID, body)
|
result, err := ctrl.svc.Location.Update(r.Context(), ctx.GID, body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Err(err).Msg("failed to update location")
|
log.Err(err).Msg("failed to update location")
|
||||||
server.RespondServerError(w)
|
return validate.NewRequestError(err, http.StatusInternalServerError)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
server.Respond(w, http.StatusOK, result)
|
return server.Respond(w, http.StatusOK, result)
|
||||||
case http.MethodDelete:
|
case http.MethodDelete:
|
||||||
err = ctrl.svc.Location.Delete(r.Context(), ctx.GID, ID)
|
err = ctrl.svc.Location.Delete(r.Context(), ctx.GID, ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Err(err).Msg("failed to delete location")
|
log.Err(err).Msg("failed to delete location")
|
||||||
server.RespondServerError(w)
|
return validate.NewRequestError(err, http.StatusInternalServerError)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
server.Respond(w, http.StatusNoContent, nil)
|
return server.Respond(w, http.StatusNoContent, nil)
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/hay-kot/homebox/backend/internal/repo"
|
"github.com/hay-kot/homebox/backend/internal/repo"
|
||||||
"github.com/hay-kot/homebox/backend/internal/services"
|
"github.com/hay-kot/homebox/backend/internal/services"
|
||||||
|
"github.com/hay-kot/homebox/backend/internal/sys/validate"
|
||||||
"github.com/hay-kot/homebox/backend/pkgs/server"
|
"github.com/hay-kot/homebox/backend/pkgs/server"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
@ -17,29 +18,26 @@ import (
|
||||||
// @Param payload body services.UserRegistration true "User Data"
|
// @Param payload body services.UserRegistration true "User Data"
|
||||||
// @Success 204
|
// @Success 204
|
||||||
// @Router /v1/users/register [Post]
|
// @Router /v1/users/register [Post]
|
||||||
func (ctrl *V1Controller) HandleUserRegistration() http.HandlerFunc {
|
func (ctrl *V1Controller) HandleUserRegistration() server.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) error {
|
||||||
regData := services.UserRegistration{}
|
regData := services.UserRegistration{}
|
||||||
|
|
||||||
if err := server.Decode(r, ®Data); err != nil {
|
if err := server.Decode(r, ®Data); err != nil {
|
||||||
log.Err(err).Msg("failed to decode user registration data")
|
log.Err(err).Msg("failed to decode user registration data")
|
||||||
server.RespondError(w, http.StatusInternalServerError, err)
|
return validate.NewRequestError(err, http.StatusInternalServerError)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !ctrl.allowRegistration && regData.GroupToken == "" {
|
if !ctrl.allowRegistration && regData.GroupToken == "" {
|
||||||
server.RespondError(w, http.StatusForbidden, nil)
|
return validate.NewRequestError(nil, http.StatusForbidden)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := ctrl.svc.User.RegisterUser(r.Context(), regData)
|
_, err := ctrl.svc.User.RegisterUser(r.Context(), regData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Err(err).Msg("failed to register user")
|
log.Err(err).Msg("failed to register user")
|
||||||
server.RespondError(w, http.StatusInternalServerError, err)
|
return validate.NewRequestError(err, http.StatusInternalServerError)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
server.Respond(w, http.StatusNoContent, nil)
|
return server.Respond(w, http.StatusNoContent, nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,17 +48,16 @@ func (ctrl *V1Controller) HandleUserRegistration() http.HandlerFunc {
|
||||||
// @Success 200 {object} server.Result{item=repo.UserOut}
|
// @Success 200 {object} server.Result{item=repo.UserOut}
|
||||||
// @Router /v1/users/self [GET]
|
// @Router /v1/users/self [GET]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleUserSelf() http.HandlerFunc {
|
func (ctrl *V1Controller) HandleUserSelf() server.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) error {
|
||||||
token := services.UseTokenCtx(r.Context())
|
token := services.UseTokenCtx(r.Context())
|
||||||
usr, err := ctrl.svc.User.GetSelf(r.Context(), token)
|
usr, err := ctrl.svc.User.GetSelf(r.Context(), token)
|
||||||
if usr.ID == uuid.Nil || err != nil {
|
if usr.ID == uuid.Nil || err != nil {
|
||||||
log.Err(err).Msg("failed to get user")
|
log.Err(err).Msg("failed to get user")
|
||||||
server.RespondServerError(w)
|
return validate.NewRequestError(err, http.StatusInternalServerError)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
server.Respond(w, http.StatusOK, server.Wrap(usr))
|
return server.Respond(w, http.StatusOK, server.Wrap(usr))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,24 +69,22 @@ func (ctrl *V1Controller) HandleUserSelf() http.HandlerFunc {
|
||||||
// @Success 200 {object} server.Result{item=repo.UserUpdate}
|
// @Success 200 {object} server.Result{item=repo.UserUpdate}
|
||||||
// @Router /v1/users/self [PUT]
|
// @Router /v1/users/self [PUT]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleUserSelfUpdate() http.HandlerFunc {
|
func (ctrl *V1Controller) HandleUserSelfUpdate() server.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) error {
|
||||||
updateData := repo.UserUpdate{}
|
updateData := repo.UserUpdate{}
|
||||||
if err := server.Decode(r, &updateData); err != nil {
|
if err := server.Decode(r, &updateData); err != nil {
|
||||||
log.Err(err).Msg("failed to decode user update data")
|
log.Err(err).Msg("failed to decode user update data")
|
||||||
server.RespondError(w, http.StatusBadRequest, err)
|
return validate.NewRequestError(err, http.StatusBadRequest)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
actor := services.UseUserCtx(r.Context())
|
actor := services.UseUserCtx(r.Context())
|
||||||
newData, err := ctrl.svc.User.UpdateSelf(r.Context(), actor.ID, updateData)
|
newData, err := ctrl.svc.User.UpdateSelf(r.Context(), actor.ID, updateData)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
server.RespondError(w, http.StatusInternalServerError, err)
|
return validate.NewRequestError(err, http.StatusInternalServerError)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
server.Respond(w, http.StatusOK, server.Wrap(newData))
|
return server.Respond(w, http.StatusOK, server.Wrap(newData))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,20 +95,18 @@ func (ctrl *V1Controller) HandleUserSelfUpdate() http.HandlerFunc {
|
||||||
// @Success 204
|
// @Success 204
|
||||||
// @Router /v1/users/self [DELETE]
|
// @Router /v1/users/self [DELETE]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleUserSelfDelete() http.HandlerFunc {
|
func (ctrl *V1Controller) HandleUserSelfDelete() server.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) error {
|
||||||
if ctrl.isDemo {
|
if ctrl.isDemo {
|
||||||
server.RespondError(w, http.StatusForbidden, nil)
|
return validate.NewRequestError(nil, http.StatusForbidden)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
actor := services.UseUserCtx(r.Context())
|
actor := services.UseUserCtx(r.Context())
|
||||||
if err := ctrl.svc.User.DeleteSelf(r.Context(), actor.ID); err != nil {
|
if err := ctrl.svc.User.DeleteSelf(r.Context(), actor.ID); err != nil {
|
||||||
server.RespondError(w, http.StatusInternalServerError, err)
|
return validate.NewRequestError(err, http.StatusInternalServerError)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
server.Respond(w, http.StatusNoContent, nil)
|
return server.Respond(w, http.StatusNoContent, nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,11 +124,10 @@ type (
|
||||||
// @Param payload body ChangePassword true "Password Payload"
|
// @Param payload body ChangePassword true "Password Payload"
|
||||||
// @Router /v1/users/change-password [PUT]
|
// @Router /v1/users/change-password [PUT]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleUserSelfChangePassword() http.HandlerFunc {
|
func (ctrl *V1Controller) HandleUserSelfChangePassword() server.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) error {
|
||||||
if ctrl.isDemo {
|
if ctrl.isDemo {
|
||||||
server.RespondError(w, http.StatusForbidden, nil)
|
return validate.NewRequestError(nil, http.StatusForbidden)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var cp ChangePassword
|
var cp ChangePassword
|
||||||
|
@ -148,10 +140,9 @@ func (ctrl *V1Controller) HandleUserSelfChangePassword() http.HandlerFunc {
|
||||||
|
|
||||||
ok := ctrl.svc.User.ChangePassword(ctx, cp.Current, cp.New)
|
ok := ctrl.svc.User.ChangePassword(ctx, cp.Current, cp.New)
|
||||||
if !ok {
|
if !ok {
|
||||||
server.RespondError(w, http.StatusInternalServerError, err)
|
return validate.NewRequestError(err, http.StatusInternalServerError)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
server.Respond(w, http.StatusNoContent, nil)
|
return server.Respond(w, http.StatusNoContent, nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ func (a *app) setupLogger() {
|
||||||
// Logger Init
|
// Logger Init
|
||||||
// zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
|
// zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
|
||||||
if a.conf.Log.Format != config.LogFormatJSON {
|
if a.conf.Log.Format != config.LogFormatJSON {
|
||||||
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
|
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}).With().Caller().Logger()
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Level(getLevel(a.conf.Log.Level))
|
log.Level(getLevel(a.conf.Log.Level))
|
||||||
|
|
|
@ -15,6 +15,7 @@ import (
|
||||||
"github.com/hay-kot/homebox/backend/internal/migrations"
|
"github.com/hay-kot/homebox/backend/internal/migrations"
|
||||||
"github.com/hay-kot/homebox/backend/internal/repo"
|
"github.com/hay-kot/homebox/backend/internal/repo"
|
||||||
"github.com/hay-kot/homebox/backend/internal/services"
|
"github.com/hay-kot/homebox/backend/internal/services"
|
||||||
|
"github.com/hay-kot/homebox/backend/internal/web/mid"
|
||||||
"github.com/hay-kot/homebox/backend/pkgs/server"
|
"github.com/hay-kot/homebox/backend/pkgs/server"
|
||||||
_ "github.com/mattn/go-sqlite3"
|
_ "github.com/mattn/go-sqlite3"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
@ -114,17 +115,25 @@ func run(cfg *config.Config) error {
|
||||||
app.services = services.New(app.repos)
|
app.services = services.New(app.repos)
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Start Server
|
// Start Server\
|
||||||
|
logger := log.With().Caller().Logger()
|
||||||
|
|
||||||
|
mwLogger := mid.Logger(logger)
|
||||||
|
if app.conf.Mode == config.ModeDevelopment {
|
||||||
|
mwLogger = mid.SugarLogger(logger)
|
||||||
|
}
|
||||||
|
|
||||||
app.server = server.NewServer(
|
app.server = server.NewServer(
|
||||||
server.WithHost(app.conf.Web.Host),
|
server.WithHost(app.conf.Web.Host),
|
||||||
server.WithPort(app.conf.Web.Port),
|
server.WithPort(app.conf.Web.Port),
|
||||||
|
server.WithMiddleware(
|
||||||
|
mwLogger,
|
||||||
|
mid.Errors(logger),
|
||||||
|
mid.Panic(app.conf.Mode == config.ModeDevelopment),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
routes := app.newRouter(app.repos)
|
app.mountRoutes(app.repos)
|
||||||
|
|
||||||
if app.conf.Mode != config.ModeDevelopment {
|
|
||||||
app.logRoutes(routes)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info().Msgf("Starting HTTP Server on %s:%s", app.server.Host, app.server.Port)
|
log.Info().Msgf("Starting HTTP Server on %s:%s", app.server.Host, app.server.Port)
|
||||||
|
|
||||||
|
@ -163,5 +172,5 @@ func run(cfg *config.Config) error {
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
return app.server.Start(routes)
|
return app.server.Start()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,143 +1,34 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
|
||||||
"github.com/go-chi/chi/v5/middleware"
|
|
||||||
"github.com/hay-kot/homebox/backend/internal/config"
|
|
||||||
"github.com/hay-kot/homebox/backend/internal/services"
|
"github.com/hay-kot/homebox/backend/internal/services"
|
||||||
|
"github.com/hay-kot/homebox/backend/internal/sys/validate"
|
||||||
"github.com/hay-kot/homebox/backend/pkgs/server"
|
"github.com/hay-kot/homebox/backend/pkgs/server"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (a *app) setGlobalMiddleware(r *chi.Mux) {
|
|
||||||
// =========================================================================
|
|
||||||
// Middleware
|
|
||||||
r.Use(middleware.RequestID)
|
|
||||||
r.Use(middleware.RealIP)
|
|
||||||
r.Use(mwStripTrailingSlash)
|
|
||||||
|
|
||||||
// Use struct logger in production for requests, but use
|
|
||||||
// pretty console logger in development.
|
|
||||||
if a.conf.Mode == config.ModeDevelopment {
|
|
||||||
r.Use(a.mwSummaryLogger)
|
|
||||||
} else {
|
|
||||||
r.Use(a.mwStructLogger)
|
|
||||||
}
|
|
||||||
r.Use(middleware.Recoverer)
|
|
||||||
|
|
||||||
// Set a timeout value on the request context (ctx), that will signal
|
|
||||||
// through ctx.Done() that the request has timed out and further
|
|
||||||
// processing should be stopped.
|
|
||||||
r.Use(middleware.Timeout(60 * time.Second))
|
|
||||||
}
|
|
||||||
|
|
||||||
// mwAuthToken is a middleware that will check the database for a stateful token
|
// mwAuthToken is a middleware that will check the database for a stateful token
|
||||||
// and attach it to the request context with the user, or return a 401 if it doesn't exist.
|
// and attach it to the request context with the user, or return a 401 if it doesn't exist.
|
||||||
func (a *app) mwAuthToken(next http.Handler) http.Handler {
|
func (a *app) mwAuthToken(next server.Handler) server.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return server.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
||||||
requestToken := r.Header.Get("Authorization")
|
requestToken := r.Header.Get("Authorization")
|
||||||
|
|
||||||
if requestToken == "" {
|
if requestToken == "" {
|
||||||
server.RespondUnauthorized(w)
|
return validate.NewRequestError(errors.New("Authorization header is required"), http.StatusUnauthorized)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
requestToken = strings.TrimPrefix(requestToken, "Bearer ")
|
requestToken = strings.TrimPrefix(requestToken, "Bearer ")
|
||||||
usr, err := a.services.User.GetSelf(r.Context(), requestToken)
|
usr, err := a.services.User.GetSelf(r.Context(), requestToken)
|
||||||
|
|
||||||
// Check the database for the token
|
// Check the database for the token
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
server.RespondUnauthorized(w)
|
return validate.NewRequestError(errors.New("Authorization header is required"), http.StatusUnauthorized)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
r = r.WithContext(services.SetUserCtx(r.Context(), &usr, requestToken))
|
r = r.WithContext(services.SetUserCtx(r.Context(), &usr, requestToken))
|
||||||
|
return next.ServeHTTP(w, r)
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// mqStripTrailingSlash is a middleware that will strip trailing slashes from the request path.
|
|
||||||
func mwStripTrailingSlash(next http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
r.URL.Path = strings.TrimSuffix(r.URL.Path, "/")
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
type StatusRecorder struct {
|
|
||||||
http.ResponseWriter
|
|
||||||
Status int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *StatusRecorder) WriteHeader(status int) {
|
|
||||||
r.Status = status
|
|
||||||
r.ResponseWriter.WriteHeader(status)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *app) mwStructLogger(next http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
record := &StatusRecorder{ResponseWriter: w, Status: http.StatusOK}
|
|
||||||
next.ServeHTTP(record, r)
|
|
||||||
|
|
||||||
scheme := "http"
|
|
||||||
if r.TLS != nil {
|
|
||||||
scheme = "https"
|
|
||||||
}
|
|
||||||
|
|
||||||
url := fmt.Sprintf("%s://%s%s %s", scheme, r.Host, r.RequestURI, r.Proto)
|
|
||||||
|
|
||||||
log.Info().
|
|
||||||
Str("id", middleware.GetReqID(r.Context())).
|
|
||||||
Str("method", r.Method).
|
|
||||||
Str("remote_addr", r.RemoteAddr).
|
|
||||||
Int("status", record.Status).
|
|
||||||
Msg(url)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *app) mwSummaryLogger(next http.Handler) http.Handler {
|
|
||||||
bold := func(s string) string { return "\033[1m" + s + "\033[0m" }
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
record := &StatusRecorder{ResponseWriter: w, Status: http.StatusOK}
|
|
||||||
next.ServeHTTP(record, r) // Blocks until the next handler returns.
|
|
||||||
|
|
||||||
scheme := "http"
|
|
||||||
if r.TLS != nil {
|
|
||||||
scheme = "https"
|
|
||||||
}
|
|
||||||
|
|
||||||
url := fmt.Sprintf("%s://%s%s %s", scheme, r.Host, r.RequestURI, r.Proto)
|
|
||||||
|
|
||||||
log.Info().
|
|
||||||
Msgf("%s %s %s",
|
|
||||||
bold(orange(""+r.Method+"")),
|
|
||||||
aqua(url),
|
|
||||||
bold(fmtCode(record.Status)),
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,11 +10,11 @@ import (
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
|
||||||
"github.com/hay-kot/homebox/backend/app/api/handlers/debughandlers"
|
"github.com/hay-kot/homebox/backend/app/api/handlers/debughandlers"
|
||||||
v1 "github.com/hay-kot/homebox/backend/app/api/handlers/v1"
|
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/app/api/static/docs"
|
||||||
"github.com/hay-kot/homebox/backend/internal/repo"
|
"github.com/hay-kot/homebox/backend/internal/repo"
|
||||||
|
"github.com/hay-kot/homebox/backend/pkgs/server"
|
||||||
httpSwagger "github.com/swaggo/http-swagger" // http-swagger middleware
|
httpSwagger "github.com/swaggo/http-swagger" // http-swagger middleware
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -35,101 +35,76 @@ func (a *app) debugRouter() *http.ServeMux {
|
||||||
}
|
}
|
||||||
|
|
||||||
// registerRoutes registers all the routes for the API
|
// registerRoutes registers all the routes for the API
|
||||||
func (a *app) newRouter(repos *repo.AllRepos) *chi.Mux {
|
func (a *app) mountRoutes(repos *repo.AllRepos) {
|
||||||
registerMimes()
|
registerMimes()
|
||||||
|
|
||||||
r := chi.NewRouter()
|
a.server.Get("/swagger/*", server.ToHandler(httpSwagger.Handler(
|
||||||
a.setGlobalMiddleware(r)
|
|
||||||
|
|
||||||
r.Get("/swagger/*", httpSwagger.Handler(
|
|
||||||
httpSwagger.URL(fmt.Sprintf("%s://%s/swagger/doc.json", a.conf.Swagger.Scheme, a.conf.Swagger.Host)),
|
httpSwagger.URL(fmt.Sprintf("%s://%s/swagger/doc.json", a.conf.Swagger.Scheme, a.conf.Swagger.Host)),
|
||||||
))
|
)))
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// API Version 1
|
// API Version 1
|
||||||
|
|
||||||
v1Base := v1.BaseUrlFunc(prefix)
|
v1Base := v1.BaseUrlFunc(prefix)
|
||||||
v1Ctrl := v1.NewControllerV1(a.services,
|
|
||||||
|
v1Ctrl := v1.NewControllerV1(
|
||||||
|
a.services,
|
||||||
v1.WithMaxUploadSize(a.conf.Web.MaxUploadSize),
|
v1.WithMaxUploadSize(a.conf.Web.MaxUploadSize),
|
||||||
v1.WithRegistration(a.conf.AllowRegistration),
|
v1.WithRegistration(a.conf.AllowRegistration),
|
||||||
v1.WithDemoStatus(a.conf.Demo), // Disable Password Change in Demo Mode
|
v1.WithDemoStatus(a.conf.Demo), // Disable Password Change in Demo Mode
|
||||||
)
|
)
|
||||||
r.Get(v1Base("/status"), v1Ctrl.HandleBase(func() bool { return true }, v1.Build{
|
|
||||||
|
a.server.Get(v1Base("/status"), v1Ctrl.HandleBase(func() bool { return true }, v1.Build{
|
||||||
Version: version,
|
Version: version,
|
||||||
Commit: commit,
|
Commit: commit,
|
||||||
BuildTime: buildTime,
|
BuildTime: buildTime,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
r.Post(v1Base("/users/register"), v1Ctrl.HandleUserRegistration())
|
a.server.Post(v1Base("/users/register"), v1Ctrl.HandleUserRegistration())
|
||||||
r.Post(v1Base("/users/login"), v1Ctrl.HandleAuthLogin())
|
a.server.Post(v1Base("/users/login"), v1Ctrl.HandleAuthLogin())
|
||||||
|
|
||||||
// Attachment download URl needs a `token` query param to be passed in the request.
|
// Attachment download URl needs a `token` query param to be passed in the request.
|
||||||
// and also needs to be outside of the `auth` middleware.
|
// and also needs to be outside of the `auth` middleware.
|
||||||
r.Get(v1Base("/items/{id}/attachments/download"), v1Ctrl.HandleItemAttachmentDownload())
|
a.server.Get(v1Base("/items/{id}/attachments/download"), v1Ctrl.HandleItemAttachmentDownload())
|
||||||
|
|
||||||
r.Group(func(r chi.Router) {
|
a.server.Get(v1Base("/users/self"), v1Ctrl.HandleUserSelf(), a.mwAuthToken)
|
||||||
r.Use(a.mwAuthToken)
|
a.server.Put(v1Base("/users/self"), v1Ctrl.HandleUserSelfUpdate(), a.mwAuthToken)
|
||||||
r.Get(v1Base("/users/self"), v1Ctrl.HandleUserSelf())
|
a.server.Delete(v1Base("/users/self"), v1Ctrl.HandleUserSelfDelete(), a.mwAuthToken)
|
||||||
r.Put(v1Base("/users/self"), v1Ctrl.HandleUserSelfUpdate())
|
a.server.Post(v1Base("/users/logout"), v1Ctrl.HandleAuthLogout(), a.mwAuthToken)
|
||||||
r.Delete(v1Base("/users/self"), v1Ctrl.HandleUserSelfDelete())
|
a.server.Get(v1Base("/users/refresh"), v1Ctrl.HandleAuthRefresh(), a.mwAuthToken)
|
||||||
r.Post(v1Base("/users/logout"), v1Ctrl.HandleAuthLogout())
|
a.server.Put(v1Base("/users/self/change-password"), v1Ctrl.HandleUserSelfChangePassword(), a.mwAuthToken)
|
||||||
r.Get(v1Base("/users/refresh"), v1Ctrl.HandleAuthRefresh())
|
|
||||||
r.Put(v1Base("/users/self/change-password"), v1Ctrl.HandleUserSelfChangePassword())
|
|
||||||
|
|
||||||
r.Post(v1Base("/groups/invitations"), v1Ctrl.HandleGroupInvitationsCreate())
|
a.server.Post(v1Base("/groups/invitations"), v1Ctrl.HandleGroupInvitationsCreate(), a.mwAuthToken)
|
||||||
|
|
||||||
// TODO: I don't like /groups being the URL for users
|
// TODO: I don't like /groups being the URL for users
|
||||||
r.Get(v1Base("/groups"), v1Ctrl.HandleGroupGet())
|
a.server.Get(v1Base("/groups"), v1Ctrl.HandleGroupGet(), a.mwAuthToken)
|
||||||
r.Put(v1Base("/groups"), v1Ctrl.HandleGroupUpdate())
|
a.server.Put(v1Base("/groups"), v1Ctrl.HandleGroupUpdate(), a.mwAuthToken)
|
||||||
|
|
||||||
r.Get(v1Base("/locations"), v1Ctrl.HandleLocationGetAll())
|
a.server.Get(v1Base("/locations"), v1Ctrl.HandleLocationGetAll(), a.mwAuthToken)
|
||||||
r.Post(v1Base("/locations"), v1Ctrl.HandleLocationCreate())
|
a.server.Post(v1Base("/locations"), v1Ctrl.HandleLocationCreate(), a.mwAuthToken)
|
||||||
r.Get(v1Base("/locations/{id}"), v1Ctrl.HandleLocationGet())
|
a.server.Get(v1Base("/locations/{id}"), v1Ctrl.HandleLocationGet(), a.mwAuthToken)
|
||||||
r.Put(v1Base("/locations/{id}"), v1Ctrl.HandleLocationUpdate())
|
a.server.Put(v1Base("/locations/{id}"), v1Ctrl.HandleLocationUpdate(), a.mwAuthToken)
|
||||||
r.Delete(v1Base("/locations/{id}"), v1Ctrl.HandleLocationDelete())
|
a.server.Delete(v1Base("/locations/{id}"), v1Ctrl.HandleLocationDelete(), a.mwAuthToken)
|
||||||
|
|
||||||
r.Get(v1Base("/labels"), v1Ctrl.HandleLabelsGetAll())
|
a.server.Get(v1Base("/labels"), v1Ctrl.HandleLabelsGetAll(), a.mwAuthToken)
|
||||||
r.Post(v1Base("/labels"), v1Ctrl.HandleLabelsCreate())
|
a.server.Post(v1Base("/labels"), v1Ctrl.HandleLabelsCreate(), a.mwAuthToken)
|
||||||
r.Get(v1Base("/labels/{id}"), v1Ctrl.HandleLabelGet())
|
a.server.Get(v1Base("/labels/{id}"), v1Ctrl.HandleLabelGet(), a.mwAuthToken)
|
||||||
r.Put(v1Base("/labels/{id}"), v1Ctrl.HandleLabelUpdate())
|
a.server.Put(v1Base("/labels/{id}"), v1Ctrl.HandleLabelUpdate(), a.mwAuthToken)
|
||||||
r.Delete(v1Base("/labels/{id}"), v1Ctrl.HandleLabelDelete())
|
a.server.Delete(v1Base("/labels/{id}"), v1Ctrl.HandleLabelDelete(), a.mwAuthToken)
|
||||||
|
|
||||||
r.Get(v1Base("/items"), v1Ctrl.HandleItemsGetAll())
|
a.server.Get(v1Base("/items"), v1Ctrl.HandleItemsGetAll(), a.mwAuthToken)
|
||||||
r.Post(v1Base("/items/import"), v1Ctrl.HandleItemsImport())
|
a.server.Post(v1Base("/items/import"), v1Ctrl.HandleItemsImport(), a.mwAuthToken)
|
||||||
r.Post(v1Base("/items"), v1Ctrl.HandleItemsCreate())
|
a.server.Post(v1Base("/items"), v1Ctrl.HandleItemsCreate(), a.mwAuthToken)
|
||||||
r.Get(v1Base("/items/{id}"), v1Ctrl.HandleItemGet())
|
a.server.Get(v1Base("/items/{id}"), v1Ctrl.HandleItemGet(), a.mwAuthToken)
|
||||||
r.Put(v1Base("/items/{id}"), v1Ctrl.HandleItemUpdate())
|
a.server.Put(v1Base("/items/{id}"), v1Ctrl.HandleItemUpdate(), a.mwAuthToken)
|
||||||
r.Delete(v1Base("/items/{id}"), v1Ctrl.HandleItemDelete())
|
a.server.Delete(v1Base("/items/{id}"), v1Ctrl.HandleItemDelete(), a.mwAuthToken)
|
||||||
|
|
||||||
r.Post(v1Base("/items/{id}/attachments"), v1Ctrl.HandleItemAttachmentCreate())
|
a.server.Post(v1Base("/items/{id}/attachments"), v1Ctrl.HandleItemAttachmentCreate(), a.mwAuthToken)
|
||||||
r.Get(v1Base("/items/{id}/attachments/{attachment_id}"), v1Ctrl.HandleItemAttachmentToken())
|
a.server.Get(v1Base("/items/{id}/attachments/{attachment_id}"), v1Ctrl.HandleItemAttachmentToken(), a.mwAuthToken)
|
||||||
r.Put(v1Base("/items/{id}/attachments/{attachment_id}"), v1Ctrl.HandleItemAttachmentUpdate())
|
a.server.Put(v1Base("/items/{id}/attachments/{attachment_id}"), v1Ctrl.HandleItemAttachmentUpdate(), a.mwAuthToken)
|
||||||
r.Delete(v1Base("/items/{id}/attachments/{attachment_id}"), v1Ctrl.HandleItemAttachmentDelete())
|
a.server.Delete(v1Base("/items/{id}/attachments/{attachment_id}"), v1Ctrl.HandleItemAttachmentDelete(), a.mwAuthToken)
|
||||||
})
|
|
||||||
|
|
||||||
r.NotFound(notFoundHandler())
|
a.server.NotFound(notFoundHandler())
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
// logRoutes logs the routes of the server that are registered within Server.registerRoutes(). This is useful for debugging.
|
|
||||||
// See https://github.com/go-chi/chi/issues/332 for details and inspiration.
|
|
||||||
func (a *app) logRoutes(r *chi.Mux) {
|
|
||||||
desiredSpaces := 10
|
|
||||||
|
|
||||||
walkFunc := func(method string, route string, handler http.Handler, middleware ...func(http.Handler) http.Handler) error {
|
|
||||||
text := "[" + method + "]"
|
|
||||||
|
|
||||||
for len(text) < desiredSpaces {
|
|
||||||
text = text + " "
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("Registered Route: %s%s\n", text, route)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := chi.Walk(r, walkFunc); err != nil {
|
|
||||||
fmt.Printf("Logging err: %s\n", err.Error())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func registerMimes() {
|
func registerMimes() {
|
||||||
|
@ -146,7 +121,7 @@ func registerMimes() {
|
||||||
|
|
||||||
// notFoundHandler perform the main logic around handling the internal SPA embed and ensuring that
|
// notFoundHandler perform the main logic around handling the internal SPA embed and ensuring that
|
||||||
// the client side routing is handled correctly.
|
// the client side routing is handled correctly.
|
||||||
func notFoundHandler() http.HandlerFunc {
|
func notFoundHandler() server.HandlerFunc {
|
||||||
tryRead := func(fs embed.FS, prefix, requestedPath string, w http.ResponseWriter) error {
|
tryRead := func(fs embed.FS, prefix, requestedPath string, w http.ResponseWriter) error {
|
||||||
f, err := fs.Open(path.Join(prefix, requestedPath))
|
f, err := fs.Open(path.Join(prefix, requestedPath))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -165,14 +140,16 @@ func notFoundHandler() http.HandlerFunc {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) error {
|
||||||
err := tryRead(public, "static/public", r.URL.Path, w)
|
err := tryRead(public, "static/public", r.URL.Path, w)
|
||||||
if err == nil {
|
if err != nil {
|
||||||
return
|
// Fallback to the index.html file.
|
||||||
}
|
// should succeed in all cases.
|
||||||
err = tryRead(public, "static/public", "index.html", w)
|
err = tryRead(public, "static/public", "index.html", w)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -395,10 +395,7 @@ const docTemplate = `{
|
||||||
"422": {
|
"422": {
|
||||||
"description": "Unprocessable Entity",
|
"description": "Unprocessable Entity",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "array",
|
"$ref": "#/definitions/server.ErrorResponse"
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/server.ValidationError"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1735,6 +1732,20 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"server.ErrorResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"fields": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"server.Result": {
|
"server.Result": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
@ -1754,17 +1765,6 @@ const docTemplate = `{
|
||||||
"items": {}
|
"items": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"server.ValidationError": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"field": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"reason": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"services.UserRegistration": {
|
"services.UserRegistration": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
|
@ -387,10 +387,7 @@
|
||||||
"422": {
|
"422": {
|
||||||
"description": "Unprocessable Entity",
|
"description": "Unprocessable Entity",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "array",
|
"$ref": "#/definitions/server.ErrorResponse"
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/server.ValidationError"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1727,6 +1724,20 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"server.ErrorResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"fields": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"server.Result": {
|
"server.Result": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
@ -1746,17 +1757,6 @@
|
||||||
"items": {}
|
"items": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"server.ValidationError": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"field": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"reason": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"services.UserRegistration": {
|
"services.UserRegistration": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
|
@ -394,6 +394,15 @@ definitions:
|
||||||
name:
|
name:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
server.ErrorResponse:
|
||||||
|
properties:
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
|
fields:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
type: object
|
||||||
server.Result:
|
server.Result:
|
||||||
properties:
|
properties:
|
||||||
details: {}
|
details: {}
|
||||||
|
@ -407,13 +416,6 @@ definitions:
|
||||||
properties:
|
properties:
|
||||||
items: {}
|
items: {}
|
||||||
type: object
|
type: object
|
||||||
server.ValidationError:
|
|
||||||
properties:
|
|
||||||
field:
|
|
||||||
type: string
|
|
||||||
reason:
|
|
||||||
type: string
|
|
||||||
type: object
|
|
||||||
services.UserRegistration:
|
services.UserRegistration:
|
||||||
properties:
|
properties:
|
||||||
email:
|
email:
|
||||||
|
@ -708,9 +710,7 @@ paths:
|
||||||
"422":
|
"422":
|
||||||
description: Unprocessable Entity
|
description: Unprocessable Entity
|
||||||
schema:
|
schema:
|
||||||
items:
|
$ref: '#/definitions/server.ErrorResponse'
|
||||||
$ref: '#/definitions/server.ValidationError'
|
|
||||||
type: array
|
|
||||||
security:
|
security:
|
||||||
- Bearer: []
|
- Bearer: []
|
||||||
summary: imports items into the database
|
summary: imports items into the database
|
||||||
|
|
119
backend/internal/sys/validate/errors.go
Normal file
119
backend/internal/sys/validate/errors.go
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
package validate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UnauthorizedError struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err *UnauthorizedError) Error() string {
|
||||||
|
return "unauthorized"
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsUnauthorizedError(err error) bool {
|
||||||
|
var re *UnauthorizedError
|
||||||
|
return errors.As(err, &re)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUnauthorizedError() error {
|
||||||
|
return &UnauthorizedError{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type InvalidRouteKeyError struct {
|
||||||
|
key string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err *InvalidRouteKeyError) Error() string {
|
||||||
|
return "invalid route key: " + err.key
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewInvalidRouteKeyError(key string) error {
|
||||||
|
return &InvalidRouteKeyError{key}
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsInvalidRouteKeyError(err error) bool {
|
||||||
|
var re *InvalidRouteKeyError
|
||||||
|
return errors.As(err, &re)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrorResponse is the form used for API responses from failures in the API.
|
||||||
|
type ErrorResponse struct {
|
||||||
|
Error string `json:"error"`
|
||||||
|
Fields string `json:"fields,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequestError is used to pass an error during the request through the
|
||||||
|
// application with web specific context.
|
||||||
|
type RequestError struct {
|
||||||
|
Err error
|
||||||
|
Status int
|
||||||
|
Fields error
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRequestError wraps a provided error with an HTTP status code. This
|
||||||
|
// function should be used when handlers encounter expected errors.
|
||||||
|
func NewRequestError(err error, status int) error {
|
||||||
|
return &RequestError{err, status, nil}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err *RequestError) Error() string {
|
||||||
|
return err.Err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsRequestError checks if an error of type RequestError exists.
|
||||||
|
func IsRequestError(err error) bool {
|
||||||
|
var re *RequestError
|
||||||
|
return errors.As(err, &re)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FieldError is used to indicate an error with a specific request field.
|
||||||
|
type FieldError struct {
|
||||||
|
Field string `json:"field"`
|
||||||
|
Error string `json:"error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FieldErrors represents a collection of field errors.
|
||||||
|
type FieldErrors []FieldError
|
||||||
|
|
||||||
|
func (fe FieldErrors) Append(field, reason string) FieldErrors {
|
||||||
|
return append(fe, FieldError{
|
||||||
|
Field: field,
|
||||||
|
Error: reason,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fe FieldErrors) Nil() bool {
|
||||||
|
return len(fe) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error implments the error interface.
|
||||||
|
func (fe FieldErrors) Error() string {
|
||||||
|
d, err := json.Marshal(fe)
|
||||||
|
if err != nil {
|
||||||
|
return err.Error()
|
||||||
|
}
|
||||||
|
return string(d)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFieldErrors(errs ...FieldError) FieldErrors {
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsFieldError(err error) bool {
|
||||||
|
v := FieldErrors{}
|
||||||
|
return errors.As(err, &v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cause iterates through all the wrapped errors until the root
|
||||||
|
// error value is reached.
|
||||||
|
func Cause(err error) error {
|
||||||
|
root := err
|
||||||
|
for {
|
||||||
|
if err = errors.Unwrap(root); err == nil {
|
||||||
|
return root
|
||||||
|
}
|
||||||
|
root = err
|
||||||
|
}
|
||||||
|
}
|
70
backend/internal/web/mid/errors.go
Normal file
70
backend/internal/web/mid/errors.go
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
package mid
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/hay-kot/homebox/backend/ent"
|
||||||
|
"github.com/hay-kot/homebox/backend/internal/sys/validate"
|
||||||
|
"github.com/hay-kot/homebox/backend/pkgs/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 {
|
||||||
|
err := h.ServeHTTP(w, r)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
var resp server.ErrorResponse
|
||||||
|
var code int
|
||||||
|
|
||||||
|
log.Err(err).
|
||||||
|
Str("trace_id", server.GetTraceID(r.Context())).
|
||||||
|
Msg("ERROR occurred")
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case validate.IsUnauthorizedError(err):
|
||||||
|
code = http.StatusUnauthorized
|
||||||
|
resp = server.ErrorResponse{
|
||||||
|
Error: "unauthorized",
|
||||||
|
}
|
||||||
|
case validate.IsInvalidRouteKeyError(err):
|
||||||
|
code = http.StatusBadRequest
|
||||||
|
resp = server.ErrorResponse{
|
||||||
|
Error: err.Error(),
|
||||||
|
}
|
||||||
|
case validate.IsFieldError(err):
|
||||||
|
fieldErrors := err.(validate.FieldErrors)
|
||||||
|
resp.Error = "Validation Error"
|
||||||
|
resp.Fields = map[string]string{}
|
||||||
|
|
||||||
|
for _, fieldError := range fieldErrors {
|
||||||
|
resp.Fields[fieldError.Field] = fieldError.Error
|
||||||
|
}
|
||||||
|
case validate.IsRequestError(err):
|
||||||
|
requestError := err.(*validate.RequestError)
|
||||||
|
resp.Error = requestError.Error()
|
||||||
|
code = requestError.Status
|
||||||
|
case ent.IsNotFound(err):
|
||||||
|
resp.Error = "Not Found"
|
||||||
|
code = http.StatusNotFound
|
||||||
|
default:
|
||||||
|
resp.Error = "Unknown Error"
|
||||||
|
code = http.StatusInternalServerError
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := server.Respond(w, code, resp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If Showdown error, return error
|
||||||
|
if server.IsShutdownError(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
97
backend/internal/web/mid/logger.go
Normal file
97
backend/internal/web/mid/logger.go
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
package mid
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/hay-kot/homebox/backend/pkgs/server"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
)
|
||||||
|
|
||||||
|
type statusRecorder struct {
|
||||||
|
http.ResponseWriter
|
||||||
|
Status int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *statusRecorder) WriteHeader(status int) {
|
||||||
|
r.Status = status
|
||||||
|
r.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())
|
||||||
|
|
||||||
|
log.Info().
|
||||||
|
Str("trace_id", traceId).
|
||||||
|
Str("method", r.Method).
|
||||||
|
Str("path", r.URL.Path).
|
||||||
|
Str("remove_address", r.RemoteAddr).
|
||||||
|
Msg("request started")
|
||||||
|
|
||||||
|
record := &statusRecorder{ResponseWriter: w, Status: http.StatusOK}
|
||||||
|
|
||||||
|
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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
33
backend/internal/web/mid/panic.go
Normal file
33
backend/internal/web/mid/panic.go
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
23
backend/pkgs/server/errors.go
Normal file
23
backend/pkgs/server/errors.go
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
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)
|
||||||
|
}
|
25
backend/pkgs/server/handler.go
Normal file
25
backend/pkgs/server/handler.go
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
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
|
||||||
|
})
|
||||||
|
}
|
38
backend/pkgs/server/middleware.go
Normal file
38
backend/pkgs/server/middleware.go
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
103
backend/pkgs/server/mux.go
Normal file
103
backend/pkgs/server/mux.go
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
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))
|
||||||
|
}
|
|
@ -16,7 +16,7 @@ func Decode(r *http.Request, val interface{}) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetId is a shotcut to get the id from the request URL or return a default value
|
// 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 {
|
func GetParam(r *http.Request, key, d string) string {
|
||||||
val := r.URL.Query().Get(key)
|
val := r.URL.Query().Get(key)
|
||||||
|
|
||||||
|
@ -27,22 +27,22 @@ func GetParam(r *http.Request, key, d string) string {
|
||||||
return val
|
return val
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSkip is a shotcut to get the skip from the request URL parameters
|
// GetSkip is a shortcut to get the skip from the request URL parameters
|
||||||
func GetSkip(r *http.Request, d string) string {
|
func GetSkip(r *http.Request, d string) string {
|
||||||
return GetParam(r, "skip", d)
|
return GetParam(r, "skip", d)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSkip is a shotcut to get the skip from the request URL parameters
|
// GetSkip is a shortcut to get the skip from the request URL parameters
|
||||||
func GetId(r *http.Request, d string) string {
|
func GetId(r *http.Request, d string) string {
|
||||||
return GetParam(r, "id", d)
|
return GetParam(r, "id", d)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetLimit is a shotcut to get the limit from the request URL parameters
|
// GetLimit is a shortcut to get the limit from the request URL parameters
|
||||||
func GetLimit(r *http.Request, d string) string {
|
func GetLimit(r *http.Request, d string) string {
|
||||||
return GetParam(r, "limit", d)
|
return GetParam(r, "limit", d)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetQuery is a shotcut to get the sort from the request URL parameters
|
// GetQuery is a shortcut to get the sort from the request URL parameters
|
||||||
func GetQuery(r *http.Request, d string) string {
|
func GetQuery(r *http.Request, d string) string {
|
||||||
return GetParam(r, "query", d)
|
return GetParam(r, "query", d)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,16 +2,20 @@ package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"net/http"
|
"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.
|
// Respond converts a Go value to JSON and sends it to the client.
|
||||||
// Adapted from https://github.com/ardanlabs/service/tree/master/foundation/web
|
// Adapted from https://github.com/ardanlabs/service/tree/master/foundation/web
|
||||||
func Respond(w http.ResponseWriter, statusCode int, data interface{}) {
|
func Respond(w http.ResponseWriter, statusCode int, data interface{}) error {
|
||||||
if statusCode == http.StatusNoContent {
|
if statusCode == http.StatusNoContent {
|
||||||
w.WriteHeader(statusCode)
|
w.WriteHeader(statusCode)
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert the response value to JSON.
|
// Convert the response value to JSON.
|
||||||
|
@ -28,31 +32,8 @@ func Respond(w http.ResponseWriter, statusCode int, data interface{}) {
|
||||||
|
|
||||||
// Send the result back to the client.
|
// Send the result back to the client.
|
||||||
if _, err := w.Write(jsonData); err != nil {
|
if _, err := w.Write(jsonData); err != nil {
|
||||||
panic(err)
|
return err
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// ResponseError is a helper function that sends a JSON response of an error message
|
return nil
|
||||||
func RespondError(w http.ResponseWriter, statusCode int, err error) {
|
|
||||||
eb := ErrorBuilder{}
|
|
||||||
eb.AddError(err)
|
|
||||||
eb.Respond(w, statusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RespondServerError is a wrapper around RespondError that sends a 500 internal server error. Useful for
|
|
||||||
// Sending generic errors when everything went wrong.
|
|
||||||
func RespondServerError(w http.ResponseWriter) {
|
|
||||||
RespondError(w, http.StatusInternalServerError, errors.New("internal server error"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// RespondNotFound is a helper utility for responding with a generic
|
|
||||||
// "unauthorized" error.
|
|
||||||
func RespondUnauthorized(w http.ResponseWriter) {
|
|
||||||
RespondError(w, http.StatusUnauthorized, errors.New("unauthorized"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// RespondForbidden is a helper utility for responding with a generic
|
|
||||||
// "forbidden" error.
|
|
||||||
func RespondForbidden(w http.ResponseWriter) {
|
|
||||||
RespondError(w, http.StatusForbidden, errors.New("forbidden"))
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,76 +0,0 @@
|
||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ValidationError struct {
|
|
||||||
Field string `json:"field"`
|
|
||||||
Reason string `json:"reason"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ValidationErrors []ValidationError
|
|
||||||
|
|
||||||
func (ve *ValidationErrors) HasErrors() bool {
|
|
||||||
if (ve == nil) || (len(*ve) == 0) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, err := range *ve {
|
|
||||||
if err.Field != "" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ve ValidationErrors) Append(field, reasons string) ValidationErrors {
|
|
||||||
return append(ve, ValidationError{
|
|
||||||
Field: field,
|
|
||||||
Reason: reasons,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ErrorBuilder is a helper type to build a response that contains an array of errors.
|
|
||||||
// Typical use cases are for returning an array of validation errors back to the user.
|
|
||||||
//
|
|
||||||
// Example:
|
|
||||||
//
|
|
||||||
// {
|
|
||||||
// "errors": [
|
|
||||||
// "invalid id",
|
|
||||||
// "invalid name",
|
|
||||||
// "invalid description"
|
|
||||||
// ],
|
|
||||||
// "message": "Unprocessable Entity",
|
|
||||||
// "status": 422
|
|
||||||
// }
|
|
||||||
type ErrorBuilder struct {
|
|
||||||
errs []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// HasErrors returns true if the ErrorBuilder has any errors.
|
|
||||||
func (eb *ErrorBuilder) HasErrors() bool {
|
|
||||||
if (eb.errs == nil) || (len(eb.errs) == 0) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddError adds an error to the ErrorBuilder if an error is not nil. If the
|
|
||||||
// Error is nil, then nothing is added.
|
|
||||||
func (eb *ErrorBuilder) AddError(err error) {
|
|
||||||
if err != nil {
|
|
||||||
if eb.errs == nil {
|
|
||||||
eb.errs = make([]string, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
eb.errs = append(eb.errs, err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Respond sends a JSON response with the ErrorBuilder's errors. If there are no errors, then
|
|
||||||
// the errors field will be an empty array.
|
|
||||||
func (eb *ErrorBuilder) Respond(w http.ResponseWriter, statusCode int) {
|
|
||||||
Respond(w, statusCode, Wrap(nil).AddError(http.StatusText(statusCode), eb.errs))
|
|
||||||
}
|
|
|
@ -1,107 +0,0 @@
|
||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/hay-kot/homebox/backend/pkgs/faker"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Test_ErrorBuilder_HasErrors_NilList(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
var ebNilList = ErrorBuilder{}
|
|
||||||
assert.False(t, ebNilList.HasErrors(), "ErrorBuilder.HasErrors() should return false when list is nil")
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_ErrorBuilder_HasErrors_EmptyList(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
var ebEmptyList = ErrorBuilder{
|
|
||||||
errs: []string{},
|
|
||||||
}
|
|
||||||
assert.False(t, ebEmptyList.HasErrors(), "ErrorBuilder.HasErrors() should return false when list is empty")
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_ErrorBuilder_HasErrors_WithError(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
var ebList = ErrorBuilder{}
|
|
||||||
ebList.AddError(errors.New("test error"))
|
|
||||||
|
|
||||||
assert.True(t, ebList.HasErrors(), "ErrorBuilder.HasErrors() should return true when list is not empty")
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_ErrorBuilder_AddError(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
randomError := make([]error, 10)
|
|
||||||
|
|
||||||
f := faker.NewFaker()
|
|
||||||
|
|
||||||
errorStrings := make([]string, 10)
|
|
||||||
|
|
||||||
for i := 0; i < 10; i++ {
|
|
||||||
err := errors.New(f.Str(10))
|
|
||||||
randomError[i] = err
|
|
||||||
errorStrings[i] = err.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check Results
|
|
||||||
var ebList = ErrorBuilder{}
|
|
||||||
|
|
||||||
for _, err := range randomError {
|
|
||||||
ebList.AddError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, errorStrings, ebList.errs, "ErrorBuilder.AddError() should add an error to the list")
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_ErrorBuilder_Respond(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
f := faker.NewFaker()
|
|
||||||
|
|
||||||
randomError := make([]error, 5)
|
|
||||||
|
|
||||||
for i := 0; i < 5; i++ {
|
|
||||||
err := errors.New(f.Str(5))
|
|
||||||
randomError[i] = err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check Results
|
|
||||||
var ebList = ErrorBuilder{}
|
|
||||||
|
|
||||||
for _, err := range randomError {
|
|
||||||
ebList.AddError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fakeWriter := httptest.NewRecorder()
|
|
||||||
|
|
||||||
ebList.Respond(fakeWriter, 422)
|
|
||||||
|
|
||||||
assert.Equal(t, 422, fakeWriter.Code, "ErrorBuilder.Respond() should return a status code of 422")
|
|
||||||
|
|
||||||
// Check errors payload is correct
|
|
||||||
|
|
||||||
errorsStruct := struct {
|
|
||||||
Errors []string `json:"details"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
Error bool `json:"error"`
|
|
||||||
}{
|
|
||||||
Errors: ebList.errs,
|
|
||||||
Message: http.StatusText(http.StatusUnprocessableEntity),
|
|
||||||
Error: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
asJson, _ := json.Marshal(errorsStruct)
|
|
||||||
assert.JSONEq(t, string(asJson), fakeWriter.Body.String(), "ErrorBuilder.Respond() should return a JSON response with the errors")
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,7 +1,6 @@
|
||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -17,7 +16,8 @@ func Test_Respond_NoContent(t *testing.T) {
|
||||||
Name: "dummy",
|
Name: "dummy",
|
||||||
}
|
}
|
||||||
|
|
||||||
Respond(recorder, http.StatusNoContent, dummystruct)
|
err := Respond(recorder, http.StatusNoContent, dummystruct)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, http.StatusNoContent, recorder.Code)
|
assert.Equal(t, http.StatusNoContent, recorder.Code)
|
||||||
assert.Empty(t, recorder.Body.String())
|
assert.Empty(t, recorder.Body.String())
|
||||||
|
@ -31,48 +31,11 @@ func Test_Respond_JSON(t *testing.T) {
|
||||||
Name: "dummy",
|
Name: "dummy",
|
||||||
}
|
}
|
||||||
|
|
||||||
Respond(recorder, http.StatusCreated, dummystruct)
|
err := Respond(recorder, http.StatusCreated, dummystruct)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, http.StatusCreated, recorder.Code)
|
assert.Equal(t, http.StatusCreated, recorder.Code)
|
||||||
assert.JSONEq(t, recorder.Body.String(), `{"name":"dummy"}`)
|
assert.JSONEq(t, recorder.Body.String(), `{"name":"dummy"}`)
|
||||||
assert.Equal(t, "application/json", recorder.Header().Get("Content-Type"))
|
assert.Equal(t, "application/json", recorder.Header().Get("Content-Type"))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_RespondError(t *testing.T) {
|
|
||||||
recorder := httptest.NewRecorder()
|
|
||||||
var customError = errors.New("custom error")
|
|
||||||
|
|
||||||
RespondError(recorder, http.StatusBadRequest, customError)
|
|
||||||
|
|
||||||
assert.Equal(t, http.StatusBadRequest, recorder.Code)
|
|
||||||
assert.JSONEq(t, recorder.Body.String(), `{"details":["custom error"], "message":"Bad Request", "error":true}`)
|
|
||||||
|
|
||||||
}
|
|
||||||
func Test_RespondInternalServerError(t *testing.T) {
|
|
||||||
recorder := httptest.NewRecorder()
|
|
||||||
|
|
||||||
RespondServerError(recorder)
|
|
||||||
|
|
||||||
assert.Equal(t, http.StatusInternalServerError, recorder.Code)
|
|
||||||
assert.JSONEq(t, recorder.Body.String(), `{"details":["internal server error"], "message":"Internal Server Error", "error":true}`)
|
|
||||||
|
|
||||||
}
|
|
||||||
func Test_RespondUnauthorized(t *testing.T) {
|
|
||||||
recorder := httptest.NewRecorder()
|
|
||||||
|
|
||||||
RespondUnauthorized(recorder)
|
|
||||||
|
|
||||||
assert.Equal(t, http.StatusUnauthorized, recorder.Code)
|
|
||||||
assert.JSONEq(t, recorder.Body.String(), `{"details":["unauthorized"], "message":"Unauthorized", "error":true}`)
|
|
||||||
|
|
||||||
}
|
|
||||||
func Test_RespondForbidden(t *testing.T) {
|
|
||||||
recorder := httptest.NewRecorder()
|
|
||||||
|
|
||||||
RespondForbidden(recorder)
|
|
||||||
|
|
||||||
assert.Equal(t, http.StatusForbidden, recorder.Code)
|
|
||||||
assert.JSONEq(t, recorder.Body.String(), `{"details":["forbidden"], "message":"Forbidden", "error":true}`)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
|
@ -17,15 +17,3 @@ func Wrap(data interface{}) Result {
|
||||||
Item: data,
|
Item: data,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r Result) AddMessage(message string) Result {
|
|
||||||
r.Message = message
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r Result) AddError(err string, details interface{}) Result {
|
|
||||||
r.Message = err
|
|
||||||
r.Details = details
|
|
||||||
r.Error = true
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
|
@ -10,6 +10,8 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -23,6 +25,10 @@ type Server struct {
|
||||||
Worker Worker
|
Worker Worker
|
||||||
|
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
|
mux *chi.Mux
|
||||||
|
|
||||||
|
// mw is the global middleware chain for the server.
|
||||||
|
mw []Middleware
|
||||||
|
|
||||||
started bool
|
started bool
|
||||||
activeServer *http.Server
|
activeServer *http.Server
|
||||||
|
@ -36,6 +42,7 @@ func NewServer(opts ...Option) *Server {
|
||||||
s := &Server{
|
s := &Server{
|
||||||
Host: "localhost",
|
Host: "localhost",
|
||||||
Port: "8080",
|
Port: "8080",
|
||||||
|
mux: chi.NewRouter(),
|
||||||
Worker: NewSimpleWorker(),
|
Worker: NewSimpleWorker(),
|
||||||
idleTimeout: 30 * time.Second,
|
idleTimeout: 30 * time.Second,
|
||||||
readTimeout: 10 * time.Second,
|
readTimeout: 10 * time.Second,
|
||||||
|
@ -75,14 +82,14 @@ func (s *Server) Shutdown(sig string) error {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) Start(router http.Handler) error {
|
func (s *Server) Start() error {
|
||||||
if s.started {
|
if s.started {
|
||||||
return ErrServerAlreadyStarted
|
return ErrServerAlreadyStarted
|
||||||
}
|
}
|
||||||
|
|
||||||
s.activeServer = &http.Server{
|
s.activeServer = &http.Server{
|
||||||
Addr: s.Host + ":" + s.Port,
|
Addr: s.Host + ":" + s.Port,
|
||||||
Handler: router,
|
Handler: s.mux,
|
||||||
IdleTimeout: s.idleTimeout,
|
IdleTimeout: s.idleTimeout,
|
||||||
ReadTimeout: s.readTimeout,
|
ReadTimeout: s.readTimeout,
|
||||||
WriteTimeout: s.writeTimeout,
|
WriteTimeout: s.writeTimeout,
|
||||||
|
|
|
@ -4,6 +4,13 @@ import "time"
|
||||||
|
|
||||||
type Option = func(s *Server) error
|
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 {
|
func WithWorker(w Worker) Option {
|
||||||
return func(s *Server) error {
|
return func(s *Server) error {
|
||||||
s.Worker = w
|
s.Worker = w
|
||||||
|
|
|
@ -12,8 +12,11 @@ import (
|
||||||
func testServer(t *testing.T, r http.Handler) *Server {
|
func testServer(t *testing.T, r http.Handler) *Server {
|
||||||
svr := NewServer(WithHost("127.0.0.1"), WithPort("19245"))
|
svr := NewServer(WithHost("127.0.0.1"), WithPort("19245"))
|
||||||
|
|
||||||
|
if r != nil {
|
||||||
|
svr.mux.Mount("/", r)
|
||||||
|
}
|
||||||
go func() {
|
go func() {
|
||||||
err := svr.Start(r)
|
err := svr.Start()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
@ -42,7 +45,7 @@ func Test_ServerShutdown_Error(t *testing.T) {
|
||||||
func Test_ServerStarts_Error(t *testing.T) {
|
func Test_ServerStarts_Error(t *testing.T) {
|
||||||
svr := testServer(t, nil)
|
svr := testServer(t, nil)
|
||||||
|
|
||||||
err := svr.Start(nil)
|
err := svr.Start()
|
||||||
assert.ErrorIs(t, err, ErrServerAlreadyStarted)
|
assert.ErrorIs(t, err, ErrServerAlreadyStarted)
|
||||||
|
|
||||||
err = svr.Shutdown("test")
|
err = svr.Shutdown("test")
|
||||||
|
|
|
@ -247,6 +247,11 @@ export interface UserUpdate {
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ServerErrorResponse {
|
||||||
|
error: string;
|
||||||
|
fields: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ServerResult {
|
export interface ServerResult {
|
||||||
details: any;
|
details: any;
|
||||||
error: boolean;
|
error: boolean;
|
||||||
|
@ -258,11 +263,6 @@ export interface ServerResults {
|
||||||
items: any;
|
items: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ServerValidationError {
|
|
||||||
field: string;
|
|
||||||
reason: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UserRegistration {
|
export interface UserRegistration {
|
||||||
email: string;
|
email: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
const { data: status } = useAsyncData(async () => {
|
const { data: status } = useAsyncData(async () => {
|
||||||
const { data } = await api.status();
|
const { data } = await api.status();
|
||||||
|
|
||||||
if (data) {
|
if (data.demo) {
|
||||||
username.value = "demo@example.com";
|
username.value = "demo@example.com";
|
||||||
password.value = "demo";
|
password.value = "demo";
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue