diff --git a/lib/membership/member.ex b/lib/membership/member.ex index 06dbf57..476501c 100644 --- a/lib/membership/member.ex +++ b/lib/membership/member.ex @@ -153,6 +153,10 @@ defmodule Mv.Membership.Member do change manage_relationship(:custom_field_values, on_match: :update, on_no_match: :create) + # When :user argument is present and nil/empty, unrelate (admin-only via policy). + # Must run before manage_relationship; on_missing: :ignore then does nothing for nil input. + change Mv.Membership.Member.Changes.UnrelateUserWhenArgumentNil + # Manage the user relationship during member update # on_missing: :ignore so that omitting :user does NOT unlink (security: only admins may # change the link; unlink is explicit via user: nil, forbidden for non-admins by policy). diff --git a/lib/membership/member/changes/unrelate_user_when_argument_nil.ex b/lib/membership/member/changes/unrelate_user_when_argument_nil.ex new file mode 100644 index 0000000..dc4d097 --- /dev/null +++ b/lib/membership/member/changes/unrelate_user_when_argument_nil.ex @@ -0,0 +1,50 @@ +defmodule Mv.Membership.Member.Changes.UnrelateUserWhenArgumentNil do + @moduledoc """ + When :user argument is present and nil/empty on update_member, unrelate the current user. + + With on_missing: :ignore, manage_relationship does not unrelate when input is nil/[]. + This change handles explicit unlink (user: nil or user: %{}) by updating the linked + User to set member_id = nil. Only runs when the argument key is present (policy + ForbidMemberUserLinkUnlessAdmin ensures only admins can pass :user). + """ + use Ash.Resource.Change + + @spec change(Ash.Changeset.t(), keyword(), Ash.Resource.Change.context()) :: Ash.Changeset.t() + def change(changeset, _opts, _context) do + if unlink_requested?(changeset) do + unrelate_current_user(changeset) + else + changeset + end + end + + defp unlink_requested?(changeset) do + args = changeset.arguments || %{} + + if Map.has_key?(args, :user) or Map.has_key?(args, "user") do + user_arg = Ash.Changeset.get_argument(changeset, :user) + user_arg == nil or (is_map(user_arg) and map_size(user_arg) == 0) + else + false + end + end + + defp unrelate_current_user(changeset) do + member = changeset.data + actor = Map.get(changeset.context || %{}, :actor) + + case Ash.load(member, :user, domain: Mv.Membership, authorize?: false) do + {:ok, %{user: user}} when not is_nil(user) -> + # User's :update action only accepts [:email]; use :update_user so + # manage_relationship(:member, ..., on_missing: :unrelate) runs and clears member_id. + user + |> Ash.Changeset.for_update(:update_user, %{member: nil}, domain: Mv.Accounts) + |> Ash.update(domain: Mv.Accounts, actor: actor, authorize?: false) + + changeset + + _ -> + changeset + end + end +end