feat(12.1-01): cabana.ForbiddenError answers a refused write with 403
- hooks and bulk, record, toolbar and widget actions may return it - 403 forbidden with the localized message and field details; the write's transaction is rolled back; other errors stay the opaque 500 - form shows a refused save as a persistent banner and keeps the values; a refused delete is a toast - smoke tests, OpenAPI notes, dist, README, docs
This commit is contained in:
@@ -178,6 +178,30 @@ func (c rosterController) ListRowStates(ctx context.Context, db *gorm.DB, record
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// rosterLocked is the sentinel name of a person the roster's actions refuse.
|
||||
const rosterLocked = "Locked"
|
||||
|
||||
// rosterRefused is a shared refusal value: the framework must localize a
|
||||
// copy and never write into it.
|
||||
var rosterRefused = &cabana.ForbiddenError{Message: "acme.roster::lang.people.locked"}
|
||||
|
||||
// FormBeforeUpdate refuses the reserved name with a ForbiddenError naming
|
||||
// the field, and fails with a plain error for the name Boom.
|
||||
func (rosterController) FormBeforeUpdate(_ context.Context, model any) error {
|
||||
switch model.(*rosterPerson).Name {
|
||||
case "Reserved":
|
||||
return &cabana.ForbiddenError{
|
||||
Message: "acme.roster::lang.people.refused",
|
||||
Details: map[string]any{"name": []string{"acme.roster::lang.people.refused_name"}},
|
||||
}
|
||||
case "Silent":
|
||||
return &cabana.ForbiddenError{}
|
||||
case "Boom":
|
||||
return fmt.Errorf("the roster database said hunter2")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FormAfterDelete removes the person for good inside the delete's
|
||||
// transaction: the list keeps soft-deleted people, so deleting one there is
|
||||
// permanent.
|
||||
@@ -226,6 +250,11 @@ func (c rosterController) AdminBulkActions() []pact.AdminBulkAction {
|
||||
return pact.AdminBulkActionResult{}, fmt.Errorf("no transaction on the context")
|
||||
}
|
||||
for _, record := range in.Records {
|
||||
// A refusal after earlier rows were written: the whole
|
||||
// selection must roll back.
|
||||
if record.(*rosterPerson).Name == rosterLocked {
|
||||
return pact.AdminBulkActionResult{}, rosterRefused
|
||||
}
|
||||
if err := tx.Delete(record).Error; err != nil {
|
||||
return pact.AdminBulkActionResult{}, err
|
||||
}
|
||||
@@ -270,6 +299,10 @@ func (c rosterController) AdminRecordActions() []pact.AdminRecordAction {
|
||||
if err := tx.Unscoped().Model(in.Record).Update("banned", false).Error; err != nil {
|
||||
return pact.AdminRecordActionResult{}, err
|
||||
}
|
||||
// Refused after the write: the transaction must roll it back.
|
||||
if in.Record.(*rosterPerson).Name == rosterLocked {
|
||||
return pact.AdminRecordActionResult{}, rosterRefused
|
||||
}
|
||||
return pact.AdminRecordActionResult{}, nil
|
||||
},
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user