defmodule PowEmailConfirmation.Ecto.Schema do @moduledoc """ Handles the e-mail confirmation schema for user. ## Customize PowEmailConfirmation fields If you need to modify any of the fields that `PowEmailConfirmation` adds to the user schema, you can override them by defining them before `pow_user_fields/0`: defmodule MyApp.Users.User do use Ecto.Schema use Pow.Ecto.Schema use Pow.Extension.Ecto.Schema, extensions: [PowEmailConfirmation] schema "users" do field :email_confirmation_token, :string field :email_confirmed_at, :utc_datetime field :unconfirmed_email, :string pow_user_fields() timestamps() end end """ use Pow.Extension.Ecto.Schema.Base alias Ecto.Changeset alias Pow.{Config, Extension.Ecto.Schema, UUID} @doc false @impl true def validate!(_config, module) do Schema.require_schema_field!(module, :email, PowEmailConfirmation) end @doc false @impl true def attrs(_config) do [ {:email_confirmation_token, :string}, {:email_confirmed_at, :utc_datetime}, {:unconfirmed_email, :string} ] end @doc false @impl true def indexes(_config) do [{:email_confirmation_token, true}] end @doc false @impl true defmacro __using__(_config) do quote do def confirm_email_changeset(changeset, attrs), do: pow_confirm_email_changeset(changeset, attrs) defdelegate pow_confirm_email_changeset(changeset, attrs), to: unquote(__MODULE__), as: :confirm_email_changeset defoverridable confirm_email_changeset: 2 end end @doc """ Handles e-mail confirmation if e-mail is updated. The `:email_confirmation_token` will always be set if the struct isn't persisted to the database. For structs persisted to the database, no changes will happen if there is no `:email` in the params. Likewise, no changes will happen if the `:email` change is the same as the persisted `:unconfirmed_email` value. If the `:email` change is the same as the persisted `:email` value then both `:email_confirmation_token` and `:unconfirmed_email` will be set to nil. Otherwise the `:email` change will be copied over to `:unconfirmed_email` and the `:email` change will be reverted back to the original persisted `:email` value. A unique `:email_confirmation_token` will be generated. """ @impl true @spec changeset(Changeset.t(), map(), Config.t()) :: Changeset.t() def changeset(%{valid?: true} = changeset, attrs, _config) do cond do built?(changeset) -> put_email_confirmation_token(changeset) email_reverted?(changeset, attrs) -> changeset |> Changeset.put_change(:email_confirmation_token, nil) |> Changeset.put_change(:unconfirmed_email, nil) email_changed?(changeset) -> current_email = changeset.data.email changed_email = Changeset.get_field(changeset, :email) changeset = set_unconfirmed_email(changeset, current_email, changed_email) case unconfirmed_email_changed?(changeset) do true -> put_email_confirmation_token(changeset) false -> changeset end true -> changeset end end def changeset(changeset, _attrs, _config), do: changeset defp built?(changeset), do: Ecto.get_meta(changeset.data, :state) == :built defp email_reverted?(changeset, attrs) do param = Map.get(attrs, :email) || Map.get(attrs, "email") current = changeset.data.email param == current end defp email_changed?(changeset) do case Changeset.get_change(changeset, :email) do nil -> false _any -> true end end defp put_email_confirmation_token(changeset) do changeset |> Changeset.put_change(:email_confirmation_token, UUID.generate()) |> Changeset.unique_constraint(:email_confirmation_token) end defp set_unconfirmed_email(changeset, current_email, new_email) do changeset |> Changeset.put_change(:email, current_email) |> Changeset.put_change(:unconfirmed_email, new_email) end defp unconfirmed_email_changed?(changeset) do case Changeset.get_change(changeset, :unconfirmed_email) do nil -> false _any -> true end end @doc """ Sets the e-mail as confirmed. This updates `:email_confirmed_at` and sets `:email_confirmation_token` to nil. If the struct has a `:unconfirmed_email` value, then the `:email` will be changed to this value, and `:unconfirmed_email` will be set to nil. """ @spec confirm_email_changeset(Ecto.Schema.t() | Changeset.t(), map()) :: Changeset.t() def confirm_email_changeset(%Changeset{data: %{unconfirmed_email: unconfirmed_email}} = changeset, params) when not is_nil(unconfirmed_email) do confirm_email(changeset, unconfirmed_email, params) end def confirm_email_changeset(%Changeset{data: %{email_confirmed_at: nil, email: email}} = changeset, params) do confirm_email(changeset, email, params) end def confirm_email_changeset(%Changeset{} = changeset, _params), do: changeset def confirm_email_changeset(user, params) do user |> Changeset.change() |> confirm_email_changeset(params) end defp confirm_email(changeset, email, _params) do confirmed_at = Pow.Ecto.Schema.__timestamp_for__(changeset.data.__struct__, :email_confirmed_at) changes = [ email_confirmed_at: confirmed_at, email: email, unconfirmed_email: nil, email_confirmation_token: nil ] changeset |> Changeset.change(changes) |> Changeset.unique_constraint(:email) end # TODO: Remove by 1.1.0 @deprecated "Use `confirm_email_changeset/2` instead" @doc false def confirm_email_changeset(changeset), do: confirm_email_changeset(changeset, %{}) end