View Source Custom controllers

Please note that if you just wish to modify the templates, then you should follow the Modify templates section(s) in the README. This guide is meant for allowing complete control over flow.

Pow makes it easy to use custom controllers leveraging the underlying Pow logic. It is ideal for cases where you need to control the flow, e.g., protect the registration process in a certain way.

First you should follow the Getting Started section in README until before the router.ex modification.

Routes

Modify your WEB_PATH/router.ex to set up the Pow plugs in :protected and :not_authenticated pipelines with a custom error handler, and add the routes for your custom session and registration controllers:

defmodule MyAppWeb.Router do
  use MyAppWeb, :router

  # ... pipelines

  pipeline :protected do
    plug Pow.Plug.RequireAuthenticated,
      error_handler: MyAppWeb.AuthErrorHandler
  end

  pipeline :not_authenticated do
    plug Pow.Plug.RequireNotAuthenticated,
      error_handler: MyAppWeb.AuthErrorHandler
  end

  scope "/", MyAppWeb do
    pipe_through [:browser, :not_authenticated]

    get "/signup", RegistrationController, :new, as: :signup
    post "/signup", RegistrationController, :create, as: :signup
    get "/login", SessionController, :new, as: :login
    post "/login", SessionController, :create, as: :login
  end

  scope "/", MyAppWeb do
    pipe_through [:browser, :protected]

    delete "/logout", SessionController, :delete, as: :logout
  end

  # ... routes
end

And create WEB_PATH/auth_error_handler.ex:

defmodule MyAppWeb.AuthErrorHandler do
  use MyAppWeb, :controller
  alias Plug.Conn

  @spec call(Conn.t(), atom()) :: Conn.t()
  def call(conn, :not_authenticated) do
    conn
    |> put_flash(:error, "You've to be authenticated first")
    |> redirect(to: ~p"/login")
  end

  @spec call(Conn.t(), atom()) :: Conn.t()
  def call(conn, :already_authenticated) do
    conn
    |> put_flash(:error, "You're already authenticated")
    |> redirect(to: ~p"/")
  end
end

This module will make sure that unauthenticated user can't log out, and authenticated users can't sign in again.

Controllers

We'll be using Pow.Plug for the heavy lifting, and customizing the response handling in our controllers.

Create WEB_PATH/controllers/registration_controller.ex:

defmodule MyAppWeb.RegistrationController do
  use MyAppWeb, :controller

  def new(conn, _params) do
    # We'll leverage `Pow.Plug`, but you can also follow the classic Phoenix way:
    # changeset = MyApp.Users.User.changeset(%MyApp.Users.User{}, %{})

    changeset = Pow.Plug.change_user(conn)

    render(conn, "new.html", changeset: changeset)
  end

  def create(conn, %{"user" => user_params}) do
    # We'll leverage `Pow.Plug`, but you can also follow the classic Phoenix way:
    # user =
    #   %MyApp.Users.User{}
    #   |> MyApp.Users.User.changeset(user_params)
    #   |> MyApp.Repo.insert()

    conn
    |> Pow.Plug.create_user(user_params)
    |> case do
      {:ok, user, conn} ->
        conn
        |> put_flash(:info, "Welcome!")
        |> redirect(to: ~p"/")

      {:error, changeset, conn} ->
        render(conn, "new.html", changeset: changeset)
    end
  end
end

Create WEB_PATH/controllers/session_controller.ex:

defmodule MyAppWeb.SessionController do
  use MyAppWeb, :controller

  def new(conn, _params) do
    changeset = Pow.Plug.change_user(conn)

    render(conn, "new.html", changeset: changeset)
  end

  def create(conn, %{"user" => user_params}) do
    conn
    |> Pow.Plug.authenticate_user(user_params)
    |> case do
      {:ok, conn} ->
        conn
        |> put_flash(:info, "Welcome back!")
        |> redirect(to: ~p"/")

      {:error, conn} ->
        changeset = Pow.Plug.change_user(conn, conn.params["user"])

        conn
        |> put_flash(:info, "Invalid email or password")
        |> render("new.html", changeset: changeset)
    end
  end

  def delete(conn, _params) do
    conn
    |> Pow.Plug.delete()
    |> redirect(to: ~p"/")
  end
end

Templates

Create WEB_PATH/controllers/registration_html/new.html.heex:

<div class="mx-auto max-w-sm">
  <.header class="text-center">
    Register account
    <:subtitle>
      Already registered?
      <.link navigate={~p"/login"} class="font-semibold text-brand hover:underline">
        Log in
      </.link>
      instead.
    </:subtitle>
  </.header>

  <.simple_form :let={f} for={@changeset} action={~p"/signup"}>
    <.error :if={@changeset.action == :insert}>
      Oops, something went wrong! Please check the errors below.
    </.error>

    <.input field={f[:email]} type="email" label="Email" required />
    <.input field={f[:password]} type="password" label="Password" required />
    <.input field={f[:password_confirmation]} type="password" label="Confirm password" required />

    <:actions>
      <.button phx-disable-with="Registering account..." class="w-full">Register</.button>
    </:actions>
  </.simple_form>
</div>

Create WEB_PATH/controllers/session_html/new.html.heex:

<div class="mx-auto max-w-sm">
  <.header class="text-center">
    Sign in
    <:subtitle>
      No account?
      <.link navigate={~p"/signup"} class="font-semibold text-brand hover:underline">
        Register
      </.link>
      instead.
    </:subtitle>
  </.header>

  <.simple_form :let={f} for={@changeset} action={~p"/login"}>
    <.error :if={@changeset.action == :insert}>
      Oops, something went wrong! Please check the errors below.
    </.error>

    <.input field={f[:email]} type="email" label="Email" required />
    <.input field={f[:password]} type="password" label="Password" required />

    <:actions>
      <.button phx-disable-with="Registering account..." class="w-full">Register</.button>
    </:actions>
  </.simple_form>
</div>

Remember to create the template files WEB_PATH/controllers/registration_html.ex and WEB_PATH/controllers/session_html.ex too.

Further customization

That's all you need to do to have custom controllers with Pow! From here on you can customize your flow.

You may want to utilize some of the extensions, but since you have created a custom controller, it's highly recommended that you do not rely on any controller actions in the extensions. Instead, you should implement the logic yourself to keep your controllers as explicit as possible. This is only an example:

defmodule MyAppWeb.SessionController do
  # ...

  def create(conn, %{"user" => user_params}) do
    conn
    |> Pow.Plug.authenticate_user(user_params)
    |> verify_confirmed()
  end

  defp verify_confirmed({:ok, conn}) do
    conn
    |> Pow.Plug.current_user()
    |> email_confirmed?()
    |> case do
      true ->
        conn
        |> put_flash(:info, "Welcome back!")
        |> redirect(to: ~p"/")

      false ->
        conn
        |> Pow.Plug.delete()
        |> put_flash(:info, "Your e-mail address has not been confirmed.")
        |> redirect(to: ~p"/login")
    end
  end
  defp verify_confirmed({:error, conn}) do
    changeset = Pow.Plug.change_user(conn, conn.params["user"])

    conn
    |> put_flash(:info, "Invalid email or password")
    |> render("login.html", changeset: changeset)
  end

  defp email_confirmed?(%{email_confirmed_at: nil, email_confirmation_token: token, unconfirmed_email: nil}) when not is_nil(token), do: false
  defp email_confirmed?(_user), do: true
  # ...
end