Compare commits
1 commit
84abe0a4b5
...
37c948c91a
| Author | SHA1 | Date | |
|---|---|---|---|
| 37c948c91a |
6 changed files with 35 additions and 167 deletions
|
|
@ -61,7 +61,7 @@ defmodule Mv.Accounts.User do
|
||||||
|
|
||||||
actions do
|
actions do
|
||||||
defaults [:read, :create, :destroy]
|
defaults [:read, :create, :destroy]
|
||||||
|
|
||||||
update :update do
|
update :update do
|
||||||
primary? true
|
primary? true
|
||||||
require_atomic? false
|
require_atomic? false
|
||||||
|
|
@ -77,14 +77,10 @@ defmodule Mv.Accounts.User do
|
||||||
|
|
||||||
# Manage the member relationship during user creation
|
# Manage the member relationship during user creation
|
||||||
change manage_relationship(:member, :member,
|
change manage_relationship(:member, :member,
|
||||||
# Look up existing member and relate to it
|
on_lookup: :relate, # Look up existing member and relate to it
|
||||||
on_lookup: :relate,
|
on_no_match: :error, # Error if member doesn't exist in database
|
||||||
# Error if member doesn't exist in database
|
on_match: :ignore, # If member already linked to this user, ignore (shouldn't happen in create)
|
||||||
on_no_match: :error,
|
on_missing: :ignore # If no member provided, that's fine (optional relationship)
|
||||||
# If member already linked to this user, ignore (shouldn't happen in create)
|
|
||||||
on_match: :ignore,
|
|
||||||
# If no member provided, that's fine (optional relationship)
|
|
||||||
on_missing: :ignore
|
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -99,15 +95,11 @@ defmodule Mv.Accounts.User do
|
||||||
|
|
||||||
# Manage the member relationship during user update
|
# Manage the member relationship during user update
|
||||||
change manage_relationship(:member, :member,
|
change manage_relationship(:member, :member,
|
||||||
# Look up existing member and relate to it
|
on_lookup: :relate, # Look up existing member and relate to it
|
||||||
on_lookup: :relate,
|
on_no_match: :error, # Error if member doesn't exist in database
|
||||||
# Error if member doesn't exist in database
|
on_match: :ignore, # If same member provided, that's fine (allows updates with same member)
|
||||||
on_no_match: :error,
|
on_missing: :unrelate # If no member provided, remove existing relationship (allows member removal)
|
||||||
# If same member provided, that's fine (allows updates with same member)
|
)
|
||||||
on_match: :ignore,
|
|
||||||
# If no member provided, remove existing relationship (allows member removal)
|
|
||||||
on_missing: :unrelate
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Admin action for direct password changes in admin panel
|
# Admin action for direct password changes in admin panel
|
||||||
|
|
@ -165,7 +157,7 @@ defmodule Mv.Accounts.User do
|
||||||
validate string_length(:password, min: 8) do
|
validate string_length(:password, min: 8) do
|
||||||
where action_is([:register_with_password, :admin_set_password])
|
where action_is([:register_with_password, :admin_set_password])
|
||||||
end
|
end
|
||||||
|
|
||||||
# Prevent overwriting existing member relationship
|
# Prevent overwriting existing member relationship
|
||||||
# This validation ensures race condition safety by requiring explicit two-step process:
|
# This validation ensures race condition safety by requiring explicit two-step process:
|
||||||
# 1. Remove existing member (set member to nil)
|
# 1. Remove existing member (set member to nil)
|
||||||
|
|
@ -174,15 +166,13 @@ defmodule Mv.Accounts.User do
|
||||||
validate fn changeset, _context ->
|
validate fn changeset, _context ->
|
||||||
member_arg = Ash.Changeset.get_argument(changeset, :member)
|
member_arg = Ash.Changeset.get_argument(changeset, :member)
|
||||||
current_member_id = changeset.data.member_id
|
current_member_id = changeset.data.member_id
|
||||||
|
|
||||||
# Only trigger if:
|
# Only trigger if:
|
||||||
# - member argument is provided AND has an ID
|
# - member argument is provided AND has an ID
|
||||||
# - user currently has a member
|
# - user currently has a member
|
||||||
# - the new member ID is different from current member ID
|
# - the new member ID is different from current member ID
|
||||||
if member_arg && member_arg[:id] && current_member_id &&
|
if member_arg && member_arg[:id] && current_member_id && member_arg[:id] != current_member_id do
|
||||||
member_arg[:id] != current_member_id do
|
{:error, field: :member, message: "User already has a member. Remove existing member first."}
|
||||||
{:error,
|
|
||||||
field: :member, message: "User already has a member. Remove existing member first."}
|
|
||||||
else
|
else
|
||||||
:ok
|
:ok
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -39,14 +39,10 @@ defmodule Mv.Membership.Member do
|
||||||
|
|
||||||
# Manage the user relationship during member creation
|
# Manage the user relationship during member creation
|
||||||
change manage_relationship(:user, :user,
|
change manage_relationship(:user, :user,
|
||||||
# Look up existing user and relate to it
|
on_lookup: :relate, # Look up existing user and relate to it
|
||||||
on_lookup: :relate,
|
on_no_match: :error, # Error if user doesn't exist in database
|
||||||
# Error if user doesn't exist in database
|
on_match: :error, # Error if user is already linked to another member (prevents "stealing")
|
||||||
on_no_match: :error,
|
on_missing: :ignore # If no user provided, that's fine (optional relationship)
|
||||||
# Error if user is already linked to another member (prevents "stealing")
|
|
||||||
on_match: :error,
|
|
||||||
# If no user provided, that's fine (optional relationship)
|
|
||||||
on_missing: :ignore
|
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -80,14 +76,10 @@ defmodule Mv.Membership.Member do
|
||||||
|
|
||||||
# Manage the user relationship during member update
|
# Manage the user relationship during member update
|
||||||
change manage_relationship(:user, :user,
|
change manage_relationship(:user, :user,
|
||||||
# Look up existing user and relate to it
|
on_lookup: :relate, # Look up existing user and relate to it
|
||||||
on_lookup: :relate,
|
on_no_match: :error, # Error if user doesn't exist in database
|
||||||
# Error if user doesn't exist in database
|
on_match: :error, # Error if user is already linked to another member (prevents "stealing")
|
||||||
on_no_match: :error,
|
on_missing: :unrelate # If no user provided, remove existing relationship (allows user removal)
|
||||||
# Error if user is already linked to another member (prevents "stealing")
|
|
||||||
on_match: :error,
|
|
||||||
# If no user provided, remove existing relationship (allows user removal)
|
|
||||||
on_missing: :unrelate
|
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -99,7 +91,7 @@ defmodule Mv.Membership.Member do
|
||||||
validate present(:first_name)
|
validate present(:first_name)
|
||||||
validate present(:last_name)
|
validate present(:last_name)
|
||||||
validate present(:email)
|
validate present(:email)
|
||||||
|
|
||||||
# Prevent linking to a user that already has a member
|
# Prevent linking to a user that already has a member
|
||||||
# This validation prevents "stealing" users from other members by checking
|
# This validation prevents "stealing" users from other members by checking
|
||||||
# if the target user is already linked to a different member
|
# if the target user is already linked to a different member
|
||||||
|
|
@ -107,25 +99,18 @@ defmodule Mv.Membership.Member do
|
||||||
# if the user is already linked to THIS specific member, not ANY member
|
# if the user is already linked to THIS specific member, not ANY member
|
||||||
validate fn changeset, _context ->
|
validate fn changeset, _context ->
|
||||||
user_arg = Ash.Changeset.get_argument(changeset, :user)
|
user_arg = Ash.Changeset.get_argument(changeset, :user)
|
||||||
|
|
||||||
if user_arg && user_arg[:id] do
|
if user_arg && user_arg[:id] do
|
||||||
user_id = user_arg[:id]
|
user_id = user_arg[:id]
|
||||||
current_member_id = changeset.data.id
|
current_member_id = changeset.data.id
|
||||||
|
|
||||||
# Check the current state of the user in the database
|
# Check the current state of the user in the database
|
||||||
case Ash.get(Mv.Accounts.User, user_id) do
|
case Ash.get(Mv.Accounts.User, user_id) do
|
||||||
# User is free to be linked
|
{:ok, %{member_id: nil}} -> :ok # User is free to be linked
|
||||||
{:ok, %{member_id: nil}} ->
|
{:ok, %{member_id: ^current_member_id}} -> :ok # User already linked to this member (update scenario)
|
||||||
:ok
|
|
||||||
|
|
||||||
# User already linked to this member (update scenario)
|
|
||||||
{:ok, %{member_id: ^current_member_id}} ->
|
|
||||||
:ok
|
|
||||||
|
|
||||||
{:ok, %{member_id: _other_member_id}} ->
|
{:ok, %{member_id: _other_member_id}} ->
|
||||||
# User is linked to a different member - prevent "stealing"
|
# User is linked to a different member - prevent "stealing"
|
||||||
{:error, field: :user, message: "User is already linked to another member"}
|
{:error, field: :user, message: "User is already linked to another member"}
|
||||||
|
|
||||||
{:error, _} ->
|
{:error, _} ->
|
||||||
{:error, field: :user, message: "User not found"}
|
{:error, field: :user, message: "User not found"}
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -37,16 +37,6 @@ defmodule MvWeb.MemberLive.Show do
|
||||||
<:item title={gettext("Street")}>{@member.street}</:item>
|
<:item title={gettext("Street")}>{@member.street}</:item>
|
||||||
<:item title={gettext("House Number")}>{@member.house_number}</:item>
|
<:item title={gettext("House Number")}>{@member.house_number}</:item>
|
||||||
<:item title={gettext("Postal Code")}>{@member.postal_code}</:item>
|
<:item title={gettext("Postal Code")}>{@member.postal_code}</:item>
|
||||||
<:item title={gettext("Linked User")}>
|
|
||||||
<%= if @member.user do %>
|
|
||||||
<.link navigate={~p"/users/#{@member.user}"} class="text-blue-600 hover:text-blue-800 underline">
|
|
||||||
<.icon name="hero-user" class="h-4 w-4 inline mr-1" />
|
|
||||||
{@member.user.email}
|
|
||||||
</.link>
|
|
||||||
<% else %>
|
|
||||||
<span class="text-gray-500 italic">{gettext("No user linked")}</span>
|
|
||||||
<% end %>
|
|
||||||
</:item>
|
|
||||||
</.list>
|
</.list>
|
||||||
|
|
||||||
<h3 class="mt-8 mb-2 text-lg font-semibold">{gettext("Custom Properties")}</h3>
|
<h3 class="mt-8 mb-2 text-lg font-semibold">{gettext("Custom Properties")}</h3>
|
||||||
|
|
@ -77,7 +67,7 @@ defmodule MvWeb.MemberLive.Show do
|
||||||
query =
|
query =
|
||||||
Mv.Membership.Member
|
Mv.Membership.Member
|
||||||
|> filter(id == ^id)
|
|> filter(id == ^id)
|
||||||
|> load([:user, properties: [:property_type]])
|
|> load(properties: [:property_type])
|
||||||
|
|
||||||
member = Ash.read_one!(query)
|
member = Ash.read_one!(query)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,16 +26,6 @@ defmodule MvWeb.UserLive.Show do
|
||||||
<:item title={gettext("Password Authentication")}>
|
<:item title={gettext("Password Authentication")}>
|
||||||
{if @user.hashed_password, do: gettext("Enabled"), else: gettext("Not enabled")}
|
{if @user.hashed_password, do: gettext("Enabled"), else: gettext("Not enabled")}
|
||||||
</:item>
|
</:item>
|
||||||
<:item title={gettext("Linked Member")}>
|
|
||||||
<%= if @user.member do %>
|
|
||||||
<.link navigate={~p"/members/#{@user.member}"} class="text-blue-600 hover:text-blue-800 underline">
|
|
||||||
<.icon name="hero-users" class="h-4 w-4 inline mr-1" />
|
|
||||||
{@user.member.first_name} {@user.member.last_name}
|
|
||||||
</.link>
|
|
||||||
<% else %>
|
|
||||||
<span class="text-gray-500 italic">{gettext("No member linked")}</span>
|
|
||||||
<% end %>
|
|
||||||
</:item>
|
|
||||||
</.list>
|
</.list>
|
||||||
</Layouts.app>
|
</Layouts.app>
|
||||||
"""
|
"""
|
||||||
|
|
@ -43,11 +33,9 @@ defmodule MvWeb.UserLive.Show do
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def mount(%{"id" => id}, _session, socket) do
|
def mount(%{"id" => id}, _session, socket) do
|
||||||
user = Ash.get!(Mv.Accounts.User, id, domain: Mv.Accounts, load: [:member])
|
|
||||||
|
|
||||||
{:ok,
|
{:ok,
|
||||||
socket
|
socket
|
||||||
|> assign(:page_title, gettext("Show User"))
|
|> assign(:page_title, gettext("Show User"))
|
||||||
|> assign(:user, user)}
|
|> assign(:user, Ash.get!(Mv.Accounts.User, id, domain: Mv.Accounts))}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -92,86 +92,3 @@ for member_attrs <- [
|
||||||
] do
|
] do
|
||||||
Membership.create_member!(member_attrs)
|
Membership.create_member!(member_attrs)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Create additional users for user-member linking examples
|
|
||||||
additional_users = [
|
|
||||||
%{email: "hans.mueller@example.de"},
|
|
||||||
%{email: "greta.schmidt@example.de"},
|
|
||||||
%{email: "maria.weber@example.de"},
|
|
||||||
%{email: "thomas.klein@example.de"}
|
|
||||||
]
|
|
||||||
|
|
||||||
created_users =
|
|
||||||
Enum.map(additional_users, fn user_attrs ->
|
|
||||||
Accounts.create_user!(user_attrs, upsert?: true, upsert_identity: :unique_email)
|
|
||||||
|> Ash.Changeset.for_update(:admin_set_password, %{password: "testpassword"})
|
|
||||||
|> Ash.update!()
|
|
||||||
end)
|
|
||||||
|
|
||||||
# Create members with linked users to demonstrate the 1:1 relationship
|
|
||||||
# Only create if users don't already have members
|
|
||||||
linked_members = [
|
|
||||||
%{
|
|
||||||
first_name: "Maria",
|
|
||||||
last_name: "Weber",
|
|
||||||
email: "maria.weber@example.de",
|
|
||||||
birth_date: ~D[1992-07-14],
|
|
||||||
join_date: ~D[2023-03-15],
|
|
||||||
paid: true,
|
|
||||||
phone_number: "+49301357924",
|
|
||||||
city: "Frankfurt",
|
|
||||||
street: "Goetheplatz",
|
|
||||||
house_number: "5",
|
|
||||||
postal_code: "60313",
|
|
||||||
notes: "Linked to user account",
|
|
||||||
# Link to the third user (maria.weber@example.de)
|
|
||||||
user: Enum.at(created_users, 2)
|
|
||||||
},
|
|
||||||
%{
|
|
||||||
first_name: "Thomas",
|
|
||||||
last_name: "Klein",
|
|
||||||
email: "thomas.klein@example.de",
|
|
||||||
birth_date: ~D[1988-12-03],
|
|
||||||
join_date: ~D[2023-04-01],
|
|
||||||
paid: false,
|
|
||||||
phone_number: "+49302468135",
|
|
||||||
city: "Köln",
|
|
||||||
street: "Rheinstraße",
|
|
||||||
house_number: "23",
|
|
||||||
postal_code: "50667",
|
|
||||||
notes: "Linked to user account - needs payment follow-up",
|
|
||||||
# Link to the fourth user (thomas.klein@example.de)
|
|
||||||
user: Enum.at(created_users, 3)
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
# Create the linked members only if the users don't already have members
|
|
||||||
Enum.each(linked_members, fn member_attrs ->
|
|
||||||
user = member_attrs.user
|
|
||||||
member_attrs_without_user = Map.delete(member_attrs, :user)
|
|
||||||
|
|
||||||
# Check if user already has a member
|
|
||||||
if user.member_id == nil do
|
|
||||||
# User is free, create member and link
|
|
||||||
Membership.create_member!(Map.put(member_attrs_without_user, :user, %{id: user.id}))
|
|
||||||
else
|
|
||||||
# User already has a member, just create the member without linking
|
|
||||||
Membership.create_member!(member_attrs_without_user)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
IO.puts("✅ Seeds completed successfully!")
|
|
||||||
IO.puts("📝 Created sample data:")
|
|
||||||
IO.puts(" - Property types: String, Date, Boolean, Email")
|
|
||||||
IO.puts(" - Admin user: admin@mv.local (password: testpassword)")
|
|
||||||
IO.puts(" - Sample members: Hans, Greta, Friedrich")
|
|
||||||
|
|
||||||
IO.puts(
|
|
||||||
" - Additional users: hans.mueller@example.de, greta.schmidt@example.de, maria.weber@example.de, thomas.klein@example.de"
|
|
||||||
)
|
|
||||||
|
|
||||||
IO.puts(
|
|
||||||
" - Linked members: Maria Weber ↔ maria.weber@example.de, Thomas Klein ↔ thomas.klein@example.de"
|
|
||||||
)
|
|
||||||
|
|
||||||
IO.puts("🔗 Visit the application to see user-member relationships in action!")
|
|
||||||
|
|
|
||||||
|
|
@ -148,8 +148,7 @@ defmodule Mv.Accounts.UserMemberRelationshipTest do
|
||||||
email: "dave@example.com"
|
email: "dave@example.com"
|
||||||
})
|
})
|
||||||
|
|
||||||
assert {:error, %Ash.Error.Invalid{}} =
|
assert {:error, %Ash.Error.Invalid{}} = Accounts.update_user(user, %{member: %{id: member2.id}})
|
||||||
Accounts.update_user(user, %{member: %{id: member2.id}})
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "prevents linking user to already linked member on update" do
|
test "prevents linking user to already linked member on update" do
|
||||||
|
|
@ -160,8 +159,7 @@ defmodule Mv.Accounts.UserMemberRelationshipTest do
|
||||||
|
|
||||||
{:ok, user2} = Accounts.create_user(%{email: "test5@example.com"})
|
{:ok, user2} = Accounts.create_user(%{email: "test5@example.com"})
|
||||||
|
|
||||||
assert {:error, %Ash.Error.Invalid{}} =
|
assert {:error, %Ash.Error.Invalid{}} = Accounts.update_user(user2, %{member: %{id: member.id}})
|
||||||
Accounts.update_user(user2, %{member: %{id: member.id}})
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "prevents linking member to already linked user on creation" do
|
test "prevents linking member to already linked user on creation" do
|
||||||
|
|
@ -186,10 +184,10 @@ defmodule Mv.Accounts.UserMemberRelationshipTest do
|
||||||
{:ok, _updated_user} = Accounts.update_user(existing_user, %{member: %{id: member.id}})
|
{:ok, _updated_user} = Accounts.update_user(existing_user, %{member: %{id: member.id}})
|
||||||
|
|
||||||
assert {:error, %Ash.Error.Invalid{}} =
|
assert {:error, %Ash.Error.Invalid{}} =
|
||||||
Accounts.create_user(%{
|
Accounts.create_user(%{
|
||||||
email: "test5@example.com",
|
email: "test5@example.com",
|
||||||
member: %{id: member.id}
|
member: %{id: member.id}
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue