UserLive.Form: gate Member-Linking to admin, use :update for non-admin

- Show Member-Linking UI only when can_manage_member_linking (admin)
- perform_member_link_action runs only for admin
- assign_form: non-admin uses :update (email), admin uses :update_user
- Load members for linking only when can_manage_member_linking
This commit is contained in:
Moritz 2026-01-30 11:13:28 +01:00
parent 14fa873640
commit 06d6531569

View file

@ -36,6 +36,7 @@ defmodule MvWeb.UserLive.Form do
require Jason
import MvWeb.LiveHelpers, only: [current_actor: 1, submit_form: 3]
import MvWeb.Authorization, only: [can?: 3]
@impl true
def render(assigns) do
@ -125,7 +126,8 @@ defmodule MvWeb.UserLive.Form do
<% end %>
</div>
<!-- Member Linking Section -->
<!-- Member Linking Section (admin only: only admins can link/unlink users to members) -->
<%= if @can_manage_member_linking do %>
<div class="mt-6">
<h2 class="mb-3 text-base font-semibold">{gettext("Linked Member")}</h2>
@ -214,7 +216,9 @@ defmodule MvWeb.UserLive.Form do
)
]}
>
<p class="font-medium">{MvWeb.Helpers.MemberHelpers.display_name(member)}</p>
<p class="font-medium">
{MvWeb.Helpers.MemberHelpers.display_name(member)}
</p>
<p class="text-sm text-base-content/70">{member.email}</p>
</div>
<% end %>
@ -248,6 +252,7 @@ defmodule MvWeb.UserLive.Form do
</div>
<% end %>
</div>
<% end %>
<div class="mt-4">
<.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,6 +490,8 @@ defmodule MvWeb.UserLive.Form do
end
defp perform_member_link_action(socket, user, actor) do
# 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 ->
@ -495,6 +507,9 @@ defmodule MvWeb.UserLive.Form do
true ->
{:ok, user}
end
else
{:ok, user}
end
end
defp handle_save_success(socket, updated_user) do
@ -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