Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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[]
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
// @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 @@ -164,6 +164,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 @@ -179,6 +181,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
73 changes: 3 additions & 70 deletions backend/go.sum

Large diffs are not rendered by default.

Loading