Fix member unlink: use User update_user action
Some checks reported errors
continuous-integration/drone/push Build was killed
continuous-integration/drone/promote/production Build is passing

UnrelateUserWhenArgumentNil used User :update which only accepts :email.
Switch to :update_user with member: nil so manage_relationship clears member_id.
This commit is contained in:
Moritz 2026-02-04 14:44:39 +01:00
parent 5194b20b5c
commit 95472424b1
Signed by: moritz
GPG key ID: 1020A035E5DD0824
2 changed files with 54 additions and 0 deletions

View file

@ -153,6 +153,10 @@ defmodule Mv.Membership.Member do
change manage_relationship(:custom_field_values, on_match: :update, on_no_match: :create) 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 # Manage the user relationship during member update
# on_missing: :ignore so that omitting :user does NOT unlink (security: only admins may # 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). # change the link; unlink is explicit via user: nil, forbidden for non-admins by policy).

View file

@ -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