package useCases import ( "RenewCMS/internal/domain/audit" "RenewCMS/internal/domain/contenttype" "RenewCMS/internal/domain/role" "RenewCMS/internal/domain/user" "context" "time" ) type UpdateRoleUseCase struct { roles role.Repository users user.Repository contentTypes contenttype.Repository recorder audit.Recorder now func() time.Time viewer roleViewer } type UpdateRoleCommand struct { Caller ContentCaller ID uint32 Name string Description string Grants []role.Grant Version uint32 } type ResetRoleCommand struct { Caller ContentCaller ID uint32 Version uint32 } func NewUpdateRoleUseCase(roles role.Repository, users user.Repository, contentTypes contenttype.Repository, recorder audit.Recorder, now func() time.Time) *UpdateRoleUseCase { return &UpdateRoleUseCase{roles: roles, users: users, contentTypes: contentTypes, recorder: recorder, now: now, viewer: roleViewer{roles, users, contentTypes}} } func (u *UpdateRoleUseCase) Update(ctx context.Context, command UpdateRoleCommand) (RoleView, error) { target, err := u.roles.Get(command.ID) if err != nil { return RoleView{}, err } if err := u.checkTarget(command, target); err != nil { return RoleView{}, err } name, err := role.NormalizeName(command.Name) if err != nil { return RoleView{}, err } if err := validateRoleGrants(u.contentTypes, command.Grants); err != nil { return RoleView{}, err } after := target after.Name = name after.Description = command.Description after.Grants = normalizedGrants(command.Grants) if err := ensureCovered(command.Caller, u.contentTypes, after); err != nil { return RoleView{}, err } if err := ensureNameFree(u.roles, name, target.ID); err != nil { return RoleView{}, err } return u.write(ctx, command.Caller, target, after) } func (u *UpdateRoleUseCase) Reset(ctx context.Context, command ResetRoleCommand) (RoleView, error) { target, err := u.roles.Get(command.ID) if err != nil { return RoleView{}, err } if err := ensureEditable(target); err != nil { return RoleView{}, err } if !target.BuiltIn { return RoleView{}, role.ErrNotBuiltIn } if err := requireOwnerOnlyEditing(command.Caller); err != nil { return RoleView{}, err } if command.Version != target.Version { return RoleView{}, role.ErrRoleConflict } seeded, found := seededRole(target.Name) if !found { return RoleView{}, role.ErrNotBuiltIn } after := target after.Description = seeded.Description after.Grants = seeded.EffectiveGrants() return u.write(ctx, command.Caller, target, after) } func (u *UpdateRoleUseCase) checkTarget(command UpdateRoleCommand, target role.Role) error { if err := ensureEditable(target); err != nil { return err } if target.BuiltIn { if err := requireOwnerOnlyEditing(command.Caller); err != nil { return err } if renamesBuiltIn(command.Name, target) { return role.ErrBuiltInName } } if err := ensureNotOwnRole(u.roles, u.users, command.Caller, target); err != nil { return err } if err := ensureCovered(command.Caller, u.contentTypes, target); err != nil { return err } if command.Version != target.Version { return role.ErrRoleConflict } return nil } func (u *UpdateRoleUseCase) write(ctx context.Context, caller ContentCaller, before role.Role, after role.Role) (RoleView, error) { members, err := u.viewer.members(before.ID) if err != nil { return RoleView{}, err } live, err := u.viewer.liveGrants(before.EffectiveGrants()) if err != nil { return RoleView{}, err } diff := role.DiffGrants(live, after.Grants) if diff.Empty() && before.Name == after.Name && before.Description == after.Description { return u.viewer.one(before, members) } event := audit.NewEvent(audit.ActionRoleEdit, audit.CategoryRoles) event.Target = roleTarget(before) editor := caller.UserID after.UpdatedAt = u.now().UTC() after.UpdatedBy = &editor saved, err := u.roles.Save(after) if err != nil { recordRoleWriteFailure(ctx, u.recorder, event, err) return RoleView{}, err } event.Target = roleTarget(saved) event.Params = grantsParams(diff, members) event.Changes = audit.AppendTextChange(event.Changes, audit.FieldName, before.Name, saved.Name) event.Changes = audit.AppendTextChange(event.Changes, audit.FieldDescription, before.Description, saved.Description) u.recorder.Record(ctx, event) return u.viewer.one(saved, members) } func requireOwnerOnlyEditing(caller ContentCaller) error { if !caller.Grants.Holds(role.KeyRolesEditOwnerOnly) { return role.MissingPermissionError{Key: role.KeyRolesEditOwnerOnly} } return nil } func renamesBuiltIn(requested string, target role.Role) bool { trimmed, err := role.NormalizeName(requested) return err != nil || trimmed != target.Name } func seededRole(name string) (role.Role, bool) { for _, seeded := range role.BuiltInRoles() { if seeded.Name == name { return seeded, true } } return role.Role{}, false }