From 4e6f5a517ab7b27a0085e8e112029fd7cc889e43 Mon Sep 17 00:00:00 2001 From: Moritz Date: Thu, 24 Jul 2025 20:15:01 +0200 Subject: [PATCH] WIP feat: member user relation --- lib/accounts/accounts.ex | 11 ++ lib/accounts/user.ex | 20 ++- lib/accounts/user/member_creation_notifier.ex | 71 ++++++++ lib/membership/member.ex | 1 + lib/membership/membership.ex | 1 + lib/mv_web/components/layouts.ex | 3 +- lib/mv_web/components/layouts/navbar.ex | 4 +- lib/mv_web/live/member_live/form.ex | 6 +- lib/mv_web/live/member_live/index.html.heex | 2 +- lib/mv_web/live/member_live/show.ex | 2 +- lib/mv_web/live/property_live/form.ex | 2 +- lib/mv_web/live/property_live/index.ex | 2 +- lib/mv_web/live/property_live/show.ex | 2 +- lib/mv_web/live/property_type_live/form.ex | 2 +- lib/mv_web/live/property_type_live/index.ex | 2 +- lib/mv_web/live/property_type_live/show.ex | 2 +- lib/mv_web/live/user_live/form.ex | 91 +++++++++- lib/mv_web/live/user_live/index.ex | 5 +- lib/mv_web/live/user_live/index.html.heex | 7 +- lib/mv_web/live/user_live/show.ex | 16 +- mix.lock | 1 + priv/gettext/de/LC_MESSAGES/default.po | 158 ++++++++++------ priv/gettext/default.pot | 160 +++++++++++------ priv/gettext/en/LC_MESSAGES/default.po | 168 ++++++++++++------ .../20250724161006_add_unique_member_id.exs | 17 ++ .../20250724161939_add_admin_to_users.exs | 21 +++ .../repo/users/20250724161006.json | 141 +++++++++++++++ .../repo/users/20250724161939.json | 153 ++++++++++++++++ test/accounts/registration_member_test.exs | 107 +++++++++++ test/accounts/user_delete_member_test.exs | 20 +++ .../accounts/user_member_integration_test.exs | 77 ++++++++ .../controllers/oidc_integration_test.exs | 4 +- .../admin_member_assignment_test.exs | 81 +++++++++ test/mv_web/user_live/form_test.exs | 4 +- test/mv_web/user_live/member_display_test.exs | 36 ++++ 35 files changed, 1208 insertions(+), 192 deletions(-) create mode 100644 lib/accounts/user/member_creation_notifier.ex create mode 100644 priv/repo/migrations/20250724161006_add_unique_member_id.exs create mode 100644 priv/repo/migrations/20250724161939_add_admin_to_users.exs create mode 100644 priv/resource_snapshots/repo/users/20250724161006.json create mode 100644 priv/resource_snapshots/repo/users/20250724161939.json create mode 100644 test/accounts/registration_member_test.exs create mode 100644 test/accounts/user_delete_member_test.exs create mode 100644 test/accounts/user_member_integration_test.exs create mode 100644 test/mv_web/user_live/admin_member_assignment_test.exs create mode 100644 test/mv_web/user_live/member_display_test.exs diff --git a/lib/accounts/accounts.ex b/lib/accounts/accounts.ex index 7bc5073..a96b5ac 100644 --- a/lib/accounts/accounts.ex +++ b/lib/accounts/accounts.ex @@ -21,4 +21,15 @@ defmodule Mv.Accounts do resource Mv.Accounts.Token end + + @doc """ + Register a new user with password using AshAuthentication's standard action. + This creates a user and the notifier will automatically create a member. + """ + def register_with_password(params) do + # Use AshAuthentication's standard register_with_password action + Mv.Accounts.User + |> Ash.Changeset.for_create(:register_with_password, params) + |> Ash.create(domain: __MODULE__) + end end diff --git a/lib/accounts/user.ex b/lib/accounts/user.ex index d8c7a66..c4c0bbe 100644 --- a/lib/accounts/user.ex +++ b/lib/accounts/user.ex @@ -5,7 +5,8 @@ defmodule Mv.Accounts.User do use Ash.Resource, domain: Mv.Accounts, data_layer: AshPostgres.DataLayer, - extensions: [AshAuthentication] + extensions: [AshAuthentication], + notifiers: [Mv.Accounts.User.MemberCreationNotifier] # authorizers: [Ash.Policy.Authorizer] @@ -64,11 +65,11 @@ defmodule Mv.Accounts.User do defaults [:read, :create, :destroy, :update] create :create_user do - accept [:email] + accept [:email, :member_id] end update :update_user do - accept [:email] + accept [:email, :member_id] end # Admin action for direct password changes in admin panel @@ -121,9 +122,16 @@ defmodule Mv.Accounts.User do # Global validations - applied to all relevant actions validations do - # Password strength policy: minimum 8 characters for all password-related actions + # Password strength policy: minimum 8 characters + # Note: register_with_password has built-in AshAuthentication validation, but admin_set_password doesn't validate string_length(:password, min: 8) do - where action_is([:register_with_password, :admin_set_password]) + # Only needed for admin actions, AshAuthentication handles register_with_password + where action_is([:admin_set_password]) + end + + # Email uniqueness for registration actions + validate attribute_does_not_equal(:email, nil) do + where action_is([:register_with_password, :register_with_rauthy]) end end @@ -143,6 +151,7 @@ defmodule Mv.Accounts.User do attribute :email, :ci_string, allow_nil?: false, public?: true attribute :hashed_password, :string, sensitive?: true, allow_nil?: true attribute :oidc_id, :string, allow_nil?: true + attribute :admin?, :boolean, allow_nil?: false, default: false, public?: true end relationships do @@ -152,6 +161,7 @@ defmodule Mv.Accounts.User do identities do identity :unique_email, [:email] identity :unique_oidc_id, [:oidc_id] + identity :unique_member_id, [:member_id] end # You can customize this if you wish, but this is a safe default that diff --git a/lib/accounts/user/member_creation_notifier.ex b/lib/accounts/user/member_creation_notifier.ex new file mode 100644 index 0000000..5506775 --- /dev/null +++ b/lib/accounts/user/member_creation_notifier.ex @@ -0,0 +1,71 @@ +defmodule Mv.Accounts.User.MemberCreationNotifier do + @moduledoc """ + Notifier that automatically creates a member for newly registered users. + + This runs after user creation/registration and ensures every user has an associated member. + It's designed to work with AshAuthentication without interfering with LiveView integration. + """ + use Ash.Notifier + + require Logger + + @impl Ash.Notifier + def notify(%Ash.Notifier.Notification{ + action: %{name: action_name}, + resource: Mv.Accounts.User, + data: user + }) + when action_name in [:register_with_password, :register_with_rauthy, :create_user] do + # Only create member if user doesn't already have one + if should_create_member?(user) do + create_member_for_user(user) + end + + :ok + end + + @impl Ash.Notifier + def notify(_), do: :ok + + defp should_create_member?(user) do + # Check if user has a member_id and if that member actually exists + case user.member_id do + nil -> + true + + member_id -> + case Ash.get(Mv.Membership.Member, member_id, domain: Mv.Membership) do + {:ok, _member} -> false + {:error, _} -> true + end + end + end + + defp create_member_for_user(user) do + member_params = %{ + email: to_string(user.email), + first_name: "User", + last_name: "Generated" + } + + case Mv.Membership.create_member(member_params) do + {:ok, member} -> + # Update user with member_id + case Ash.Changeset.for_update(user, :update_user, %{member_id: member.id}) + |> Ash.update(domain: Mv.Accounts) do + {:ok, _updated_user} -> + Logger.info( + "Successfully created and assigned member #{member.id} to user #{user.id}" + ) + + {:error, error} -> + Logger.warning( + "Failed to assign member #{member.id} to user #{user.id}: #{inspect(error)}" + ) + end + + {:error, error} -> + Logger.warning("Failed to create member for user #{user.id}: #{inspect(error)}") + end + end +end diff --git a/lib/membership/member.ex b/lib/membership/member.ex index 583f173..a533292 100644 --- a/lib/membership/member.ex +++ b/lib/membership/member.ex @@ -170,5 +170,6 @@ defmodule Mv.Membership.Member do relationships do has_many :properties, Mv.Membership.Property + has_one :user, Mv.Accounts.User, destination_attribute: :member_id end end diff --git a/lib/membership/membership.ex b/lib/membership/membership.ex index 0c7c14d..afed432 100644 --- a/lib/membership/membership.ex +++ b/lib/membership/membership.ex @@ -10,6 +10,7 @@ defmodule Mv.Membership do resource Mv.Membership.Member do define :create_member, action: :create_member define :list_members, action: :read + define :get_member!, action: :read, get_by: [:id] define :update_member, action: :update_member define :destroy_member, action: :destroy end diff --git a/lib/mv_web/components/layouts.ex b/lib/mv_web/components/layouts.ex index 1495bb1..ef0c990 100644 --- a/lib/mv_web/components/layouts.ex +++ b/lib/mv_web/components/layouts.ex @@ -24,6 +24,7 @@ defmodule MvWeb.Layouts do """ attr :flash, :map, required: true, doc: "the map of flash messages" + attr :current_user, :map, default: nil, doc: "the current user" attr :current_scope, :map, default: nil, @@ -33,7 +34,7 @@ defmodule MvWeb.Layouts do def app(assigns) do ~H""" - <.navbar /> + <.navbar current_user={@current_user} />
{render_slot(@inner_block)} diff --git a/lib/mv_web/components/layouts/navbar.ex b/lib/mv_web/components/layouts/navbar.ex index b917ddc..1a6a7c6 100644 --- a/lib/mv_web/components/layouts/navbar.ex +++ b/lib/mv_web/components/layouts/navbar.ex @@ -5,6 +5,8 @@ defmodule MvWeb.Layouts.Navbar do use Phoenix.Component use Gettext, backend: MvWeb.Gettext + attr :current_user, :map, default: nil + def navbar(assigns) do ~H"""