Fix incorrect translations: - 'Listing Roles' -> 'Rollen auflisten' (was 'Benutzer*innen auflisten') - 'Custom' -> 'Benutzerdefiniert' (was 'Benutzerdefinierte Felder')
175 lines
4.8 KiB
Elixir
175 lines
4.8 KiB
Elixir
defmodule MvWeb.RoleLive.Index do
|
|
@moduledoc """
|
|
LiveView for displaying and managing the role list.
|
|
|
|
## Features
|
|
- List all roles with name, description, permission_set_name, is_system_role
|
|
- Create new roles
|
|
- Navigate to role details and edit forms
|
|
- Delete non-system roles
|
|
|
|
## Events
|
|
- `delete` - Remove a role from the database (only non-system roles)
|
|
|
|
## Security
|
|
Only admins can access this page (enforced by authorization).
|
|
"""
|
|
use MvWeb, :live_view
|
|
|
|
alias Mv.Accounts
|
|
alias Mv.Authorization
|
|
|
|
require Ash.Query
|
|
|
|
import MvWeb.RoleLive.Helpers
|
|
|
|
on_mount {MvWeb.LiveHelpers, :ensure_user_role_loaded}
|
|
|
|
@impl true
|
|
def mount(_params, _session, socket) do
|
|
actor = socket.assigns[:current_user]
|
|
roles = load_roles(actor)
|
|
user_counts = load_user_counts(roles, actor)
|
|
|
|
{:ok,
|
|
socket
|
|
|> assign(:page_title, gettext("Listing Roles"))
|
|
|> assign(:roles, roles)
|
|
|> assign(:user_counts, user_counts)}
|
|
end
|
|
|
|
@impl true
|
|
def handle_event("delete", %{"id" => id}, socket) do
|
|
case Authorization.get_role(id, actor: socket.assigns.current_user) do
|
|
{:ok, role} ->
|
|
handle_delete_role(role, id, socket)
|
|
|
|
{:error, %Ash.Error.Query.NotFound{}} ->
|
|
{:noreply,
|
|
put_flash(
|
|
socket,
|
|
:error,
|
|
gettext("Role not found.")
|
|
)}
|
|
|
|
{:error, error} ->
|
|
error_message = format_error(error)
|
|
|
|
{:noreply,
|
|
put_flash(
|
|
socket,
|
|
:error,
|
|
gettext("Failed to delete role: %{error}", error: error_message)
|
|
)}
|
|
end
|
|
end
|
|
|
|
defp handle_delete_role(role, id, socket) do
|
|
if role.is_system_role do
|
|
{:noreply,
|
|
put_flash(
|
|
socket,
|
|
:error,
|
|
gettext("System roles cannot be deleted.")
|
|
)}
|
|
else
|
|
user_count = recalculate_user_count(role, socket.assigns.current_user)
|
|
|
|
if user_count > 0 do
|
|
{:noreply,
|
|
put_flash(
|
|
socket,
|
|
:error,
|
|
gettext(
|
|
"Cannot delete role. %{count} user(s) are still assigned to this role. Please assign them to another role first.",
|
|
count: user_count
|
|
)
|
|
)}
|
|
else
|
|
perform_role_deletion(role, id, socket)
|
|
end
|
|
end
|
|
end
|
|
|
|
defp perform_role_deletion(role, id, socket) do
|
|
case Authorization.destroy_role(role, actor: socket.assigns.current_user) do
|
|
:ok ->
|
|
updated_roles = Enum.reject(socket.assigns.roles, &(&1.id == id))
|
|
updated_counts = Map.delete(socket.assigns.user_counts, id)
|
|
|
|
{:noreply,
|
|
socket
|
|
|> assign(:roles, updated_roles)
|
|
|> assign(:user_counts, updated_counts)
|
|
|> put_flash(:info, gettext("Role deleted successfully."))}
|
|
|
|
{:error, error} ->
|
|
error_message = format_error(error)
|
|
|
|
{:noreply,
|
|
put_flash(
|
|
socket,
|
|
:error,
|
|
gettext("Failed to delete role: %{error}", error: error_message)
|
|
)}
|
|
end
|
|
end
|
|
|
|
@spec load_roles(map() | nil) :: [Mv.Authorization.Role.t()]
|
|
defp load_roles(actor) do
|
|
opts = if actor, do: [actor: actor], else: []
|
|
|
|
case Authorization.list_roles(opts) do
|
|
{:ok, roles} -> Enum.sort_by(roles, & &1.name)
|
|
{:error, _} -> []
|
|
end
|
|
end
|
|
|
|
# Loads all user counts for roles in a single query to avoid N+1 queries
|
|
@spec load_user_counts([Mv.Authorization.Role.t()], map() | nil) :: %{
|
|
Ecto.UUID.t() => non_neg_integer()
|
|
}
|
|
defp load_user_counts(roles, actor) do
|
|
role_ids = Enum.map(roles, & &1.id)
|
|
|
|
# Load all users with role_id in a single query
|
|
opts = [domain: Mv.Accounts]
|
|
opts = if actor, do: Keyword.put(opts, :actor, actor), else: opts
|
|
|
|
users =
|
|
case Ash.read(
|
|
Accounts.User
|
|
|> Ash.Query.filter(role_id in ^role_ids)
|
|
|> Ash.Query.select([:role_id]),
|
|
opts
|
|
) do
|
|
{:ok, users_list} -> users_list
|
|
{:error, _} -> []
|
|
end
|
|
|
|
# Group by role_id and count
|
|
users
|
|
|> Enum.group_by(& &1.role_id)
|
|
|> Enum.map(fn {role_id, users_list} -> {role_id, length(users_list)} end)
|
|
|> Map.new()
|
|
end
|
|
|
|
# Gets user count from preloaded assigns map
|
|
@spec get_user_count(Mv.Authorization.Role.t(), %{Ecto.UUID.t() => non_neg_integer()}) ::
|
|
non_neg_integer()
|
|
defp get_user_count(role, user_counts) do
|
|
Map.get(user_counts, role.id, 0)
|
|
end
|
|
|
|
# Recalculates user count for a specific role (used before deletion)
|
|
@spec recalculate_user_count(Mv.Authorization.Role.t(), map() | nil) :: non_neg_integer()
|
|
defp recalculate_user_count(role, actor) do
|
|
opts = [domain: Mv.Accounts]
|
|
opts = if actor, do: Keyword.put(opts, :actor, actor), else: opts
|
|
|
|
case Ash.count(Accounts.User |> Ash.Query.filter(role_id == ^role.id), opts) do
|
|
{:ok, count} -> count
|
|
_ -> 0
|
|
end
|
|
end
|
|
end
|