Packages

phoenix_kit

1.6.6
1.7.208 1.7.207 1.7.206 1.7.205 1.7.204 1.7.203 1.7.202 1.7.201 1.7.200 1.7.199 1.7.198 1.7.197 1.7.196 1.7.194 1.7.193 1.7.192 1.7.191 1.7.190 1.7.189 1.7.187 1.7.186 1.7.185 1.7.184 1.7.183 1.7.182 1.7.181 1.7.180 1.7.179 1.7.178 1.7.177 1.7.176 1.7.175 1.7.174 1.7.173 1.7.172 1.7.171 1.7.170 1.7.169 1.7.168 1.7.167 1.7.166 1.7.165 1.7.164 1.7.162 1.7.161 1.7.160 1.7.159 1.7.157 1.7.156 1.7.155 1.7.154 1.7.153 1.7.152 1.7.151 1.7.150 1.7.149 1.7.146 1.7.145 1.7.144 1.7.143 1.7.138 1.7.133 1.7.132 1.7.131 1.7.130 1.7.128 1.7.126 1.7.125 1.7.121 1.7.120 1.7.119 1.7.118 1.7.117 1.7.116 1.7.115 1.7.114 1.7.113 1.7.112 1.7.111 1.7.110 1.7.109 1.7.108 1.7.107 1.7.106 1.7.105 1.7.104 1.7.103 1.7.102 1.7.101 1.7.100 1.7.99 1.7.98 1.7.97 1.7.96 1.7.95 1.7.94 1.7.93 1.7.92 1.7.91 1.7.90 1.7.89 1.7.88 1.7.87 1.7.86 1.7.85 1.7.84 1.7.83 1.7.82 1.7.81 1.7.80 1.7.79 1.7.78 1.7.77 1.7.76 1.7.75 1.7.74 1.7.71 1.7.70 1.7.69 1.7.66 1.7.65 1.7.64 1.7.63 1.7.62 1.7.61 1.7.59 1.7.58 1.7.57 1.7.56 1.7.55 1.7.54 1.7.53 1.7.52 1.7.51 1.7.49 1.7.44 1.7.43 1.7.42 1.7.41 1.7.39 1.7.38 1.7.37 1.7.36 1.7.34 1.7.33 1.7.31 1.7.30 1.7.29 1.7.28 1.7.27 1.7.26 1.7.25 1.7.24 1.7.23 1.7.22 1.7.21 1.7.20 1.7.19 1.7.18 1.7.17 1.7.16 1.7.15 1.7.14 1.7.13 1.7.12 1.7.11 1.7.10 1.7.9 1.7.8 1.7.7 1.7.6 1.7.5 1.7.4 1.7.3 1.7.2 1.7.1 1.7.0 1.6.20 1.6.19 1.6.18 1.6.17 1.6.16 1.6.15 1.6.14 1.6.13 1.6.12 1.6.11 1.6.10 1.6.9 1.6.8 1.6.7 1.6.6 1.6.5 1.6.4 1.6.3 1.5.2 1.5.1 1.5.0 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.2 1.3.1 1.3.0 1.2.10 1.2.9 1.2.8 1.2.7 1.2.5 1.2.4 1.2.2 1.2.1 1.2.0 1.1.0 1.0.0

A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more

Current section

Files

Jump to
phoenix_kit lib phoenix_kit users roles.ex
Raw

lib/phoenix_kit/users/roles.ex

defmodule PhoenixKit.Users.Roles do
@moduledoc """
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
## 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{}]
"""
import Ecto.Query, warn: false
alias Ecto.Adapters.SQL
alias PhoenixKit.Admin.Events
alias PhoenixKit.RepoHelper
alias PhoenixKit.Users.Auth.User
alias PhoenixKit.Users.{Role, RoleAssignment, ScopeNotifier}
@doc """
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
## 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}
"""
def assign_role(%User{} = user, role_name, assigned_by \\ nil, opts \\ [])
when is_binary(role_name) do
roles = Role.system_roles()
# Prevent manual assignment of Owner role
if role_name == roles.owner do
{:error, :owner_role_protected}
else
assign_role_internal(user, role_name, assigned_by, opts)
end
end
# Internal function for assigning roles without Owner protection
# Used by system functions like ensure_first_user_is_owner/1
defp assign_role_internal(%User{} = user, role_name, assigned_by \\ nil, opts \\ [])
when is_binary(role_name) do
repo = RepoHelper.repo()
broadcast? = Keyword.get(opts, :broadcast, true)
case get_role_by_name(role_name) do
nil ->
{:error, :role_not_found}
role ->
attrs = %{
user_id: user.id,
role_id: role.id,
assigned_by: assigned_by && assigned_by.id
}
# Use upsert with ON CONFLICT DO NOTHING for idempotency
# This prevents duplicate role assignment errors during concurrent operations
%RoleAssignment{}
|> RoleAssignment.changeset(attrs)
|> repo.insert(
on_conflict: :nothing,
conflict_target: [:user_id, :role_id]
)
|> case do
{:ok, assignment} ->
if broadcast? do
# Broadcast role assignment event
Events.broadcast_user_role_assigned(user, role_name)
# Notify active LiveView sessions to refresh cached scope
ScopeNotifier.broadcast_roles_updated(user)
end
{:ok, assignment}
{:error, changeset} ->
{:error, changeset}
end
end
end
@doc """
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
## Examples
iex> remove_role(user, "Admin")
{:ok, %RoleAssignment{}}
iex> remove_role(user, "NonexistentRole")
{:error, :assignment_not_found}
"""
def remove_role(%User{} = user, role_name, opts \\ []) when is_binary(role_name) do
repo = RepoHelper.repo()
broadcast? = Keyword.get(opts, :broadcast, true)
case get_assignment(user.id, role_name) do
nil ->
{:error, :assignment_not_found}
assignment ->
case repo.delete(assignment) do
{:ok, deleted_assignment} ->
if broadcast? do
# Broadcast role removal event
Events.broadcast_user_role_removed(user, role_name)
# Notify active LiveView sessions to refresh cached scope
ScopeNotifier.broadcast_roles_updated(user)
end
{:ok, deleted_assignment}
{:error, changeset} ->
{:error, changeset}
end
end
end
@doc """
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
"""
def user_has_role?(%User{} = user, role_name) when is_binary(role_name) do
repo = RepoHelper.repo()
query =
from assignment in RoleAssignment,
join: role in assoc(assignment, :role),
where: assignment.user_id == ^user.id,
where: role.name == ^role_name
repo.exists?(query)
end
@doc """
Checks if a user has an "Owner" role.
## Parameters
- `user`: The user to check
## Examples
iex> user_has_role_owner?(user)
true
"""
def user_has_role_owner?(%User{} = user) do
roles = Role.system_roles()
user_has_role?(user, roles.owner)
end
@doc """
Checks if a user has an "Admin" role.
## Parameters
- `user`: The user to check
## Examples
iex> user_has_role_admin?(user)
true
"""
def user_has_role_admin?(%User{} = user) do
roles = Role.system_roles()
user_has_role?(user, roles.admin)
end
@doc """
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)
[]
"""
def get_user_roles(%User{} = user) do
repo = RepoHelper.repo()
query =
from assignment in RoleAssignment,
join: role in assoc(assignment, :role),
where: assignment.user_id == ^user.id,
select: role.name,
order_by: role.name
repo.all(query)
end
@doc """
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")
[]
"""
def users_with_role(role_name) when is_binary(role_name) do
repo = RepoHelper.repo()
query =
from user in User,
join: assignment in assoc(user, :role_assignments),
join: role in assoc(assignment, :role),
where: role.name == ^role_name,
distinct: user.id,
order_by: user.email
repo.all(query)
end
@doc """
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{}}
"""
def create_role(attrs \\ %{}) do
repo = RepoHelper.repo()
case %Role{}
|> Role.changeset(attrs)
|> repo.insert() do
{:ok, role} ->
# Broadcast role creation event
Events.broadcast_role_created(role)
{:ok, role}
{:error, changeset} ->
{:error, changeset}
end
end
@doc """
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
"""
def get_role_by_name(name) when is_binary(name) do
repo = RepoHelper.repo()
repo.get_by(Role, name: name)
end
@doc """
Lists all roles.
## Examples
iex> list_roles()
[%Role{}, %Role{}, %Role{}]
"""
def list_roles do
repo = RepoHelper.repo()
query =
from role in Role,
order_by: [desc: role.is_system_role, asc: role.name]
repo.all(query)
end
@doc """
Gets role statistics for dashboard display.
## Examples
iex> get_role_stats()
%{
total_users: 10,
owner_count: 1,
admin_count: 2,
user_count: 7
}
"""
def get_role_stats do
repo = RepoHelper.repo()
roles = Role.system_roles()
total_users_query = from(u in User, select: count(u.id))
total_users = repo.one(total_users_query)
owner_count = count_users_with_role(roles.owner)
admin_count = count_users_with_role(roles.admin)
user_count = count_users_with_role(roles.user)
%{
total_users: total_users,
owner_count: owner_count,
admin_count: admin_count,
user_count: user_count
}
end
@doc """
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
}
"""
def get_extended_stats do
repo = RepoHelper.repo()
# Single optimized query combining all statistics
query = """
SELECT
COUNT(*) as total_users,
COUNT(*) FILTER (WHERE is_active = true) as active_users,
COUNT(*) FILTER (WHERE is_active = false) as inactive_users,
COUNT(*) FILTER (WHERE confirmed_at IS NOT NULL) as confirmed_users,
COUNT(*) FILTER (WHERE confirmed_at IS NULL) as pending_users,
COALESCE((
SELECT COUNT(*)
FROM phoenix_kit_user_role_assignments ra
JOIN phoenix_kit_user_roles r ON r.id = ra.role_id
WHERE r.name = $1
), 0) as owner_count,
COALESCE((
SELECT COUNT(*)
FROM phoenix_kit_user_role_assignments ra
JOIN phoenix_kit_user_roles r ON r.id = ra.role_id
WHERE r.name = $2
), 0) as admin_count,
COALESCE((
SELECT COUNT(*)
FROM phoenix_kit_user_role_assignments ra
JOIN phoenix_kit_user_roles r ON r.id = ra.role_id
WHERE r.name = $3
), 0) as user_count
FROM phoenix_kit_users;
"""
roles = Role.system_roles()
result =
SQL.query!(repo, query, [
roles.owner,
roles.admin,
roles.user
])
case result.rows do
[
[
total_users,
active_users,
inactive_users,
confirmed_users,
pending_users,
owner_count,
admin_count,
user_count
]
] ->
%{
total_users: total_users,
active_users: active_users,
inactive_users: inactive_users,
confirmed_users: confirmed_users,
pending_users: pending_users,
owner_count: owner_count,
admin_count: admin_count,
user_count: user_count
}
_ ->
# Fallback to individual queries if the optimized query fails
get_extended_stats_fallback()
end
end
# Fallback function using original approach
defp get_extended_stats_fallback do
repo = RepoHelper.repo()
# Basic role stats
base_stats = get_role_stats()
# Activity stats
active_users = repo.one(from u in User, where: u.is_active == true, select: count(u.id))
inactive_users = repo.one(from u in User, where: u.is_active == false, select: count(u.id))
# Confirmation stats
confirmed_users =
repo.one(from u in User, where: not is_nil(u.confirmed_at), select: count(u.id))
pending_users = repo.one(from u in User, where: is_nil(u.confirmed_at), select: count(u.id))
Map.merge(base_stats, %{
active_users: active_users,
inactive_users: inactive_users,
confirmed_users: confirmed_users,
pending_users: pending_users
})
end
@doc """
Counts users with a specific role.
## Parameters
- `role_name`: The name of the role to count
## Examples
iex> count_users_with_role("Admin")
3
"""
def count_users_with_role(role_name) when is_binary(role_name) do
repo = RepoHelper.repo()
query =
from assignment in RoleAssignment,
join: role in assoc(assignment, :role),
where: role.name == ^role_name,
select: count(assignment.id)
repo.one(query) || 0
end
@doc """
Promotes a user to admin role.
## Parameters
- `user`: The user to promote
- `assigned_by` (optional): The user who is doing the promotion
## Examples
iex> promote_to_admin(user)
{:ok, %RoleAssignment{}}
"""
def promote_to_admin(%User{} = user, assigned_by \\ nil) do
# Admin role can be assigned through normal process
roles = Role.system_roles()
assign_role_internal(user, roles.admin, assigned_by)
end
@doc """
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
## Examples
iex> demote_to_user(admin_user)
{:ok, %RoleAssignment{}}
iex> demote_to_user(last_owner)
{:error, :cannot_demote_last_owner}
"""
def demote_to_user(%User{} = user) do
roles = Role.system_roles()
cond do
user_has_role?(user, roles.owner) ->
# Use safe removal for Owner role
safely_remove_role(user, roles.owner)
user_has_role?(user, roles.admin) ->
# Admin can be demoted safely
remove_role(user, roles.admin)
true ->
# User has no roles to demote from
{:error, :no_role_to_demote}
end
end
@doc """
Counts active users with Owner role.
Critical security function - ensures we never have zero owners.
## Examples
iex> count_active_owners()
1
"""
def count_active_owners do
repo = RepoHelper.repo()
roles = Role.system_roles()
query =
from user in User,
join: assignment in assoc(user, :role_assignments),
join: role in assoc(assignment, :role),
where: role.name == ^roles.owner,
where: user.is_active == true,
select: count(user.id)
repo.one(query) || 0
end
@doc """
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}}
"""
def assign_roles_to_existing_users(opts \\ []) do
repo = RepoHelper.repo()
make_first_owner = Keyword.get(opts, :make_first_owner, true)
repo.transaction(fn ->
# Find users without any role assignments
users_without_roles =
repo.all(
from u in User,
left_join: assignment in assoc(u, :role_assignments),
where: is_nil(assignment.id),
where: u.is_active == true,
order_by: u.inserted_at
)
case users_without_roles do
[] ->
%{assigned_owner: 0, assigned_users: 0, total_processed: 0}
users ->
# Check if we need to assign Owner role
existing_owner_count = count_active_owners()
{owner_assignments, user_assignments} =
assign_roles_to_users(users, make_first_owner, existing_owner_count)
%{
assigned_owner: owner_assignments,
assigned_users: user_assignments,
total_processed: length(users)
}
end
end)
end
# Private helper to assign roles to users with reduced nesting
defp assign_roles_to_users(users, make_first_owner, existing_owner_count) do
roles = Role.system_roles()
if make_first_owner && existing_owner_count == 0 do
# Assign Owner to first user, User to rest
[first_user | rest_users] = users
owner_result = assign_role_internal(first_user, roles.owner)
user_results = Enum.map(rest_users, &assign_role_internal(&1, roles.user))
{
if(match?({:ok, _}, owner_result), do: 1, else: 0),
Enum.count(user_results, &match?({:ok, _}, &1))
}
else
# Assign User role to all
user_results = Enum.map(users, &assign_role_internal(&1, roles.user))
{0, Enum.count(user_results, &match?({:ok, _}, &1))}
end
end
@doc """
Safely assigns Owner role to first user using database transaction.
This function prevents race conditions by using FOR UPDATE lock.
## 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}
"""
def ensure_first_user_is_owner(%User{} = user) do
repo = RepoHelper.repo()
roles = Role.system_roles()
repo.transaction(fn ->
# Lock the Owner role to prevent race conditions during concurrent user registrations
# Note: Cannot use FOR UPDATE with aggregate functions, so we split into two queries
owner_role =
repo.one(
from r in Role,
where: r.name == ^roles.owner,
lock: "FOR UPDATE"
)
# Count existing active owners in a separate query (within the same transaction)
owner_count =
repo.one(
from assignment in RoleAssignment,
join: u in User,
on: assignment.user_id == u.id and u.is_active == true,
where: assignment.role_id == ^owner_role.id,
select: count(u.id)
)
case owner_count do
0 ->
# No active owners exist, make this user Owner
case assign_role_internal(user, roles.owner) do
{:ok, _assignment} ->
# Activate and confirm first owner
activate_first_owner(user, :owner, repo)
{:error, reason} ->
repo.rollback(reason)
end
count when is_integer(count) and count > 0 ->
# Active owners exist, assign default role
default_role_name = get_safe_default_role()
case assign_role_internal(user, default_role_name) do
{:ok, _assignment} ->
String.to_atom(String.downcase(default_role_name))
{:error, reason} ->
repo.rollback(reason)
end
end
end)
end
# Activate and confirm first owner
defp activate_first_owner(user, role_type, repo) do
changes = build_owner_changes(user)
apply_owner_changes(user, changes, role_type, repo)
end
# Build changes map for first owner (activation and email confirmation)
defp build_owner_changes(user) do
%{}
|> maybe_add_is_active(user.is_active)
|> maybe_add_confirmed_at(user.confirmed_at)
end
# Add is_active flag if user is not active
defp maybe_add_is_active(changes, true), do: changes
defp maybe_add_is_active(changes, false), do: Map.put(changes, :is_active, true)
# Add confirmed_at timestamp if user email is not confirmed
defp maybe_add_confirmed_at(changes, nil) do
Map.put(changes, :confirmed_at, NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second))
end
defp maybe_add_confirmed_at(changes, _confirmed_at), do: changes
# Apply changes to user if any exist
defp apply_owner_changes(user, changes, role_type, repo) when map_size(changes) > 0 do
case repo.update(Ecto.Changeset.change(user, changes)) do
{:ok, _updated_user} -> role_type
{:error, reason} -> repo.rollback(reason)
end
end
defp apply_owner_changes(_user, _changes, role_type, _repo), do: role_type
@doc """
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
## Examples
iex> safely_remove_role(owner_user, "Owner")
{:error, :cannot_remove_last_owner}
iex> safely_remove_role(admin_user, "Admin")
{:ok, %RoleAssignment{}}
"""
def safely_remove_role(%User{} = user, role_name) when role_name == "Owner" do
repo = RepoHelper.repo()
repo.transaction(fn ->
remaining_owners = count_remaining_owners(repo, user.id)
if remaining_owners < 1 do
repo.rollback(:cannot_remove_last_owner)
else
execute_role_removal(user, role_name, repo)
end
end)
end
def safely_remove_role(%User{} = user, role_name) do
# Non-Owner roles can be removed safely
remove_role(user, role_name)
end
@doc """
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
"""
def can_deactivate_user?(%User{} = user) do
roles = Role.system_roles()
if user_has_role?(user, roles.owner) do
case count_active_owners() do
count when count <= 1 -> {:error, :cannot_deactivate_last_owner}
_ -> :ok
end
else
:ok
end
end
# Private helper functions
defp execute_role_removal(user, role_name, repo) do
case remove_role(user, role_name) do
{:ok, assignment} -> assignment
{:error, reason} -> repo.rollback(reason)
end
end
defp count_remaining_owners(repo, excluding_user_id) do
roles = Role.system_roles()
repo.one(
from u in User,
join: assignment in assoc(u, :role_assignments),
join: role in assoc(assignment, :role),
where: role.name == ^roles.owner,
where: u.is_active == true,
where: u.id != ^excluding_user_id,
select: count(u.id)
) || 0
end
@doc """
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{}}
"""
def update_role(%Role{} = role, attrs) do
repo = RepoHelper.repo()
case role
|> Role.changeset(attrs)
|> repo.update() do
{:ok, updated_role} ->
# Broadcast role update event
Events.broadcast_role_updated(updated_role)
{:ok, updated_role}
{:error, changeset} ->
{:error, changeset}
end
end
@doc """
Deletes a role safely.
Prevents deletion of system roles and roles currently assigned to users.
## Parameters
- `role`: The role to delete
## 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}
"""
def delete_role(%Role{} = role) do
repo = RepoHelper.repo()
cond do
role.is_system_role ->
{:error, :system_role_protected}
role_has_assignments?(role) ->
{:error, :role_in_use}
true ->
case repo.delete(role) do
{:ok, deleted_role} ->
# Broadcast role deletion event
Events.broadcast_role_deleted(deleted_role)
{:ok, deleted_role}
{:error, changeset} ->
{:error, changeset}
end
end
end
@doc """
Synchronizes user roles with a given list of role names.
This function ensures the user has exactly the specified roles.
## Parameters
- `user`: The user to synchronize roles for
- `role_names`: List of role names the user should have
## Examples
iex> sync_user_roles(user, ["Admin", "Manager"])
{:ok, [%RoleAssignment{}, %RoleAssignment{}]}
"""
def sync_user_roles(%User{} = user, role_names) when is_list(role_names) do
repo = RepoHelper.repo()
case repo.transaction(fn ->
# Get current user roles
current_roles = get_user_roles(user)
# Determine roles to add and remove
roles_to_add = role_names -- current_roles
roles_to_remove = current_roles -- role_names
# Remove roles that should not be present
Enum.each(roles_to_remove, &remove_role_or_rollback(user, &1, repo))
# Add roles that should be present
assignments = Enum.map(roles_to_add, &assign_role_or_rollback(user, &1, repo))
new_user_roles = get_user_roles(user)
%{assignments: assignments, new_user_roles: new_user_roles}
end) do
{:ok, %{assignments: assignments, new_user_roles: new_user_roles}} ->
Events.broadcast_user_roles_synced(user, new_user_roles)
ScopeNotifier.broadcast_roles_updated(user)
{:ok, assignments}
{:error, reason} ->
{:error, reason}
end
end
@doc """
Gets only custom (non-system) roles.
## Examples
iex> get_custom_roles()
[%Role{name: "Manager"}, %Role{name: "Editor"}]
"""
def get_custom_roles do
repo = RepoHelper.repo()
query =
from role in Role,
where: role.is_system_role == false,
order_by: role.name
repo.all(query)
end
# Private helper functions
defp remove_role_or_rollback(user, role_name, repo) do
case remove_role(user, role_name, broadcast: false) do
{:ok, _} -> :ok
{:error, reason} -> repo.rollback(reason)
end
end
defp assign_role_or_rollback(user, role_name, repo) do
case assign_role(user, role_name, nil, broadcast: false) do
{:ok, assignment} -> assignment
{:error, reason} -> repo.rollback(reason)
end
end
defp role_has_assignments?(%Role{} = role) do
repo = RepoHelper.repo()
query =
from assignment in RoleAssignment,
where: assignment.role_id == ^role.id
repo.exists?(query)
end
defp get_assignment(user_id, role_name) do
repo = RepoHelper.repo()
query =
from assignment in RoleAssignment,
join: role in assoc(assignment, :role),
where: assignment.user_id == ^user_id,
where: role.name == ^role_name
repo.one(query)
end
# Gets the safe default role for new users from settings
# Always falls back to "User" role if setting is invalid or missing
# Allows any valid role except Owner (which is reserved for first user)
defp get_safe_default_role do
roles = Role.system_roles()
# Get configured role with fallback to "User"
configured_role = PhoenixKit.Settings.get_setting("new_user_default_role", roles.user)
# Validate the setting value against all non-Owner roles for security
all_roles = list_roles()
allowed_roles =
all_roles
|> Enum.reject(fn role -> role.name == roles.owner end)
|> Enum.map(fn role -> role.name end)
if configured_role in allowed_roles do
configured_role
else
# Safe fallback if setting is corrupted, invalid, or somehow set to Owner
roles.user
end
end
end