Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions backend/app/api/handlers/v1/v1_ctrl_maintenance_plan.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package v1

import (
"net/http"

"github.com/google/uuid"
"github.com/hay-kot/httpkit/errchain"
"github.com/sysadminsmedia/homebox/backend/internal/core/services"
"github.com/sysadminsmedia/homebox/backend/internal/data/repo"
"github.com/sysadminsmedia/homebox/backend/internal/web/adapters"
)

// HandleMaintenancePlanGetAll godoc
//
// @Summary Query Maintenance Plans
// @Tags Item Maintenance
// @Produce json
// @Param id path string true "Item ID"
// @Success 200 {array} repo.MaintenancePlan
// @Router /v1/entities/{id}/maintenance/plans [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleMaintenancePlanGetAll() errchain.HandlerFunc {
fn := func(r *http.Request, itemID uuid.UUID, _ struct{}) ([]repo.MaintenancePlan, error) {
auth := services.NewContext(r.Context())
return ctrl.repo.MaintEntry.ListPlansByItemID(auth, auth.GID, itemID)
}

return adapters.QueryID("id", fn, http.StatusOK)
}

// HandleMaintenancePlanCreate godoc
//
// @Summary Create Maintenance Plan
// @Tags Item Maintenance
// @Produce json
// @Param id path string true "Item ID"
// @Param payload body repo.MaintenancePlanCreate true "Plan Data"
// @Success 201 {object} repo.MaintenancePlan
// @Router /v1/entities/{id}/maintenance/plans [POST]
// @Security Bearer
func (ctrl *V1Controller) HandleMaintenancePlanCreate() errchain.HandlerFunc {
fn := func(r *http.Request, itemID uuid.UUID, body repo.MaintenancePlanCreate) (repo.MaintenancePlan, error) {
auth := services.NewContext(r.Context())
return ctrl.repo.MaintEntry.CreatePlan(auth, itemID, body)
}

return adapters.ActionID("id", fn, http.StatusCreated)
}
Comment on lines +41 to +48

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm no other call site invokes Validate() on these DTOs (to be sure the handler is the only entry point).
rg -nP '\b(MaintenancePlanCreate|MaintenancePlanUpdate)\b' --type=go -g '!**/vendor/**'
rg -nP '\.Validate\(\)' --type=go -g '!**/vendor/**' | rg -i 'maintenance'

Repository: sysadminsmedia/homebox

Length of output: 13038


🏁 Script executed:

#!/bin/bash
# Check the actual Validate() implementations
sed -n '56,75p' backend/internal/data/repo/repo_maintenance_plan.go
echo "---"
sed -n '64,75p' backend/internal/data/repo/repo_maintenance_plan.go
echo "---"
# Check the full handler file
cat -n backend/app/api/handlers/v1/v1_ctrl_maintenance_plan.go | head -75

Repository: sysadminsmedia/homebox

Length of output: 4032


🏁 Script executed:

#!/bin/bash
# Check the CreatePlan and UpdatePlan implementations
sed -n '125,160p' backend/internal/data/repo/repo_maintenance_plan.go
echo "---"
# Check if validate package is used in other handlers
rg -n 'validate\.' backend/app/api/handlers/v1/ --type=go | head -20
echo "---"
# Check if CreatePlan calls Validate()
rg -A10 'func.*CreatePlan' backend/internal/data/repo/repo_maintenance_plan.go | head -20

Repository: sysadminsmedia/homebox

Length of output: 4020


🏁 Script executed:

#!/bin/bash
# Check coding guidelines for handler validation requirements
grep -n "Validate\|validation\|validate" backend/app/api/handlers/v1/*.go | grep -i "pattern\|should\|must" | head -10
echo "---"
# Check other handlers with body validation to see the pattern
rg -B3 'body\.Validate' backend/app/api/handlers/v1/ --type=go | head -30

Repository: sysadminsmedia/homebox

Length of output: 70


🏁 Script executed:

#!/bin/bash
# Check if any handler calls Validate() on body
rg 'body\.Validate\(\)' backend/app/api/handlers/ --type=go
echo "---"
# Check if CreatePlan/UpdatePlan methods have any validation despite not calling Validate()
sed -n '125,175p' backend/internal/data/repo/repo_maintenance_plan.go

Repository: sysadminsmedia/homebox

Length of output: 1551


Call body.Validate() in both handlers before passing to repository methods.

MaintenancePlanCreate.Validate() and MaintenancePlanUpdate.Validate() enforce critical constraints (IntervalValue >= 1, known IntervalUnit), but neither handler invokes them. Invalid input bypasses these checks and flows directly into the repository layer, where only Ent's enum validation provides defense at best. A client posting {"intervalValue":0} will not be caught.

Affected handlers and fix

HandleMaintenancePlanCreate (lines 42–47):

 	fn := func(r *http.Request, itemID uuid.UUID, body repo.MaintenancePlanCreate) (repo.MaintenancePlan, error) {
+		if err := body.Validate(); err != nil {
+			return repo.MaintenancePlan{}, validate.NewRequestError(err, http.StatusBadRequest)
+		}
 		auth := services.NewContext(r.Context())
 		return ctrl.repo.MaintEntry.CreatePlan(auth, itemID, body)
 	}

HandleMaintenancePlanUpdate (lines 61–66): Apply the same pattern.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/api/handlers/v1/v1_ctrl_maintenance_plan.go` around lines 41 -
48, Both HandleMaintenancePlanCreate and HandleMaintenancePlanUpdate currently
forward the incoming body to the repo without running its validation; call
body.Validate() at the start of each handler (in HandleMaintenancePlanCreate's
fn and in HandleMaintenancePlanUpdate's fn) and if it returns an error, return
that error immediately instead of calling ctrl.repo.MaintEntry.CreatePlan /
UpdatePlan so invalid payloads (e.g., IntervalValue < 1 or unknown IntervalUnit)
are rejected before reaching the repository.


// HandleMaintenancePlanUpdate godoc
//
// @Summary Update Maintenance Plan
// @Tags Maintenance
// @Produce json
// @Param id path string true "Plan ID"
// @Param payload body repo.MaintenancePlanUpdate true "Plan Data"
// @Success 200 {object} repo.MaintenancePlan
// @Router /v1/maintenance/plans/{id} [PUT]
// @Security Bearer
func (ctrl *V1Controller) HandleMaintenancePlanUpdate() errchain.HandlerFunc {
fn := func(r *http.Request, planID uuid.UUID, body repo.MaintenancePlanUpdate) (repo.MaintenancePlan, error) {
auth := services.NewContext(r.Context())
return ctrl.repo.MaintEntry.UpdatePlan(auth, planID, body)
}

return adapters.ActionID("id", fn, http.StatusOK)
}

// HandleMaintenancePlanDelete godoc
//
// @Summary Delete Maintenance Plan
// @Tags Maintenance
// @Produce json
// @Param id path string true "Plan ID"
// @Success 204
// @Router /v1/maintenance/plans/{id} [DELETE]
// @Security Bearer
func (ctrl *V1Controller) HandleMaintenancePlanDelete() errchain.HandlerFunc {
fn := func(r *http.Request, planID uuid.UUID) (any, error) {
auth := services.NewContext(r.Context())
return nil, ctrl.repo.MaintEntry.DeletePlan(auth, planID)
}

return adapters.CommandID("id", fn, http.StatusNoContent)
}
4 changes: 4 additions & 0 deletions backend/app/api/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,8 @@ func (a *app) mountRoutes(r *chi.Mux, chain *errchain.ErrChain, repos *repo.AllR
// Entity maintenance endpoints
r.Get("/entities/{id}/maintenance", chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceLogGet(), userMW...))
r.Post("/entities/{id}/maintenance", chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceEntryCreate(), userMW...))
r.Get("/entities/{id}/maintenance/plans", chain.ToHandlerFunc(v1Ctrl.HandleMaintenancePlanGetAll(), userMW...))
r.Post("/entities/{id}/maintenance/plans", chain.ToHandlerFunc(v1Ctrl.HandleMaintenancePlanCreate(), userMW...))

r.Get("/assets/{id}", chain.ToHandlerFunc(v1Ctrl.HandleAssetGet(), userMW...))

Expand All @@ -209,6 +211,8 @@ func (a *app) mountRoutes(r *chi.Mux, chain *errchain.ErrChain, repos *repo.AllR
r.Get("/maintenance", chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceGetAll(), userMW...))
r.Put("/maintenance/{id}", chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceEntryUpdate(), userMW...))
r.Delete("/maintenance/{id}", chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceEntryDelete(), userMW...))
r.Put("/maintenance/plans/{id}", chain.ToHandlerFunc(v1Ctrl.HandleMaintenancePlanUpdate(), userMW...))
r.Delete("/maintenance/plans/{id}", chain.ToHandlerFunc(v1Ctrl.HandleMaintenancePlanDelete(), userMW...))

// Notifiers
r.Get("/notifiers", chain.ToHandlerFunc(v1Ctrl.HandleGetUserNotifiers(), userMW...))
Expand Down
18 changes: 0 additions & 18 deletions backend/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -143,12 +143,6 @@ github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1x
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/clipperhouse/displaywidth v0.6.2 h1:ZDpTkFfpHOKte4RG5O/BOyf3ysnvFswpyYrV7z2uAKo=
github.com/clipperhouse/displaywidth v0.6.2/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik=
Expand Down Expand Up @@ -390,14 +384,6 @@ github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOF
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/olahol/melody v1.4.0 h1:Pa5SdeZL/zXPi1tJuMAPDbl4n3gQOThSL6G1p4qZ4SI=
github.com/olahol/melody v1.4.0/go.mod h1:GgkTl6Y7yWj/HtfD48Q5vLKPVoZOH+Qqgfa7CvJgJM4=
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
github.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0 h1:jrYnow5+hy3WRDCBypUFvVKNSPPCdqgSXIE9eJDD8LM=
github.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0/go.mod h1:b52bVQRRPObe+yyBl0TxNfhesL0nedD4Cht0/zx55Ew=
github.com/olekukonko/tablewriter v1.1.3 h1:VSHhghXxrP0JHl+0NnKid7WoEmd9/urKRJLysb70nnA=
github.com/olekukonko/tablewriter v1.1.3/go.mod h1:9VU0knjhmMkXjnMKrZ3+L2JhhtsQ/L38BbL3CRNE8tM=
github.com/onsi/ginkgo/v2 v2.9.2 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU=
github.com/onsi/ginkgo/v2 v2.9.2/go.mod h1:WHcJJG2dIlcCqVfBAwUCrJxSPFb6v4azBwgxeMeDuts=
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
Expand Down Expand Up @@ -445,10 +431,6 @@ github.com/shirou/gopsutil/v4 v4.26.4 h1:B4SXVbcwTyrocPHEmWBC4uCYr4Xcu3MK1TXqbpr
github.com/shirou/gopsutil/v4 v4.26.4/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo=
github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
Expand Down
Loading
Loading