forked from mirrors/homebox
feat: expanded search for items (#46)
* expanded search for items * range domain from email to example * implement pagination for items
This commit is contained in:
parent
1b20a69c5e
commit
30014a77ca
31 changed files with 751 additions and 346 deletions
|
@ -21,7 +21,7 @@ func (a *app) SetupDemo() {
|
|||
|
||||
var (
|
||||
registration = services.UserRegistration{
|
||||
Email: "demo@email.com",
|
||||
Email: "demo@example.com",
|
||||
Name: "Demo",
|
||||
Password: "demo",
|
||||
}
|
||||
|
|
|
@ -70,26 +70,51 @@ const docTemplate = `{
|
|||
"Items"
|
||||
],
|
||||
"summary": "Get All Items",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "search string",
|
||||
"name": "q",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "page number",
|
||||
"name": "page",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "items per page",
|
||||
"name": "pageSize",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"collectionFormat": "multi",
|
||||
"description": "label Ids",
|
||||
"name": "labels",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"collectionFormat": "multi",
|
||||
"description": "location Ids",
|
||||
"name": "locations",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/server.Results"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/repo.ItemSummary"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
"$ref": "#/definitions/repo.PaginationResult-repo_ItemSummary"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -153,7 +178,7 @@ const docTemplate = `{
|
|||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
"description": "No Content"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -254,7 +279,7 @@ const docTemplate = `{
|
|||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
"description": "No Content"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -354,7 +379,7 @@ const docTemplate = `{
|
|||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": ""
|
||||
"description": "OK"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -470,7 +495,7 @@ const docTemplate = `{
|
|||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
"description": "No Content"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -634,7 +659,7 @@ const docTemplate = `{
|
|||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
"description": "No Content"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -798,7 +823,7 @@ const docTemplate = `{
|
|||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
"description": "No Content"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -846,7 +871,7 @@ const docTemplate = `{
|
|||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
"description": "No Content"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -903,7 +928,7 @@ const docTemplate = `{
|
|||
"summary": "User Logout",
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
"description": "No Content"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -922,7 +947,7 @@ const docTemplate = `{
|
|||
"summary": "User Token Refresh",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": ""
|
||||
"description": "OK"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -949,7 +974,7 @@ const docTemplate = `{
|
|||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
"description": "No Content"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1049,7 +1074,7 @@ const docTemplate = `{
|
|||
"summary": "Deletes the user account",
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
"description": "No Content"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1070,7 +1095,7 @@ const docTemplate = `{
|
|||
"summary": "Update the current user's password // TODO:",
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
"description": "No Content"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1488,6 +1513,26 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"repo.PaginationResult-repo_ItemSummary": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/repo.ItemSummary"
|
||||
}
|
||||
},
|
||||
"page": {
|
||||
"type": "integer"
|
||||
},
|
||||
"pageSize": {
|
||||
"type": "integer"
|
||||
},
|
||||
"total": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repo.UserOut": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -1541,9 +1586,7 @@ const docTemplate = `{
|
|||
"server.Results": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"items": {
|
||||
"type": "any"
|
||||
}
|
||||
"items": {}
|
||||
}
|
||||
},
|
||||
"server.ValidationError": {
|
||||
|
|
|
@ -62,26 +62,51 @@
|
|||
"Items"
|
||||
],
|
||||
"summary": "Get All Items",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "search string",
|
||||
"name": "q",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "page number",
|
||||
"name": "page",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "items per page",
|
||||
"name": "pageSize",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"collectionFormat": "multi",
|
||||
"description": "label Ids",
|
||||
"name": "labels",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"collectionFormat": "multi",
|
||||
"description": "location Ids",
|
||||
"name": "locations",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/server.Results"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/repo.ItemSummary"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
"$ref": "#/definitions/repo.PaginationResult-repo_ItemSummary"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -145,7 +170,7 @@
|
|||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
"description": "No Content"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -246,7 +271,7 @@
|
|||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
"description": "No Content"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -346,7 +371,7 @@
|
|||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": ""
|
||||
"description": "OK"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -462,7 +487,7 @@
|
|||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
"description": "No Content"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -626,7 +651,7 @@
|
|||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
"description": "No Content"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -790,7 +815,7 @@
|
|||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
"description": "No Content"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -838,7 +863,7 @@
|
|||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
"description": "No Content"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -895,7 +920,7 @@
|
|||
"summary": "User Logout",
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
"description": "No Content"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -914,7 +939,7 @@
|
|||
"summary": "User Token Refresh",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": ""
|
||||
"description": "OK"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -941,7 +966,7 @@
|
|||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
"description": "No Content"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1041,7 +1066,7 @@
|
|||
"summary": "Deletes the user account",
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
"description": "No Content"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1062,7 +1087,7 @@
|
|||
"summary": "Update the current user's password // TODO:",
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
"description": "No Content"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1480,6 +1505,26 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"repo.PaginationResult-repo_ItemSummary": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/repo.ItemSummary"
|
||||
}
|
||||
},
|
||||
"page": {
|
||||
"type": "integer"
|
||||
},
|
||||
"pageSize": {
|
||||
"type": "integer"
|
||||
},
|
||||
"total": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repo.UserOut": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -1533,9 +1578,7 @@
|
|||
"server.Results": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"items": {
|
||||
"type": "any"
|
||||
}
|
||||
"items": {}
|
||||
}
|
||||
},
|
||||
"server.ValidationError": {
|
||||
|
|
|
@ -275,6 +275,19 @@ definitions:
|
|||
updatedAt:
|
||||
type: string
|
||||
type: object
|
||||
repo.PaginationResult-repo_ItemSummary:
|
||||
properties:
|
||||
items:
|
||||
items:
|
||||
$ref: '#/definitions/repo.ItemSummary'
|
||||
type: array
|
||||
page:
|
||||
type: integer
|
||||
pageSize:
|
||||
type: integer
|
||||
total:
|
||||
type: integer
|
||||
type: object
|
||||
repo.UserOut:
|
||||
properties:
|
||||
email:
|
||||
|
@ -310,8 +323,7 @@ definitions:
|
|||
type: object
|
||||
server.Results:
|
||||
properties:
|
||||
items:
|
||||
type: any
|
||||
items: {}
|
||||
type: object
|
||||
server.ValidationError:
|
||||
properties:
|
||||
|
@ -426,20 +438,40 @@ paths:
|
|||
- User
|
||||
/v1/items:
|
||||
get:
|
||||
parameters:
|
||||
- description: search string
|
||||
in: query
|
||||
name: q
|
||||
type: string
|
||||
- description: page number
|
||||
in: query
|
||||
name: page
|
||||
type: integer
|
||||
- description: items per page
|
||||
in: query
|
||||
name: pageSize
|
||||
type: integer
|
||||
- collectionFormat: multi
|
||||
description: label Ids
|
||||
in: query
|
||||
items:
|
||||
type: string
|
||||
name: labels
|
||||
type: array
|
||||
- collectionFormat: multi
|
||||
description: location Ids
|
||||
in: query
|
||||
items:
|
||||
type: string
|
||||
name: locations
|
||||
type: array
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/server.Results'
|
||||
- properties:
|
||||
items:
|
||||
items:
|
||||
$ref: '#/definitions/repo.ItemSummary'
|
||||
type: array
|
||||
type: object
|
||||
$ref: '#/definitions/repo.PaginationResult-repo_ItemSummary'
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: Get All Items
|
||||
|
@ -477,7 +509,7 @@ paths:
|
|||
- application/json
|
||||
responses:
|
||||
"204":
|
||||
description: ""
|
||||
description: No Content
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: deletes a item
|
||||
|
@ -583,7 +615,7 @@ paths:
|
|||
type: string
|
||||
responses:
|
||||
"204":
|
||||
description: ""
|
||||
description: No Content
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: retrieves an attachment for an item
|
||||
|
@ -658,7 +690,7 @@ paths:
|
|||
- application/octet-stream
|
||||
responses:
|
||||
"200":
|
||||
description: ""
|
||||
description: OK
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: retrieves an attachment for an item
|
||||
|
@ -676,7 +708,7 @@ paths:
|
|||
- application/json
|
||||
responses:
|
||||
"204":
|
||||
description: ""
|
||||
description: No Content
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: imports items into the database
|
||||
|
@ -735,7 +767,7 @@ paths:
|
|||
- application/json
|
||||
responses:
|
||||
"204":
|
||||
description: ""
|
||||
description: No Content
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: deletes a label
|
||||
|
@ -832,7 +864,7 @@ paths:
|
|||
- application/json
|
||||
responses:
|
||||
"204":
|
||||
description: ""
|
||||
description: No Content
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: deletes a location
|
||||
|
@ -899,7 +931,7 @@ paths:
|
|||
$ref: '#/definitions/v1.ChangePassword'
|
||||
responses:
|
||||
"204":
|
||||
description: ""
|
||||
description: No Content
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: Updates the users password
|
||||
|
@ -935,7 +967,7 @@ paths:
|
|||
post:
|
||||
responses:
|
||||
"204":
|
||||
description: ""
|
||||
description: No Content
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: User Logout
|
||||
|
@ -948,7 +980,7 @@ paths:
|
|||
This does not validate that the user still exists within the database.
|
||||
responses:
|
||||
"200":
|
||||
description: ""
|
||||
description: OK
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: User Token Refresh
|
||||
|
@ -967,7 +999,7 @@ paths:
|
|||
- application/json
|
||||
responses:
|
||||
"204":
|
||||
description: ""
|
||||
description: No Content
|
||||
summary: Get the current user
|
||||
tags:
|
||||
- User
|
||||
|
@ -977,7 +1009,7 @@ paths:
|
|||
- application/json
|
||||
responses:
|
||||
"204":
|
||||
description: ""
|
||||
description: No Content
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: Deletes the user account
|
||||
|
@ -1032,7 +1064,7 @@ paths:
|
|||
- application/json
|
||||
responses:
|
||||
"204":
|
||||
description: ""
|
||||
description: No Content
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: 'Update the current user''s password // TODO:'
|
||||
|
|
|
@ -25,16 +25,16 @@ var (
|
|||
BuildTime = "now"
|
||||
)
|
||||
|
||||
// @title Go API Templates
|
||||
// @version 1.0
|
||||
// @description This is a simple Rest API Server Template that implements some basic User and Authentication patterns to help you get started and bootstrap your next project!.
|
||||
// @contact.name Don't
|
||||
// @license.name MIT
|
||||
// @BasePath /api
|
||||
// @securityDefinitions.apikey Bearer
|
||||
// @in header
|
||||
// @name Authorization
|
||||
// @description "Type 'Bearer TOKEN' to correctly set the API Key"
|
||||
// @title Go API Templates
|
||||
// @version 1.0
|
||||
// @description This is a simple Rest API Server Template that implements some basic User and Authentication patterns to help you get started and bootstrap your next project!.
|
||||
// @contact.name Don't
|
||||
// @license.name MIT
|
||||
// @BasePath /api
|
||||
// @securityDefinitions.apikey Bearer
|
||||
// @in header
|
||||
// @name Authorization
|
||||
// @description "Type 'Bearer TOKEN' to correctly set the API Key"
|
||||
func main() {
|
||||
cfg, err := config.New()
|
||||
if err != nil {
|
||||
|
|
|
@ -66,11 +66,11 @@ func NewControllerV1(svc *services.AllServices, options ...func(*V1Controller))
|
|||
type ReadyFunc func() bool
|
||||
|
||||
// HandleBase godoc
|
||||
// @Summary Retrieves the basic information about the API
|
||||
// @Tags Base
|
||||
// @Produce json
|
||||
// @Success 200 {object} ApiSummary
|
||||
// @Router /v1/status [GET]
|
||||
// @Summary Retrieves the basic information about the API
|
||||
// @Tags Base
|
||||
// @Produce json
|
||||
// @Success 200 {object} ApiSummary
|
||||
// @Router /v1/status [GET]
|
||||
func (ctrl *V1Controller) HandleBase(ready ReadyFunc, build Build) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
server.Respond(w, http.StatusOK, ApiSummary{
|
||||
|
|
|
@ -23,15 +23,15 @@ type (
|
|||
)
|
||||
|
||||
// HandleAuthLogin godoc
|
||||
// @Summary User Login
|
||||
// @Tags Authentication
|
||||
// @Accept x-www-form-urlencoded
|
||||
// @Accept application/json
|
||||
// @Param username formData string false "string" example(admin@admin.com)
|
||||
// @Param password formData string false "string" example(admin)
|
||||
// @Produce json
|
||||
// @Success 200 {object} TokenResponse
|
||||
// @Router /v1/users/login [POST]
|
||||
// @Summary User Login
|
||||
// @Tags Authentication
|
||||
// @Accept x-www-form-urlencoded
|
||||
// @Accept application/json
|
||||
// @Param username formData string false "string" example(admin@admin.com)
|
||||
// @Param password formData string false "string" example(admin)
|
||||
// @Produce json
|
||||
// @Success 200 {object} TokenResponse
|
||||
// @Router /v1/users/login [POST]
|
||||
func (ctrl *V1Controller) HandleAuthLogin() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
loginForm := &LoginForm{}
|
||||
|
@ -80,11 +80,11 @@ func (ctrl *V1Controller) HandleAuthLogin() http.HandlerFunc {
|
|||
}
|
||||
|
||||
// HandleAuthLogout godoc
|
||||
// @Summary User Logout
|
||||
// @Tags Authentication
|
||||
// @Success 204
|
||||
// @Router /v1/users/logout [POST]
|
||||
// @Security Bearer
|
||||
// @Summary User Logout
|
||||
// @Tags Authentication
|
||||
// @Success 204
|
||||
// @Router /v1/users/logout [POST]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleAuthLogout() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
token := services.UseTokenCtx(r.Context())
|
||||
|
@ -106,13 +106,13 @@ func (ctrl *V1Controller) HandleAuthLogout() http.HandlerFunc {
|
|||
}
|
||||
|
||||
// HandleAuthLogout godoc
|
||||
// @Summary User Token Refresh
|
||||
// @Description handleAuthRefresh returns a handler that will issue a new token from an existing token.
|
||||
// @Description This does not validate that the user still exists within the database.
|
||||
// @Tags Authentication
|
||||
// @Success 200
|
||||
// @Router /v1/users/refresh [GET]
|
||||
// @Security Bearer
|
||||
// @Summary User Token Refresh
|
||||
// @Description handleAuthRefresh returns a handler that will issue a new token from an existing token.
|
||||
// @Description This does not validate that the user still exists within the database.
|
||||
// @Tags Authentication
|
||||
// @Success 200
|
||||
// @Router /v1/users/refresh [GET]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleAuthRefresh() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
requestToken := services.UseTokenCtx(r.Context())
|
||||
|
|
|
@ -23,13 +23,13 @@ type (
|
|||
)
|
||||
|
||||
// HandleUserSelf godoc
|
||||
// @Summary Get the current user
|
||||
// @Tags User
|
||||
// @Produce json
|
||||
// @Param payload body GroupInvitationCreate true "User Data"
|
||||
// @Success 200 {object} GroupInvitation
|
||||
// @Router /v1/groups/invitations [Post]
|
||||
// @Security Bearer
|
||||
// @Summary Get the current user
|
||||
// @Tags User
|
||||
// @Produce json
|
||||
// @Param payload body GroupInvitationCreate true "User Data"
|
||||
// @Success 200 {object} GroupInvitation
|
||||
// @Router /v1/groups/invitations [Post]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleGroupInvitationsCreate() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
data := GroupInvitationCreate{}
|
||||
|
|
|
@ -3,41 +3,84 @@ package v1
|
|||
import (
|
||||
"encoding/csv"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/homebox/backend/internal/repo"
|
||||
"github.com/hay-kot/homebox/backend/internal/services"
|
||||
"github.com/hay-kot/homebox/backend/pkgs/server"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func uuidList(params url.Values, key string) []uuid.UUID {
|
||||
var ids []uuid.UUID
|
||||
for _, id := range params[key] {
|
||||
uid, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
ids = append(ids, uid)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func intOrNegativeOne(s string) int {
|
||||
i, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
func extractQuery(r *http.Request) repo.ItemQuery {
|
||||
params := r.URL.Query()
|
||||
|
||||
page := intOrNegativeOne(params.Get("page"))
|
||||
perPage := intOrNegativeOne(params.Get("perPage"))
|
||||
|
||||
return repo.ItemQuery{
|
||||
Page: page,
|
||||
PageSize: perPage,
|
||||
Search: params.Get("q"),
|
||||
LocationIDs: uuidList(params, "locations"),
|
||||
LabelIDs: uuidList(params, "labels"),
|
||||
}
|
||||
}
|
||||
|
||||
// HandleItemsGetAll godoc
|
||||
// @Summary Get All Items
|
||||
// @Tags Items
|
||||
// @Produce json
|
||||
// @Success 200 {object} server.Results{items=[]repo.ItemSummary}
|
||||
// @Router /v1/items [GET]
|
||||
// @Security Bearer
|
||||
// @Summary Get All Items
|
||||
// @Tags Items
|
||||
// @Produce json
|
||||
// @Param q query string false "search string"
|
||||
// @Param page query int false "page number"
|
||||
// @Param pageSize query int false "items per page"
|
||||
// @Param labels query []string false "label Ids" collectionFormat(multi)
|
||||
// @Param locations query []string false "location Ids" collectionFormat(multi)
|
||||
// @Success 200 {object} repo.PaginationResult[repo.ItemSummary]{}
|
||||
// @Router /v1/items [GET]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleItemsGetAll() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
user := services.UseUserCtx(r.Context())
|
||||
items, err := ctrl.svc.Items.GetAll(r.Context(), user.GroupID)
|
||||
ctx := services.NewContext(r.Context())
|
||||
items, err := ctrl.svc.Items.Query(ctx, extractQuery(r))
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to get items")
|
||||
server.RespondServerError(w)
|
||||
return
|
||||
}
|
||||
server.Respond(w, http.StatusOK, server.Results{Items: items})
|
||||
server.Respond(w, http.StatusOK, items)
|
||||
}
|
||||
}
|
||||
|
||||
// HandleItemsCreate godoc
|
||||
// @Summary Create a new item
|
||||
// @Tags Items
|
||||
// @Produce json
|
||||
// @Param payload body repo.ItemCreate true "Item Data"
|
||||
// @Success 200 {object} repo.ItemSummary
|
||||
// @Router /v1/items [POST]
|
||||
// @Security Bearer
|
||||
// @Summary Create a new item
|
||||
// @Tags Items
|
||||
// @Produce json
|
||||
// @Param payload body repo.ItemCreate true "Item Data"
|
||||
// @Success 200 {object} repo.ItemSummary
|
||||
// @Router /v1/items [POST]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleItemsCreate() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
createData := repo.ItemCreate{}
|
||||
|
@ -60,13 +103,13 @@ func (ctrl *V1Controller) HandleItemsCreate() http.HandlerFunc {
|
|||
}
|
||||
|
||||
// HandleItemDelete godocs
|
||||
// @Summary deletes a item
|
||||
// @Tags Items
|
||||
// @Produce json
|
||||
// @Param id path string true "Item ID"
|
||||
// @Success 204
|
||||
// @Router /v1/items/{id} [DELETE]
|
||||
// @Security Bearer
|
||||
// @Summary deletes a item
|
||||
// @Tags Items
|
||||
// @Produce json
|
||||
// @Param id path string true "Item ID"
|
||||
// @Success 204
|
||||
// @Router /v1/items/{id} [DELETE]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleItemDelete() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
uid, user, err := ctrl.partialParseIdAndUser(w, r)
|
||||
|
@ -85,13 +128,13 @@ func (ctrl *V1Controller) HandleItemDelete() http.HandlerFunc {
|
|||
}
|
||||
|
||||
// HandleItemGet godocs
|
||||
// @Summary Gets a item and fields
|
||||
// @Tags Items
|
||||
// @Produce json
|
||||
// @Param id path string true "Item ID"
|
||||
// @Success 200 {object} repo.ItemOut
|
||||
// @Router /v1/items/{id} [GET]
|
||||
// @Security Bearer
|
||||
// @Summary Gets a item and fields
|
||||
// @Tags Items
|
||||
// @Produce json
|
||||
// @Param id path string true "Item ID"
|
||||
// @Success 200 {object} repo.ItemOut
|
||||
// @Router /v1/items/{id} [GET]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleItemGet() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
uid, user, err := ctrl.partialParseIdAndUser(w, r)
|
||||
|
@ -110,14 +153,14 @@ func (ctrl *V1Controller) HandleItemGet() http.HandlerFunc {
|
|||
}
|
||||
|
||||
// HandleItemUpdate godocs
|
||||
// @Summary updates a item
|
||||
// @Tags Items
|
||||
// @Produce json
|
||||
// @Param id path string true "Item ID"
|
||||
// @Param payload body repo.ItemUpdate true "Item Data"
|
||||
// @Success 200 {object} repo.ItemOut
|
||||
// @Router /v1/items/{id} [PUT]
|
||||
// @Security Bearer
|
||||
// @Summary updates a item
|
||||
// @Tags Items
|
||||
// @Produce json
|
||||
// @Param id path string true "Item ID"
|
||||
// @Param payload body repo.ItemUpdate true "Item Data"
|
||||
// @Success 200 {object} repo.ItemOut
|
||||
// @Router /v1/items/{id} [PUT]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleItemUpdate() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
body := repo.ItemUpdate{}
|
||||
|
@ -143,13 +186,13 @@ func (ctrl *V1Controller) HandleItemUpdate() http.HandlerFunc {
|
|||
}
|
||||
|
||||
// HandleItemsImport godocs
|
||||
// @Summary imports items into the database
|
||||
// @Tags Items
|
||||
// @Produce json
|
||||
// @Success 204
|
||||
// @Param csv formData file true "Image to upload"
|
||||
// @Router /v1/items/import [Post]
|
||||
// @Security Bearer
|
||||
// @Summary imports items into the database
|
||||
// @Tags Items
|
||||
// @Produce json
|
||||
// @Success 204
|
||||
// @Param csv formData file true "Image to upload"
|
||||
// @Router /v1/items/import [Post]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleItemsImport() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
|
|
|
@ -21,17 +21,17 @@ type (
|
|||
)
|
||||
|
||||
// HandleItemsImport godocs
|
||||
// @Summary imports items into the database
|
||||
// @Tags Items
|
||||
// @Produce json
|
||||
// @Param id path string true "Item ID"
|
||||
// @Param file formData file true "File attachment"
|
||||
// @Param type formData string true "Type of file"
|
||||
// @Param name formData string true "name of the file including extension"
|
||||
// @Success 200 {object} repo.ItemOut
|
||||
// @Failure 422 {object} []server.ValidationError
|
||||
// @Router /v1/items/{id}/attachments [POST]
|
||||
// @Security Bearer
|
||||
// @Summary imports items into the database
|
||||
// @Tags Items
|
||||
// @Produce json
|
||||
// @Param id path string true "Item ID"
|
||||
// @Param file formData file true "File attachment"
|
||||
// @Param type formData string true "Type of file"
|
||||
// @Param name formData string true "name of the file including extension"
|
||||
// @Success 200 {object} repo.ItemOut
|
||||
// @Failure 422 {object} []server.ValidationError
|
||||
// @Router /v1/items/{id}/attachments [POST]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleItemAttachmentCreate() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
err := r.ParseMultipartForm(ctrl.maxUploadSize << 20)
|
||||
|
@ -98,14 +98,14 @@ func (ctrl *V1Controller) HandleItemAttachmentCreate() http.HandlerFunc {
|
|||
}
|
||||
|
||||
// HandleItemAttachmentGet godocs
|
||||
// @Summary retrieves an attachment for an item
|
||||
// @Tags Items
|
||||
// @Produce application/octet-stream
|
||||
// @Param id path string true "Item ID"
|
||||
// @Param token query string true "Attachment token"
|
||||
// @Success 200
|
||||
// @Router /v1/items/{id}/attachments/download [GET]
|
||||
// @Security Bearer
|
||||
// @Summary retrieves an attachment for an item
|
||||
// @Tags Items
|
||||
// @Produce application/octet-stream
|
||||
// @Param id path string true "Item ID"
|
||||
// @Param token query string true "Attachment token"
|
||||
// @Success 200
|
||||
// @Router /v1/items/{id}/attachments/download [GET]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleItemAttachmentDownload() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
token := server.GetParam(r, "token", "")
|
||||
|
@ -125,39 +125,39 @@ func (ctrl *V1Controller) HandleItemAttachmentDownload() http.HandlerFunc {
|
|||
}
|
||||
|
||||
// HandleItemAttachmentToken godocs
|
||||
// @Summary retrieves an attachment for an item
|
||||
// @Tags Items
|
||||
// @Produce application/octet-stream
|
||||
// @Param id path string true "Item ID"
|
||||
// @Param attachment_id path string true "Attachment ID"
|
||||
// @Success 200 {object} ItemAttachmentToken
|
||||
// @Router /v1/items/{id}/attachments/{attachment_id} [GET]
|
||||
// @Security Bearer
|
||||
// @Summary retrieves an attachment for an item
|
||||
// @Tags Items
|
||||
// @Produce application/octet-stream
|
||||
// @Param id path string true "Item ID"
|
||||
// @Param attachment_id path string true "Attachment ID"
|
||||
// @Success 200 {object} ItemAttachmentToken
|
||||
// @Router /v1/items/{id}/attachments/{attachment_id} [GET]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleItemAttachmentToken() http.HandlerFunc {
|
||||
return ctrl.handleItemAttachmentsHandler
|
||||
}
|
||||
|
||||
// HandleItemAttachmentDelete godocs
|
||||
// @Summary retrieves an attachment for an item
|
||||
// @Tags Items
|
||||
// @Param id path string true "Item ID"
|
||||
// @Param attachment_id path string true "Attachment ID"
|
||||
// @Success 204
|
||||
// @Router /v1/items/{id}/attachments/{attachment_id} [DELETE]
|
||||
// @Security Bearer
|
||||
// @Summary retrieves an attachment for an item
|
||||
// @Tags Items
|
||||
// @Param id path string true "Item ID"
|
||||
// @Param attachment_id path string true "Attachment ID"
|
||||
// @Success 204
|
||||
// @Router /v1/items/{id}/attachments/{attachment_id} [DELETE]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleItemAttachmentDelete() http.HandlerFunc {
|
||||
return ctrl.handleItemAttachmentsHandler
|
||||
}
|
||||
|
||||
// HandleItemAttachmentUpdate godocs
|
||||
// @Summary retrieves an attachment for an item
|
||||
// @Tags Items
|
||||
// @Param id path string true "Item ID"
|
||||
// @Param attachment_id path string true "Attachment ID"
|
||||
// @Param payload body repo.ItemAttachmentUpdate true "Attachment Update"
|
||||
// @Success 200 {object} repo.ItemOut
|
||||
// @Router /v1/items/{id}/attachments/{attachment_id} [PUT]
|
||||
// @Security Bearer
|
||||
// @Summary retrieves an attachment for an item
|
||||
// @Tags Items
|
||||
// @Param id path string true "Item ID"
|
||||
// @Param attachment_id path string true "Attachment ID"
|
||||
// @Param payload body repo.ItemAttachmentUpdate true "Attachment Update"
|
||||
// @Success 200 {object} repo.ItemOut
|
||||
// @Router /v1/items/{id}/attachments/{attachment_id} [PUT]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleItemAttachmentUpdate() http.HandlerFunc {
|
||||
return ctrl.handleItemAttachmentsHandler
|
||||
}
|
||||
|
|
|
@ -11,12 +11,12 @@ import (
|
|||
)
|
||||
|
||||
// HandleLabelsGetAll godoc
|
||||
// @Summary Get All Labels
|
||||
// @Tags Labels
|
||||
// @Produce json
|
||||
// @Success 200 {object} server.Results{items=[]repo.LabelOut}
|
||||
// @Router /v1/labels [GET]
|
||||
// @Security Bearer
|
||||
// @Summary Get All Labels
|
||||
// @Tags Labels
|
||||
// @Produce json
|
||||
// @Success 200 {object} server.Results{items=[]repo.LabelOut}
|
||||
// @Router /v1/labels [GET]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleLabelsGetAll() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
user := services.UseUserCtx(r.Context())
|
||||
|
@ -31,13 +31,13 @@ func (ctrl *V1Controller) HandleLabelsGetAll() http.HandlerFunc {
|
|||
}
|
||||
|
||||
// HandleLabelsCreate godoc
|
||||
// @Summary Create a new label
|
||||
// @Tags Labels
|
||||
// @Produce json
|
||||
// @Param payload body repo.LabelCreate true "Label Data"
|
||||
// @Success 200 {object} repo.LabelSummary
|
||||
// @Router /v1/labels [POST]
|
||||
// @Security Bearer
|
||||
// @Summary Create a new label
|
||||
// @Tags Labels
|
||||
// @Produce json
|
||||
// @Param payload body repo.LabelCreate true "Label Data"
|
||||
// @Success 200 {object} repo.LabelSummary
|
||||
// @Router /v1/labels [POST]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleLabelsCreate() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
createData := repo.LabelCreate{}
|
||||
|
@ -61,13 +61,13 @@ func (ctrl *V1Controller) HandleLabelsCreate() http.HandlerFunc {
|
|||
}
|
||||
|
||||
// HandleLabelDelete godocs
|
||||
// @Summary deletes a label
|
||||
// @Tags Labels
|
||||
// @Produce json
|
||||
// @Param id path string true "Label ID"
|
||||
// @Success 204
|
||||
// @Router /v1/labels/{id} [DELETE]
|
||||
// @Security Bearer
|
||||
// @Summary deletes a label
|
||||
// @Tags Labels
|
||||
// @Produce json
|
||||
// @Param id path string true "Label ID"
|
||||
// @Success 204
|
||||
// @Router /v1/labels/{id} [DELETE]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleLabelDelete() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
uid, user, err := ctrl.partialParseIdAndUser(w, r)
|
||||
|
@ -86,13 +86,13 @@ func (ctrl *V1Controller) HandleLabelDelete() http.HandlerFunc {
|
|||
}
|
||||
|
||||
// HandleLabelGet godocs
|
||||
// @Summary Gets a label and fields
|
||||
// @Tags Labels
|
||||
// @Produce json
|
||||
// @Param id path string true "Label ID"
|
||||
// @Success 200 {object} repo.LabelOut
|
||||
// @Router /v1/labels/{id} [GET]
|
||||
// @Security Bearer
|
||||
// @Summary Gets a label and fields
|
||||
// @Tags Labels
|
||||
// @Produce json
|
||||
// @Param id path string true "Label ID"
|
||||
// @Success 200 {object} repo.LabelOut
|
||||
// @Router /v1/labels/{id} [GET]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleLabelGet() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
uid, user, err := ctrl.partialParseIdAndUser(w, r)
|
||||
|
@ -118,13 +118,13 @@ func (ctrl *V1Controller) HandleLabelGet() http.HandlerFunc {
|
|||
}
|
||||
|
||||
// HandleLabelUpdate godocs
|
||||
// @Summary updates a label
|
||||
// @Tags Labels
|
||||
// @Produce json
|
||||
// @Param id path string true "Label ID"
|
||||
// @Success 200 {object} repo.LabelOut
|
||||
// @Router /v1/labels/{id} [PUT]
|
||||
// @Security Bearer
|
||||
// @Summary updates a label
|
||||
// @Tags Labels
|
||||
// @Produce json
|
||||
// @Param id path string true "Label ID"
|
||||
// @Success 200 {object} repo.LabelOut
|
||||
// @Router /v1/labels/{id} [PUT]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleLabelUpdate() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
body := repo.LabelUpdate{}
|
||||
|
|
|
@ -11,12 +11,12 @@ import (
|
|||
)
|
||||
|
||||
// HandleLocationGetAll godoc
|
||||
// @Summary Get All Locations
|
||||
// @Tags Locations
|
||||
// @Produce json
|
||||
// @Success 200 {object} server.Results{items=[]repo.LocationOutCount}
|
||||
// @Router /v1/locations [GET]
|
||||
// @Security Bearer
|
||||
// @Summary Get All Locations
|
||||
// @Tags Locations
|
||||
// @Produce json
|
||||
// @Success 200 {object} server.Results{items=[]repo.LocationOutCount}
|
||||
// @Router /v1/locations [GET]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleLocationGetAll() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
user := services.UseUserCtx(r.Context())
|
||||
|
@ -32,13 +32,13 @@ func (ctrl *V1Controller) HandleLocationGetAll() http.HandlerFunc {
|
|||
}
|
||||
|
||||
// HandleLocationCreate godoc
|
||||
// @Summary Create a new location
|
||||
// @Tags Locations
|
||||
// @Produce json
|
||||
// @Param payload body repo.LocationCreate true "Location Data"
|
||||
// @Success 200 {object} repo.LocationSummary
|
||||
// @Router /v1/locations [POST]
|
||||
// @Security Bearer
|
||||
// @Summary Create a new location
|
||||
// @Tags Locations
|
||||
// @Produce json
|
||||
// @Param payload body repo.LocationCreate true "Location Data"
|
||||
// @Success 200 {object} repo.LocationSummary
|
||||
// @Router /v1/locations [POST]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleLocationCreate() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
createData := repo.LocationCreate{}
|
||||
|
@ -61,13 +61,13 @@ func (ctrl *V1Controller) HandleLocationCreate() http.HandlerFunc {
|
|||
}
|
||||
|
||||
// HandleLocationDelete godocs
|
||||
// @Summary deletes a location
|
||||
// @Tags Locations
|
||||
// @Produce json
|
||||
// @Param id path string true "Location ID"
|
||||
// @Success 204
|
||||
// @Router /v1/locations/{id} [DELETE]
|
||||
// @Security Bearer
|
||||
// @Summary deletes a location
|
||||
// @Tags Locations
|
||||
// @Produce json
|
||||
// @Param id path string true "Location ID"
|
||||
// @Success 204
|
||||
// @Router /v1/locations/{id} [DELETE]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleLocationDelete() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
uid, user, err := ctrl.partialParseIdAndUser(w, r)
|
||||
|
@ -86,13 +86,13 @@ func (ctrl *V1Controller) HandleLocationDelete() http.HandlerFunc {
|
|||
}
|
||||
|
||||
// HandleLocationGet godocs
|
||||
// @Summary Gets a location and fields
|
||||
// @Tags Locations
|
||||
// @Produce json
|
||||
// @Param id path string true "Location ID"
|
||||
// @Success 200 {object} repo.LocationOut
|
||||
// @Router /v1/locations/{id} [GET]
|
||||
// @Security Bearer
|
||||
// @Summary Gets a location and fields
|
||||
// @Tags Locations
|
||||
// @Produce json
|
||||
// @Param id path string true "Location ID"
|
||||
// @Success 200 {object} repo.LocationOut
|
||||
// @Router /v1/locations/{id} [GET]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleLocationGet() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
uid, user, err := ctrl.partialParseIdAndUser(w, r)
|
||||
|
@ -123,13 +123,13 @@ func (ctrl *V1Controller) HandleLocationGet() http.HandlerFunc {
|
|||
}
|
||||
|
||||
// HandleLocationUpdate godocs
|
||||
// @Summary updates a location
|
||||
// @Tags Locations
|
||||
// @Produce json
|
||||
// @Param id path string true "Location ID"
|
||||
// @Success 200 {object} repo.LocationOut
|
||||
// @Router /v1/locations/{id} [PUT]
|
||||
// @Security Bearer
|
||||
// @Summary updates a location
|
||||
// @Tags Locations
|
||||
// @Produce json
|
||||
// @Param id path string true "Location ID"
|
||||
// @Success 200 {object} repo.LocationOut
|
||||
// @Router /v1/locations/{id} [PUT]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleLocationUpdate() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
body := repo.LocationUpdate{}
|
||||
|
|
|
@ -11,12 +11,12 @@ import (
|
|||
)
|
||||
|
||||
// HandleUserSelf godoc
|
||||
// @Summary Get the current user
|
||||
// @Tags User
|
||||
// @Produce json
|
||||
// @Param payload body services.UserRegistration true "User Data"
|
||||
// @Success 204
|
||||
// @Router /v1/users/register [Post]
|
||||
// @Summary Get the current user
|
||||
// @Tags User
|
||||
// @Produce json
|
||||
// @Param payload body services.UserRegistration true "User Data"
|
||||
// @Success 204
|
||||
// @Router /v1/users/register [Post]
|
||||
func (ctrl *V1Controller) HandleUserRegistration() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
regData := services.UserRegistration{}
|
||||
|
@ -39,12 +39,12 @@ func (ctrl *V1Controller) HandleUserRegistration() http.HandlerFunc {
|
|||
}
|
||||
|
||||
// HandleUserSelf godoc
|
||||
// @Summary Get the current user
|
||||
// @Tags User
|
||||
// @Produce json
|
||||
// @Success 200 {object} server.Result{item=repo.UserOut}
|
||||
// @Router /v1/users/self [GET]
|
||||
// @Security Bearer
|
||||
// @Summary Get the current user
|
||||
// @Tags User
|
||||
// @Produce json
|
||||
// @Success 200 {object} server.Result{item=repo.UserOut}
|
||||
// @Router /v1/users/self [GET]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleUserSelf() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
token := services.UseTokenCtx(r.Context())
|
||||
|
@ -60,13 +60,13 @@ func (ctrl *V1Controller) HandleUserSelf() http.HandlerFunc {
|
|||
}
|
||||
|
||||
// HandleUserSelfUpdate godoc
|
||||
// @Summary Update the current user
|
||||
// @Tags User
|
||||
// @Produce json
|
||||
// @Param payload body repo.UserUpdate true "User Data"
|
||||
// @Success 200 {object} server.Result{item=repo.UserUpdate}
|
||||
// @Router /v1/users/self [PUT]
|
||||
// @Security Bearer
|
||||
// @Summary Update the current user
|
||||
// @Tags User
|
||||
// @Produce json
|
||||
// @Param payload body repo.UserUpdate true "User Data"
|
||||
// @Success 200 {object} server.Result{item=repo.UserUpdate}
|
||||
// @Router /v1/users/self [PUT]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleUserSelfUpdate() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
updateData := repo.UserUpdate{}
|
||||
|
@ -90,24 +90,24 @@ func (ctrl *V1Controller) HandleUserSelfUpdate() http.HandlerFunc {
|
|||
}
|
||||
|
||||
// HandleUserUpdatePassword godoc
|
||||
// @Summary Update the current user's password // TODO:
|
||||
// @Tags User
|
||||
// @Produce json
|
||||
// @Success 204
|
||||
// @Router /v1/users/self/password [PUT]
|
||||
// @Security Bearer
|
||||
// @Summary Update the current user's password // TODO:
|
||||
// @Tags User
|
||||
// @Produce json
|
||||
// @Success 204
|
||||
// @Router /v1/users/self/password [PUT]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleUserUpdatePassword() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// HandleUserSelfDelete godoc
|
||||
// @Summary Deletes the user account
|
||||
// @Tags User
|
||||
// @Produce json
|
||||
// @Success 204
|
||||
// @Router /v1/users/self [DELETE]
|
||||
// @Security Bearer
|
||||
// @Summary Deletes the user account
|
||||
// @Tags User
|
||||
// @Produce json
|
||||
// @Success 204
|
||||
// @Router /v1/users/self [DELETE]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleUserSelfDelete() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
actor := services.UseUserCtx(r.Context())
|
||||
|
@ -128,12 +128,12 @@ type (
|
|||
)
|
||||
|
||||
// HandleUserSelfChangePassword godoc
|
||||
// @Summary Updates the users password
|
||||
// @Tags User
|
||||
// @Success 204
|
||||
// @Param payload body ChangePassword true "Password Payload"
|
||||
// @Router /v1/users/change-password [PUT]
|
||||
// @Security Bearer
|
||||
// @Summary Updates the users password
|
||||
// @Tags User
|
||||
// @Success 204
|
||||
// @Param payload body ChangePassword true "Password Payload"
|
||||
// @Router /v1/users/change-password [PUT]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleUserSelfChangePassword() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if ctrl.isDemo {
|
||||
|
|
12
backend/internal/repo/pagination.go
Normal file
12
backend/internal/repo/pagination.go
Normal file
|
@ -0,0 +1,12 @@
|
|||
package repo
|
||||
|
||||
type PaginationResult[T any] struct {
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"pageSize"`
|
||||
Total int `json:"total"`
|
||||
Items []T `json:"items"`
|
||||
}
|
||||
|
||||
func calculateOffset(page, pageSize int) int {
|
||||
return (page - 1) * pageSize
|
||||
}
|
|
@ -8,6 +8,8 @@ import (
|
|||
"github.com/hay-kot/homebox/backend/ent"
|
||||
"github.com/hay-kot/homebox/backend/ent/group"
|
||||
"github.com/hay-kot/homebox/backend/ent/item"
|
||||
"github.com/hay-kot/homebox/backend/ent/label"
|
||||
"github.com/hay-kot/homebox/backend/ent/location"
|
||||
"github.com/hay-kot/homebox/backend/ent/predicate"
|
||||
)
|
||||
|
||||
|
@ -16,6 +18,14 @@ type ItemsRepository struct {
|
|||
}
|
||||
|
||||
type (
|
||||
ItemQuery struct {
|
||||
Page int
|
||||
PageSize int
|
||||
Search string `json:"search"`
|
||||
LocationIDs []uuid.UUID `json:"locationIds"`
|
||||
LabelIDs []uuid.UUID `json:"labelIds"`
|
||||
}
|
||||
|
||||
ItemCreate struct {
|
||||
ImportRef string `json:"-"`
|
||||
Name string `json:"name"`
|
||||
|
@ -206,6 +216,64 @@ func (e *ItemsRepository) GetOneByGroup(ctx context.Context, gid, id uuid.UUID)
|
|||
return e.getOne(ctx, item.ID(id), item.HasGroupWith(group.ID(gid)))
|
||||
}
|
||||
|
||||
// QueryByGroup returns a list of items that belong to a specific group based on the provided query.
|
||||
func (e *ItemsRepository) QueryByGroup(ctx context.Context, gid uuid.UUID, q ItemQuery) (PaginationResult[ItemSummary], error) {
|
||||
qb := e.db.Item.Query().Where(item.HasGroupWith(group.ID(gid)))
|
||||
|
||||
if len(q.LabelIDs) > 0 {
|
||||
labels := make([]predicate.Item, 0, len(q.LabelIDs))
|
||||
for _, l := range q.LabelIDs {
|
||||
labels = append(labels, item.HasLabelWith(label.ID(l)))
|
||||
}
|
||||
qb = qb.Where(item.Or(labels...))
|
||||
}
|
||||
|
||||
if len(q.LocationIDs) > 0 {
|
||||
locations := make([]predicate.Item, 0, len(q.LocationIDs))
|
||||
for _, l := range q.LocationIDs {
|
||||
locations = append(locations, item.HasLocationWith(location.ID(l)))
|
||||
}
|
||||
qb = qb.Where(item.Or(locations...))
|
||||
}
|
||||
|
||||
if q.Search != "" {
|
||||
qb.Where(
|
||||
item.Or(
|
||||
item.NameContainsFold(q.Search),
|
||||
item.DescriptionContainsFold(q.Search),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
if q.Page != -1 || q.PageSize != -1 {
|
||||
qb = qb.
|
||||
Offset(calculateOffset(q.Page, q.PageSize)).
|
||||
Limit(q.PageSize)
|
||||
}
|
||||
|
||||
items, err := mapItemsSummaryErr(
|
||||
qb.WithLabel().
|
||||
WithLocation().
|
||||
All(ctx),
|
||||
)
|
||||
if err != nil {
|
||||
return PaginationResult[ItemSummary]{}, err
|
||||
}
|
||||
|
||||
count, err := qb.Count(ctx)
|
||||
if err != nil {
|
||||
return PaginationResult[ItemSummary]{}, err
|
||||
}
|
||||
|
||||
return PaginationResult[ItemSummary]{
|
||||
Page: q.Page,
|
||||
PageSize: q.PageSize,
|
||||
Total: count,
|
||||
Items: items,
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
// GetAll returns all the items in the database with the Labels and Locations eager loaded.
|
||||
func (e *ItemsRepository) GetAll(ctx context.Context, gid uuid.UUID) ([]ItemSummary, error) {
|
||||
return mapItemsSummaryErr(e.db.Item.Query().
|
||||
|
|
|
@ -28,6 +28,10 @@ func (svc *ItemService) GetOne(ctx context.Context, gid uuid.UUID, id uuid.UUID)
|
|||
return svc.repo.Items.GetOneByGroup(ctx, gid, id)
|
||||
}
|
||||
|
||||
func (svc *ItemService) Query(ctx Context, q repo.ItemQuery) (repo.PaginationResult[repo.ItemSummary], error) {
|
||||
return svc.repo.Items.QueryByGroup(ctx, ctx.GID, q)
|
||||
}
|
||||
|
||||
func (svc *ItemService) GetAll(ctx context.Context, gid uuid.UUID) ([]repo.ItemSummary, error) {
|
||||
return svc.repo.Items.GetAll(ctx, gid)
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ func (f *Faker) Path() string {
|
|||
}
|
||||
|
||||
func (f *Faker) Email() string {
|
||||
return f.Str(10) + "@email.com"
|
||||
return f.Str(10) + "@example.com"
|
||||
}
|
||||
|
||||
func (f *Faker) Bool() bool {
|
||||
|
|
|
@ -18,6 +18,10 @@
|
|||
name: "Home",
|
||||
href: "/home",
|
||||
},
|
||||
{
|
||||
name: "Items",
|
||||
href: "/items",
|
||||
},
|
||||
{
|
||||
name: "Logout",
|
||||
action: logout,
|
||||
|
|
|
@ -11,7 +11,8 @@
|
|||
</div>
|
||||
<ul
|
||||
tabindex="0"
|
||||
class="dropdown-content mb-1 menu shadow border border-gray-400 rounded bg-base-100 w-full z-[9999] max-h-60 overflow-y-scroll scroll-bar"
|
||||
style="display: inline"
|
||||
class="dropdown-content mb-1 menu shadow border border-gray-400 rounded bg-base-100 w-full z-[9999] max-h-60 overflow-y-scroll"
|
||||
>
|
||||
<li
|
||||
v-for="(obj, idx) in items"
|
||||
|
|
|
@ -3,13 +3,13 @@
|
|||
<label class="label">
|
||||
<span class="label-text">{{ label }}</span>
|
||||
</label>
|
||||
<input ref="input" v-model="value" :type="type" class="input input-bordered w-full" />
|
||||
<input ref="input" v-model="value" :placeholder="placeholder" :type="type" class="input input-bordered w-full" />
|
||||
</div>
|
||||
<div v-else class="sm:grid sm:grid-cols-4 sm:items-start sm:gap-4">
|
||||
<label class="label">
|
||||
<span class="label-text">{{ label }}</span>
|
||||
</label>
|
||||
<input v-model="value" class="input input-bordered col-span-3 w-full mt-2" />
|
||||
<input v-model="value" :placeholder="placeholder" class="input input-bordered col-span-3 w-full mt-2" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -35,6 +35,10 @@
|
|||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
});
|
||||
|
||||
const input = ref<HTMLElement | null>(null);
|
||||
|
|
32
frontend/composables/use-min-loader.ts
Normal file
32
frontend/composables/use-min-loader.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
import { WritableComputedRef } from "vue";
|
||||
|
||||
export function useMinLoader(ms = 500): WritableComputedRef<boolean> {
|
||||
const loading = ref(false);
|
||||
|
||||
const locked = ref(false);
|
||||
|
||||
const minLoading = computed({
|
||||
get: () => loading.value,
|
||||
set: value => {
|
||||
if (value) {
|
||||
loading.value = true;
|
||||
|
||||
if (!locked.value) {
|
||||
locked.value = true;
|
||||
setTimeout(() => {
|
||||
locked.value = false;
|
||||
}, ms);
|
||||
}
|
||||
}
|
||||
|
||||
if (!value && !locked.value) {
|
||||
loading.value = false;
|
||||
} else if (!value && locked.value) {
|
||||
setTimeout(() => {
|
||||
loading.value = false;
|
||||
}, ms);
|
||||
}
|
||||
},
|
||||
});
|
||||
return minLoading;
|
||||
}
|
|
@ -8,12 +8,19 @@ import {
|
|||
ItemSummary,
|
||||
ItemUpdate,
|
||||
} from "../types/data-contracts";
|
||||
import { AttachmentTypes } from "../types/non-generated";
|
||||
import { Results } from "./types";
|
||||
import { AttachmentTypes, PaginationResult } from "../types/non-generated";
|
||||
|
||||
export type ItemsQuery = {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
locations?: string[];
|
||||
labels?: string[];
|
||||
q?: string;
|
||||
};
|
||||
|
||||
export class ItemsApi extends BaseAPI {
|
||||
getAll() {
|
||||
return this.http.get<Results<ItemSummary>>({ url: route("/items") });
|
||||
getAll(q: ItemsQuery = {}) {
|
||||
return this.http.get<PaginationResult<ItemSummary>>({ url: route("/items", q) });
|
||||
}
|
||||
|
||||
create(item: ItemCreate) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { BaseAPI, route } from "../base";
|
||||
import { LabelCreate, LabelOut } from "../types/data-contracts";
|
||||
import { Results } from "./types";
|
||||
import { Results } from "../types/non-generated";
|
||||
|
||||
export class LabelsApi extends BaseAPI {
|
||||
getAll() {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { BaseAPI, route } from "../base";
|
||||
import { LocationOutCount, LocationCreate, LocationOut } from "../types/data-contracts";
|
||||
import { Results } from "./types";
|
||||
import { Results } from "../types/non-generated";
|
||||
|
||||
export type LocationUpdate = LocationCreate;
|
||||
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
export type Results<T> = {
|
||||
items: T[];
|
||||
};
|
|
@ -187,6 +187,7 @@ export interface LocationSummary {
|
|||
updatedAt: Date;
|
||||
}
|
||||
|
||||
|
||||
export interface UserOut {
|
||||
email: string;
|
||||
groupId: string;
|
||||
|
|
|
@ -8,3 +8,14 @@ export enum AttachmentTypes {
|
|||
export type Result<T> = {
|
||||
item: T;
|
||||
};
|
||||
|
||||
export type Results<T> = {
|
||||
items: T[];
|
||||
};
|
||||
|
||||
export interface PaginationResult<T> {
|
||||
items: T[];
|
||||
page: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
}
|
||||
|
|
|
@ -157,13 +157,6 @@
|
|||
</BaseCard>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<BaseSectionHeader class="mb-5"> Labels </BaseSectionHeader>
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<LabelChip v-for="label in labels" :key="label.id" size="lg" :label="label" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<BaseSectionHeader class="mb-5"> Storage Locations </BaseSectionHeader>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 card md:grid-cols-3 gap-4">
|
||||
|
@ -172,9 +165,9 @@
|
|||
</section>
|
||||
|
||||
<section>
|
||||
<BaseSectionHeader class="mb-5"> Items </BaseSectionHeader>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<ItemCard v-for="item in items" :key="item.id" :item="item" />
|
||||
<BaseSectionHeader class="mb-5"> Labels </BaseSectionHeader>
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<LabelChip v-for="label in labels" :key="label.id" size="lg" :label="label" />
|
||||
</div>
|
||||
</section>
|
||||
</BaseContainer>
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
if (data) {
|
||||
console.log(data);
|
||||
username.value = "demo@email.com";
|
||||
username.value = "demo@example.com";
|
||||
password.value = "demo";
|
||||
}
|
||||
return data;
|
||||
|
@ -24,7 +24,7 @@
|
|||
|
||||
whenever(status, status => {
|
||||
if (status?.demo) {
|
||||
email.value = "demo@email.com";
|
||||
email.value = "demo@example.com";
|
||||
loginPassword.value = "demo";
|
||||
}
|
||||
});
|
||||
|
@ -198,7 +198,7 @@
|
|||
</h2>
|
||||
<template v-if="status && status.demo">
|
||||
<p class="text-xs italic text-center">This is a demo instance</p>
|
||||
<p class="text-xs text-center"><b>Email</b> demo@email.com</p>
|
||||
<p class="text-xs text-center"><b>Email</b> demo@example.com</p>
|
||||
<p class="text-xs text-center"><b>Password</b> demo</p>
|
||||
</template>
|
||||
<FormTextField v-model="email" label="Email" />
|
||||
|
|
109
frontend/pages/items.vue
Normal file
109
frontend/pages/items.vue
Normal file
|
@ -0,0 +1,109 @@
|
|||
<script setup lang="ts">
|
||||
import { ItemSummary } from "~~/lib/api/types/data-contracts";
|
||||
import { useLabelStore } from "~~/stores/labels";
|
||||
import { useLocationStore } from "~~/stores/locations";
|
||||
|
||||
definePageMeta({
|
||||
layout: "home",
|
||||
});
|
||||
useHead({
|
||||
title: "Homebox | Home",
|
||||
});
|
||||
|
||||
const api = useUserApi();
|
||||
|
||||
const query = ref("");
|
||||
const loading = useMinLoader(2000);
|
||||
const results = ref<ItemSummary[]>([]);
|
||||
|
||||
async function search() {
|
||||
loading.value = true;
|
||||
|
||||
const locations = selectedLocations.value.map(l => l.id);
|
||||
const labels = selectedLabels.value.map(l => l.id);
|
||||
|
||||
const { data, error } = await api.items.getAll({ q: query.value, locations, labels });
|
||||
if (error) {
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
results.value = data.items;
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
search();
|
||||
});
|
||||
|
||||
const locationsStore = useLocationStore();
|
||||
const locations = computed(() => locationsStore.locations);
|
||||
|
||||
const labelStore = useLabelStore();
|
||||
const labels = computed(() => labelStore.labels);
|
||||
|
||||
const advanced = ref(false);
|
||||
const selectedLocations = ref([]);
|
||||
const selectedLabels = ref([]);
|
||||
|
||||
watchEffect(() => {
|
||||
if (!advanced.value) {
|
||||
selectedLocations.value = [];
|
||||
selectedLabels.value = [];
|
||||
}
|
||||
});
|
||||
|
||||
watchDebounced(query, search, { debounce: 250, maxWait: 1000 });
|
||||
watchDebounced(selectedLocations, search, { debounce: 250, maxWait: 1000 });
|
||||
watchDebounced(selectedLabels, search, { debounce: 250, maxWait: 1000 });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseContainer class="mb-16">
|
||||
<FormTextField v-model="query" placeholder="Search" />
|
||||
<div class="flex mt-1">
|
||||
<label class="ml-auto label cursor-pointer">
|
||||
<input v-model="advanced" type="checkbox" class="toggle toggle-primary" />
|
||||
<span class="label-text text-neutral-content ml-2"> Filters </span>
|
||||
</label>
|
||||
</div>
|
||||
<BaseCard v-if="advanced" class="my-1 overflow-visible">
|
||||
<template #title> Filters </template>
|
||||
<template #subtitle>
|
||||
Location and label filters use the 'OR' operation. If more than one is selected only one will be required for a
|
||||
match
|
||||
</template>
|
||||
<div class="px-4 pb-4">
|
||||
<FormMultiselect v-model="selectedLabels" label="Labels" :items="labels ?? []" />
|
||||
<FormMultiselect v-model="selectedLocations" label="Labels" :items="locations ?? []" />
|
||||
</div>
|
||||
</BaseCard>
|
||||
<section class="mt-10">
|
||||
<BaseSectionHeader class="mb-5"> Items </BaseSectionHeader>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<TransitionGroup name="list">
|
||||
<ItemCard v-for="item in results" :key="item.id" :item="item" />
|
||||
</TransitionGroup>
|
||||
<div class="hidden first:inline text-xl">No Items Found</div>
|
||||
</div>
|
||||
</section>
|
||||
</BaseContainer>
|
||||
</template>
|
||||
|
||||
<style lang="css">
|
||||
.list-move,
|
||||
.list-enter-active,
|
||||
.list-leave-active {
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
|
||||
.list-enter-from,
|
||||
.list-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
|
||||
.list-leave-active {
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
|
@ -22,6 +22,7 @@ def date_types(*names: list[str]) -> dict[re.Pattern, str]:
|
|||
|
||||
|
||||
regex_replace: dict[re.Pattern, str] = {
|
||||
re.compile(r" PaginationResultRepo"): "PaginationResult",
|
||||
re.compile(r" Repo"): " ",
|
||||
re.compile(r" Services"): " ",
|
||||
re.compile(r" V1"): " ",
|
||||
|
|
Loading…
Reference in a new issue