feat: /v1/role list, get, create, update and delete. Reassign members on delete. Write-time validation (ValidateGrants, duplicates, typed keys against real content types). Name uniqueness (role_name_taken), role_locked, role_has_members, role_conflict on version mismatch. No-escalation: nobody grants a permission they don't hold, and roles.edit_owner_only guards the Owner role. Activity events for role changes. Typed-key exposure in /auth/me. Cleanup of typed rows when a content type is deleted. | Backend only. Will carry the deferred ListGrants decision.
This commit is contained in:
1 parent
a043f7f41a
commit
094df63a7f
51 files changed
+4822
-115
No files matched your search
@@ -0,0 +1,143 @@
|
||||
package useCases
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/domain/contenttype"
|
||||
"RenewCMS/internal/domain/role"
|
||||
"RenewCMS/internal/domain/user"
|
||||
"errors"
|
||||
"slices"
|
||||
"time"
|
||||
)
|
||||
|
||||
type RoleEditor struct {
|
||||
ID uint32 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type RoleView struct {
|
||||
role.Role
|
||||
Members int `json:"members"`
|
||||
UpdatedAt *time.Time `json:"updated_at"`
|
||||
UpdatedBy *RoleEditor `json:"updated_by"`
|
||||
}
|
||||
|
||||
type roleViewer struct {
|
||||
roles role.Repository
|
||||
users user.Repository
|
||||
contentTypes contenttype.Repository
|
||||
}
|
||||
|
||||
func (v roleViewer) liveGrants(grants []role.Grant) ([]role.Grant, error) {
|
||||
typeKeys, err := allowedTypeKeys(v.contentTypes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
live := make([]role.Grant, 0, len(grants))
|
||||
for _, grant := range grants {
|
||||
if _, typeKey, parseErr := role.ParseTypedKey(grant.Key); parseErr == nil && !slices.Contains(typeKeys, typeKey) {
|
||||
continue
|
||||
}
|
||||
live = append(live, grant)
|
||||
}
|
||||
return live, nil
|
||||
}
|
||||
|
||||
func (v roleViewer) views(found []role.Role) ([]RoleView, error) {
|
||||
counts, err := memberCounts(v.roles, v.users.GetAll())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
editors, err := v.editors(found)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
views := make([]RoleView, 0, len(found))
|
||||
for _, item := range found {
|
||||
view := toRoleView(item, counts[item.ID], editors)
|
||||
if view.Grants, err = v.liveGrants(view.Grants); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
views = append(views, view)
|
||||
}
|
||||
return views, nil
|
||||
}
|
||||
|
||||
func (v roleViewer) one(found role.Role, members int) (RoleView, error) {
|
||||
editors, err := v.editors([]role.Role{found})
|
||||
if err != nil {
|
||||
return RoleView{}, err
|
||||
}
|
||||
view := toRoleView(found, members, editors)
|
||||
if view.Grants, err = v.liveGrants(view.Grants); err != nil {
|
||||
return RoleView{}, err
|
||||
}
|
||||
return view, nil
|
||||
}
|
||||
|
||||
func (v roleViewer) members(roleID uint32) (int, error) {
|
||||
counts, err := memberCounts(v.roles, v.users.GetAll())
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return counts[roleID], nil
|
||||
}
|
||||
|
||||
func (v roleViewer) editors(found []role.Role) (map[uint32]string, error) {
|
||||
ids := []uint32{}
|
||||
for _, item := range found {
|
||||
if item.UpdatedBy != nil {
|
||||
ids = append(ids, *item.UpdatedBy)
|
||||
}
|
||||
}
|
||||
names := map[uint32]string{}
|
||||
if len(ids) == 0 {
|
||||
return names, nil
|
||||
}
|
||||
editors, err := v.users.FindByIDs(ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, editor := range editors {
|
||||
names[editor.ID] = editor.NameOrUsername()
|
||||
}
|
||||
return names, nil
|
||||
}
|
||||
|
||||
func toRoleView(found role.Role, members int, editors map[uint32]string) RoleView {
|
||||
view := RoleView{Role: found, Members: members}
|
||||
view.Grants = found.EffectiveGrants()
|
||||
if !found.UpdatedAt.IsZero() {
|
||||
stamp := found.UpdatedAt.UTC()
|
||||
view.UpdatedAt = &stamp
|
||||
}
|
||||
if found.UpdatedBy != nil {
|
||||
if name, present := editors[*found.UpdatedBy]; present {
|
||||
view.UpdatedBy = &RoleEditor{ID: *found.UpdatedBy, Name: name}
|
||||
}
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
func memberCounts(roles role.Repository, accounts []user.User) (map[uint32]int, error) {
|
||||
counts := map[uint32]int{}
|
||||
legacyIDs := map[user.Role]uint32{}
|
||||
for _, account := range accounts {
|
||||
id := account.RoleID
|
||||
if id == 0 {
|
||||
resolved, cached := legacyIDs[account.Role]
|
||||
if !cached {
|
||||
found, err := resolveRole(roles, account)
|
||||
if err != nil && !errors.Is(err, role.ErrRoleNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
resolved = found.ID
|
||||
legacyIDs[account.Role] = resolved
|
||||
}
|
||||
id = resolved
|
||||
}
|
||||
if id != 0 {
|
||||
counts[id]++
|
||||
}
|
||||
}
|
||||
return counts, nil
|
||||
}
|
||||
Reference in new issue
Block a user