179 lines
5.8 KiB
Elixir
179 lines
5.8 KiB
Elixir
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"""
|
|
<Layouts.app flash={@flash} current_user={@current_user}>
|
|
<.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")}
|
|
</.button>
|
|
</:leading>
|
|
{gettext("User")} {@user.email}
|
|
<:subtitle>{gettext("This is a user record from your database.")}</:subtitle>
|
|
|
|
<: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")}
|
|
</.button>
|
|
<% end %>
|
|
</:actions>
|
|
</.header>
|
|
|
|
<.list>
|
|
<:item title={gettext("Email")}>{@user.email}</:item>
|
|
<:item title={gettext("Role")}>{@user.role.name}</:item>
|
|
<:item title={gettext("Password Authentication")}>
|
|
{if MvWeb.Helpers.UserHelpers.has_password?(@user),
|
|
do: gettext("Enabled"),
|
|
else: gettext("Not enabled")}
|
|
</:item>
|
|
<:item title={gettext("OIDC")}>
|
|
{if MvWeb.Helpers.UserHelpers.has_oidc?(@user),
|
|
do: gettext("Linked"),
|
|
else: gettext("Not linked")}
|
|
</:item>
|
|
<: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)}
|
|
</.link>
|
|
<% else %>
|
|
<span class="italic text-gray-500">{gettext("No member linked")}</span>
|
|
<% end %>
|
|
</:item>
|
|
</.list>
|
|
|
|
<%!-- Danger zone: canonical pattern (same as member show) --%>
|
|
<%= if can?(@current_user, :destroy, @user) and not Mv.Helpers.SystemActor.system_user?(@user) do %>
|
|
<section class="mt-8 mb-6" aria-labelledby="danger-zone-heading">
|
|
<h2 id="danger-zone-heading" class="text-lg font-semibold mb-3 text-error">
|
|
{gettext("Danger zone")}
|
|
</h2>
|
|
<div class="border border-base-300 rounded-lg p-4 bg-base-100">
|
|
<p class="text-base-content/70 mb-4">
|
|
{gettext(
|
|
"Deleting this user cannot be undone. The user account and any linked member association will be affected."
|
|
)}
|
|
</p>
|
|
<.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")}
|
|
</.button>
|
|
</div>
|
|
</section>
|
|
<% end %>
|
|
</Layouts.app>
|
|
"""
|
|
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
|