Here’s another solution we use Guardian, but the token generation is done via Phoenix.
Context:
##
# User activation API
##
@doc """
Generates activation token and sends activation email to the given user.
"""
@spec initiate_activation(%User{}) :: struct()
defdelegate initiate_activation(user), to: ActivationService, as: :process_email_confirmation
@doc """
Finds and validates user for the given token, marks user's email as confirmed.
Returns {:ok, message} or {:error, reason} tuple.
"""
@spec finalize_activation(String.t) :: {:ok, String.t} | {:error, String.t}
defdelegate finalize_activation(token), to: ActivationService, as: :check_confirmation_token
Here’s the (somewhat simplified) service module:
defmodule App.Accounts.Service.Activation do
@moduledoc false
alias App.{Mailer, Repo, Accounts.User}
alias AppWeb.Endpoint
alias App.Email.User, as: UserEmail
alias App.Email.Admin, as: AdminEmail
alias Ecto.{Changeset, Multi}
alias Phoenix.Token
require Logger
import Ecto
import Ecto.Query
@token_max_age :app_name |> Application.get_env(:auth) |> Keyword.get(:normal_token_max_age)
@doc false
def process_email_confirmation(%User{} = user) do
token = generate_token(user)
result =
user
|> persist_token_multi(token)
|> Repo.transaction()
case result do
{:ok, _params} -> send_confirmation_email(user, token)
{:error, failed_operation, failed_value, changes_so_far} ->
params = %{failed_operation: failed_operation, failed_value: failed_value, changes_so_far: changes_so_far}
process_error(user, params)
end
end
@doc false
def check_confirmation_token(token) when is_binary(token) do
with(
{:ok, user_id} <- Token.verify(Endpoint, "user", token, [max_age: @token_max_age]),
user <- Repo.get(User, user_id),
1 <- user |> assoc(:user_tokens) |> where([token: ^token, type: "email_confirmation"]) |> Repo.aggregate(:count, :id)
) do
case Repo.transaction(persist_confirmation(user)) do
{:ok, _params} -> {:ok, "Email address confirmed"}
{:error, failed_operation, failed_value, changes_so_far} ->
params = %{failed_operation: failed_operation, failed_value: failed_value, changes_so_far: changes_so_far}
process_error(user, params)
{:error, "We are having technitcal problems, please retry confirmation"}
end
else
_ -> {:error, "Invalid token"}
end
end
defp send_confirmation_email(user, token) do
user
|> UserEmail.registration_email(token)
|> Mailer.deliver_later!()
end
defp persist_token_multi(user, token) do
Multi.new
|> Multi.delete_all(:delete_old_tokens, get_old_tokens_query(user))
|> Multi.insert(:insert_token, build_assoc(user, :user_tokens, %{token: token, type: "email_confirmation"}))
end
defp persist_confirmation(user) do
Multi.new
|> Multi.delete_all(:delete_old_tokens, get_old_tokens_query(user))
|> Multi.update(:update_user, Changeset.change(user, [email_confirmed: true]))
end
defp get_old_tokens_query(user) do
user
|> assoc(:user_tokens)
|> where([t], [type: "email_confirmation"])
end
defp generate_token(user) do
Token.sign(Endpoint, "user", user.id)
end
defp process_error(user, params) do
Logger.error("Unable to complete email confirmation, extra data:")
Logger.error(inspect(user))
Logger.error(inspect(params))
end
end
You can call Accounts.initiate_activation(user)
after creating the user and in the confirmation controller action something like that:
case Accounts.finalize_activation(token) do
{:ok, _} ->
conn
|> put_flash(:info, "Email confirmed")
|> redirect(to: some_path(conn, :index))
{:error, message} ->
conn
|> put_flash(:error, message)
|> redirect(to: some_path(conn, :index))
end