-
-
- <%= if length(@available_members) > 0 do %>
-
- <%= for {member, index} <- Enum.with_index(@available_members) do %>
-
-
{MvWeb.Helpers.MemberHelpers.display_name(member)}
-
{member.email}
-
- <% end %>
+ <%= if @user && @user.member && !@unlink_member do %>
+
+
+
+
+
+ {MvWeb.Helpers.MemberHelpers.display_name(@user.member)}
+
+
{@user.member.email}
- <% end %>
+
+ {gettext("Unlink Member")}
+
+
-
- <%= if @user && @user.email && @available_members != [] && Enum.all?(@available_members, &(&1.email == to_string(@user.email))) do %>
-
+ <% else %>
+ <%= if @unlink_member do %>
+
+
- {gettext("Note")}: {gettext(
- "A member with this email already exists. To link with a different member, please change one of the email addresses first."
+ {gettext("Unlinking scheduled")}: {gettext(
+ "Member will be unlinked when you save. Cannot select new member until saved."
)}
<% end %>
+
+
+
+
- <%= if @selected_member_id && @selected_member_name do %>
-
-
- {gettext("Selected")}: {@selected_member_name}
-
-
- {gettext("Save to confirm linking.")}
-
+ <%= if length(@available_members) > 0 do %>
+
+ <%= for {member, index} <- Enum.with_index(@available_members) do %>
+
+
+ {MvWeb.Helpers.MemberHelpers.display_name(member)}
+
+
{member.email}
+
+ <% end %>
+
+ <% end %>
- <% end %>
-
- <% end %>
-
+
+ <%= if @user && @user.email && @available_members != [] && Enum.all?(@available_members, &(&1.email == to_string(@user.email))) do %>
+
+
+ {gettext("Note")}: {gettext(
+ "A member with this email already exists. To link with a different member, please change one of the email addresses first."
+ )}
+
+
+ <% end %>
+
+ <%= if @selected_member_id && @selected_member_name do %>
+
+
+ {gettext("Selected")}: {@selected_member_name}
+
+
+ {gettext("Save to confirm linking.")}
+
+
+ <% end %>
+
+ <% end %>
+
+ <% end %>
<.button phx-disable-with={gettext("Saving...")} variant="primary">
@@ -289,14 +294,19 @@ defmodule MvWeb.UserLive.Form do
end
defp mount_continue(user, params, socket) do
+ actor = current_actor(socket)
action = if is_nil(user), do: gettext("New"), else: gettext("Edit")
page_title = action <> " " <> gettext("User")
+ # Only admins can link/unlink users to members (permission docs; prevents privilege escalation).
+ can_manage_member_linking = can?(actor, :destroy, Mv.Accounts.User)
+
{:ok,
socket
|> assign(:return_to, return_to(params["return_to"]))
|> assign(user: user)
|> assign(:page_title, page_title)
+ |> assign(:can_manage_member_linking, can_manage_member_linking)
|> assign(:show_password_fields, false)
|> assign(:member_search_query, "")
|> assign(:available_members, [])
@@ -329,9 +339,9 @@ defmodule MvWeb.UserLive.Form do
def handle_event("validate", %{"user" => user_params}, socket) do
validated_form = AshPhoenix.Form.validate(socket.assigns.form, user_params)
- # Reload members if email changed (for email-match priority)
+ # Reload members if email changed (for email-match priority; only when member linking UI is shown)
socket =
- if Map.has_key?(user_params, "email") do
+ if Map.has_key?(user_params, "email") and socket.assigns[:can_manage_member_linking] do
user_email = user_params["email"]
members = load_members_for_linking(user_email, socket.assigns.member_search_query, socket)
@@ -480,20 +490,25 @@ defmodule MvWeb.UserLive.Form do
end
defp perform_member_link_action(socket, user, actor) do
- cond do
- # Selected member ID takes precedence (new link)
- socket.assigns.selected_member_id ->
- Mv.Accounts.update_user(user, %{member: %{id: socket.assigns.selected_member_id}},
- actor: actor
- )
+ # Only admins may link/unlink (backend policy also restricts update_user; UI must not call it).
+ if can?(actor, :destroy, Mv.Accounts.User) do
+ cond do
+ # Selected member ID takes precedence (new link)
+ socket.assigns.selected_member_id ->
+ Mv.Accounts.update_user(user, %{member: %{id: socket.assigns.selected_member_id}},
+ actor: actor
+ )
- # Unlink flag is set
- socket.assigns[:unlink_member] ->
- Mv.Accounts.update_user(user, %{member: nil}, actor: actor)
+ # Unlink flag is set
+ socket.assigns[:unlink_member] ->
+ Mv.Accounts.update_user(user, %{member: nil}, actor: actor)
- # No changes to member relationship
- true ->
- {:ok, user}
+ # No changes to member relationship
+ true ->
+ {:ok, user}
+ end
+ else
+ {:ok, user}
end
end
@@ -552,13 +567,28 @@ defmodule MvWeb.UserLive.Form do
end
@spec assign_form(Phoenix.LiveView.Socket.t()) :: Phoenix.LiveView.Socket.t()
- defp assign_form(%{assigns: %{user: user, show_password_fields: show_password_fields}} = socket) do
+ defp assign_form(
+ %{
+ assigns: %{
+ user: user,
+ show_password_fields: show_password_fields,
+ can_manage_member_linking: can_manage_member_linking
+ }
+ } = socket
+ ) do
actor = current_actor(socket)
form =
if user do
- # For existing users, use admin password action if password fields are shown
- action = if show_password_fields, do: :admin_set_password, else: :update_user
+ # For existing users: admin uses update_user (email + member); non-admin uses update (email only).
+ # Password change uses admin_set_password for both.
+ action =
+ cond do
+ show_password_fields -> :admin_set_password
+ can_manage_member_linking -> :update_user
+ true -> :update
+ end
+
AshPhoenix.Form.for_update(user, action, domain: Mv.Accounts, as: "user", actor: actor)
else
# For new users, use password registration if password fields are shown