Last time we set up a basic installation of Pow. Today we’ll add on session persistence, registration confirmation and password reset functionality.
So let’s get at it!
Getting started
If you’ve been following along you can continue with the code from part 1, or you can grab the code from GitHub.
Clone the Repo:
If cloning:
Terminal
git clone -b part-01 https://github.com/riebeekn/phx-auth-with-pow warehouse
cd warehouse
Let’s create a branch for today’s work:
Terminal
git checkout -b part-02
And make sure we have our dependencies and database set-up.
Terminal
mix deps.get
Terminal
cd assets && npm install
Note: before running ecto.setup
you may need to update the username / password database settings in dev.config
to match your local postgres settings, i.e.
/config/dev.config …line 69
# Configure your database
config :warehouse, Warehouse.Repo,
username: "postgres",
password: "postgres",
database: "warehouse_dev",
hostname: "localhost",
pool_size: 10
Terminal
cd ..
mix ecto.setup
With that out of the way… let’s see where we are starting from.
Terminal
mix phx.server
If we navigate to http://localhost:4000/ we’ll see our application.
If we create a user and login, we’ll see we have access to our products page and the navigation links update intelligently based on the fact that a user is logged in.
One thing you’ll notice is if we close and re-open our browser we lose our user session (i.e. we get logged out).
This isn’t always the behaviour we’d prefer, so let’s see how we can add session persistence.
Adding session persistence
Pow
provides us with an easy way of including session persistence, the first step is to update config.exs
.
/config/config.exs …line 20
# Pow configuration
config :warehouse, :pow,
user: Warehouse.Users.User,
repo: Warehouse.Repo,
web_module: WarehouseWeb,
extensions: [PowPersistentSession],
controller_callbacks: Pow.Extension.Phoenix.ControllerCallbacks
We’ve added the PowPersistentSession
extension as well as a controller_callback
.
Next we need to update endpoint.ex
, adding a new Plug
for session persistence.
/lib/warehouse_web/endpoint.ex …line 45
# enable Pow session based authentication
plug Pow.Plug.Session, otp_app: :warehouse
# enable Pow persistent sessions
plug PowPersistentSession.Plug.Cookie
Finally we should update the login form to give the user the option of whether they want their session to be persisted or not. We’ll add a simple check-box.
/lib/warehouse_web/templates/pow/session/new.html.eex …line 18
<%= label f, :persistent_session, "Remember me" %>
<%= checkbox f, :persistent_session %>
<div>
<%= submit "Sign in now!" %>
</div>
To see this all in action we need to restart the server.
Terminal
mix phx.server
Now we have the option of persistenting sessions:
If we select the Remember me
option we’ll find we don’t need to log in again after closing and re-opening our browser. Session persistence… done! Was that easy or what!
Next let’s see how we can add registration confirmation and password resets.
Adding registration confirmation and password resets
We’ll handle registration confirmation and password resets in one go as the set-up steps are fairly similar for both.
We need a migration for the email confirmation functionality so let’s start by creating that.
Terminal
mix pow.extension.ecto.gen.migrations --extension PowEmailConfirmation
We’ll then run the migration.
Terminal
mix ecto.migrate
Next we need to update config.exs
with the new extensions.
/config/config.exs …line 20
# Pow configuration
config :warehouse, :pow,
user: Warehouse.Users.User,
repo: Warehouse.Repo,
web_module: WarehouseWeb,
extensions: [PowPersistentSession, PowResetPassword, PowEmailConfirmation],
controller_callbacks: Pow.Extension.Phoenix.ControllerCallbacks
Now we need to update user.ex
, including the extensions and adding a changeset
.
/lib/warehouse/users/user.ex
defmodule Warehouse.Users.User do
use Ecto.Schema
use Pow.Ecto.Schema
use Pow.Extension.Ecto.Schema,
extensions: [PowResetPassword, PowEmailConfirmation]
schema "users" do
pow_user_fields()
timestamps()
end
def changeset(user_or_changeset, attrs) do
user_or_changeset
|> pow_changeset(attrs)
|> pow_extension_changeset(attrs)
end
end
Finally we also need to add some routes.
/lib/warehouse_web/router.ex
defmodule WarehouseWeb.Router do
use WarehouseWeb, :router
use Pow.Phoenix.Router
use Pow.Extension.Phoenix.Router, otp_app: :warehouse
...
...
scope "/" do
pipe_through :browser
pow_routes()
pow_extension_routes()
end
...
...
We’ve added a new use
statement (use Pow.Extension.Phoenix.Router, otp_app: :warehouse
) and then updated the first scope
section to include pow_extension_routes
.
Before testing things out, let’s add a reset password link in our sign in form.
/lib/warehouse_web/templates/pow/session/new.html.eex …line 26
<div>
<%= link "Reset password", to: Routes.pow_reset_password_reset_password_path(@conn, :new) %>
</div>
<span><%= link "Register", to: Routes.pow_registration_path(@conn, :new) %></span>
Now if we start up our server we should be good to go.
Terminal
mix phx.server
We see our new Reset password
link in the sign in form.
And if we click it we get:
Doh… so what has happened here? Well if you recall we are using customized versions of the Pow
templates, i.e. we generated the templates and added web_module: WarehouseWeb
to config.exs
. This means that our newly added functionality also expects generated versus default templates. So we need to generate the templates.
Terminal
mix pow.extension.phoenix.gen.templates --extension PowResetPassword --extension PowEmailConfirmation
And with that, we should be good.
But wait, we have one more step left, if we submit the reset password form as things currently stand we’ll get.
As per the Pow
README
we also need to set-up mailer support. This is easy to do, the instructions in the README
provide instructions for setting up a mock that will output our email contents to the console log. This works perfect for the purposes of development.
First we create the mailer.
Terminal
mkdir lib/warehouse_web/pow
touch lib/warehouse_web/pow/mailer.ex
/lib/warehouse_web/pow/mailer.ex
defmodule WarehouseWeb.Pow.Mailer do
use Pow.Phoenix.Mailer
require Logger
def cast(%{user: user, subject: subject, text: text, html: html, assigns: _assigns}) do
# Build email struct to be used in `process/1`
%{to: user.email, subject: subject, text: text, html: html}
end
def process(email) do
# Send email
Logger.debug("E-mail sent: #{inspect email}")
end
end
And then we need to update the Pow
configuration in config.exs
.
/config/config.exs …line 20
# Pow configuration
config :warehouse, :pow,
user: Warehouse.Users.User,
repo: Warehouse.Repo,
web_module: WarehouseWeb,
extensions: [PowPersistentSession, PowResetPassword, PowEmailConfirmation],
controller_callbacks: Pow.Extension.Phoenix.ControllerCallbacks,
mailer_backend: WarehouseWeb.Pow.Mailer
After a server restart everything is good.
Terminal
mix phx.server
Hitting submit yields the following:
And in our console we see:
Following the link in the console we get.
Sweet!
If we register a new user, we’ll see the registration confirmation functionality is now up and working as well.
Following the link in the console gives us:
So that does it for adding registration confirmation and password resets!
Let’s have a quick look at a few other customizations before finishing off for today.
Other customizations
One thing I’ve noticed is when signing out of the application the user is taken to the sign in page. I’d prefer if the user was redirected to the root of the application. We can use Callback routes to change this behaviour.
Callback routes
The list of available callback routes can be seen in the source code at: https://github.com/danschultzer/pow/blob/master/lib/pow/phoenix/routes.ex.
https://github.com/danschultzer/pow/blob/master/lib/pow/phoenix/routes.ex
...
...
@callback user_not_authenticated_path(Conn.t()) :: binary()
@callback user_already_authenticated_path(Conn.t()) :: binary()
@callback after_sign_out_path(Conn.t()) :: binary()
@callback after_sign_in_path(Conn.t()) :: binary()
@callback after_registration_path(Conn.t()) :: binary()
@callback after_user_updated_path(Conn.t()) :: binary()
@callback after_user_deleted_path(Conn.t()) :: binary()
@callback session_path(Conn.t(), atom(), list()) :: binary()
@callback registration_path(Conn.t(), atom()) :: binary()
@callback path_for(Conn.t(), atom(), atom(), list(), Keyword.t()) :: binary()
@callback url_for(Conn.t(), atom(), atom(), list(), Keyword.t()) :: binary()
...
...
We’re going to want to make use of the after_sign_out_path
callback. We can do so by creating a pow.routes
file.
Terminal
touch lib/warehouse_web/pow/routes.ex
And adding the following contents.
/lib/warehouse_web/pow/routes.ex
defmodule WarehouseWeb.Pow.Routes do
use Pow.Phoenix.Routes
alias WarehouseWeb.Router.Helpers, as: Routes
def after_sign_out_path(conn), do: Routes.page_path(conn, :index)
end
All we’re doing above is indicating we want to go to the index
of the page_controller
on sign out.
We need another entry in config.exs
as well, specifying the routes_backend
configuration.
/config/config.exs …line 20
# Pow configuration
config :warehouse, :pow,
user: Warehouse.Users.User,
repo: Warehouse.Repo,
web_module: WarehouseWeb,
extensions: [PowPersistentSession, PowResetPassword, PowEmailConfirmation],
controller_callbacks: Pow.Extension.Phoenix.ControllerCallbacks,
mailer_backend: WarehouseWeb.Pow.Mailer,
routes_backend: WarehouseWeb.Pow.Routes
Now if we restart the server and logout we’ll be redirected to the main page of our application instead of the sign in page.
Fantastic!
Customizing or adding flash messages
Another area of Pow
that we can customize is the flash messages shown to the user. For instance, currently if the user is not signed in and tries to access a page that is protected, they are redirected to the sign in page and no flash message appears. We’ll add a message for this scenario.
The first step is to create a new module for our messages.
Terminal
touch lib/warehouse_web/pow/messages.ex
And then add a message for user_not_authenticated
.
/lib/warehouse_web/pow/messages.ex
defmodule WarehouseWeb.Pow.Messages do
use Pow.Phoenix.Messages
# since we're using the reset password and email confirmation
# extensions we need to include them as well
use Pow.Extension.Phoenix.Messages,
extensions: [PowResetPassword, PowEmailConfirmation]
import WarehouseWeb.Gettext
def user_not_authenticated(_conn), do: gettext("You need to sign in to see this page.")
end
The hex documentation provides a good reference for the messages available within Pow
:
The source code is also a good reference and is located at: https://github.com/danschultzer/pow/blob/master/lib/pow/phoenix/messages.ex.
For the reset_password
and email_confirmation
extensions the code is at: https://github.com/danschultzer/pow/blob/master/lib/extensions/reset_password/phoenix/messages.ex.
https://github.com/danschultzer/pow/blob/master/lib/extensions/reset_password/phoenix/messages.ex
...
...
def email_has_been_sent(_conn), do: "An email with reset instructions has been sent to you. Please check your inbox."
def invalid_token(_conn), do: "The reset token has expired."
def password_has_been_reset(_conn), do: "The password has been updated."
...
...
https://github.com/danschultzer/pow/blob/master/lib/extensions/email_confirmation/phoenix/messages.ex
...
...
def email_has_been_confirmed(_conn), do: "The email address has been confirmed."
def email_confirmation_failed(_conn), do: "The email address couldn't be confirmed."
def email_confirmation_required(_conn), do: "You'll need to confirm your e-mail before you can sign in. An e-mail confirmation link has been sent to you."
...
...
Like the other Pow
customizations we need to update config.exs
. We need to add a messages_backend
entry.
/config/config.exs …line 20
# Pow configuration
config :warehouse, :pow,
user: Warehouse.Users.User,
repo: Warehouse.Repo,
web_module: WarehouseWeb,
extensions: [PowPersistentSession, PowResetPassword, PowEmailConfirmation],
controller_callbacks: Pow.Extension.Phoenix.ControllerCallbacks,
mailer_backend: WarehouseWeb.Pow.Mailer,
routes_backend: WarehouseWeb.Pow.Routes,
messages_backend: WarehouseWeb.Pow.Messages
Now with a server restart, when trying to access the ‘Products’ pages without signing in, we see the message we added:
Sweet!
Summary
So that’s it for our tour of Pow
. I’d recommend you check out the README
and the source code to gain a further understanding of how things work.
I’m really impressed with the way Pow
is structured and how easy it is to use, set-up and customize. Hopefully you’ll find Pow
useful for your projects as well!
Thanks for reading and I hope you enjoyed the post!