refactor: email validations

This commit is contained in:
Moritz 2025-10-17 14:34:04 +02:00 committed by moritz
parent 6ac47bdd9c
commit d1f660e26d
2 changed files with 27 additions and 51 deletions

View file

@ -1,9 +1,7 @@
defmodule Mv.Accounts.User.Validations.EmailNotUsedByOtherMember do defmodule Mv.Accounts.User.Validations.EmailNotUsedByOtherMember do
@moduledoc """ @moduledoc """
Validates that the user's email is not already used by another member Validates that the user's email is not already used by another member.
(unless that member is linked to this user). Allows syncing with linked member (excludes member_id from check).
This prevents email conflicts when syncing between users and members.
""" """
use Ash.Resource.Validation use Ash.Resource.Validation
@ -11,42 +9,32 @@ defmodule Mv.Accounts.User.Validations.EmailNotUsedByOtherMember do
def validate(changeset, _opts, _context) do def validate(changeset, _opts, _context) do
case Ash.Changeset.fetch_change(changeset, :email) do case Ash.Changeset.fetch_change(changeset, :email) do
{:ok, new_email} -> {:ok, new_email} ->
check_email_not_used_by_other_member(changeset, new_email) member_id = Ash.Changeset.get_attribute(changeset, :member_id)
check_email_uniqueness(new_email, member_id)
:error -> :error ->
# Email not being changed
:ok :ok
end end
end end
defp check_email_not_used_by_other_member(changeset, new_email) do defp check_email_uniqueness(new_email, exclude_member_id) do
member_id = Ash.Changeset.get_attribute(changeset, :member_id)
# Check if any member has this email
# Exclude the member linked to this user (if any)
query = query =
Mv.Membership.Member Mv.Membership.Member
|> Ash.Query.filter(email == ^to_string(new_email)) |> Ash.Query.filter(email == ^to_string(new_email))
|> then(fn q -> |> maybe_exclude_id(exclude_member_id)
if member_id do
Ash.Query.filter(q, id != ^member_id)
else
q
end
end)
case Ash.read(query) do case Ash.read(query) do
{:ok, []} -> {:ok, []} ->
# No conflicting member found
:ok :ok
{:ok, members} when is_list(members) and length(members) > 0 -> {:ok, _} ->
# Email is already used by another member
{:error, field: :email, message: "is already used by another member", value: new_email} {:error, field: :email, message: "is already used by another member", value: new_email}
{:error, _} -> {:error, _} ->
# Error reading members - be safe and allow
:ok :ok
end end
end end
defp maybe_exclude_id(query, nil), do: query
defp maybe_exclude_id(query, id), do: Ash.Query.filter(query, id != ^id)
end end

View file

@ -1,9 +1,7 @@
defmodule Mv.Membership.Member.Validations.EmailNotUsedByOtherUser do defmodule Mv.Membership.Member.Validations.EmailNotUsedByOtherUser do
@moduledoc """ @moduledoc """
Validates that the member's email is not already used by another user Validates that the member's email is not already used by another user.
(unless that user is linked to this member). Allows syncing with linked user (excludes linked user from check).
This prevents email conflicts when syncing between users and members.
""" """
use Ash.Resource.Validation use Ash.Resource.Validation
@ -11,49 +9,39 @@ defmodule Mv.Membership.Member.Validations.EmailNotUsedByOtherUser do
def validate(changeset, _opts, _context) do def validate(changeset, _opts, _context) do
case Ash.Changeset.fetch_change(changeset, :email) do case Ash.Changeset.fetch_change(changeset, :email) do
{:ok, new_email} -> {:ok, new_email} ->
check_email_not_used_by_other_user(changeset, new_email) linked_user_id = get_linked_user_id(changeset.data)
check_email_uniqueness(new_email, linked_user_id)
:error -> :error ->
# Email not being changed
:ok :ok
end end
end end
defp check_email_not_used_by_other_user(changeset, new_email) do defp check_email_uniqueness(new_email, exclude_user_id) do
# Load the user relationship to check if this member is linked to a user
member_with_user =
case Ash.load(changeset.data, :user) do
{:ok, loaded} -> loaded
{:error, _} -> changeset.data
end
linked_user_id = if member_with_user.user, do: member_with_user.user.id, else: nil
# Check if any user has this email (case-insensitive)
# Exclude the user linked to this member (if any)
query = query =
Mv.Accounts.User Mv.Accounts.User
|> Ash.Query.filter(email == ^new_email) |> Ash.Query.filter(email == ^new_email)
|> then(fn q -> |> maybe_exclude_id(exclude_user_id)
if linked_user_id do
Ash.Query.filter(q, id != ^linked_user_id)
else
q
end
end)
case Ash.read(query) do case Ash.read(query) do
{:ok, []} -> {:ok, []} ->
# No conflicting user found
:ok :ok
{:ok, users} when is_list(users) and length(users) > 0 -> {:ok, _} ->
# Email is already used by another user
{:error, field: :email, message: "is already used by another user", value: new_email} {:error, field: :email, message: "is already used by another user", value: new_email}
{:error, _} -> {:error, _} ->
# Error reading users - be safe and allow
:ok :ok
end end
end end
defp maybe_exclude_id(query, nil), do: query
defp maybe_exclude_id(query, id), do: Ash.Query.filter(query, id != ^id)
defp get_linked_user_id(member_data) do
case Ash.load(member_data, :user) do
{:ok, %{user: %{id: id}}} -> id
_ -> nil
end
end
end end