feat(09-05): enforce scoped record lifecycle on admin routes

- Mount show, create, update, and delete behind the backend permission check
- Run controller and model hooks once per operation and roll back on failure
- Treat missing and out-of-scope records the same, including idempotent delete
This commit is contained in:
Jakub Zych
2026-09-24 19:39:08 +02:00
parent 94814d980b
commit e3e1c2546e
2 changed files with 293 additions and 9 deletions

View File

@@ -126,6 +126,14 @@ func (s *service) mount(r pact.Router) {
constrainController(g)
g.Get("/{vendor}/{plugin}/{controller}", s.list)
constrainController(g)
g.Post("/{vendor}/{plugin}/{controller}", s.create)
constrainController(g)
g.Get("/{vendor}/{plugin}/{controller}/{id}", s.show)
constrainController(g)
g.Put("/{vendor}/{plugin}/{controller}/{id}", s.update)
constrainController(g)
g.Delete("/{vendor}/{plugin}/{controller}/{id}", s.deleteRecord)
constrainController(g)
})
}
@@ -166,19 +174,100 @@ func (s *service) translator() *phrasebook.Translator {
}
func (s *service) show(w http.ResponseWriter, r *http.Request) {
WriteError(w, http.StatusInternalServerError, "error", msgServerError)
s.protect(w, r, func(cc *CompiledController) {
id, err := pathID(r)
if err != nil {
writeCRUDError(w, err)
return
}
svc, err := s.crud()
if err != nil {
WriteError(w, http.StatusInternalServerError, "error", msgServerError)
return
}
rec, err := svc.Show(r.Context(), cc, id)
if err != nil {
writeCRUDError(w, err)
return
}
WriteData(w, http.StatusOK, rec, nil)
})
}
func (s *service) create(w http.ResponseWriter, r *http.Request) {
WriteError(w, http.StatusInternalServerError, "error", msgServerError)
s.protect(w, r, func(cc *CompiledController) {
body, err := decodeObject(r)
if err != nil {
writeCRUDError(w, err)
return
}
svc, err := s.crud()
if err != nil {
WriteError(w, http.StatusInternalServerError, "error", msgServerError)
return
}
rec, err := svc.Create(r.Context(), cc, RecordInput{Body: body})
if err != nil {
writeCRUDError(w, err)
return
}
WriteData(w, http.StatusCreated, rec, nil)
})
}
func (s *service) update(w http.ResponseWriter, r *http.Request) {
WriteError(w, http.StatusInternalServerError, "error", msgServerError)
s.protect(w, r, func(cc *CompiledController) {
id, err := pathID(r)
if err != nil {
writeCRUDError(w, err)
return
}
body, err := decodeObject(r)
if err != nil {
writeCRUDError(w, err)
return
}
svc, err := s.crud()
if err != nil {
WriteError(w, http.StatusInternalServerError, "error", msgServerError)
return
}
rec, err := svc.Update(r.Context(), cc, id, RecordInput{Body: body})
if err != nil {
writeCRUDError(w, err)
return
}
WriteData(w, http.StatusOK, rec, nil)
})
}
func (s *service) deleteRecord(w http.ResponseWriter, r *http.Request) {
WriteError(w, http.StatusInternalServerError, "error", msgServerError)
s.protect(w, r, func(cc *CompiledController) {
id, err := pathID(r)
if err != nil {
writeCRUDError(w, err)
return
}
svc, err := s.crud()
if err != nil {
WriteError(w, http.StatusInternalServerError, "error", msgServerError)
return
}
result, err := svc.Delete(r.Context(), cc, id)
if err != nil {
writeCRUDError(w, err)
return
}
WriteData(w, http.StatusOK, result, nil)
})
}
func (s *service) crud() (CRUDService, error) {
db, err := s.db()
if err != nil {
return CRUDService{}, err
}
return CRUDService{DB: db}, nil
}
func (s *service) list(w http.ResponseWriter, r *http.Request) {