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..6604f91 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,16 @@ defmodule Mv.Accounts.User do defaults [:read, :create, :destroy, :update] create :create_user do - accept [:email] + accept [:email, :member_id] + argument :member, :map + change manage_relationship(:member, type: :create) end update :update_user do - accept [:email] + require_atomic? false + accept [:email, :member_id] + argument :member, :map + change manage_relationship(:member, on_match: :update, on_no_match: :create) end # Admin action for direct password changes in admin panel @@ -117,13 +123,30 @@ defmodule Mv.Accounts.User do |> Ash.Changeset.change_attribute(:oidc_id, user_info["sub"] || user_info["id"]) end end + + create :register_with_password do + accept [:email] + argument :password, :string, allow_nil?: false, sensitive?: true + argument :password_confirmation, :string, allow_nil?: false, sensitive?: true + argument :member, :map + change AshAuthentication.Strategy.Password.HashPasswordChange + change AshAuthentication.GenerateTokenChange + change manage_relationship(:member, type: :create) + end end # 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 +166,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 +176,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"""