From 942348b67670b80c4b850df4fed9e4c750dc7567 Mon Sep 17 00:00:00 2001 From: A Z Date: Sun, 10 May 2026 17:53:58 -0600 Subject: [PATCH 1/2] feat: add public found item contact page --- backend/app/api/app.go | 2 + .../app/api/handlers/v1/v1_ctrl_found_item.go | 53 +++++ backend/app/api/routes.go | 2 + backend/app/api/static/docs/docs.go | 188 ++++++++++++----- backend/app/api/static/docs/openapi-3.json | 191 +++++++++++++----- backend/app/api/static/docs/openapi-3.yaml | 119 ++++++++--- backend/app/api/static/docs/swagger.json | 188 ++++++++++++----- backend/app/api/static/docs/swagger.yaml | 118 +++++++---- backend/internal/data/repo/repo_entities.go | 105 ++++++++++ .../repo/repo_entities_found_item_test.go | 149 ++++++++++++++ docs/public/api/openapi-3.0.json | 191 +++++++++++++----- docs/public/api/openapi-3.0.yaml | 119 ++++++++--- docs/public/api/swagger-2.0.json | 188 ++++++++++++----- docs/public/api/swagger-2.0.yaml | 118 +++++++---- frontend/lib/api/public.ts | 25 ++- frontend/lib/api/types/data-contracts.ts | 5 + frontend/middleware/auth.ts | 29 ++- frontend/pages/found/[kind]/[id].vue | 88 ++++++++ 18 files changed, 1465 insertions(+), 413 deletions(-) create mode 100644 backend/app/api/handlers/v1/v1_ctrl_found_item.go create mode 100644 backend/internal/data/repo/repo_entities_found_item_test.go create mode 100644 frontend/pages/found/[kind]/[id].vue diff --git a/backend/app/api/app.go b/backend/app/api/app.go index df07d7758..53fadb488 100644 --- a/backend/app/api/app.go +++ b/backend/app/api/app.go @@ -20,6 +20,7 @@ type app struct { services *services.AllServices bus *eventbus.EventBus authLimiter *authRateLimiter + foundLabelLimiter *simpleRateLimiter notifierTestLimiter *simpleRateLimiter otel *otel.Provider } @@ -38,6 +39,7 @@ func new(conf *config.Config) *app { } s.authLimiter = newAuthRateLimiter(s.conf.Auth.RateLimit) + s.foundLabelLimiter = newSimpleRateLimiter(60, time.Minute, s.conf.Options.TrustProxy) // 60 requests per minute s.notifierTestLimiter = newSimpleRateLimiter(10, time.Minute, s.conf.Options.TrustProxy) // 10 requests per minute return s diff --git a/backend/app/api/handlers/v1/v1_ctrl_found_item.go b/backend/app/api/handlers/v1/v1_ctrl_found_item.go new file mode 100644 index 000000000..df97b8f67 --- /dev/null +++ b/backend/app/api/handlers/v1/v1_ctrl_found_item.go @@ -0,0 +1,53 @@ +package v1 + +import ( + "net/http" + + "github.com/go-chi/chi/v5" + "github.com/google/uuid" + "github.com/hay-kot/httpkit/errchain" + "github.com/hay-kot/httpkit/server" + "github.com/sysadminsmedia/homebox/backend/internal/data/repo" + "github.com/sysadminsmedia/homebox/backend/internal/sys/validate" + "github.com/sysadminsmedia/homebox/backend/internal/web/adapters" +) + +// HandleFoundEntityContact godoc +// +// @Summary Get found item contact +// @Tags Entities +// @Produce json +// @Param id path string true "Entity ID" +// @Success 200 {object} repo.FoundEntityContact +// @Router /v1/found/entities/{id} [GET] +func (ctrl *V1Controller) HandleFoundEntityContact() errchain.HandlerFunc { + fn := func(r *http.Request, ID uuid.UUID) (repo.FoundEntityContact, error) { + return ctrl.repo.Entities.GetFoundEntityContact(r.Context(), ID) + } + + return adapters.CommandID("id", fn, http.StatusOK) +} + +// HandleFoundAssetContact godoc +// +// @Summary Get found asset contact +// @Tags Entities +// @Produce json +// @Param id path string true "Asset ID" +// @Success 200 {object} repo.FoundEntityContact +// @Router /v1/found/assets/{id} [GET] +func (ctrl *V1Controller) HandleFoundAssetContact() errchain.HandlerFunc { + return func(rw http.ResponseWriter, r *http.Request) error { + assetID, ok := repo.ParseAssetID(chi.URLParam(r, "id")) + if !ok || assetID.Nil() { + return validate.NewRouteKeyError("id") + } + + contact, err := ctrl.repo.Entities.GetFoundEntityContactByAssetID(r.Context(), assetID) + if err != nil { + return err + } + + return server.JSON(rw, http.StatusOK, contact) + } +} diff --git a/backend/app/api/routes.go b/backend/app/api/routes.go index e354e1819..e37b6c662 100644 --- a/backend/app/api/routes.go +++ b/backend/app/api/routes.go @@ -88,6 +88,8 @@ func (a *app) mountRoutes(r *chi.Mux, chain *errchain.ErrChain, repos *repo.AllR r.Post("/users/register", chain.ToHandlerFunc(v1Ctrl.HandleUserRegistration())) r.Post("/users/login", chain.ToHandlerFunc(v1Ctrl.HandleAuthLogin(providers...), a.mwAuthRateLimit)) + r.Get("/found/entities/{id}", chain.ToHandlerFunc(v1Ctrl.HandleFoundEntityContact(), a.foundLabelLimiter.middleware)) + r.Get("/found/assets/{id}", chain.ToHandlerFunc(v1Ctrl.HandleFoundAssetContact(), a.foundLabelLimiter.middleware)) if a.conf.OIDC.Enabled { r.Get("/users/login/oidc", chain.ToHandlerFunc(v1Ctrl.HandleOIDCLogin(), a.mwAuthRateLimit)) diff --git a/backend/app/api/static/docs/docs.go b/backend/app/api/static/docs/docs.go index d45a2d10c..452a65ad0 100644 --- a/backend/app/api/static/docs/docs.go +++ b/backend/app/api/static/docs/docs.go @@ -1155,6 +1155,62 @@ const docTemplate = `{ } } }, + "/v1/found/assets/{id}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Entities" + ], + "summary": "Get found asset contact", + "parameters": [ + { + "type": "string", + "description": "Asset ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/repo.FoundEntityContact" + } + } + } + } + }, + "/v1/found/entities/{id}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Entities" + ], + "summary": "Get found item contact", + "parameters": [ + { + "type": "string", + "description": "Entity ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/repo.FoundEntityContact" + } + } + } + } + }, "/v1/groups": { "get": { "security": [ @@ -1439,36 +1495,6 @@ const docTemplate = `{ } } } - }, - "post": { - "security": [ - { - "Bearer": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "Group" - ], - "summary": "Add User to Group", - "parameters": [ - { - "description": "User ID", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/v1.GroupMemberAdd" - } - } - ], - "responses": { - "204": { - "description": "No Content" - } - } } }, "/v1/groups/members/{user_id}": { @@ -3718,6 +3744,13 @@ const docTemplate = `{ "$ref": "#/definitions/ent.Tag" } }, + "user_groups": { + "description": "UserGroups holds the value of the user_groups edge.", + "type": "array", + "items": { + "$ref": "#/definitions/ent.UserGroup" + } + }, "users": { "description": "Users holds the value of the users edge.", "type": "array", @@ -4097,14 +4130,6 @@ const docTemplate = `{ "description": "OidcSubject holds the value of the \"oidc_subject\" field.", "type": "string" }, - "role": { - "description": "Role holds the value of the \"role\" field.", - "allOf": [ - { - "$ref": "#/definitions/user.Role" - } - ] - }, "settings": { "description": "Settings holds the value of the \"settings\" field.", "type": "object", @@ -4150,6 +4175,63 @@ const docTemplate = `{ "items": { "$ref": "#/definitions/ent.Notifier" } + }, + "user_groups": { + "description": "UserGroups holds the value of the user_groups edge.", + "type": "array", + "items": { + "$ref": "#/definitions/ent.UserGroup" + } + } + } + }, + "ent.UserGroup": { + "type": "object", + "properties": { + "edges": { + "description": "Edges holds the relations/edges for other nodes in the graph.\nThe values are being populated by the UserGroupQuery when eager-loading is set.", + "allOf": [ + { + "$ref": "#/definitions/ent.UserGroupEdges" + } + ] + }, + "group_id": { + "description": "GroupID holds the value of the \"group_id\" field.", + "type": "string" + }, + "role": { + "description": "Role holds the value of the \"role\" field.", + "allOf": [ + { + "$ref": "#/definitions/usergroup.Role" + } + ] + }, + "user_id": { + "description": "UserID holds the value of the \"user_id\" field.", + "type": "string" + } + } + }, + "ent.UserGroupEdges": { + "type": "object", + "properties": { + "group": { + "description": "Group holds the value of the group edge.", + "allOf": [ + { + "$ref": "#/definitions/ent.Group" + } + ] + }, + "user": { + "description": "User holds the value of the user edge.", + "allOf": [ + { + "$ref": "#/definitions/ent.User" + } + ] } } }, @@ -5096,6 +5178,17 @@ const docTemplate = `{ } } }, + "repo.FoundEntityContact": { + "type": "object", + "properties": { + "itemId": { + "type": "string" + }, + "ownerEmail": { + "type": "string" + } + } + }, "repo.Group": { "type": "object", "properties": { @@ -5647,9 +5740,6 @@ const docTemplate = `{ "id": { "type": "string" }, - "isOwner": { - "type": "boolean" - }, "isSuperuser": { "type": "boolean" }, @@ -5673,9 +5763,6 @@ const docTemplate = `{ "id": { "type": "string" }, - "isOwner": { - "type": "boolean" - }, "name": { "type": "string" } @@ -5772,7 +5859,7 @@ const docTemplate = `{ "TypeTime" ] }, - "user.Role": { + "usergroup.Role": { "type": "string", "enum": [ "user", @@ -5944,17 +6031,6 @@ const docTemplate = `{ } } }, - "v1.GroupMemberAdd": { - "type": "object", - "required": [ - "userId" - ], - "properties": { - "userId": { - "type": "string" - } - } - }, "v1.LoginForm": { "type": "object", "properties": { diff --git a/backend/app/api/static/docs/openapi-3.json b/backend/app/api/static/docs/openapi-3.json index f3a2ab328..fdf93348f 100644 --- a/backend/app/api/static/docs/openapi-3.json +++ b/backend/app/api/static/docs/openapi-3.json @@ -1258,6 +1258,68 @@ } } }, + "/v1/found/assets/{id}": { + "get": { + "tags": [ + "Entities" + ], + "summary": "Get found asset contact", + "parameters": [ + { + "description": "Asset ID", + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/repo.FoundEntityContact" + } + } + } + } + } + } + }, + "/v1/found/entities/{id}": { + "get": { + "tags": [ + "Entities" + ], + "summary": "Get found item contact", + "parameters": [ + { + "description": "Entity ID", + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/repo.FoundEntityContact" + } + } + } + } + } + } + }, "/v1/groups": { "get": { "security": [ @@ -1548,33 +1610,6 @@ } } } - }, - "post": { - "security": [ - { - "Bearer": [] - } - ], - "tags": [ - "Group" - ], - "summary": "Add User to Group", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/v1.GroupMemberAdd" - } - } - }, - "description": "User ID", - "required": true - }, - "responses": { - "204": { - "description": "No Content" - } - } } }, "/v1/groups/members/{user_id}": { @@ -3922,6 +3957,13 @@ "$ref": "#/components/schemas/ent.Tag" } }, + "user_groups": { + "description": "UserGroups holds the value of the user_groups edge.", + "type": "array", + "items": { + "$ref": "#/components/schemas/ent.UserGroup" + } + }, "users": { "description": "Users holds the value of the users edge.", "type": "array", @@ -4301,14 +4343,6 @@ "description": "OidcSubject holds the value of the \"oidc_subject\" field.", "type": "string" }, - "role": { - "description": "Role holds the value of the \"role\" field.", - "allOf": [ - { - "$ref": "#/components/schemas/user.Role" - } - ] - }, "settings": { "description": "Settings holds the value of the \"settings\" field.", "type": "object", @@ -4354,6 +4388,63 @@ "items": { "$ref": "#/components/schemas/ent.Notifier" } + }, + "user_groups": { + "description": "UserGroups holds the value of the user_groups edge.", + "type": "array", + "items": { + "$ref": "#/components/schemas/ent.UserGroup" + } + } + } + }, + "ent.UserGroup": { + "type": "object", + "properties": { + "edges": { + "description": "Edges holds the relations/edges for other nodes in the graph.\nThe values are being populated by the UserGroupQuery when eager-loading is set.", + "allOf": [ + { + "$ref": "#/components/schemas/ent.UserGroupEdges" + } + ] + }, + "group_id": { + "description": "GroupID holds the value of the \"group_id\" field.", + "type": "string" + }, + "role": { + "description": "Role holds the value of the \"role\" field.", + "allOf": [ + { + "$ref": "#/components/schemas/usergroup.Role" + } + ] + }, + "user_id": { + "description": "UserID holds the value of the \"user_id\" field.", + "type": "string" + } + } + }, + "ent.UserGroupEdges": { + "type": "object", + "properties": { + "group": { + "description": "Group holds the value of the group edge.", + "allOf": [ + { + "$ref": "#/components/schemas/ent.Group" + } + ] + }, + "user": { + "description": "User holds the value of the user edge.", + "allOf": [ + { + "$ref": "#/components/schemas/ent.User" + } + ] } } }, @@ -5300,6 +5391,17 @@ } } }, + "repo.FoundEntityContact": { + "type": "object", + "properties": { + "itemId": { + "type": "string" + }, + "ownerEmail": { + "type": "string" + } + } + }, "repo.Group": { "type": "object", "properties": { @@ -5851,9 +5953,6 @@ "id": { "type": "string" }, - "isOwner": { - "type": "boolean" - }, "isSuperuser": { "type": "boolean" }, @@ -5877,9 +5976,6 @@ "id": { "type": "string" }, - "isOwner": { - "type": "boolean" - }, "name": { "type": "string" } @@ -5976,7 +6072,7 @@ "TypeTime" ] }, - "user.Role": { + "usergroup.Role": { "type": "string", "enum": [ "user", @@ -6148,17 +6244,6 @@ } } }, - "v1.GroupMemberAdd": { - "type": "object", - "required": [ - "userId" - ], - "properties": { - "userId": { - "type": "string" - } - } - }, "v1.LoginForm": { "type": "object", "properties": { diff --git a/backend/app/api/static/docs/openapi-3.yaml b/backend/app/api/static/docs/openapi-3.yaml index 868713ae1..3cd352aff 100644 --- a/backend/app/api/static/docs/openapi-3.yaml +++ b/backend/app/api/static/docs/openapi-3.yaml @@ -753,6 +753,44 @@ paths: responses: "204": description: No Content + "/v1/found/assets/{id}": + get: + tags: + - Entities + summary: Get found asset contact + parameters: + - description: Asset ID + name: id + in: path + required: true + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/repo.FoundEntityContact" + "/v1/found/entities/{id}": + get: + tags: + - Entities + summary: Get found item contact + parameters: + - description: Entity ID + name: id + in: path + required: true + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/repo.FoundEntityContact" /v1/groups: get: security: @@ -923,22 +961,6 @@ paths: type: array items: $ref: "#/components/schemas/repo.UserSummary" - post: - security: - - Bearer: [] - tags: - - Group - summary: Add User to Group - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/v1.GroupMemberAdd" - description: User ID - required: true - responses: - "204": - description: No Content "/v1/groups/members/{user_id}": delete: security: @@ -2409,6 +2431,11 @@ components: type: array items: $ref: "#/components/schemas/ent.Tag" + user_groups: + description: UserGroups holds the value of the user_groups edge. + type: array + items: + $ref: "#/components/schemas/ent.UserGroup" users: description: Users holds the value of the users edge. type: array @@ -2674,10 +2701,6 @@ components: oidc_subject: description: OidcSubject holds the value of the "oidc_subject" field. type: string - role: - description: Role holds the value of the "role" field. - allOf: - - $ref: "#/components/schemas/user.Role" settings: description: Settings holds the value of the "settings" field. type: object @@ -2711,6 +2734,42 @@ components: type: array items: $ref: "#/components/schemas/ent.Notifier" + user_groups: + description: UserGroups holds the value of the user_groups edge. + type: array + items: + $ref: "#/components/schemas/ent.UserGroup" + ent.UserGroup: + type: object + properties: + edges: + description: >- + Edges holds the relations/edges for other nodes in the graph. + + The values are being populated by the UserGroupQuery when eager-loading is set. + allOf: + - $ref: "#/components/schemas/ent.UserGroupEdges" + group_id: + description: GroupID holds the value of the "group_id" field. + type: string + role: + description: Role holds the value of the "role" field. + allOf: + - $ref: "#/components/schemas/usergroup.Role" + user_id: + description: UserID holds the value of the "user_id" field. + type: string + ent.UserGroupEdges: + type: object + properties: + group: + description: Group holds the value of the group edge. + allOf: + - $ref: "#/components/schemas/ent.Group" + user: + description: User holds the value of the user edge. + allOf: + - $ref: "#/components/schemas/ent.User" entityfield.Type: type: string enum: @@ -3366,6 +3425,13 @@ components: type: string warrantyExpires: type: string + repo.FoundEntityContact: + type: object + properties: + itemId: + type: string + ownerEmail: + type: string repo.Group: type: object properties: @@ -3734,8 +3800,6 @@ components: type: string id: type: string - isOwner: - type: boolean isSuperuser: type: boolean name: @@ -3751,8 +3815,6 @@ components: type: string id: type: string - isOwner: - type: boolean name: type: string repo.UserUpdate: @@ -3816,7 +3878,7 @@ components: - TypeNumber - TypeBoolean - TypeTime - user.Role: + usergroup.Role: type: string enum: - user @@ -3931,13 +3993,6 @@ components: type: integer maximum: 100 minimum: 1 - v1.GroupMemberAdd: - type: object - required: - - userId - properties: - userId: - type: string v1.LoginForm: type: object properties: diff --git a/backend/app/api/static/docs/swagger.json b/backend/app/api/static/docs/swagger.json index c6b0d506d..f73a157a3 100644 --- a/backend/app/api/static/docs/swagger.json +++ b/backend/app/api/static/docs/swagger.json @@ -1152,6 +1152,62 @@ } } }, + "/v1/found/assets/{id}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Entities" + ], + "summary": "Get found asset contact", + "parameters": [ + { + "type": "string", + "description": "Asset ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/repo.FoundEntityContact" + } + } + } + } + }, + "/v1/found/entities/{id}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Entities" + ], + "summary": "Get found item contact", + "parameters": [ + { + "type": "string", + "description": "Entity ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/repo.FoundEntityContact" + } + } + } + } + }, "/v1/groups": { "get": { "security": [ @@ -1436,36 +1492,6 @@ } } } - }, - "post": { - "security": [ - { - "Bearer": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "Group" - ], - "summary": "Add User to Group", - "parameters": [ - { - "description": "User ID", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/v1.GroupMemberAdd" - } - } - ], - "responses": { - "204": { - "description": "No Content" - } - } } }, "/v1/groups/members/{user_id}": { @@ -3715,6 +3741,13 @@ "$ref": "#/definitions/ent.Tag" } }, + "user_groups": { + "description": "UserGroups holds the value of the user_groups edge.", + "type": "array", + "items": { + "$ref": "#/definitions/ent.UserGroup" + } + }, "users": { "description": "Users holds the value of the users edge.", "type": "array", @@ -4094,14 +4127,6 @@ "description": "OidcSubject holds the value of the \"oidc_subject\" field.", "type": "string" }, - "role": { - "description": "Role holds the value of the \"role\" field.", - "allOf": [ - { - "$ref": "#/definitions/user.Role" - } - ] - }, "settings": { "description": "Settings holds the value of the \"settings\" field.", "type": "object", @@ -4147,6 +4172,63 @@ "items": { "$ref": "#/definitions/ent.Notifier" } + }, + "user_groups": { + "description": "UserGroups holds the value of the user_groups edge.", + "type": "array", + "items": { + "$ref": "#/definitions/ent.UserGroup" + } + } + } + }, + "ent.UserGroup": { + "type": "object", + "properties": { + "edges": { + "description": "Edges holds the relations/edges for other nodes in the graph.\nThe values are being populated by the UserGroupQuery when eager-loading is set.", + "allOf": [ + { + "$ref": "#/definitions/ent.UserGroupEdges" + } + ] + }, + "group_id": { + "description": "GroupID holds the value of the \"group_id\" field.", + "type": "string" + }, + "role": { + "description": "Role holds the value of the \"role\" field.", + "allOf": [ + { + "$ref": "#/definitions/usergroup.Role" + } + ] + }, + "user_id": { + "description": "UserID holds the value of the \"user_id\" field.", + "type": "string" + } + } + }, + "ent.UserGroupEdges": { + "type": "object", + "properties": { + "group": { + "description": "Group holds the value of the group edge.", + "allOf": [ + { + "$ref": "#/definitions/ent.Group" + } + ] + }, + "user": { + "description": "User holds the value of the user edge.", + "allOf": [ + { + "$ref": "#/definitions/ent.User" + } + ] } } }, @@ -5093,6 +5175,17 @@ } } }, + "repo.FoundEntityContact": { + "type": "object", + "properties": { + "itemId": { + "type": "string" + }, + "ownerEmail": { + "type": "string" + } + } + }, "repo.Group": { "type": "object", "properties": { @@ -5644,9 +5737,6 @@ "id": { "type": "string" }, - "isOwner": { - "type": "boolean" - }, "isSuperuser": { "type": "boolean" }, @@ -5670,9 +5760,6 @@ "id": { "type": "string" }, - "isOwner": { - "type": "boolean" - }, "name": { "type": "string" } @@ -5769,7 +5856,7 @@ "TypeTime" ] }, - "user.Role": { + "usergroup.Role": { "type": "string", "enum": [ "user", @@ -5941,17 +6028,6 @@ } } }, - "v1.GroupMemberAdd": { - "type": "object", - "required": [ - "userId" - ], - "properties": { - "userId": { - "type": "string" - } - } - }, "v1.LoginForm": { "type": "object", "properties": { diff --git a/backend/app/api/static/docs/swagger.yaml b/backend/app/api/static/docs/swagger.yaml index decc84daa..99879febf 100644 --- a/backend/app/api/static/docs/swagger.yaml +++ b/backend/app/api/static/docs/swagger.yaml @@ -546,6 +546,11 @@ definitions: items: $ref: '#/definitions/ent.Tag' type: array + user_groups: + description: UserGroups holds the value of the user_groups edge. + items: + $ref: '#/definitions/ent.UserGroup' + type: array users: description: Users holds the value of the users edge. items: @@ -805,10 +810,6 @@ definitions: oidc_subject: description: OidcSubject holds the value of the "oidc_subject" field. type: string - role: - allOf: - - $ref: '#/definitions/user.Role' - description: Role holds the value of the "role" field. settings: additionalProperties: true description: Settings holds the value of the "settings" field. @@ -842,6 +843,41 @@ definitions: items: $ref: '#/definitions/ent.Notifier' type: array + user_groups: + description: UserGroups holds the value of the user_groups edge. + items: + $ref: '#/definitions/ent.UserGroup' + type: array + type: object + ent.UserGroup: + properties: + edges: + allOf: + - $ref: '#/definitions/ent.UserGroupEdges' + description: |- + Edges holds the relations/edges for other nodes in the graph. + The values are being populated by the UserGroupQuery when eager-loading is set. + group_id: + description: GroupID holds the value of the "group_id" field. + type: string + role: + allOf: + - $ref: '#/definitions/usergroup.Role' + description: Role holds the value of the "role" field. + user_id: + description: UserID holds the value of the "user_id" field. + type: string + type: object + ent.UserGroupEdges: + properties: + group: + allOf: + - $ref: '#/definitions/ent.Group' + description: Group holds the value of the group edge. + user: + allOf: + - $ref: '#/definitions/ent.User' + description: User holds the value of the user edge. type: object entityfield.Type: enum: @@ -1498,6 +1534,13 @@ definitions: required: - name type: object + repo.FoundEntityContact: + properties: + itemId: + type: string + ownerEmail: + type: string + type: object repo.Group: properties: createdAt: @@ -1865,8 +1908,6 @@ definitions: type: array id: type: string - isOwner: - type: boolean isSuperuser: type: boolean name: @@ -1882,8 +1923,6 @@ definitions: type: string id: type: string - isOwner: - type: boolean name: type: string type: object @@ -1948,7 +1987,7 @@ definitions: - TypeNumber - TypeBoolean - TypeTime - user.Role: + usergroup.Role: enum: - user - user @@ -2063,13 +2102,6 @@ definitions: required: - uses type: object - v1.GroupMemberAdd: - properties: - userId: - type: string - required: - - userId - type: object v1.LoginForm: properties: password: @@ -2848,6 +2880,42 @@ paths: summary: Update Entity Type tags: - Entity Types + /v1/found/assets/{id}: + get: + parameters: + - description: Asset ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/repo.FoundEntityContact' + summary: Get found asset contact + tags: + - Entities + /v1/found/entities/{id}: + get: + parameters: + - description: Entity ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/repo.FoundEntityContact' + summary: Get found item contact + tags: + - Entities /v1/groups: delete: produces: @@ -3020,24 +3088,6 @@ paths: summary: Get All Group Members tags: - Group - post: - parameters: - - description: User ID - in: body - name: payload - required: true - schema: - $ref: '#/definitions/v1.GroupMemberAdd' - produces: - - application/json - responses: - "204": - description: No Content - security: - - Bearer: [] - summary: Add User to Group - tags: - - Group /v1/groups/members/{user_id}: delete: parameters: diff --git a/backend/internal/data/repo/repo_entities.go b/backend/internal/data/repo/repo_entities.go index b9f88973c..e10a9c9ba 100644 --- a/backend/internal/data/repo/repo_entities.go +++ b/backend/internal/data/repo/repo_entities.go @@ -21,6 +21,7 @@ import ( "github.com/sysadminsmedia/homebox/backend/internal/data/ent/maintenanceentry" "github.com/sysadminsmedia/homebox/backend/internal/data/ent/predicate" "github.com/sysadminsmedia/homebox/backend/internal/data/ent/tag" + "github.com/sysadminsmedia/homebox/backend/internal/data/ent/usergroup" "github.com/sysadminsmedia/homebox/backend/internal/data/types" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" @@ -221,6 +222,11 @@ type ( EntitySummary ItemCount float64 `json:"itemCount"` } + + FoundEntityContact struct { + ItemID uuid.UUID `json:"itemId"` + OwnerEmail string `json:"ownerEmail"` + } ) var mapEntitiesSummaryErr = mapTEachErrFunc(mapEntitySummary) @@ -515,6 +521,105 @@ func (r *EntityRepository) GetOneByGroup(ctx context.Context, gid, id uuid.UUID) return out, err } +// GetFoundEntityContact returns the minimal public contact information needed +// when someone opens an item label URL while signed out. It intentionally does +// not expose item details or attachments. +func (r *EntityRepository) GetFoundEntityContact(ctx context.Context, id uuid.UUID) (FoundEntityContact, error) { + ctx, span := entityTracer().Start(ctx, "repo.EntityRepository.GetFoundEntityContact", + trace.WithAttributes(attribute.String("entity.id", id.String()))) + defer span.End() + + entEntity, err := r.db.Entity.Query(). + Where(entity.ID(id)). + WithGroup(). + Only(ctx) + if err != nil { + recordSpanError(span, err) + return FoundEntityContact{}, err + } + + if entEntity.Edges.Group == nil { + err := &ent.NotFoundError{} + recordSpanError(span, err) + return FoundEntityContact{}, err + } + + gid := entEntity.Edges.Group.ID + span.SetAttributes(attribute.String("group.id", gid.String())) + + contact, err := r.foundEntityContactForGroup(ctx, id, gid) + recordSpanError(span, err) + return contact, err +} + +// GetFoundEntityContactByAssetID returns public contact information for asset +// label URLs only when the asset ID resolves to exactly one entity. +func (r *EntityRepository) GetFoundEntityContactByAssetID(ctx context.Context, assetID AssetID) (FoundEntityContact, error) { + ctx, span := entityTracer().Start(ctx, "repo.EntityRepository.GetFoundEntityContactByAssetID", + trace.WithAttributes(attribute.Int64("entity.asset_id", int64(assetID)))) + defer span.End() + + if assetID.Nil() { + err := &ent.NotFoundError{} + recordSpanError(span, err) + return FoundEntityContact{}, err + } + + entities, err := r.db.Entity.Query(). + Where(entity.AssetID(int64(assetID))). + WithGroup(). + Limit(2). + All(ctx) + if err != nil { + recordSpanError(span, err) + return FoundEntityContact{}, err + } + if len(entities) != 1 { + err := &ent.NotFoundError{} + recordSpanError(span, err) + return FoundEntityContact{}, err + } + + entEntity := entities[0] + if entEntity.Edges.Group == nil { + err := &ent.NotFoundError{} + recordSpanError(span, err) + return FoundEntityContact{}, err + } + + gid := entEntity.Edges.Group.ID + span.SetAttributes( + attribute.String("entity.id", entEntity.ID.String()), + attribute.String("group.id", gid.String()), + ) + + contact, err := r.foundEntityContactForGroup(ctx, entEntity.ID, gid) + recordSpanError(span, err) + return contact, err +} + +func (r *EntityRepository) foundEntityContactForGroup(ctx context.Context, itemID, gid uuid.UUID) (FoundEntityContact, error) { + membership, err := r.db.UserGroup.Query(). + Where( + usergroup.GroupID(gid), + usergroup.RoleEQ(usergroup.RoleOwner), + ). + WithUser(). + First(ctx) + if err != nil { + return FoundEntityContact{}, err + } + if membership.Edges.User == nil { + err := &ent.NotFoundError{} + return FoundEntityContact{}, err + } + + return FoundEntityContact{ + ItemID: itemID, + OwnerEmail: membership.Edges.User.Email, + }, nil +} + func entityQuerySpanAttrs(gid uuid.UUID, q EntityQuery) []attribute.KeyValue { isLocSet := q.IsLocation != nil isLocValue := false diff --git a/backend/internal/data/repo/repo_entities_found_item_test.go b/backend/internal/data/repo/repo_entities_found_item_test.go new file mode 100644 index 000000000..74b00c947 --- /dev/null +++ b/backend/internal/data/repo/repo_entities_found_item_test.go @@ -0,0 +1,149 @@ +package repo + +import ( + "context" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + "github.com/sysadminsmedia/homebox/backend/internal/data/ent" +) + +func TestEntityRepository_GetFoundEntityContact(t *testing.T) { + ctx := context.Background() + + g, err := tRepos.Groups.GroupCreate(ctx, "found-item-contact", uuid.Nil) + require.NoError(t, err) + + password := "password" + owner, err := tRepos.Users.Create(ctx, UserCreate{ + Name: "Owner", + Email: "owner@example.com", + Password: &password, + DefaultGroupID: g.ID, + IsOwner: true, + }) + require.NoError(t, err) + + member, err := tRepos.Users.Create(ctx, UserCreate{ + Name: "Member", + Email: "member@example.com", + Password: &password, + DefaultGroupID: g.ID, + }) + require.NoError(t, err) + + itemType, err := tRepos.EntityTypes.GetDefault(ctx, g.ID, false) + require.NoError(t, err) + + item, err := tRepos.Entities.Create(ctx, g.ID, EntityCreate{ + Name: "Found umbrella", + Description: "Private description", + EntityTypeID: itemType.ID, + }) + require.NoError(t, err) + + t.Cleanup(func() { + _ = tRepos.Entities.Delete(ctx, item.ID) + _ = tRepos.Users.Delete(ctx, member.ID) + _ = tRepos.Users.Delete(ctx, owner.ID) + _ = tRepos.Groups.GroupDelete(ctx, g.ID) + }) + + contact, err := tRepos.Entities.GetFoundEntityContact(ctx, item.ID) + require.NoError(t, err) + require.Equal(t, item.ID, contact.ItemID) + require.Equal(t, "owner@example.com", contact.OwnerEmail) +} + +func TestEntityRepository_GetFoundEntityContactByAssetID(t *testing.T) { + ctx := context.Background() + + g, err := tRepos.Groups.GroupCreate(ctx, "found-asset-contact", uuid.Nil) + require.NoError(t, err) + + password := "password" + owner, err := tRepos.Users.Create(ctx, UserCreate{ + Name: "Asset Owner", + Email: "asset-owner@example.com", + Password: &password, + DefaultGroupID: g.ID, + IsOwner: true, + }) + require.NoError(t, err) + + item, err := tRepos.Entities.Create(ctx, g.ID, EntityCreate{ + Name: "Found backpack", + Description: "Private description", + AssetID: AssetID(4242), + }) + require.NoError(t, err) + + t.Cleanup(func() { + _ = tRepos.Entities.Delete(ctx, item.ID) + _ = tRepos.Users.Delete(ctx, owner.ID) + _ = tRepos.Groups.GroupDelete(ctx, g.ID) + }) + + contact, err := tRepos.Entities.GetFoundEntityContactByAssetID(ctx, AssetID(4242)) + require.NoError(t, err) + require.Equal(t, item.ID, contact.ItemID) + require.Equal(t, "asset-owner@example.com", contact.OwnerEmail) +} + +func TestEntityRepository_GetFoundEntityContactByAssetID_Ambiguous(t *testing.T) { + ctx := context.Background() + password := "password" + assetID := AssetID(4343) + + firstGroup, err := tRepos.Groups.GroupCreate(ctx, "found-asset-ambiguous-1", uuid.Nil) + require.NoError(t, err) + firstOwner, err := tRepos.Users.Create(ctx, UserCreate{ + Name: "First Owner", + Email: "asset-owner-1@example.com", + Password: &password, + DefaultGroupID: firstGroup.ID, + IsOwner: true, + }) + require.NoError(t, err) + firstItem, err := tRepos.Entities.Create(ctx, firstGroup.ID, EntityCreate{ + Name: "First item", + AssetID: assetID, + }) + require.NoError(t, err) + + secondGroup, err := tRepos.Groups.GroupCreate(ctx, "found-asset-ambiguous-2", uuid.Nil) + require.NoError(t, err) + secondOwner, err := tRepos.Users.Create(ctx, UserCreate{ + Name: "Second Owner", + Email: "asset-owner-2@example.com", + Password: &password, + DefaultGroupID: secondGroup.ID, + IsOwner: true, + }) + require.NoError(t, err) + secondItem, err := tRepos.Entities.Create(ctx, secondGroup.ID, EntityCreate{ + Name: "Second item", + AssetID: assetID, + }) + require.NoError(t, err) + + t.Cleanup(func() { + _ = tRepos.Entities.Delete(ctx, secondItem.ID) + _ = tRepos.Entities.Delete(ctx, firstItem.ID) + _ = tRepos.Users.Delete(ctx, secondOwner.ID) + _ = tRepos.Users.Delete(ctx, firstOwner.ID) + _ = tRepos.Groups.GroupDelete(ctx, secondGroup.ID) + _ = tRepos.Groups.GroupDelete(ctx, firstGroup.ID) + }) + + _, err = tRepos.Entities.GetFoundEntityContactByAssetID(ctx, assetID) + require.Error(t, err) + require.True(t, ent.IsNotFound(err)) +} + +func TestEntityRepository_GetFoundEntityContact_NotFound(t *testing.T) { + _, err := tRepos.Entities.GetFoundEntityContact(context.Background(), uuid.New()) + require.Error(t, err) + require.True(t, ent.IsNotFound(err)) +} diff --git a/docs/public/api/openapi-3.0.json b/docs/public/api/openapi-3.0.json index f3a2ab328..fdf93348f 100644 --- a/docs/public/api/openapi-3.0.json +++ b/docs/public/api/openapi-3.0.json @@ -1258,6 +1258,68 @@ } } }, + "/v1/found/assets/{id}": { + "get": { + "tags": [ + "Entities" + ], + "summary": "Get found asset contact", + "parameters": [ + { + "description": "Asset ID", + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/repo.FoundEntityContact" + } + } + } + } + } + } + }, + "/v1/found/entities/{id}": { + "get": { + "tags": [ + "Entities" + ], + "summary": "Get found item contact", + "parameters": [ + { + "description": "Entity ID", + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/repo.FoundEntityContact" + } + } + } + } + } + } + }, "/v1/groups": { "get": { "security": [ @@ -1548,33 +1610,6 @@ } } } - }, - "post": { - "security": [ - { - "Bearer": [] - } - ], - "tags": [ - "Group" - ], - "summary": "Add User to Group", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/v1.GroupMemberAdd" - } - } - }, - "description": "User ID", - "required": true - }, - "responses": { - "204": { - "description": "No Content" - } - } } }, "/v1/groups/members/{user_id}": { @@ -3922,6 +3957,13 @@ "$ref": "#/components/schemas/ent.Tag" } }, + "user_groups": { + "description": "UserGroups holds the value of the user_groups edge.", + "type": "array", + "items": { + "$ref": "#/components/schemas/ent.UserGroup" + } + }, "users": { "description": "Users holds the value of the users edge.", "type": "array", @@ -4301,14 +4343,6 @@ "description": "OidcSubject holds the value of the \"oidc_subject\" field.", "type": "string" }, - "role": { - "description": "Role holds the value of the \"role\" field.", - "allOf": [ - { - "$ref": "#/components/schemas/user.Role" - } - ] - }, "settings": { "description": "Settings holds the value of the \"settings\" field.", "type": "object", @@ -4354,6 +4388,63 @@ "items": { "$ref": "#/components/schemas/ent.Notifier" } + }, + "user_groups": { + "description": "UserGroups holds the value of the user_groups edge.", + "type": "array", + "items": { + "$ref": "#/components/schemas/ent.UserGroup" + } + } + } + }, + "ent.UserGroup": { + "type": "object", + "properties": { + "edges": { + "description": "Edges holds the relations/edges for other nodes in the graph.\nThe values are being populated by the UserGroupQuery when eager-loading is set.", + "allOf": [ + { + "$ref": "#/components/schemas/ent.UserGroupEdges" + } + ] + }, + "group_id": { + "description": "GroupID holds the value of the \"group_id\" field.", + "type": "string" + }, + "role": { + "description": "Role holds the value of the \"role\" field.", + "allOf": [ + { + "$ref": "#/components/schemas/usergroup.Role" + } + ] + }, + "user_id": { + "description": "UserID holds the value of the \"user_id\" field.", + "type": "string" + } + } + }, + "ent.UserGroupEdges": { + "type": "object", + "properties": { + "group": { + "description": "Group holds the value of the group edge.", + "allOf": [ + { + "$ref": "#/components/schemas/ent.Group" + } + ] + }, + "user": { + "description": "User holds the value of the user edge.", + "allOf": [ + { + "$ref": "#/components/schemas/ent.User" + } + ] } } }, @@ -5300,6 +5391,17 @@ } } }, + "repo.FoundEntityContact": { + "type": "object", + "properties": { + "itemId": { + "type": "string" + }, + "ownerEmail": { + "type": "string" + } + } + }, "repo.Group": { "type": "object", "properties": { @@ -5851,9 +5953,6 @@ "id": { "type": "string" }, - "isOwner": { - "type": "boolean" - }, "isSuperuser": { "type": "boolean" }, @@ -5877,9 +5976,6 @@ "id": { "type": "string" }, - "isOwner": { - "type": "boolean" - }, "name": { "type": "string" } @@ -5976,7 +6072,7 @@ "TypeTime" ] }, - "user.Role": { + "usergroup.Role": { "type": "string", "enum": [ "user", @@ -6148,17 +6244,6 @@ } } }, - "v1.GroupMemberAdd": { - "type": "object", - "required": [ - "userId" - ], - "properties": { - "userId": { - "type": "string" - } - } - }, "v1.LoginForm": { "type": "object", "properties": { diff --git a/docs/public/api/openapi-3.0.yaml b/docs/public/api/openapi-3.0.yaml index 868713ae1..3cd352aff 100644 --- a/docs/public/api/openapi-3.0.yaml +++ b/docs/public/api/openapi-3.0.yaml @@ -753,6 +753,44 @@ paths: responses: "204": description: No Content + "/v1/found/assets/{id}": + get: + tags: + - Entities + summary: Get found asset contact + parameters: + - description: Asset ID + name: id + in: path + required: true + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/repo.FoundEntityContact" + "/v1/found/entities/{id}": + get: + tags: + - Entities + summary: Get found item contact + parameters: + - description: Entity ID + name: id + in: path + required: true + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/repo.FoundEntityContact" /v1/groups: get: security: @@ -923,22 +961,6 @@ paths: type: array items: $ref: "#/components/schemas/repo.UserSummary" - post: - security: - - Bearer: [] - tags: - - Group - summary: Add User to Group - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/v1.GroupMemberAdd" - description: User ID - required: true - responses: - "204": - description: No Content "/v1/groups/members/{user_id}": delete: security: @@ -2409,6 +2431,11 @@ components: type: array items: $ref: "#/components/schemas/ent.Tag" + user_groups: + description: UserGroups holds the value of the user_groups edge. + type: array + items: + $ref: "#/components/schemas/ent.UserGroup" users: description: Users holds the value of the users edge. type: array @@ -2674,10 +2701,6 @@ components: oidc_subject: description: OidcSubject holds the value of the "oidc_subject" field. type: string - role: - description: Role holds the value of the "role" field. - allOf: - - $ref: "#/components/schemas/user.Role" settings: description: Settings holds the value of the "settings" field. type: object @@ -2711,6 +2734,42 @@ components: type: array items: $ref: "#/components/schemas/ent.Notifier" + user_groups: + description: UserGroups holds the value of the user_groups edge. + type: array + items: + $ref: "#/components/schemas/ent.UserGroup" + ent.UserGroup: + type: object + properties: + edges: + description: >- + Edges holds the relations/edges for other nodes in the graph. + + The values are being populated by the UserGroupQuery when eager-loading is set. + allOf: + - $ref: "#/components/schemas/ent.UserGroupEdges" + group_id: + description: GroupID holds the value of the "group_id" field. + type: string + role: + description: Role holds the value of the "role" field. + allOf: + - $ref: "#/components/schemas/usergroup.Role" + user_id: + description: UserID holds the value of the "user_id" field. + type: string + ent.UserGroupEdges: + type: object + properties: + group: + description: Group holds the value of the group edge. + allOf: + - $ref: "#/components/schemas/ent.Group" + user: + description: User holds the value of the user edge. + allOf: + - $ref: "#/components/schemas/ent.User" entityfield.Type: type: string enum: @@ -3366,6 +3425,13 @@ components: type: string warrantyExpires: type: string + repo.FoundEntityContact: + type: object + properties: + itemId: + type: string + ownerEmail: + type: string repo.Group: type: object properties: @@ -3734,8 +3800,6 @@ components: type: string id: type: string - isOwner: - type: boolean isSuperuser: type: boolean name: @@ -3751,8 +3815,6 @@ components: type: string id: type: string - isOwner: - type: boolean name: type: string repo.UserUpdate: @@ -3816,7 +3878,7 @@ components: - TypeNumber - TypeBoolean - TypeTime - user.Role: + usergroup.Role: type: string enum: - user @@ -3931,13 +3993,6 @@ components: type: integer maximum: 100 minimum: 1 - v1.GroupMemberAdd: - type: object - required: - - userId - properties: - userId: - type: string v1.LoginForm: type: object properties: diff --git a/docs/public/api/swagger-2.0.json b/docs/public/api/swagger-2.0.json index c6b0d506d..f73a157a3 100644 --- a/docs/public/api/swagger-2.0.json +++ b/docs/public/api/swagger-2.0.json @@ -1152,6 +1152,62 @@ } } }, + "/v1/found/assets/{id}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Entities" + ], + "summary": "Get found asset contact", + "parameters": [ + { + "type": "string", + "description": "Asset ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/repo.FoundEntityContact" + } + } + } + } + }, + "/v1/found/entities/{id}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Entities" + ], + "summary": "Get found item contact", + "parameters": [ + { + "type": "string", + "description": "Entity ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/repo.FoundEntityContact" + } + } + } + } + }, "/v1/groups": { "get": { "security": [ @@ -1436,36 +1492,6 @@ } } } - }, - "post": { - "security": [ - { - "Bearer": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "Group" - ], - "summary": "Add User to Group", - "parameters": [ - { - "description": "User ID", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/v1.GroupMemberAdd" - } - } - ], - "responses": { - "204": { - "description": "No Content" - } - } } }, "/v1/groups/members/{user_id}": { @@ -3715,6 +3741,13 @@ "$ref": "#/definitions/ent.Tag" } }, + "user_groups": { + "description": "UserGroups holds the value of the user_groups edge.", + "type": "array", + "items": { + "$ref": "#/definitions/ent.UserGroup" + } + }, "users": { "description": "Users holds the value of the users edge.", "type": "array", @@ -4094,14 +4127,6 @@ "description": "OidcSubject holds the value of the \"oidc_subject\" field.", "type": "string" }, - "role": { - "description": "Role holds the value of the \"role\" field.", - "allOf": [ - { - "$ref": "#/definitions/user.Role" - } - ] - }, "settings": { "description": "Settings holds the value of the \"settings\" field.", "type": "object", @@ -4147,6 +4172,63 @@ "items": { "$ref": "#/definitions/ent.Notifier" } + }, + "user_groups": { + "description": "UserGroups holds the value of the user_groups edge.", + "type": "array", + "items": { + "$ref": "#/definitions/ent.UserGroup" + } + } + } + }, + "ent.UserGroup": { + "type": "object", + "properties": { + "edges": { + "description": "Edges holds the relations/edges for other nodes in the graph.\nThe values are being populated by the UserGroupQuery when eager-loading is set.", + "allOf": [ + { + "$ref": "#/definitions/ent.UserGroupEdges" + } + ] + }, + "group_id": { + "description": "GroupID holds the value of the \"group_id\" field.", + "type": "string" + }, + "role": { + "description": "Role holds the value of the \"role\" field.", + "allOf": [ + { + "$ref": "#/definitions/usergroup.Role" + } + ] + }, + "user_id": { + "description": "UserID holds the value of the \"user_id\" field.", + "type": "string" + } + } + }, + "ent.UserGroupEdges": { + "type": "object", + "properties": { + "group": { + "description": "Group holds the value of the group edge.", + "allOf": [ + { + "$ref": "#/definitions/ent.Group" + } + ] + }, + "user": { + "description": "User holds the value of the user edge.", + "allOf": [ + { + "$ref": "#/definitions/ent.User" + } + ] } } }, @@ -5093,6 +5175,17 @@ } } }, + "repo.FoundEntityContact": { + "type": "object", + "properties": { + "itemId": { + "type": "string" + }, + "ownerEmail": { + "type": "string" + } + } + }, "repo.Group": { "type": "object", "properties": { @@ -5644,9 +5737,6 @@ "id": { "type": "string" }, - "isOwner": { - "type": "boolean" - }, "isSuperuser": { "type": "boolean" }, @@ -5670,9 +5760,6 @@ "id": { "type": "string" }, - "isOwner": { - "type": "boolean" - }, "name": { "type": "string" } @@ -5769,7 +5856,7 @@ "TypeTime" ] }, - "user.Role": { + "usergroup.Role": { "type": "string", "enum": [ "user", @@ -5941,17 +6028,6 @@ } } }, - "v1.GroupMemberAdd": { - "type": "object", - "required": [ - "userId" - ], - "properties": { - "userId": { - "type": "string" - } - } - }, "v1.LoginForm": { "type": "object", "properties": { diff --git a/docs/public/api/swagger-2.0.yaml b/docs/public/api/swagger-2.0.yaml index decc84daa..99879febf 100644 --- a/docs/public/api/swagger-2.0.yaml +++ b/docs/public/api/swagger-2.0.yaml @@ -546,6 +546,11 @@ definitions: items: $ref: '#/definitions/ent.Tag' type: array + user_groups: + description: UserGroups holds the value of the user_groups edge. + items: + $ref: '#/definitions/ent.UserGroup' + type: array users: description: Users holds the value of the users edge. items: @@ -805,10 +810,6 @@ definitions: oidc_subject: description: OidcSubject holds the value of the "oidc_subject" field. type: string - role: - allOf: - - $ref: '#/definitions/user.Role' - description: Role holds the value of the "role" field. settings: additionalProperties: true description: Settings holds the value of the "settings" field. @@ -842,6 +843,41 @@ definitions: items: $ref: '#/definitions/ent.Notifier' type: array + user_groups: + description: UserGroups holds the value of the user_groups edge. + items: + $ref: '#/definitions/ent.UserGroup' + type: array + type: object + ent.UserGroup: + properties: + edges: + allOf: + - $ref: '#/definitions/ent.UserGroupEdges' + description: |- + Edges holds the relations/edges for other nodes in the graph. + The values are being populated by the UserGroupQuery when eager-loading is set. + group_id: + description: GroupID holds the value of the "group_id" field. + type: string + role: + allOf: + - $ref: '#/definitions/usergroup.Role' + description: Role holds the value of the "role" field. + user_id: + description: UserID holds the value of the "user_id" field. + type: string + type: object + ent.UserGroupEdges: + properties: + group: + allOf: + - $ref: '#/definitions/ent.Group' + description: Group holds the value of the group edge. + user: + allOf: + - $ref: '#/definitions/ent.User' + description: User holds the value of the user edge. type: object entityfield.Type: enum: @@ -1498,6 +1534,13 @@ definitions: required: - name type: object + repo.FoundEntityContact: + properties: + itemId: + type: string + ownerEmail: + type: string + type: object repo.Group: properties: createdAt: @@ -1865,8 +1908,6 @@ definitions: type: array id: type: string - isOwner: - type: boolean isSuperuser: type: boolean name: @@ -1882,8 +1923,6 @@ definitions: type: string id: type: string - isOwner: - type: boolean name: type: string type: object @@ -1948,7 +1987,7 @@ definitions: - TypeNumber - TypeBoolean - TypeTime - user.Role: + usergroup.Role: enum: - user - user @@ -2063,13 +2102,6 @@ definitions: required: - uses type: object - v1.GroupMemberAdd: - properties: - userId: - type: string - required: - - userId - type: object v1.LoginForm: properties: password: @@ -2848,6 +2880,42 @@ paths: summary: Update Entity Type tags: - Entity Types + /v1/found/assets/{id}: + get: + parameters: + - description: Asset ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/repo.FoundEntityContact' + summary: Get found asset contact + tags: + - Entities + /v1/found/entities/{id}: + get: + parameters: + - description: Entity ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/repo.FoundEntityContact' + summary: Get found item contact + tags: + - Entities /v1/groups: delete: produces: @@ -3020,24 +3088,6 @@ paths: summary: Get All Group Members tags: - Group - post: - parameters: - - description: User ID - in: body - name: payload - required: true - schema: - $ref: '#/definitions/v1.GroupMemberAdd' - produces: - - application/json - responses: - "204": - description: No Content - security: - - Bearer: [] - summary: Add User to Group - tags: - - Group /v1/groups/members/{user_id}: delete: parameters: diff --git a/frontend/lib/api/public.ts b/frontend/lib/api/public.ts index 513e49292..6cf1d069d 100644 --- a/frontend/lib/api/public.ts +++ b/frontend/lib/api/public.ts @@ -1,5 +1,11 @@ import { BaseAPI, route } from "./base"; -import type { APISummary, LoginForm, TokenResponse, UserRegistration } from "./types/data-contracts"; +import type { + APISummary, + FoundEntityContact, + LoginForm, + TokenResponse, + UserRegistration, +} from "./types/data-contracts"; export type StatusResult = { health: boolean; @@ -25,6 +31,21 @@ export class PublicApi extends BaseAPI { } public register(body: UserRegistration) { - return this.http.post({ url: route("/users/register"), body }); + return this.http.post({ + url: route("/users/register"), + body, + }); + } + + public foundEntityContact(id: string) { + return this.http.get({ + url: route(`/found/entities/${id}`), + }); + } + + public foundAssetContact(id: string) { + return this.http.get({ + url: route(`/found/assets/${id}`), + }); } } diff --git a/frontend/lib/api/types/data-contracts.ts b/frontend/lib/api/types/data-contracts.ts index 937db2c4a..e6fd130aa 100644 --- a/frontend/lib/api/types/data-contracts.ts +++ b/frontend/lib/api/types/data-contracts.ts @@ -930,6 +930,11 @@ export interface EntityUpdate { warrantyExpires: Date | string; } +export interface FoundEntityContact { + itemId: string; + ownerEmail: string; +} + export interface Group { createdAt: Date | string; currency: string; diff --git a/frontend/middleware/auth.ts b/frontend/middleware/auth.ts index e9f0b3753..219566892 100644 --- a/frontend/middleware/auth.ts +++ b/frontend/middleware/auth.ts @@ -1,12 +1,17 @@ -export default defineNuxtRouteMiddleware(async () => { +export default defineNuxtRouteMiddleware(async to => { const ctx = useAuthContext(); const api = useUserApi(); const redirectTo = useState("authRedirect"); if (!ctx.isAuthorized()) { - if (window.location.pathname !== "/") { + const foundPath = foundLabelPath(to.path); + if (foundPath) { + return navigateTo(foundPath); + } + + if (to.path !== "/") { console.debug("[middleware/auth] isAuthorized returned false, redirecting to /"); - redirectTo.value = window.location.pathname; + redirectTo.value = to.path; return navigateTo("/"); } } @@ -15,9 +20,9 @@ export default defineNuxtRouteMiddleware(async () => { console.log("Fetching user data"); const { data, error } = await api.user.self(); if (error) { - if (window.location.pathname !== "/") { + if (to.path !== "/") { console.debug("[middleware/user] user is null and fetch failed, redirecting to /"); - redirectTo.value = window.location.pathname; + redirectTo.value = to.path; return navigateTo("/"); } } @@ -25,3 +30,17 @@ export default defineNuxtRouteMiddleware(async () => { ctx.user = data.item; } }); + +function foundLabelPath(path: string): string | null { + const itemMatch = path.match(/^\/item\/([^/]+)/); + if (itemMatch) { + return `/found/item/${encodeURIComponent(itemMatch[1]!)}`; + } + + const assetMatch = path.match(/^\/(?:a|assets)\/([^/]+)/); + if (assetMatch) { + return `/found/asset/${encodeURIComponent(assetMatch[1]!)}`; + } + + return null; +} diff --git a/frontend/pages/found/[kind]/[id].vue b/frontend/pages/found/[kind]/[id].vue new file mode 100644 index 000000000..c721cb815 --- /dev/null +++ b/frontend/pages/found/[kind]/[id].vue @@ -0,0 +1,88 @@ + + + From 5253c095817caec08dffa4292d116d4bda61cd43 Mon Sep 17 00:00:00 2001 From: A Z Date: Sun, 10 May 2026 18:03:20 -0600 Subject: [PATCH 2/2] fix: tighten found label frontend paths --- frontend/lib/api/public.ts | 4 ++-- frontend/locales/en.json | 11 +++++++++++ frontend/middleware/auth.ts | 4 ++-- frontend/pages/found/[kind]/[id].vue | 21 ++++++++++++--------- 4 files changed, 27 insertions(+), 13 deletions(-) diff --git a/frontend/lib/api/public.ts b/frontend/lib/api/public.ts index 6cf1d069d..57604167a 100644 --- a/frontend/lib/api/public.ts +++ b/frontend/lib/api/public.ts @@ -39,13 +39,13 @@ export class PublicApi extends BaseAPI { public foundEntityContact(id: string) { return this.http.get({ - url: route(`/found/entities/${id}`), + url: route(`/found/entities/${encodeURIComponent(id)}`), }); } public foundAssetContact(id: string) { return this.http.get({ - url: route(`/found/assets/${id}`), + url: route(`/found/assets/${encodeURIComponent(id)}`), }); } } diff --git a/frontend/locales/en.json b/frontend/locales/en.json index 9fafc96cc..dc69e31a2 100644 --- a/frontend/locales/en.json +++ b/frontend/locales/en.json @@ -719,6 +719,17 @@ "templates": "Templates" }, "pages": { + "found": { + "email_owner": "Email owner", + "heading": "Did you find this item?", + "loading": "Loading contact details...", + "mail_body": "I found an item with a HomeBox label: { labelPath }", + "mail_subject": "Found item", + "sign_in": "Sign in", + "subheading": "Contact the owner directly so they can arrange its return.", + "title": "Found Item", + "unresolved": "This label could not be resolved. Sign in to open HomeBox." + }, "templates": { "no_templates": "No templates yet.", "title": "Templates" diff --git a/frontend/middleware/auth.ts b/frontend/middleware/auth.ts index 219566892..446231299 100644 --- a/frontend/middleware/auth.ts +++ b/frontend/middleware/auth.ts @@ -32,12 +32,12 @@ export default defineNuxtRouteMiddleware(async to => { }); function foundLabelPath(path: string): string | null { - const itemMatch = path.match(/^\/item\/([^/]+)/); + const itemMatch = path.match(/^\/item\/([^/]+)\/?$/); if (itemMatch) { return `/found/item/${encodeURIComponent(itemMatch[1]!)}`; } - const assetMatch = path.match(/^\/(?:a|assets)\/([^/]+)/); + const assetMatch = path.match(/^\/(?:a|assets)\/([^/]+)\/?$/); if (assetMatch) { return `/found/asset/${encodeURIComponent(assetMatch[1]!)}`; } diff --git a/frontend/pages/found/[kind]/[id].vue b/frontend/pages/found/[kind]/[id].vue index c721cb815..db3ef6cc9 100644 --- a/frontend/pages/found/[kind]/[id].vue +++ b/frontend/pages/found/[kind]/[id].vue @@ -1,4 +1,5 @@