feat: User Groups admin screen with the users count and privileged codes protected

- controller golem15.user.usergroups behind golem15.users.access_groups
- list with users_count from the list's own query; form with the checkbox
  permission editor stored in user_groups.permissions
- creating a group with a privileged code, changing a code to or from one and
  deleting a privileged group need golem15.users.manage_privileged_groups
- a group delete removes its users_groups rows in the same transaction
- UserGroup gains Fillable, Rules and the read-only UsersCount; PermissionSet.Text
- TestAdminGroups and the D-06 matrix in TestAdminPrivilegedGroups
This commit is contained in:
Jakub Zych
2026-10-05 13:52:26 +02:00
parent 9611708792
commit a3992879db
15 changed files with 987 additions and 1 deletions

View File

@@ -92,6 +92,18 @@ func permissionInt(value any) (int, bool) {
return n, true
}
// Text returns the set as the text of a permission column that is a plain
// nullable string on its model (user_groups.permissions): the JSON object
// Value writes, or nil for a set without a non-zero value.
func (p PermissionSet) Text() *string {
value, err := p.Value()
text, ok := value.(string)
if err != nil || !ok {
return nil
}
return &text
}
// Value implements driver.Valuer: NULL for a set without a non-zero value,
// else a JSON object of integers with the zero values left out.
func (p PermissionSet) Value() (driver.Value, error) {

View File

@@ -4,7 +4,8 @@ import "time"
// UserGroup is a row of user_groups, the PHP UserGroup model. Membership
// lives in the users_groups pivot (User.Groups). Permissions is the raw
// jsonable text column.
// jsonable text column; ParsePermissionSet reads it and PermissionSet.Text
// writes it.
type UserGroup struct {
ID uint `gorm:"column:id;primaryKey"`
Name string `gorm:"column:name"`
@@ -13,6 +14,11 @@ type UserGroup struct {
Permissions *string `gorm:"column:permissions"`
CreatedAt *time.Time `gorm:"column:created_at"`
UpdatedAt *time.Time `gorm:"column:updated_at"`
// UsersCount is the number of members, the PHP users_count relation. It
// is not a column of user_groups: only a query that selects an expression
// named users_count fills it (the admin list does), it is never written,
// and it is zero everywhere else.
UsersCount int64 `gorm:"column:users_count;->" json:"-"`
}
func (UserGroup) TableName() string { return "user_groups" }
@@ -24,6 +30,21 @@ const (
GroupRegistered = "registered"
)
// Fillable is the field list an admin save may set. Permissions is written
// through the permission editor, not by a fill.
func (UserGroup) Fillable() []string {
return []string{"name", "code", "description"}
}
// Rules is the PHP UserGroup rule set without its regex on code, which the
// rule engine does not know: the admin controller checks the code's format.
func (UserGroup) Rules() map[string]string {
return map[string]string{
"name": "required|between:3,64",
"code": "required|unique:user_groups",
}
}
func init() {
Register(UserGroup{})
}

View File

@@ -0,0 +1,15 @@
columns:
id:
label: golem15.user::lang.group.id
invisible: true
name:
label: golem15.user::lang.group.name
searchable: true
code:
label: golem15.user::lang.group.code
created_at:
label: golem15.user::lang.group.created_at
type: datetime
users_count:
label: golem15.user::lang.group.users_count
sortable: false

View File

@@ -0,0 +1,21 @@
fields:
name:
label: golem15.user::lang.group.name
type: text
span: left
required: true
code:
label: golem15.user::lang.group.code
comment: golem15.user::lang.group.code_comment
type: text
span: right
preset: name
description:
label: golem15.user::lang.group.description_field
type: textarea
size: tiny
permissions:
type: permissioneditor
mode: checkbox
span: full
tab: golem15.user::lang.user.permissions_tab