feat: email uniqueness constraint between user and member

This commit is contained in:
Moritz 2025-10-17 14:21:23 +02:00
parent 5a0a261cd6
commit 39afaf3999
Signed by: moritz
GPG key ID: 1020A035E5DD0824
5 changed files with 329 additions and 6 deletions

View file

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

View file

@ -0,0 +1,59 @@
defmodule Mv.Membership.Member.Validations.EmailNotUsedByOtherUser do
@moduledoc """
Validates that the member's email is not already used by another user
(unless that user is linked to this member).
This prevents email conflicts when syncing between users and members.
"""
use Ash.Resource.Validation
@impl true
def validate(changeset, _opts, _context) do
case Ash.Changeset.fetch_change(changeset, :email) do
{:ok, new_email} ->
check_email_not_used_by_other_user(changeset, new_email)
:error ->
# Email not being changed
:ok
end
end
defp check_email_not_used_by_other_user(changeset, new_email) 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 =
Mv.Accounts.User
|> Ash.Query.filter(email == ^new_email)
|> then(fn q ->
if linked_user_id do
Ash.Query.filter(q, id != ^linked_user_id)
else
q
end
end)
case Ash.read(query) do
{:ok, []} ->
# No conflicting user found
:ok
{:ok, users} when is_list(users) and length(users) > 0 ->
# Email is already used by another user
{:error, field: :email, message: "is already used by another user", value: new_email}
{:error, _} ->
# Error reading users - be safe and allow
:ok
end
end
end