Packages
phoenix_kit
1.7.84
1.7.210
1.7.209
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
Current section
Files
lib/phoenix_kit_web/live/users/roles.ex
defmodule PhoenixKitWeb.Live.Users.Roles do
@moduledoc """
Role management LiveView for PhoenixKit admin panel.
Provides interface for viewing and managing user roles and permissions.
"""
use PhoenixKitWeb, :live_view
use Gettext, backend: PhoenixKitWeb.Gettext
alias PhoenixKit.Admin.Events
alias PhoenixKit.Settings
alias PhoenixKit.Users.Auth.Scope
alias PhoenixKit.Users.Permissions
alias PhoenixKit.Users.{Role, Roles}
def mount(_params, _session, socket) do
# Attach locale hook for automatic locale handling
# Subscribe to role events for live updates
if connected?(socket) do
Events.subscribe_to_roles()
Events.subscribe_to_stats()
Events.subscribe_to_permissions()
Events.subscribe_to_modules()
end
# Load optimized role statistics once
role_stats = load_role_statistics()
# Get project title from settings
project_title = Settings.get_project_title()
socket =
socket
|> assign(:roles, [])
|> assign(:show_create_form, false)
|> assign(:show_edit_form, false)
|> assign(:create_role_form, nil)
|> assign(:edit_role_form, nil)
|> assign(:editing_role, nil)
|> assign(:delete_confirmation, %{show: false})
|> assign(:show_permissions_editor, false)
|> assign(:permissions_role, nil)
|> assign(:permissions_role_keys, MapSet.new())
|> assign(:permissions_grantable_keys, MapSet.new())
|> assign(:permissions_preserved_keys, MapSet.new())
|> assign(:page_title, gettext("Roles"))
|> assign(:role_stats, role_stats)
|> assign(:project_title, project_title)
|> assign(:can_manage_permissions, false)
|> load_roles()
{:ok, socket}
end
def handle_event("show_create_form", _params, socket) do
form = to_form(Role.changeset(%Role{}, %{}))
socket =
socket
|> assign(:show_create_form, true)
|> assign(:create_role_form, form)
{:noreply, socket}
end
def handle_event("show_edit_role", %{"role_uuid" => role_uuid}, socket) do
role = find_role_by_uuid(socket.assigns.roles, role_uuid)
if role && !role.is_system_role do
form = to_form(Role.changeset(role, %{}))
socket =
socket
|> assign(:show_edit_form, true)
|> assign(:edit_role_form, form)
|> assign(:editing_role, role)
{:noreply, socket}
else
socket = put_flash(socket, :error, gettext("System roles cannot be edited"))
{:noreply, socket}
end
end
def handle_event("hide_form", _params, socket) do
socket =
socket
|> assign(:show_create_form, false)
|> assign(:show_edit_form, false)
|> assign(:create_role_form, nil)
|> assign(:edit_role_form, nil)
|> assign(:editing_role, nil)
{:noreply, socket}
end
def handle_event("create_role", %{"role" => role_params}, socket) do
case Roles.create_role(role_params) do
{:ok, role} ->
flash_msg =
if can_manage_permissions?(socket),
do:
gettext(
"Role \"%{role_name}\" created. Click Permissions to configure access.",
role_name: role.name
),
else: gettext("Role created successfully")
socket =
socket
|> put_flash(:info, flash_msg)
|> assign(:show_create_form, false)
|> assign(:create_role_form, nil)
|> load_roles()
{:noreply, socket}
{:error, changeset} ->
{:noreply, assign(socket, :create_role_form, to_form(changeset))}
end
end
def handle_event("update_role", %{"role" => role_params}, socket) do
editing_role = socket.assigns.editing_role
if is_nil(editing_role) do
{:noreply,
socket
|> put_flash(:error, gettext("Role no longer exists"))
|> assign(:show_edit_form, false)
|> assign(:edit_role_form, nil)
|> assign(:editing_role, nil)}
else
case Roles.update_role(editing_role, role_params) do
{:ok, _role} ->
socket =
socket
|> put_flash(:info, gettext("Role updated successfully"))
|> assign(:show_edit_form, false)
|> assign(:edit_role_form, nil)
|> assign(:editing_role, nil)
|> load_roles()
{:noreply, socket}
{:error, changeset} ->
{:noreply, assign(socket, :edit_role_form, to_form(changeset))}
end
end
end
# New events for confirmation modal
def handle_event(
"request_delete_role",
%{"role_uuid" => role_uuid, "role_name" => role_name},
socket
) do
delete_confirmation = %{
show: true,
role_uuid: role_uuid,
role_name: role_name
}
{:noreply, assign(socket, :delete_confirmation, delete_confirmation)}
end
def handle_event("cancel_delete", _params, socket) do
{:noreply, assign(socket, :delete_confirmation, %{show: false})}
end
def handle_event("confirm_delete_role", _params, socket) do
role_uuid = Map.get(socket.assigns.delete_confirmation, :role_uuid)
if is_nil(role_uuid) do
{:noreply, assign(socket, :delete_confirmation, %{show: false})}
else
# Close modal first
socket = assign(socket, :delete_confirmation, %{show: false})
# Execute the deletion
handle_delete_role(role_uuid, socket)
end
end
# Keep old handler for backward compatibility
def handle_event("delete_role", %{"role_uuid" => role_uuid}, socket) do
handle_delete_role(role_uuid, socket)
end
# --- Permission Editor Events ---
def handle_event("show_permissions_editor", %{"role_uuid" => role_uuid}, socket) do
if can_manage_permissions?(socket) do
role = find_role_by_uuid(socket.assigns.roles, role_uuid)
scope = socket.assigns[:phoenix_kit_current_scope]
with true <- role != nil,
:ok <- Permissions.can_edit_role_permissions?(scope, role) do
grantable = grantable_keys(socket)
enabled = Permissions.enabled_module_keys()
# Only show keys that are both grantable AND enabled
displayable = MapSet.intersection(grantable, enabled)
current_keys = Permissions.get_permissions_for_role(role.uuid) |> MapSet.new()
editable_checked = MapSet.intersection(current_keys, displayable)
# Preserve: keys outside displayable set (not grantable OR disabled modules)
preserved = MapSet.difference(current_keys, displayable)
socket =
socket
|> assign(:show_permissions_editor, true)
|> assign(:permissions_role, role)
|> assign(:permissions_role_keys, editable_checked)
|> assign(:permissions_grantable_keys, displayable)
|> assign(:permissions_preserved_keys, preserved)
{:noreply, socket}
else
{:error, reason} ->
{:noreply, put_flash(socket, :error, reason)}
false ->
{:noreply, put_flash(socket, :error, gettext("Role not found"))}
end
else
{:noreply,
put_flash(socket, :error, gettext("You don't have permission to manage permissions"))}
end
end
def handle_event("hide_permissions_editor", _params, socket) do
socket =
socket
|> assign(:show_permissions_editor, false)
|> assign(:permissions_role, nil)
|> assign(:permissions_role_keys, MapSet.new())
|> assign(:permissions_grantable_keys, MapSet.new())
|> assign(:permissions_preserved_keys, MapSet.new())
{:noreply, socket}
end
def handle_event("toggle_permission", %{"key" => key}, socket) do
if can_manage_permissions?(socket) do
grantable = socket.assigns.permissions_grantable_keys
if MapSet.member?(grantable, key) do
keys = socket.assigns.permissions_role_keys
new_keys =
if MapSet.member?(keys, key),
do: MapSet.delete(keys, key),
else: MapSet.put(keys, key)
{:noreply, assign(socket, :permissions_role_keys, new_keys)}
else
{:noreply, put_flash(socket, :error, gettext("You can only manage permissions you have"))}
end
else
{:noreply,
put_flash(socket, :error, gettext("You don't have permission to manage permissions"))}
end
end
def handle_event("grant_all_permissions", _params, socket) do
if can_manage_permissions?(socket) do
{:noreply,
assign(socket, :permissions_role_keys, socket.assigns.permissions_grantable_keys)}
else
{:noreply,
put_flash(socket, :error, gettext("You don't have permission to manage permissions"))}
end
end
def handle_event("revoke_all_permissions", _params, socket) do
if can_manage_permissions?(socket) do
{:noreply, assign(socket, :permissions_role_keys, MapSet.new())}
else
{:noreply,
put_flash(socket, :error, gettext("You don't have permission to manage permissions"))}
end
end
def handle_event("save_permissions", _params, socket) do
role = socket.assigns.permissions_role
cond do
is_nil(role) ->
{:noreply, put_flash(socket, :error, gettext("No role selected"))}
!can_manage_permissions?(socket) ->
{:noreply,
put_flash(socket, :error, gettext("You don't have permission to manage permissions"))}
Permissions.can_edit_role_permissions?(socket.assigns[:phoenix_kit_current_scope], role) !=
:ok ->
{:noreply,
put_flash(socket, :error, gettext("You cannot edit permissions for this role"))}
true ->
preserved = socket.assigns.permissions_preserved_keys
editor_keys = socket.assigns.permissions_role_keys
final_keys = MapSet.union(preserved, editor_keys) |> MapSet.to_list()
scope = socket.assigns[:phoenix_kit_current_scope]
granted_by_uuid = if scope, do: Scope.user_uuid(scope), else: nil
case Permissions.set_permissions(role.uuid, final_keys, granted_by_uuid) do
:ok ->
socket =
socket
|> put_flash(
:info,
gettext("Permissions updated for %{role_name}", role_name: role.name)
)
|> assign(:show_permissions_editor, false)
|> assign(:permissions_role, nil)
|> assign(:permissions_role_keys, MapSet.new())
|> assign(:permissions_grantable_keys, MapSet.new())
|> assign(:permissions_preserved_keys, MapSet.new())
{:noreply, socket}
{:error, _reason} ->
{:noreply, put_flash(socket, :error, gettext("Failed to update permissions"))}
end
end
end
# Keep the old handler for backward compatibility and make it private
defp handle_delete_role(role_uuid, socket) when is_binary(role_uuid) do
role = find_role_by_uuid(socket.assigns.roles, role_uuid)
if role && !role.is_system_role do
case Roles.delete_role(role) do
{:ok, _role} ->
socket =
socket
|> put_flash(:info, gettext("Role deleted successfully"))
|> load_roles()
{:noreply, socket}
{:error, :role_in_use} ->
socket =
put_flash(
socket,
:error,
gettext("Cannot delete role: it is currently assigned to users")
)
{:noreply, socket}
{:error, _changeset} ->
socket = put_flash(socket, :error, gettext("Failed to delete role"))
{:noreply, socket}
end
else
socket = put_flash(socket, :error, gettext("System roles cannot be deleted"))
{:noreply, socket}
end
end
defp load_roles(socket) do
roles = Roles.list_roles()
scope = socket.assigns[:phoenix_kit_current_scope]
uneditable_role_uuids =
roles
|> Enum.filter(fn role ->
role.name == "Owner" or Permissions.can_edit_role_permissions?(scope, role) != :ok
end)
|> MapSet.new(fn role -> to_string(role.uuid) end)
socket
|> assign(:roles, roles)
|> assign(:uneditable_role_uuids, uneditable_role_uuids)
|> assign(:can_manage_permissions, can_manage_permissions?(socket))
end
# Load role statistics: count of users per role name
defp load_role_statistics do
Roles.list_roles()
|> Map.new(fn role -> {role.name, Roles.count_users_with_role(role.name)} end)
end
# Optimized function using cached statistics
## Live Event Handlers
def handle_info({:role_created, _role}, socket) do
socket =
socket
|> load_roles()
|> assign(:role_stats, load_role_statistics())
{:noreply, socket}
end
def handle_info({:role_updated, _role}, socket) do
socket =
socket
|> load_roles()
|> assign(:role_stats, load_role_statistics())
{:noreply, socket}
end
def handle_info({:role_deleted, role}, socket) do
socket =
socket
|> close_modals_for_deleted_role(role)
|> load_roles()
|> assign(:role_stats, load_role_statistics())
{:noreply, socket}
end
def handle_info({:stats_updated, _stats}, socket) do
socket =
socket
|> assign(:role_stats, load_role_statistics())
{:noreply, socket}
end
def handle_info({:permission_granted, role_uuid, _key}, socket) do
{:noreply, refresh_permissions_if_editing(socket, role_uuid)}
end
def handle_info({:permission_revoked, role_uuid, _key}, socket) do
{:noreply, refresh_permissions_if_editing(socket, role_uuid)}
end
def handle_info({:permissions_synced, role_uuid, _keys}, socket) do
{:noreply, refresh_permissions_if_editing(socket, role_uuid)}
end
def handle_info({:module_enabled, _key}, socket) do
{:noreply, refresh_permissions_if_editing_any(socket)}
end
def handle_info({:module_disabled, _key}, socket) do
{:noreply, refresh_permissions_if_editing_any(socket)}
end
def handle_info(_msg, socket), do: {:noreply, socket}
# --- Helpers ---
defp can_manage_permissions?(socket) do
scope = socket.assigns[:phoenix_kit_current_scope]
scope && Scope.has_module_access?(scope, "users")
end
defp grantable_keys(socket) do
scope = socket.assigns[:phoenix_kit_current_scope]
if scope && Scope.owner?(scope) do
MapSet.new(Permissions.all_module_keys())
else
Scope.accessible_modules(scope)
end
end
defp find_role_by_uuid(roles, uuid_str) when is_binary(uuid_str) do
Enum.find(roles, &(to_string(&1.uuid) == uuid_str))
end
defp close_modals_for_deleted_role(socket, deleted_role) do
socket
|> then(fn s ->
if s.assigns.show_permissions_editor &&
s.assigns.permissions_role &&
s.assigns.permissions_role.uuid == deleted_role.uuid do
s
|> assign(:show_permissions_editor, false)
|> assign(:permissions_role, nil)
|> assign(:permissions_role_keys, MapSet.new())
|> assign(:permissions_grantable_keys, MapSet.new())
|> assign(:permissions_preserved_keys, MapSet.new())
|> put_flash(
:info,
gettext("Role \"%{role_name}\" was deleted", role_name: deleted_role.name)
)
else
s
end
end)
|> then(fn s ->
if s.assigns.editing_role && s.assigns.editing_role.uuid == deleted_role.uuid do
s
|> assign(:show_edit_form, false)
|> assign(:edit_role_form, nil)
|> assign(:editing_role, nil)
|> put_flash(
:info,
gettext("Role \"%{role_name}\" was deleted", role_name: deleted_role.name)
)
else
s
end
end)
end
defp refresh_permissions_if_editing(socket, role_uuid) do
role = socket.assigns[:permissions_role]
if socket.assigns.show_permissions_editor && role &&
to_string(role.uuid) == to_string(role_uuid) do
reload_permission_editor_data(socket)
else
socket
end
end
defp refresh_permissions_if_editing_any(socket) do
if socket.assigns.show_permissions_editor && socket.assigns.permissions_role do
reload_permission_editor_data(socket)
else
socket
end
end
defp reload_permission_editor_data(socket) do
role = socket.assigns.permissions_role
grantable = grantable_keys(socket)
enabled = Permissions.enabled_module_keys()
displayable = MapSet.intersection(grantable, enabled)
current_keys = Permissions.get_permissions_for_role(role.uuid) |> MapSet.new()
editable_checked = MapSet.intersection(current_keys, displayable)
preserved = MapSet.difference(current_keys, displayable)
socket
|> assign(:permissions_grantable_keys, displayable)
|> assign(:permissions_role_keys, editable_checked)
|> assign(:permissions_preserved_keys, preserved)
end
end