I’m moving to Pow and have a profile section in my website that breaks up user data entry into several sections/pages. These pages are tied back to a UserController
. On one of the pages, the user can change their email address.
Is there any way I can piggyback on to the event hooks tied to the Pow.Phoenix.RegistrationController
but with my UserController
? Then my app could follow the default email/registration workflow provided by Pow?
The Pow.Phoenix.RegistrationController
is extremely thin. Most, or maybe all, of what you need exists in the ecto module(s), so I would recommend you to just leverage those (maybe Pow.Ecto.Schema.Changeset.user_id_field_changeset/3
is what you’re looking for).
I can provide some sample code, but would need to know some more of what you need. Do you require a password to update the email? Do you use PowEmailConfirmation
?
1 Like
Sorry, I should have mentioned that I am using PowEmailConfirmation
It was more the warn_unconfirmed
stuff in controller_callbacks that I was after. But based on what you’re saying, I’d be better off writing the workflow myself? I was just hoping that I could be lazy and avoid doing that.
For reference, here’s a sample of my User
schema:
defmodule Zoinks.Accounts.User do
use Ecto.Schema
import Ecto.Changeset
alias Zoinks.Accounts.User
use Pow.Ecto.Schema,
user_id_field: :user_name,
password_hash_methods: {
&Comeonin.Bcrypt.hashpwsalt/1,
&Comeonin.Bcrypt.checkpw/2
}
use Pow.Extension.Ecto.Schema,
extensions: [PowResetPassword, PowEmailConfirmation]
@timestamps_opts [type: :utc_datetime_usec]
schema "users" do
field :user_name, :string
field :email, :string
field :name, :string
field :bio, :string
pow_user_fields()
timestamps()
end
@doc false
def changeset( user_or_changeset, attrs ) do
user_or_changeset
|> pow_changeset(attrs)
|> pow_extension_changeset(attrs)
|> cast(attrs, [:email, :unconfirmed_email])
end
end
It’s a work in progress, but its worth mentioning that email
is not the user_id_field
- so I’ve added a cast to email
and unconfirmed_email
in changeset
Also, I thought there was a circumstance where controller_callbacks
would prevent an update (if you have an unconfirmed email, that it would prevent you from changing it to different unconfirmed email).
But re-reading the code, I was mistaken. The title of this post is misleading as a result. Sorry about that!
Yeah, it’s most likely better to set it up in a custom controller than try to reuse the controller flow in Pow. You can leverage the Pow controller flow, but you will have much less control and understanding what’s going on, e.g. you will be redirected back to registration edit page rather than the edit page for e-mail.
This is the most basic setup I can think of, that you can expand upon:
defmodule ZoinksWeb.UserController do
alias Zoinks.Accounts
# ...
def edit_email(conn, _params) do
changeset = Pow.Plug.change_user(conn)
render(conn, "edit_email.html", changeset: changeset)
end
def update_email(conn, %{"user" => user_params}) do
conn
|> Pow.Plug.update_user(user_params)
|> maybe_warn_user()
end
defp warn_unconfirmed({:ok, %{email_confirmed_at: nil, email_confirmation_token: token}, conn}) when not is_nil(token) do
conn
|> put_flash(:info, "You'll need to confirm the e-mail before it's updated. An e-mail confirmation link has been sent to you.")
|> redirect(to: Routes.user_path(conn, :edit_email))
end
defp warn_unconfirmed({:ok, _user, conn}) do
redirect(conn, to: Routes.user_path(conn, :edit_email))
end
defp warn_unconfirmed({:error, changeset, conn}) do
render(conn, "edit_email.html", changeset: changeset)
end
end
The Pow.Plug.update_user/2
method can later be replaced so the user can only update the email rather than basically be the exact same as the Pow user update action.
2 Likes
Thanks!
You’re code was really helpful. The callbacks in PowEmailConfirmation.Ecto.Schema
didn’t seem to be working for me. I eventually figured out my mistake. Here’s what I had originally:
def changeset( user_or_changeset, attrs ) do
user_or_changeset
|> pow_changeset(attrs)
|> pow_extension_changeset(attrs)
|> cast(attrs, [:email, :unconfirmed_email])
end
However, I didn’t realise that I needed to cast
above pow_changeset
. Once I did this:
def changeset( user_or_changeset, attrs ) do
user_or_changeset
|> cast(attrs, [:email, :unconfirmed_email])
|> pow_changeset(attrs)
|> pow_extension_changeset(attrs)
end
The hooks in PowEmailConfirmation.Ecto.Schema
started working as expected. Now I can test/understand the default behaviour of Pow (with regards to allowing the user to change the email
address).
It’s not necessary to cast anything, or at least you shouldn’t cast :unconfirmed_email
as it should only be used internally (when the email is casted, the PowEmailConfirmation
ecto changeset method will do the work for you). Also I forgot to add the email deliver logic so your modules should look like this:
def changeset( user_or_changeset, attrs ) do
user_or_changeset
|> pow_changeset(attrs)
|> pow_extension_changeset(attrs)
end
defmodule ZoinksWeb.UserController do
alias Zoinks.Accounts
# ...
def edit_email(conn, _params) do
changeset = Pow.Plug.change_user(conn)
render(conn, "edit_email.html", changeset: changeset)
end
def update_email(conn, %{"user" => user_params}) do
conn
|> Pow.Plug.update_user(user_params)
|> maybe_warn_user()
end
defp warn_unconfirmed({:ok, %{email_confirmed_at: nil, email_confirmation_token: token} = user, conn}) when not is_nil(token) do
PowEmailConfirmation.Phoenix.ControllerCallbacks.send_confirmation_email(user, conn)
conn
|> put_flash(:info, "You'll need to confirm the e-mail before it's updated. An e-mail confirmation link has been sent to you.")
|> redirect(to: Routes.user_path(conn, :edit_email))
end
defp warn_unconfirmed({:ok, _user, conn}) do
redirect(conn, to: Routes.user_path(conn, :edit_email))
end
defp warn_unconfirmed({:error, changeset, conn}) do
render(conn, "edit_email.html", changeset: changeset)
end
end
1 Like
After a little testing - looks like I need to cast :email
(because I’ve set user_id_field
?) but not :unconfirmed_email
You’re right, if :user_id_field
is something different than email then you need to cast it yourself.
1 Like