From 54c825bac3e74c37a19a7f49cdd0d38127b47c80 Mon Sep 17 00:00:00 2001 From: Moritz Date: Thu, 8 Jan 2026 13:28:09 +0100 Subject: [PATCH] refactor: reduce nesting depth in RoleLive handle_event functions --- lib/mv_web/live/role_live/index.ex | 121 +++++++++++++------ lib/mv_web/live/role_live/show.ex | 181 +++++++++++++++++++++-------- 2 files changed, 219 insertions(+), 83 deletions(-) diff --git a/lib/mv_web/live/role_live/index.ex b/lib/mv_web/live/role_live/index.ex index 49aa62c..a2d2c36 100644 --- a/lib/mv_web/live/role_live/index.ex +++ b/lib/mv_web/live/role_live/index.ex @@ -16,15 +16,16 @@ defmodule MvWeb.RoleLive.Index do """ use MvWeb, :live_view - alias Mv.Authorization alias Mv.Accounts + alias Mv.Authorization require Ash.Query @impl true def mount(_params, _session, socket) do socket = ensure_user_role_loaded(socket) - roles = load_roles() + actor = socket.assigns[:current_user] + roles = load_roles(actor) user_counts = load_user_counts(roles) {:ok, @@ -61,46 +62,86 @@ defmodule MvWeb.RoleLive.Index do @impl true def handle_event("delete", %{"id" => id}, socket) do - {:ok, role} = Authorization.get_role(id) - user_count = get_user_count(role, socket.assigns.user_counts) + case Authorization.get_role(id) do + {:ok, role} -> + handle_delete_role(role, id, socket) - 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 - case Authorization.destroy_role(role) do - :ok -> - updated_roles = Enum.reject(socket.assigns.roles, &(&1.id == id)) - updated_counts = Map.delete(socket.assigns.user_counts, id) + {:error, %Ash.Error.Query.NotFound{}} -> + {:noreply, + put_flash( + socket, + :error, + gettext("Role not found.") + )} - {: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) - {:error, error} -> - error_message = format_error(error) - - {:noreply, - put_flash( - socket, - :error, - gettext("Failed to delete role: %{error}", error: error_message) - )} - end + {:noreply, + put_flash( + socket, + :error, + gettext("Failed to delete role: %{error}", error: error_message) + )} end end - defp load_roles do - case Authorization.list_roles() do + defp handle_delete_role(role, id, socket) do + cond do + role.is_system_role -> + {:noreply, + put_flash( + socket, + :error, + gettext("System roles cannot be deleted.") + )} + + recalculate_user_count(role) > 0 -> + user_count = recalculate_user_count(role) + + {: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 + ) + )} + + true -> + perform_role_deletion(role, id, socket) + 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 + + 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 @@ -129,6 +170,14 @@ defmodule MvWeb.RoleLive.Index do Map.get(user_counts, role.id, 0) end + # Recalculates user count for a specific role (used before deletion) + defp recalculate_user_count(role) do + case Ash.count(Accounts.User |> Ash.Query.filter(role_id == ^role.id)) do + {:ok, count} -> count + _ -> 0 + end + end + defp format_error(%Ash.Error.Invalid{} = error) do Enum.map_join(error.errors, ", ", fn e -> e.message end) end diff --git a/lib/mv_web/live/role_live/show.ex b/lib/mv_web/live/role_live/show.ex index cc4a302..18b98f2 100644 --- a/lib/mv_web/live/role_live/show.ex +++ b/lib/mv_web/live/role_live/show.ex @@ -19,65 +19,127 @@ defmodule MvWeb.RoleLive.Show do @impl true def mount(%{"id" => id}, _session, socket) do # Ensure current_user has role loaded for authorization checks - socket = - if socket.assigns[:current_user] do - user = socket.assigns.current_user + socket = ensure_user_role_loaded(socket) - user_with_role = - case Map.get(user, :role) do - %Ash.NotLoaded{} -> Ash.load!(user, :role, domain: Mv.Accounts) - nil -> Ash.load!(user, :role, domain: Mv.Accounts) - role when not is_nil(role) -> user - end + try do + case Ash.get( + Mv.Authorization.Role, + id, + domain: Mv.Authorization, + actor: socket.assigns[:current_user] + ) do + {:ok, role} -> + user_count = load_user_count(role) - assign(socket, :current_user, user_with_role) - else - socket + {:ok, + socket + |> assign(:page_title, gettext("Show Role")) + |> assign(:role, role) + |> assign(:user_count, user_count)} + + {:error, %Ash.Error.Invalid{errors: [%Ash.Error.Query.NotFound{} | _]}} -> + {:ok, + socket + |> put_flash(:error, gettext("Role not found.")) + |> redirect(to: ~p"/admin/roles")} + + {:error, error} -> + raise error end + rescue + e in [Ash.Error.Invalid] -> + case e do + %Ash.Error.Invalid{errors: [%Ash.Error.Query.NotFound{} | _]} -> + {:ok, + socket + |> put_flash(:error, gettext("Role not found.")) + |> redirect(to: ~p"/admin/roles")} - role = Ash.get!(Mv.Authorization.Role, id, domain: Mv.Authorization) - user_count = load_user_count(role) - - {:ok, - socket - |> assign(:page_title, gettext("Show Role")) - |> assign(:role, role) - |> assign(:user_count, user_count)} + _ -> + reraise e, __STACKTRACE__ + end + end end @impl true def handle_event("delete", %{"id" => id}, socket) do - {:ok, role} = Mv.Authorization.get_role(id) - user_count = socket.assigns.user_count + case Mv.Authorization.get_role(id, actor: socket.assigns.current_user) do + {:ok, role} -> + handle_delete_role(role, socket) - 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 + {:error, %Ash.Error.Query.NotFound{}} -> + {:noreply, + put_flash( + socket, + :error, + gettext("Role not found.") ) - )} - else - case Mv.Authorization.destroy_role(role) do - :ok -> - {:noreply, - socket - |> put_flash(:info, gettext("Role deleted successfully.")) - |> push_navigate(to: ~p"/admin/roles")} + |> push_navigate(to: ~p"/admin/roles")} - {:error, error} -> - error_message = format_error(error) + {:error, error} -> + error_message = format_error(error) - {:noreply, - put_flash( - socket, - :error, - gettext("Failed to delete role: %{error}", error: error_message) - )} - end + {:noreply, + put_flash( + socket, + :error, + gettext("Failed to delete role: %{error}", error: error_message) + )} + end + end + + defp handle_delete_role(role, socket) do + cond do + role.is_system_role -> + {:noreply, + put_flash( + socket, + :error, + gettext("System roles cannot be deleted.") + )} + + recalculate_user_count(role) > 0 -> + user_count = recalculate_user_count(role) + + {: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 + ) + )} + + true -> + perform_role_deletion(role, socket) + end + end + + defp perform_role_deletion(role, socket) do + case Mv.Authorization.destroy_role(role, actor: socket.assigns.current_user) do + :ok -> + {:noreply, + socket + |> put_flash(:info, gettext("Role deleted successfully.")) + |> push_navigate(to: ~p"/admin/roles")} + + {:error, error} -> + error_message = format_error(error) + + {:noreply, + put_flash( + socket, + :error, + gettext("Failed to delete role: %{error}", error: error_message) + )} + end + end + + defp recalculate_user_count(role) do + case Ash.count(Accounts.User |> Ash.Query.filter(role_id == ^role.id)) do + {:ok, count} -> count + _ -> 0 end end @@ -151,6 +213,31 @@ defmodule MvWeb.RoleLive.Show do defp format_error(error) when is_binary(error), do: error defp format_error(_error), do: gettext("An error occurred") + defp ensure_user_role_loaded(socket) do + if socket.assigns[:current_user] do + user = socket.assigns.current_user + user_with_role = load_user_role(user) + assign(socket, :current_user, user_with_role) + else + socket + end + end + + defp load_user_role(user) do + case Map.get(user, :role) do + %Ash.NotLoaded{} -> load_role_safely(user) + nil -> load_role_safely(user) + _role -> user + end + end + + defp load_role_safely(user) do + case Ash.load(user, :role, domain: Mv.Accounts) do + {:ok, loaded_user} -> loaded_user + {:error, _} -> user + end + end + defp permission_set_badge_class("own_data"), do: "badge badge-neutral badge-sm" defp permission_set_badge_class("read_only"), do: "badge badge-info badge-sm" defp permission_set_badge_class("normal_user"), do: "badge badge-success badge-sm"