feat(12.1-02): permissioneditor field in radio or checkbox mode
- type: permissioneditor with mode radio (1, -1) or checkbox (1); the controller serves the options per request through cabana.PermissionEditorProvider and reads and stores the values - a save answers 422 for a non-object, an unknown code or a value outside the mode's set and 403 for a changed locked code; stored codes that are not offered are kept - record responses carry the stored permissions as an object - SPA: PermissionEditorField with sections by tab, locked rows and a read-only mode for the preview - README, docs, OpenAPI document, TS types and dist updated
This commit is contained in:
@@ -4,8 +4,10 @@ import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"git.golem15.com/golem15/summercms/modules/bouncer"
|
||||
"git.golem15.com/golem15/summercms/modules/cabana"
|
||||
"git.golem15.com/golem15/summercms/modules/pact"
|
||||
)
|
||||
@@ -17,6 +19,8 @@ type Member struct {
|
||||
Name string `gorm:"column:name"`
|
||||
Slug string `gorm:"column:slug"`
|
||||
Password string `gorm:"column:password" json:"-"`
|
||||
// Permissions is a JSON object of permission code to value.
|
||||
Permissions string `gorm:"column:permissions"`
|
||||
}
|
||||
|
||||
func (Member) TableName() string { return "acme_roster_members" }
|
||||
@@ -40,6 +44,8 @@ var (
|
||||
_ pact.FormRules = MembersController{}
|
||||
_ pact.FormBeforeCreate = MembersController{}
|
||||
_ pact.FormBeforeUpdate = MembersController{}
|
||||
|
||||
_ cabana.PermissionEditorProvider = MembersController{}
|
||||
)
|
||||
|
||||
func (MembersController) ID() string { return "acme.roster.members" }
|
||||
@@ -88,6 +94,40 @@ func (MembersController) FormBeforeUpdate(ctx context.Context, model any) error
|
||||
return nil
|
||||
}
|
||||
|
||||
// AdminPermissionOptions lists the permissions the `type: permissioneditor`
|
||||
// field offers, in display order. The principal on ctx decides what is locked.
|
||||
func (MembersController) AdminPermissionOptions(ctx context.Context, field string) ([]cabana.PermissionOption, error) {
|
||||
principal, _ := bouncer.User(ctx)
|
||||
mayExport := cabana.Allows(principal, []string{"acme.roster.manage"})
|
||||
return []cabana.PermissionOption{
|
||||
{Code: "posts.edit", Label: "acme.roster::lang.permissions.posts_edit", Tab: "acme.roster::lang.permissions.tab_content"},
|
||||
{Code: "posts.publish", Label: "acme.roster::lang.permissions.posts_publish", Tab: "acme.roster::lang.permissions.tab_content"},
|
||||
{Code: "reports.export", Label: "acme.roster::lang.permissions.reports_export", Tab: "acme.roster::lang.permissions.tab_reports", Locked: !mayExport},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// AdminPermissionValues reads the permissions stored on the record.
|
||||
func (MembersController) AdminPermissionValues(_ context.Context, field string, record any) (map[string]int, error) {
|
||||
values := map[string]int{}
|
||||
if raw := record.(*Member).Permissions; raw != "" {
|
||||
if err := json.Unmarshal([]byte(raw), &values); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return values, nil
|
||||
}
|
||||
|
||||
// AdminSetPermissionValues stores the checked set on the model. The save
|
||||
// writes the row afterwards, in the same transaction.
|
||||
func (MembersController) AdminSetPermissionValues(_ context.Context, field string, record any, values map[string]int) error {
|
||||
raw, err := json.Marshal(values)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
record.(*Member).Permissions = string(raw)
|
||||
return nil
|
||||
}
|
||||
|
||||
// hashPassword stands in for the application's password hasher.
|
||||
func hashPassword(plain string) string {
|
||||
sum := sha256.Sum256([]byte(plain))
|
||||
@@ -107,9 +147,23 @@ func Example_formSeams() {
|
||||
_, inSave := cabana.VirtualFieldsFromContext(ctx)
|
||||
err := ctl.FormBeforeCreate(ctx, member)
|
||||
fmt.Println(inSave, err, member.Password == "")
|
||||
|
||||
// The permission editor: three options, the last one locked for an
|
||||
// administrator without acme.roster.manage (here: nobody is signed in).
|
||||
options, _ := ctl.AdminPermissionOptions(ctx, "permissions")
|
||||
for _, option := range options {
|
||||
fmt.Println(option.Code, option.Locked)
|
||||
}
|
||||
_ = ctl.AdminSetPermissionValues(ctx, "permissions", member, map[string]int{"posts.edit": 1, "posts.publish": -1})
|
||||
values, _ := ctl.AdminPermissionValues(ctx, "permissions", member)
|
||||
fmt.Println(member.Permissions, len(values))
|
||||
// Output:
|
||||
// [password password_confirmation notify]
|
||||
// create: required|between:8,255|confirmed
|
||||
// update: nullable|between:8,255|confirmed
|
||||
// false <nil> true
|
||||
// posts.edit false
|
||||
// posts.publish false
|
||||
// reports.export true
|
||||
// {"posts.edit":1,"posts.publish":-1} 2
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user