Pow: "user_id_field: :phone_number" but still use PowEmailConfirmation?

Created on 12 Nov 2019  路  6Comments  路  Source: danschultzer/pow

Hi,

I don't want to use PowEmailConfirmation right after registration but when user enters email for the first time and on subsequent times. For the registration I use phone_number so no entering email there.

Is there an easy way of doing that or do I need custom controllers?

All 6 comments

By default a user would not be able to sign in if the email is not confirmed, but this is not true for subsequent email changes (related #66). So we can leverage that here, by customizing the changeset:

  def changeset(user_or_changeset, attrs) do
    user_or_changeset
    |> cast_and_validate_email(params)
    |> pow_changeset(attrs)
    |> pow_extension_changeset(attrs)
    |> maybe_nilify_confirmation_token()
  end

  defp cast_and_validate_email(user_or_changeset, params) do
    user_or_changeset
    |> Changeset.cast(params, [:email])
    |> Changeset.validate_change(changeset, :email, fn :email, email ->
      case Pow.Ecto.Schema.Changeset.validate_email(email) do
        :ok              -> []
        :error           -> [email: {"has invalid format"}]
        {:error, reason} -> [email: {"has invalid format", reason: reason}]
      end
    end)
  end

  def maybe_nilify_confirmation_token(changeset) do
    case Ecto.get_meta(changeset.data, :state) do
      :built -> Ecto.Changeset.put_change(changeset, :email_confirmation_token, nil)
      _any -> changeset
    end
  end

As long as the :email_confirmation_token is nil after registration, the user can sign in. Once the user inputs an e-mail, the unconfirmed mail logic kicks in. As this is kinda a hack that leverages some internal stuff in Pow, it's important that you test both controller (registration and sign in) and changeset to ensure any future changes in Pow won't break this logic.

You should at the very least test that a user that have just registered can sign in, and that subsequent email change works.

Let me know if this works!

I assume the above solved your issue

@danschultzer

Once the user inputs an e-mail, the unconfirmed mail logic kicks in.

this doesn't happen. Email just gets updated.

def changeset(user_or_changeset, attrs) do
    user_or_changeset
    |> pow_changeset(attrs)
    |> pow_extension_changeset(attrs)
    |> maybe_nilify_confirmation_token()
  end

doesn't maybe_nilify_confirmation_token() prevent emails when updating email field too? Not only for registration but prevents all emails being sent for Pow?

I also have user_id_field set to phone number. Maybe that affects the flow?

doesn't maybe_nilify_confirmation_token() prevent emails when updating email field too? Not only for registration but prevents all emails being sent for Pow?

No, as you can see it only removes it if the struct doesn't exist in the DB (when state: :built):

  def maybe_nilify_confirmation_token(changeset) do
    case Ecto.get_meta(changeset.data, :state) do
      :built -> Ecto.Changeset.put_change(changeset, :email_confirmation_token, nil)
      _any -> changeset
    end
  end

So only for new users will the email confirmation token be nil.

Email just gets updated.

Hmm, after looking at this I'm curious how the email could have been updated. Doesn't seem like it should even update with that changeset. How do you update the email?

You would need to manually cast and validate the e-mail since you are using the phone number as the :user_id_field. I've updated the example with the following:

  def changeset(user_or_changeset, attrs) do
    user_or_changeset
    |> cast_and_validate_email(params)
    |> pow_changeset(attrs)
    |> pow_extension_changeset(attrs)
    |> maybe_nilify_confirmation_token()
  end

  defp cast_and_validate_email(user_or_changeset, params) do
    user_or_changeset
    |> Changeset.cast(params, [:email])
    |> Changeset.validate_change(changeset, :email, fn :email, email ->
      case Pow.Ecto.Schema.Changeset.validate_email(email) do
        :ok              -> []
        :error           -> [email: {"has invalid format"}]
        {:error, reason} -> [email: {"has invalid format", reason: reason}]
      end
    end)
  end

It uses the email validator from Pow.

Thank you so so much @danschultzer !

def changeset(user_or_changeset, attrs) do
    user_or_changeset
    |> pow_changeset(attrs)
    |> pow_extension_changeset(attrs)
    |> Ecto.Changeset.cast(attrs, [:email, :phone_number, :unconfirmed_phone_number])
    |> Ecto.Changeset.validate_required([:phone_number])
    |> maybe_nilify_confirmation_token()
  end

this was my non-working code. What is the difference between placing cast before pow_changeset(attrs) and pow_extension_changeset(attrs) and after them?

Since your working code had :email before pow's changesets.

Because pow_extension_changeset/2 will trigger the PowEmailConfirmation changeset, but if the email hasn't been cast nothing will happen. In general I recommend casting before pow changesets. Also you won't need to cast or require :phone_number since it's set as the :user_id_field and the pow changeset will already do that as part of the user id validation.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

JonRCahill picture JonRCahill  路  3Comments

joepstender picture joepstender  路  6Comments

sepowitz picture sepowitz  路  4Comments

dweremeichik picture dweremeichik  路  7Comments

jung-hunsoo picture jung-hunsoo  路  3Comments