defmodule MvWeb.GroupLive.Show do @moduledoc """ LiveView for displaying a single group's details. ## Features - Display group information (name, description, member count) - List all members in the group - Navigate to edit form - Return to groups list - Delete group (with confirmation) ## Security - All users with read permission can view groups - Only admin users can edit/delete groups """ use MvWeb, :live_view import MvWeb.LiveHelpers, only: [current_actor: 1] import MvWeb.Authorization alias Mv.Membership @impl true def mount(_params, _session, socket) do {:ok, socket} end @impl true def handle_params(%{"slug" => slug}, _url, socket) do actor = current_actor(socket) # Check if user can read groups if can?(actor, :read, Mv.Membership.Group) do load_group_by_slug(socket, slug, actor) else {:noreply, redirect(socket, to: ~p"/members")} end end defp load_group_by_slug(socket, slug, actor) do case Membership.get_group_by_slug(slug, actor: actor, load: [:members, :member_count]) do {:ok, nil} -> {:noreply, socket |> put_flash(:error, gettext("Group not found.")) |> redirect(to: ~p"/groups")} {:ok, group} -> {:noreply, socket |> assign(:page_title, group.name) |> assign(:group, group)} {:error, _error} -> {:noreply, socket |> put_flash(:error, gettext("Failed to load group.")) |> redirect(to: ~p"/groups")} end end @impl true def render(assigns) do ~H""" <%!-- Header with Back button, Name, and Edit/Delete buttons --%>
<.button navigate={~p"/groups"} aria-label={gettext("Back to groups list")}> <.icon name="hero-arrow-left" class="size-4" /> {gettext("Back")}

{@group.name}

<%= if can?(@current_user, :update, Mv.Membership.Group) do %> <.button variant="primary" navigate={~p"/groups/#{@group.slug}/edit"}> {gettext("Edit")} <% end %> <%= if can?(@current_user, :destroy, Mv.Membership.Group) do %> <.button class="btn-error" phx-click="open_delete_modal"> {gettext("Delete")} <% end %>
<%!-- Group Information --%>

{gettext("Description")}

<%= if @group.description && String.trim(@group.description) != "" do %>

{@group.description}

<% else %>

{gettext("No description")}

<% end %>

{gettext("Members")}

{ngettext( "Total: %{count} member", "Total: %{count} members", @group.member_count || 0, count: @group.member_count || 0 )}

<%= if Enum.empty?(@group.members || []) do %>

{gettext("No members in this group")}

<% else %>
<%= for member <- @group.members do %> <% end %>
{gettext("Name")} {gettext("Email")}
<.link navigate={~p"/members/#{member.id}"} class="link link-primary" > {MvWeb.Helpers.MemberHelpers.display_name(member)} <%= if member.email do %> {member.email} <% else %> <% end %>
<% end %>
<%!-- Delete Confirmation Modal --%> <%= if assigns[:show_delete_modal] do %> <% end %>
""" end @impl true def handle_event("open_delete_modal", _params, socket) do {:noreply, assign(socket, show_delete_modal: true, name_confirmation: "")} end def handle_event("cancel_delete", _params, socket) do {:noreply, socket |> assign(:show_delete_modal, false) |> assign(:name_confirmation, "")} end def handle_event("update_name_confirmation", %{"name" => name}, socket) do {:noreply, assign(socket, :name_confirmation, name)} end def handle_event("confirm_delete", %{"slug" => slug}, socket) do actor = current_actor(socket) group = socket.assigns.group # Verify slug matches the group in assigns (prevents tampering) if group.slug == slug do # Server-side authorization check on the specific group record if can?(actor, :destroy, group) do handle_delete_confirmation(socket, group, actor) else {:noreply, socket |> put_flash(:error, gettext("Not authorized.")) |> redirect(to: ~p"/groups")} end else {:noreply, socket |> put_flash(:error, gettext("Group not found.")) |> redirect(to: ~p"/groups")} end end defp handle_delete_confirmation(socket, group, actor) do if socket.assigns.name_confirmation == group.name do perform_group_deletion(socket, group, actor) else {:noreply, socket |> put_flash(:error, gettext("Group name does not match.")) |> assign(:show_delete_modal, true)} end end defp perform_group_deletion(socket, group, actor) do case Membership.destroy_group(group, actor: actor) do :ok -> {:noreply, socket |> put_flash(:info, gettext("Group deleted successfully.")) |> redirect(to: ~p"/groups")} {:error, error} -> error_message = format_error(error) {:noreply, socket |> put_flash( :error, gettext("Failed to delete group: %{error}", error: error_message) ) |> assign(:show_delete_modal, false) |> assign(:name_confirmation, "")} end end defp format_error(%{message: message}) when is_binary(message), do: message defp format_error(error), do: inspect(error) end