# `PhoenixKit.Users.Roles`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v3.0.0/lib/phoenix_kit/users/roles.ex#L1)

API for managing user roles in PhoenixKit.

This module provides functions for assigning, removing, and querying user roles.
It works with the role system to provide authorization capabilities.

## Role Management

- Assign and remove roles from users
- Query users by role
- Check user permissions
- Bulk role operations

## Who may change roles

Every entry point that gives a role, takes one away or deletes one accepts an
`:actor` (a `%User{}` or a `%Scope{}`; omitted or `:system` = unrestricted, for
seeding and system callers). With a real actor the rule is
`can_manage_role?/2`: an Owner may do anything; anyone else only for a role
that is neither Owner nor Admin and whose every permission key they hold
themselves (a role holding `"*"` only by a holder of `"*"`). Nobody but an
Owner changes their own roles (`{:error, :cannot_modify_own_roles}`).

## System Roles

PhoenixKit includes three built-in system roles:

- **Owner**: System owner with full access (assigned automatically to first user)
- **Admin**: Administrator with elevated privileges
- **User**: Standard user with basic access (default for new users)

## Examples

    # Check if user has a role
    iex> user_has_role?(user, "Admin")
    true

    # Get all user roles
    iex> get_user_roles(user)
    ["Admin", "User"]

    # Assign a role to user
    iex> assign_role(user, "Admin")
    {:ok, %RoleAssignment{}}

    # Get all users with a specific role
    iex> users_with_role("Admin")
    [%User{}, %User{}]

# `assign_role`

Assigns a role to a user.

## Security

The Owner role cannot be assigned through this function to maintain system security.
Only one Owner is automatically assigned during initialization.

## Parameters

- `user`: The user to assign the role to
- `role_name`: The name of the role to assign
- `assigned_by` (optional): The user who is assigning the role
- `opts`: `:broadcast` (default `true`); `:actor` — the acting `%User{}` or
  `%Scope{}`, checked against `can_manage_role?/2` and the own-roles rule
  (`{:error, :not_permitted}` / `{:error, :cannot_modify_own_roles}`).
  Omitted = a system caller, unchecked.

## Examples

    iex> assign_role(user, "Admin")
    {:ok, %RoleAssignment{}}

    iex> assign_role(user, "Admin", assigned_by_user)
    {:ok, %RoleAssignment{}}

    iex> assign_role(user, "Owner")
    {:error, :owner_role_protected}

    iex> assign_role(user, "NonexistentRole")
    {:error, :role_not_found}

# `assign_roles_to_existing_users`

Assigns roles to existing users who don't have any PhoenixKit roles.

This is useful for migration scenarios where PhoenixKit is installed
into an existing application with users.

## Parameters

- `opts`: Options for role assignment
  - `:make_first_owner` (default: true) - Make first user without roles an Owner

## Returns

- `{:ok, stats}` with assignment statistics
- `{:error, reason}` on failure

## Examples

    iex> assign_roles_to_existing_users()
    {:ok, %{assigned_owner: 1, assigned_users: 5, total_processed: 6}}

# `can_deactivate_user?`

Checks if user can be deactivated safely.

Prevents deactivation of last active Owner.

## Parameters

- `user`: User to check for deactivation

## Examples

    iex> can_deactivate_user?(last_owner)
    {:error, :cannot_deactivate_last_owner}

    iex> can_deactivate_user?(regular_user)
    :ok

# `can_manage_role?`

```elixir
@spec can_manage_role?(
  PhoenixKit.Users.Auth.User.t() | PhoenixKit.Users.Auth.Scope.t() | :system,
  PhoenixKit.Users.Role.t() | String.t()
) :: boolean()
```

Whether `actor` may give `role` to someone, take it away from someone, delete
it, or make it the new-user default.

`actor` is a `%User{}`, a `%Scope{}` or `:system` (always true). An Owner (by
their real, held roles) may manage any role. Anyone else may manage a role
only when it is neither Owner nor Admin and they hold every permission key
the role holds — checked against their effective permissions (the active
role, when they act as one), so a role holding `"*"` needs `"*"`. `role` is a
`%Role{}` or a role name; an unknown name answers false.

# `count_active_owners`

Counts active users with Owner role.

Critical security function - ensures we never have zero owners.

## Examples

    iex> count_active_owners()
    1

# `count_roles`

Role counts for the admin list's summary cards: `%{total: n, system: n, custom: n}`.

# `count_users_with_role`

Counts users with a specific role.

## Parameters

- `role_name`: The name of the role to count

## Examples

    iex> count_users_with_role("Admin")
    3

# `create_role`

Creates a new role.

## Parameters

- `attrs`: Attributes for the new role

## Examples

    iex> create_role(%{name: "Manager", description: "Department manager"})
    {:ok, %Role{}}

    iex> create_role(%{name: ""})
    {:error, %Ecto.Changeset{}}

# `default_role_for_new_users`

```elixir
@spec default_role_for_new_users() :: String.t()
```

The role a new self-registered account gets: the `new_user_default_role`
setting when it names an existing role other than Owner and Admin, else
"User". Owner is reserved for the first user, and Admin would hand every
self-registered account the admin panel, so a stored value naming either is
never used.

# `delete_role`

Deletes a role safely.

Prevents deletion of system roles and roles currently assigned to users.

## Parameters

- `role`: The role to delete
- `opts`: `:actor` — the acting `%User{}` or `%Scope{}`. Only an actor who
  holds every key of the role may delete it (`can_manage_role?/2`; an Owner
  always): `{:error, :not_permitted}` otherwise. Omitted = a system caller.

## Examples

    iex> delete_role(custom_role)
    {:ok, %Role{}}

    iex> delete_role(system_role)
    {:error, :system_role_protected}

    iex> delete_role(role_with_users)
    {:error, :role_in_use}

# `demote_to_user`

Safely demotes a user from Admin or Owner role with protection.

Prevents demotion of last Owner to maintain system security.

## Parameters

- `user`: The user to demote
- `opts`: `:actor` — as for `remove_role/3`: only an Owner actor may take
  Owner or Admin away. Omitted = a system caller, unchecked.

## Examples

    iex> demote_to_user(admin_user)
    {:ok, %RoleAssignment{}}

    iex> demote_to_user(last_owner)
    {:error, :cannot_demote_last_owner}

# `ensure_first_user_is_owner`

Safely assigns Owner role to first user using database transaction.

Concurrent registrations are serialized by a row lock on the Owner role row,
taken only while no active Owner exists yet — see the transaction body.

## Parameters

- `user`: The user to potentially make Owner

## Returns

- `{:ok, :owner}` if user became Owner
- `{:ok, :user}` if user became regular User
- `{:error, reason}` on failure

## Examples

    iex> ensure_first_user_is_owner(user)
    {:ok, :owner}

# `get_custom_roles`

Gets only custom (non-system) roles.

## Examples

    iex> get_custom_roles()
    [%Role{name: "Manager"}, %Role{name: "Editor"}]

# `get_extended_stats`

Gets comprehensive user statistics including activity and confirmation status.

## Examples

    iex> get_extended_stats()
    %{
      total_users: 10,
      owner_count: 1,
      admin_count: 2,
      user_count: 7,
      active_users: 8,
      inactive_users: 2,
      confirmed_users: 9,
      pending_users: 1
    }

# `get_role_by_name`

Gets a role by its name.

## Parameters

- `name`: The name of the role

## Examples

    iex> get_role_by_name("Admin")
    %Role{name: "Admin"}

    iex> get_role_by_name("NonexistentRole")
    nil

# `get_role_by_uuid`

Gets a single role by its UUID.

Returns `nil` if no role is found.

## Parameters

- `uuid`: The UUID of the role to find

## Examples

    iex> get_role_by_uuid("01234567-89ab-cdef-0123-456789abcdef")
    %Role{uuid: "01234567-89ab-cdef-0123-456789abcdef", name: "Editor"}

    iex> get_role_by_uuid("nonexistent-uuid")
    nil

# `get_role_records_for_users`

```elixir
@spec get_role_records_for_users([String.t()]) :: %{
  optional(String.t()) =&gt; [%{uuid: String.t(), name: String.t()}]
}
```

`get_user_role_records/1` for many users at once: `%{user_uuid => [role]}`,
each list in role order. Users holding no role are absent.

# `get_role_stats`

Gets role statistics for dashboard display.

## Examples

    iex> get_role_stats()
    %{
      total_users: 10,
      owner_count: 1,
      admin_count: 2,
      user_count: 7
    }

# `get_user_role_records`

```elixir
@spec get_user_role_records(PhoenixKit.Users.Auth.User.t()) :: [
  %{uuid: String.t(), name: String.t()}
]
```

Gets the roles a user holds as `%{uuid, name}` maps, in **role order**
(`position`, then name).

The same query as `get_user_roles/1`, keeping the uuid: the active-role
resolution in `PhoenixKit.Users.ActiveRole` identifies roles by uuid, because
role names are editable — and relies on this order: the first switchable
role here is a session's default.

# `get_user_roles`

Gets all active roles for a user.

## Parameters

- `user`: The user to get roles for

## Examples

    iex> get_user_roles(user)
    ["Admin", "User"]

    iex> get_user_roles(user_with_no_roles)
    []

# `list_roles`

Lists all roles.

## Examples

    iex> list_roles()
    [%Role{}, %Role{}, %Role{}]

# `list_roles_paginated`

One page of roles for the admin list, system roles first, then by name.

Options `:page` (default 1) and `:per_page` (default 25). Returns
`%{roles: [...], total_count: n, total_pages: n, page: n}` — `page` is
clamped into `[1, total_pages]` before the offset is applied, so a link
past the end (the last custom role on the last page was just deleted)
shows the last page rather than an empty one.

# `manageable_role_names`

```elixir
@spec manageable_role_names(
  PhoenixKit.Users.Auth.User.t()
  | PhoenixKit.Users.Auth.Scope.t()
  | :system
) ::
  [String.t()]
```

The names of the roles `actor` may manage (`can_manage_role?/2`), in role
order. One permissions query for all roles — for a UI that disables the rest.

# `move_role`

```elixir
@spec move_role(String.t(), :up | :down) :: :ok
```

Moves one role one step `:up` or `:down` in role order. A move past either
end is a no-op.

# `promote_to_admin`

Promotes a user to admin role.

## Parameters

- `user`: The user to promote
- `assigned_by` (optional): The user who is doing the promotion
- `opts`: `:actor` — as for `assign_role/4`. Admin is a system role, so only
  an Owner actor may promote (`{:error, :not_permitted}` otherwise). Omitted =
  a system caller (seeding, fixtures), unchecked — no web page calls this.

## Examples

    iex> promote_to_admin(user)
    {:ok, %RoleAssignment{}}

# `remove_role`

Removes a role from a user by deleting the assignment.

## Parameters

- `user`: The user to remove the role from
- `role_name`: The name of the role to remove
- `opts`: `:broadcast` (default `true`); `:actor`, as for `assign_role/4`

## Examples

    iex> remove_role(user, "Admin")
    {:ok, %RoleAssignment{}}

    iex> remove_role(user, "NonexistentRole")
    {:error, :assignment_not_found}

# `reorder_roles`

```elixir
@spec reorder_roles([String.t()]) :: :ok
```

Sets the role order (`position`) to the order of `ordered_uuids`; any role
not listed keeps its relative order after them.

The order decides the default role of a new session and how the role
switcher lists roles (`PhoenixKit.Users.ActiveRole`). Broadcasts
`:role_updated` for the roles whose position changed.

# `safely_remove_role`

Safely removes role with Owner protection.

Prevents removal of Owner role if it would leave system without owners.

## Parameters

- `user`: User to remove role from
- `role_name`: Name of role to remove
- `opts`: `:actor` — as for `remove_role/3`. Omitted = a system caller.

## Examples

    iex> safely_remove_role(owner_user, "Owner")
    {:error, :cannot_remove_last_owner}

    iex> safely_remove_role(admin_user, "Admin")
    {:ok, %RoleAssignment{}}

# `sync_user_roles`

Synchronizes user roles with a given list of role names.

This function ensures the user has exactly the specified roles.

## Authorization (`:actor` option)

This is the authorization boundary for role changes — **never** trust the
caller UI to have gated it. Pass `actor: %User{}` (the acting admin) and:

- Changes to the **system roles** (Owner, Admin) are applied only when the
  actor is an Owner. A non-Owner's attempt to grant Owner/Admin to another
  account (privilege escalation) or to strip them (the disabled-checkbox
  form drops Owner from the submitted set, which would otherwise read as a
  removal) is DROPPED — that role is left exactly as it was.
- Any other role is changed only when the actor may manage it
  (`can_manage_role?/2`: the actor holds every permission key of the role).
  A change the actor may not make is DROPPED like a system-role change and
  its role name is returned in `:refused`, so the caller can say so.
- A non-Owner actor may not change **their own** roles at all: the whole
  call fails with `{:error, :cannot_modify_own_roles}` when it would change
  anything.
- The **last Owner** can never be removed, regardless of actor, via the
  same guard as `safely_remove_role/2`.

Omitting `:actor` (or `actor: :system`) skips the actor gate — for
seeding/system callers only — but the last-Owner guard still applies.

## Parameters

- `user`: The user to synchronize roles for
- `role_names`: List of role names the user should have
- `opts`: `:actor` — the acting `%User{}` or `%Scope{}` (see above)

Returns `{:ok, %{assignments: [...], roles_before: [...], roles_after: [...],
refused: [...]}}` on success — `roles_before`/`roles_after` are the role-name lists captured
inside the transaction, so callers can audit the exact delta this transaction
applied (which can differ from what was submitted when unauthorized changes
are dropped or a concurrent edit intervened).

## Examples

    iex> sync_user_roles(user, ["Admin", "Manager"], actor: current_user)
    {:ok, %{assignments: [%RoleAssignment{}], roles_before: ["Manager"], roles_after: ["Admin", "Manager"]}}

# `update_role`

Updates a role.

## Parameters

- `role`: The role to update
- `attrs`: Attributes to update

## Examples

    iex> update_role(role, %{description: "Updated description"})
    {:ok, %Role{}}

    iex> update_role(system_role, %{name: "NewName"})
    {:error, %Ecto.Changeset{}}

# `user_has_role?`

Checks if a user has a specific role.

## Parameters

- `user`: The user to check
- `role_name`: The name of the role to check for

## Examples

    iex> user_has_role?(user, "Admin")
    true

    iex> user_has_role?(user, "Owner")
    false

# `user_has_role_admin?`

Checks if a user has an "Admin" role.

## Parameters

- `user`: The user to check

## Examples

    iex> user_has_role_admin?(user)
    true

# `user_has_role_owner?`

Checks if a user has an "Owner" role.

## Parameters

- `user`: The user to check

## Examples

    iex> user_has_role_owner?(user)
    true

# `users_with_role`

Gets all users who have a specific role.

## Parameters

- `role_name`: The name of the role to search for

## Examples

    iex> users_with_role("Admin")
    [%User{}, %User{}]

    iex> users_with_role("NonexistentRole")
    []

---

*Consult [api-reference.md](api-reference.md) for complete listing*
