feat: the author edits a material in the app
- PUT /_bm/api/v1/materials/{id} (jwt.auth only, no token twin): every
key of the edit body required, revision checked on the row locked FOR
UPDATE, 409 with the student detail of the server version
- editableMaterialForCaller: 404 for what the caller cannot see, 403 for
what they see but may not manage
- tags from the dictionary only (at most 4), http(s) urls, the body
normalised as bm-notatka push does; course, author and publication kept
- loadMaterialDetail shared by GET /materials/{id} and the edit answers
This commit is contained in:
@@ -15,9 +15,11 @@ import (
|
||||
"git.golem15.com/golem15/summercms/modules/cabana"
|
||||
"git.golem15.com/golem15/summercms/modules/lagoon"
|
||||
"git.golem15.com/golem15/summercms/modules/surf"
|
||||
"git.golem15.com/golem15/summercms/modules/wire"
|
||||
"git.golem15.com/jakub/sm-bm-plugin/controllers"
|
||||
"git.golem15.com/jakub/sm-bm-plugin/models"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
// Private materials: a material with is_private is visible only to its
|
||||
@@ -229,6 +231,80 @@ func (h *Handlers) managedMaterialForCaller(w http.ResponseWriter, r *http.Reque
|
||||
return m, true
|
||||
}
|
||||
|
||||
// writeForbidden answers 403 to a caller who may see a resource but not
|
||||
// change it.
|
||||
func writeForbidden(w http.ResponseWriter) {
|
||||
wire.WriteJSON(w, http.StatusForbidden, map[string]any{"error": true, "message": "Forbidden."})
|
||||
}
|
||||
|
||||
// editableMaterialForCaller loads the material of the {id} parameter for the
|
||||
// author's routes (edit, unpublish, image upload). Unlike
|
||||
// managedMaterialForCaller it tells "not yours" from "not there" (D-07): a
|
||||
// material the caller cannot see (private and not theirs, a draft, not
|
||||
// published yet, unknown) is a 404, and one they see but may not manage (no
|
||||
// jz.bm.access_materials, or another author) a 403. Both helpers stay: the
|
||||
// access panel keeps its 404. It answers itself and returns ok false then;
|
||||
// manager tells whether the caller holds jz.bm.access_materials, for the
|
||||
// re-check on the locked row.
|
||||
func (h *Handlers) editableMaterialForCaller(w http.ResponseWriter, r *http.Request, db *gorm.DB, userID uint) (models.Material, bool, bool) {
|
||||
var m models.Material
|
||||
id, ok := surf.IntParam(r, "id")
|
||||
if !ok {
|
||||
writeNotFound(w)
|
||||
return m, false, false
|
||||
}
|
||||
res := visibleMaterials(db, h.now(), userID).Where("jz_bm_materials.id = ?", id).Limit(1).Find(&m)
|
||||
if res.Error != nil {
|
||||
writeOpaque500(w)
|
||||
return m, false, false
|
||||
}
|
||||
if res.RowsAffected == 0 {
|
||||
writeNotFound(w)
|
||||
return m, false, false
|
||||
}
|
||||
manager, err := h.managesMaterials(r.Context(), db, userID)
|
||||
if err != nil {
|
||||
writeOpaque500(w)
|
||||
return m, false, false
|
||||
}
|
||||
if !canManageMaterial(m, userID, manager) {
|
||||
writeForbidden(w)
|
||||
return m, manager, false
|
||||
}
|
||||
return m, manager, true
|
||||
}
|
||||
|
||||
// editOutcome is what lockEditableMaterial found on the locked row.
|
||||
type editOutcome int
|
||||
|
||||
const (
|
||||
editAllowed editOutcome = iota
|
||||
// editGone: the material is no longer visible to the caller.
|
||||
editGone
|
||||
// editForbidden: the caller may no longer manage it.
|
||||
editForbidden
|
||||
)
|
||||
|
||||
// lockEditableMaterial re-loads material id inside tx, locked FOR UPDATE,
|
||||
// and re-checks that the caller still sees it and may manage it, so a
|
||||
// concurrent access or publication change cannot slip between the check
|
||||
// of editableMaterialForCaller and the write.
|
||||
func (h *Handlers) lockEditableMaterial(tx *gorm.DB, userID, id uint, manager bool) (models.Material, editOutcome, error) {
|
||||
var m models.Material
|
||||
res := visibleMaterials(tx.Clauses(clause.Locking{Strength: "UPDATE"}), h.now(), userID).
|
||||
Where("jz_bm_materials.id = ?", id).Limit(1).Find(&m)
|
||||
if res.Error != nil {
|
||||
return m, editGone, res.Error
|
||||
}
|
||||
if res.RowsAffected == 0 {
|
||||
return m, editGone, nil
|
||||
}
|
||||
if !canManageMaterial(m, userID, manager) {
|
||||
return m, editForbidden, nil
|
||||
}
|
||||
return m, editAllowed, nil
|
||||
}
|
||||
|
||||
// writeMaterialAccess answers the access of m as the caller sees it.
|
||||
func writeMaterialAccess(w http.ResponseWriter, db *gorm.DB, m models.Material, userID uint) {
|
||||
shares, err := materialShareRefs(db, []uint{m.ID})
|
||||
|
||||
Reference in New Issue
Block a user