Resend email confirmation link

by Dan Schultzer on 07 March 2020

You may wish to add a link to the account edit page so users can manually request the confirmation e-mail to be send again in case they didn’t receive it the first time.

First add a controller with an action to resend the confirmation e-mail. Create WEB_PATH/controllers/registration_controller.ex:

defmodule MyAppWeb.RegistrationController do
  use MyAppWeb, :controller

  def resend_confirmation_email(conn, _params) do
    case PowEmailConfirmation.Plug.pending_email_change?(conn) do
      true ->
        send_confirmation_email(conn)

        conn
        |> put_flash(:info, "E-mail sent, please check your inbox.")
        |> redirect(to: Routes.pow_registration_path(conn, :edit))

      false ->
        conn
        |> put_flash(:info, "E-mail has already been confirmed.")
        |> redirect(to: Routes.pow_registration_path(conn, :edit))
    end
  end

  defp send_confirmation_email(conn) do
    user = Pow.Plug.current_user(conn)

    PowEmailConfirmation.Phoenix.ControllerCallbacks.send_confirmation_email(user, conn)
  end
end

Update WEB_PATH/router.ex with the route (and it should only accessible for authenticated users):

defmodule MyAppWeb.Router do
  use MyAppWeb, :router
  use Pow.Phoenix.Router

  # ... pipelines

  pipeline :protected do
    plug Pow.Plug.RequireAuthenticated,
      error_handler: Pow.Phoenix.PlugErrorHandler
  end

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

    post "/registration/send-confirmation-email", RegistrationController, :resend_confirmation_email
  end

  # ...
end

Add the following section to your WEB_PATH/templates/pow/registration/edit.html.eex template (you may need to generate the templates first) after the pow_user_id_field field:

<%= if @changeset.data.unconfirmed_email do %>
  <div>
    <p>Click the link in the confirmation email to change your email to <%= content_tag(:span, @changeset.data.unconfirmed_email) %>. Still haven't received the email? <%= link("Click here to send again", to: Routes.registration_path(@conn, :resend_confirmation_email), method: :post) %>.</p>
  </div>
<% end %>

That’s it!

Security

As mentioned in the production checklist, you should consider adding rate limitation to e-mail delivery. Otherwise the platform may be vulnerable to resource usage attacks.

Controller test

defmodule MyAppWeb.RegistrationControllerTest do
  use MyAppWeb.ConnCase

  alias MyApp.{Users.User, Repo}

  setup %{conn: conn} do
    {:ok, user} =
      Pow.Ecto.Context.create(%{
        email: "test@example.com",
        password: "secret1234",
        password_confirmation: "secret1234"
      }, repo: Repo, user: User)

    conn = Pow.Plug.assign_current_user(conn, user, otp_app: :my_app)

    {:ok, conn: conn, user: user}
  end

  describe "resend_confirmation_email/2" do
    test "sends confirmation email", %{conn: conn} do
      conn = post conn, Routes.registration_path(conn, :resend_confirmation_email)

      assert redirected_to(conn) == Routes.pow_registration_path(conn, :edit)
      assert get_flash(conn, :info) == "E-mail sent, please check your inbox."
    end

    test "with already confirmed email", %{conn: conn, user: user} do
      user = PowEmailConfirmation.Ecto.Context.confirm_email(user, otp_app: :my_app)

      conn =
        conn
        |> Pow.Plug.assign_current_user(user, otp_app: :my_app)
        |> post(Routes.registration_path(conn, :resend_confirmation_email))

      assert redirected_to(conn) == Routes.pow_registration_path(conn, :edit)
      assert get_flash(conn, :info) == "E-mail has already been confirmed."
    end
  end
end