defmodule MvWeb.UserLive.Show do @moduledoc """ LiveView for displaying a single user's details. ## Features - Display user information (email, OIDC ID) - Show authentication methods (password, OIDC) - Display linked member account (if exists) - Navigate to edit form - Return to user list ## Displayed Information - Email address - OIDC ID (if authenticated via OIDC) - Password authentication status - Linked member (name and email) ## Authentication Status Shows which authentication methods are enabled for the user: - Password authentication (has hashed_password) - OIDC authentication (has oidc_id) ## Navigation - Back to user list - Edit user (with return_to parameter for back navigation) """ use MvWeb, :live_view import MvWeb.LiveHelpers, only: [current_actor: 1] import MvWeb.ErrorHelpers, only: [format_ash_error: 1] @impl true def render(assigns) do ~H""" <.header> <:leading> <.button navigate={~p"/users"} variant="neutral" aria-label={gettext("Back to users list")} > <.icon name="hero-arrow-left" class="size-4" /> {gettext("Back")} {gettext("User")} {@user.email} <:subtitle>{gettext("This is a user record from your database.")} <:actions> <%= if can?(@current_user, :update, @user) do %> <.button variant="primary" navigate={~p"/users/#{@user}/edit?return_to=show"} data-testid="user-edit" > <.icon name="hero-pencil-square" /> {gettext("Edit User")} <% end %> <.list> <:item title={gettext("Email")}>{@user.email} <:item title={gettext("Role")}>{@user.role.name} <:item title={gettext("Password Authentication")}> {if MvWeb.Helpers.UserHelpers.has_password?(@user), do: gettext("Enabled"), else: gettext("Not enabled")} <:item title={gettext("OIDC")}> {if MvWeb.Helpers.UserHelpers.has_oidc?(@user), do: gettext("Linked"), else: gettext("Not linked")} <:item title={gettext("Linked Member")}> <%= if @user.member do %> <.link navigate={~p"/members/#{@user.member}"} class="text-blue-600 underline hover:text-blue-800" > <.icon name="hero-users" class="inline w-4 h-4 mr-1" /> {MvWeb.Helpers.MemberHelpers.display_name(@user.member)} <% else %> {gettext("No member linked")} <% end %> <%!-- Danger zone: canonical pattern (same as member show) --%> <%= if can?(@current_user, :destroy, @user) and not Mv.Helpers.SystemActor.system_user?(@user) do %>

{gettext("Danger zone")}

{gettext( "Deleting this user cannot be undone. The user account and any linked member association will be affected." )}

<.button variant="danger" phx-click="delete" phx-value-id={@user.id} data-confirm={ gettext( "Are you sure you want to delete the user %{email}? This action cannot be undone.", email: @user.email ) } data-testid="user-delete" aria-label={gettext("Delete user %{email}", email: @user.email)} > <.icon name="hero-trash" class="size-4" /> {gettext("Delete user")}
<% end %>
""" end @impl true def mount(%{"id" => id}, _session, socket) do actor = current_actor(socket) user = Ash.get!(Mv.Accounts.User, id, domain: Mv.Accounts, load: [:member, :role], actor: actor) if Mv.Helpers.SystemActor.system_user?(user) do {:ok, socket |> put_flash(:error, gettext("This user cannot be viewed.")) |> push_navigate(to: ~p"/users")} else {:ok, socket |> assign(:page_title, gettext("Show User")) |> assign(:user, user)} end end @impl true def handle_event("delete", %{"id" => id}, socket) do user = socket.assigns.user actor = current_actor(socket) cond do to_string(id) != to_string(user.id) -> {:noreply, put_flash(socket, :error, gettext("User not found"))} Mv.Helpers.SystemActor.system_user?(user) -> {:noreply, put_flash(socket, :error, gettext("System user cannot be deleted."))} true -> handle_user_delete_destroy(socket, user, actor) end end defp handle_user_delete_destroy(socket, user, actor) do case Ash.destroy(user, domain: Mv.Accounts, actor: actor) do :ok -> {:noreply, socket |> put_flash(:success, gettext("User deleted successfully")) |> push_navigate(to: ~p"/users")} {:error, %Ash.Error.Forbidden{}} -> {:noreply, put_flash(socket, :error, gettext("You do not have permission to delete this user"))} {:error, error} -> {:noreply, put_flash(socket, :error, format_ash_error(error))} end end end