lib/pow_assent/ecto/schema.ex

defmodule PowAssent.Ecto.Schema do
  @moduledoc """
  Handles the Ecto schema for user.

  `user_id_attrs` is provided by the user in a registration step.

  ## Usage

  Configure `lib/my_project/users/user.ex` the following way:

      defmodule MyApp.Users.User do
        use Ecto.Schema
        use Pow.Ecto.Schema
        use PowAssent.Ecto.Schema

        schema "users" do
          field :custom_field, :string

          pow_user_fields()

          timestamps()
        end

        def changeset(user_or_changeset, attrs) do
          user_or_changeset
          |> Ecto.Changeset.cast(attrs, [:custom_field])
          |> pow_changeset(attrs)
        end

        def user_identity_changeset(user_or_changeset, user_identity, attrs, user_id_attrs) do
          user_or_changeset
          |> Ecto.Changeset.cast(attrs, [:custom_field])
          |> pow_assent_user_identity_changeset(user_identity, attrs, user_id_attrs)
        end
      end
  """
  alias Ecto.{Changeset, Schema}
  alias Pow.UUID
  alias PowAssent.Config

  @callback user_identity_changeset(Schema.t() | Changeset.t(), Schema.t(), map(), map() | nil) :: Changeset.t()

  @doc false
  defmacro __using__(_config) do
    quote do
      @behaviour unquote(__MODULE__)

      @spec user_identity_changeset(Schema.t() | Changeset.t(), Schema.t(), map(), map() | nil) :: Changeset.t()
      def user_identity_changeset(user_or_changeset, user_identity, attrs, user_id_attrs), do: pow_assent_user_identity_changeset(user_or_changeset, user_identity, attrs, user_id_attrs)

      @spec pow_assent_user_identity_changeset(Schema.t() | Changeset.t(), Schema.t(), map(), map() | nil) :: Changeset.t()
      def pow_assent_user_identity_changeset(user_or_changeset, user_identity, attrs, user_id_attrs) do
        unquote(__MODULE__).changeset(user_or_changeset, user_identity, attrs, user_id_attrs, @pow_config)
      end

      unquote(__MODULE__).__has_many__()

      defoverridable unquote(__MODULE__)
    end
  end

  @doc false
  defmacro __has_many__ do
    quote do
      @pow_assocs {:has_many, :user_identities, unquote(__MODULE__).__user_identities_module__(__MODULE__), foreign_key: :user_id, on_delete: :delete_all}
    end
  end

  @doc false
  def __user_identities_module__(module) do
    module
    |> Module.split()
    |> Enum.reverse()
    |> case do
      [_schema, base] -> [base]
      [_schema, _context | rest] -> rest
    end
    |> Enum.reverse()
    |> Enum.concat([UserIdentities, UserIdentity])
    |> Module.concat()
  end

  @doc """
  Changeset for creating or updating users with a user identity.

  Only `Pow.Ecto.Schema.Changeset.user_id_field_changeset/3` is used for
  validation as password is not required.
  """
  @spec changeset(Schema.t() | Changeset.t(), Schema.t(), map(), map() | nil, Config.t()) :: Changeset.t()
  def changeset(user_or_changeset, user_identity, attrs, user_id_attrs, _config) do
    user_or_changeset
    |> Changeset.change()
    |> maybe_accept_invitation()
    |> user_id_field_changeset(attrs, user_id_attrs)
    |> maybe_email_confirmation_changeset(attrs)
    |> Changeset.cast(%{user_identities: [user_identity]}, [])
    |> Changeset.cast_assoc(:user_identities)
  end

  defp maybe_accept_invitation(%Changeset{data: %user_mod{invitation_token: token, invitation_accepted_at: nil} = changeset}) when not is_nil(token) do
    accepted_at = Pow.Ecto.Schema.__timestamp_for__(user_mod, :invitation_accepted_at)

    Changeset.change(changeset, invitation_accepted_at: accepted_at)
  end
  defp maybe_accept_invitation(changeset), do: changeset

  defp user_id_field_changeset(changeset, attrs, nil), do: changeset.data.__struct__.pow_user_id_field_changeset(changeset, attrs)
  defp user_id_field_changeset(changeset, _attrs, user_id_attrs), do: changeset.data.__struct__.pow_user_id_field_changeset(changeset, user_id_attrs)

  defp maybe_email_confirmation_changeset(%Changeset{data: %{unconfirmed_email: _any}} = changeset, attrs) do
    email = Changeset.get_change(changeset, :email)

    case email_verified?(email, attrs) do
      true  -> confirm_email_changeset(changeset)
      false -> email_confirmation_token_changeset(changeset)
    end
  end
  defp maybe_email_confirmation_changeset(changeset, _attrs), do: changeset

  defp confirm_email_changeset(%Changeset{data: %user_mod{}} = changeset) do
    confirmed_at = Pow.Ecto.Schema.__timestamp_for__(user_mod, :email_confirmed_at)

    Changeset.change(changeset, email_confirmed_at: confirmed_at)
  end

  defp email_confirmation_token_changeset(changeset) do
    changeset
    |> Changeset.put_change(:email_confirmation_token, UUID.generate())
    |> Changeset.unique_constraint(:email_confirmation_token)
  end

  defp email_verified?(email, %{"email" => email, "email_verified" => true}), do: true
  defp email_verified?(email, %{email: email, email_verified: true}), do: true
  defp email_verified?(_email, _attrs), do: false
end