| 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 |
|
|