lib/ash_authentication/add_ons/confirmation/confirm_change.ex

defmodule AshAuthentication.AddOn.Confirmation.ConfirmChange do
  @moduledoc """
  Performs a change based on the contents of a confirmation token.
  """

  use Ash.Resource.Change
  alias AshAuthentication.{AddOn.Confirmation.Actions, Info, Jwt}

  alias Ash.{
    Changeset,
    Error.Changes.InvalidArgument,
    Error.Framework.AssumptionFailed,
    Resource.Change
  }

  @doc false
  @impl true
  @spec change(Changeset.t(), keyword, Change.context()) :: Changeset.t()
  def change(changeset, _opts, _context) do
    case Info.strategy_for_action(changeset.resource, changeset.action.name) do
      {:ok, strategy} ->
        do_change(changeset, strategy)

      :error ->
        raise AssumptionFailed,
          message: "Action does not correlate with an authentication strategy"
    end
  end

  defp do_change(changeset, strategy) do
    changeset
    |> Changeset.before_action(fn changeset ->
      with token when is_binary(token) <- Changeset.get_argument(changeset, :confirm),
           {:ok, %{"act" => action, "jti" => jti}, _} <-
             Jwt.verify(token, changeset.resource),
           true <- to_string(strategy.confirm_action_name) == action,
           {:ok, changes} <- Actions.get_changes(strategy, jti) do
        allowed_changes =
          if strategy.inhibit_updates?,
            do: Map.take(changes, Enum.map(strategy.monitor_fields, &to_string/1)),
            else: %{}

        changeset
        |> Changeset.change_attributes(allowed_changes)
        |> Changeset.change_attribute(strategy.confirmed_at_field, DateTime.utc_now())
      else
        _ ->
          raise InvalidArgument, field: :confirm, message: "is not valid"
      end
    end)
  end
end