package useCases import ( "RenewCMS/internal/domain/audit" "RenewCMS/internal/domain/contenttype" "RenewCMS/internal/domain/role" "RenewCMS/internal/domain/user" "context" "errors" ) type ChangeUserRoleUseCase struct { users user.Repository roles role.Repository contentTypes contenttype.Repository recorder audit.Recorder } type ChangeUserRoleCommand struct { Caller ContentCaller TargetID uint32 RoleID uint32 } func NewChangeUserRoleUseCase(users user.Repository, roles role.Repository, contentTypes contenttype.Repository, recorder audit.Recorder) *ChangeUserRoleUseCase { return &ChangeUserRoleUseCase{users: users, roles: roles, contentTypes: contentTypes, recorder: recorder} } var roleChangeRefusals = []error{user.ErrRoleChanged, user.ErrUserNotFound, role.ErrRoleNotFound, role.ErrUnknownRole} func (u *ChangeUserRoleUseCase) ChangeRole(ctx context.Context, command ChangeUserRoleCommand) (DirectoryEntry, error) { target, err := u.users.Get(command.TargetID) if err != nil { return DirectoryEntry{}, err } current, held, err := u.heldRole(target) if err != nil { return DirectoryEntry{}, err } if held && current.Locked { return DirectoryEntry{}, role.ErrRoleLocked } next, err := assignableRole(u.roles, command.RoleID) if err != nil { return DirectoryEntry{}, err } if err := u.checkCoverage(command, target, current, held, next); err != nil { return DirectoryEntry{}, err } if held && current.ID == next.ID { ref := NewRoleRef(next) return userEntry(target, &ref), nil } return u.write(ctx, target, current, next) } func (u *ChangeUserRoleUseCase) heldRole(target user.User) (role.Role, bool, error) { current, err := resolveRole(u.roles, target) if errors.Is(err, role.ErrRoleNotFound) { return role.Role{}, false, nil } if err != nil { return role.Role{}, false, err } return current, true, nil } func (u *ChangeUserRoleUseCase) checkCoverage(command ChangeUserRoleCommand, target user.User, current role.Role, held bool, next role.Role) error { if target.ID == command.Caller.UserID { return role.ErrOwnRole } typeKeys, err := allowedTypeKeys(u.contentTypes) if err != nil { return err } if held { if err := ensureCoveredBy(command.Caller, typeKeys, current); err != nil { return err } } return ensureCoveredBy(command.Caller, typeKeys, next) } func (u *ChangeUserRoleUseCase) write(ctx context.Context, target user.User, current role.Role, next role.Role) (DirectoryEntry, error) { event := audit.NewEvent(audit.ActionUserChangeRole, audit.CategoryUsers) event.Target = audit.NewTarget(audit.TargetUser, target.ID, target.Username) updated, err := u.users.UpdateRole(target.ID, target.RoleID, next.ID) if errors.Is(err, role.ErrRoleNotFound) { return DirectoryEntry{}, role.ErrUnknownRole } if err != nil { u.recordWriteFailure(ctx, event, err) return DirectoryEntry{}, err } event.Params[audit.ParamFrom] = current.ID event.Params[audit.ParamTo] = next.ID event.Changes = audit.AppendTextChange(event.Changes, audit.FieldRole, current.Name, next.Name) u.recorder.Record(ctx, event) ref := NewRoleRef(next) return userEntry(updated, &ref), nil } func (u *ChangeUserRoleUseCase) recordWriteFailure(ctx context.Context, event audit.Event, err error) { for _, refusal := range roleChangeRefusals { if errors.Is(err, refusal) { return } } recordFailure(ctx, u.recorder, event, err) }