analytics/lib/plausible/auth/email_verification.ex

92 lines
2.3 KiB
Elixir

defmodule Plausible.Auth.EmailVerification do
@moduledoc """
API for verifying emails.
"""
import Ecto.Query, only: [from: 2]
alias Plausible.Auth
alias Plausible.Auth.EmailActivationCode
alias Plausible.Repo
require Logger
@expiration_hours 4
@spec any?(Auth.User.t()) :: boolean()
def any?(user) do
Repo.exists?(from(v in EmailActivationCode, where: v.user_id == ^user.id))
end
@spec issue_code(Auth.User.t(), NaiveDateTime.t()) ::
{:ok, EmailActivationCode.t()} | {:error, :hard_bounce | :unknown_error}
def issue_code(user, now \\ NaiveDateTime.utc_now()) do
now = NaiveDateTime.truncate(now, :second)
verification =
user
|> EmailActivationCode.new(now)
|> Repo.insert!(
on_conflict: [
set: [
issued_at: now,
code: EmailActivationCode.generate_code()
]
],
conflict_target: :user_id,
returning: true
)
email_template = PlausibleWeb.Email.activation_email(user, verification.code)
case Plausible.Mailer.send(email_template) do
:ok ->
Logger.debug(
"E-mail verification e-mail sent. In dev environment GET /sent-emails for details."
)
{:ok, verification}
error ->
error
end
end
@spec verify_code(Auth.User.t(), String.t() | non_neg_integer()) ::
:ok | {:error, :incorrect | :expired}
def verify_code(user, code) do
with {:ok, verification} <- get_verification(user, code) do
Repo.transaction(fn ->
user
|> Ecto.Changeset.change(email_verified: true)
|> Repo.update!()
Repo.delete_all(from(c in EmailActivationCode, where: c.id == ^verification.id))
end)
:ok
end
end
defp get_verification(user, code) do
verification = Repo.get_by(EmailActivationCode, user_id: user.id, code: code)
cond do
is_nil(verification) ->
{:error, :incorrect}
expired?(verification) ->
{:error, :expired}
true ->
{:ok, verification}
end
end
@spec expired?(EmailActivationCode.t()) :: boolean()
def expired?(verification) do
expiration_time = NaiveDateTime.shift(NaiveDateTime.utc_now(), hour: -1 * @expiration_hours)
NaiveDateTime.before?(verification.issued_at, expiration_time)
end
end