lib/ash_authentication/add_ons/confirmation/actions.ex

defmodule AshAuthentication.AddOn.Confirmation.Actions do
  @moduledoc """
  Actions for the confirmation add-on.

  Provides the code interface for working with resources via confirmation.
  """

  alias Ash.{Changeset, Error.Framework.AssumptionFailed, Query, Resource}

  alias AshAuthentication.{
    AddOn.Confirmation,
    Errors.InvalidToken,
    Info,
    Jwt,
    Strategy,
    TokenResource
  }

  @doc """
  Attempt to confirm a user.
  """
  @spec confirm(Confirmation.t(), map, keyword) :: {:ok, Resource.record()} | {:error, any}
  def confirm(strategy, params, opts \\ []) do
    with {:ok, api} <- Info.authentication_api(strategy.resource),
         {:ok, token} <- Map.fetch(params, "confirm"),
         {:ok, %{"sub" => subject}, _} <- Jwt.verify(token, strategy.resource),
         {:ok, user} <- AshAuthentication.subject_to_user(subject, strategy.resource) do
      user
      |> Changeset.new()
      |> Changeset.set_context(%{
        private: %{
          ash_authentication?: true
        }
      })
      |> Changeset.for_update(strategy.confirm_action_name, params)
      |> api.update(opts)
    else
      :error -> {:error, InvalidToken.exception(type: :confirmation)}
      {:error, reason} -> {:error, reason}
    end
  end

  @doc """
  Store changes in the tokens resource for later re-use.
  """
  @spec store_changes(Confirmation.t(), String.t(), Changeset.t(), keyword) :: :ok | {:error, any}
  def store_changes(strategy, token, changeset, opts \\ []) do
    changes =
      strategy.monitor_fields
      |> Stream.filter(&Changeset.changing_attribute?(changeset, &1))
      |> Stream.map(&{to_string(&1), to_string(Changeset.get_attribute(changeset, &1))})
      |> Map.new()

    with {:ok, token_resource} <- Info.authentication_tokens_token_resource(strategy.resource),
         {:ok, api} <- TokenResource.Info.token_api(token_resource),
         {:ok, store_changes_action} <-
           TokenResource.Info.token_confirmation_store_changes_action_name(token_resource),
         {:ok, _token_record} <-
           token_resource
           |> Changeset.new()
           |> Changeset.set_context(%{
             private: %{
               ash_authentication?: true
             }
           })
           |> Changeset.for_create(store_changes_action, %{
             token: token,
             extra_data: changes,
             purpose: to_string(Strategy.name(strategy))
           })
           |> api.create(Keyword.merge(opts, upsert?: true)) do
      :ok
    else
      {:error, reason} ->
        {:error, reason}

      :error ->
        {:error,
         AssumptionFailed.exception(
           message: "Configuration error storing confirmation token data"
         )}
    end
  end

  @doc """
  Get changes from the tokens resource for application.
  """
  @spec get_changes(Confirmation.t(), String.t(), keyword) :: {:ok, map} | :error
  def get_changes(strategy, jti, opts \\ []) do
    with {:ok, token_resource} <- Info.authentication_tokens_token_resource(strategy.resource),
         {:ok, api} <- TokenResource.Info.token_api(token_resource),
         {:ok, get_changes_action} <-
           TokenResource.Info.token_confirmation_get_changes_action_name(token_resource),
         {:ok, [token_record]} <-
           token_resource
           |> Query.new()
           |> Query.set_context(%{
             private: %{
               ash_authentication?: true
             }
           })
           |> Query.set_context(%{strategy: strategy})
           |> Query.for_read(get_changes_action, %{"jti" => jti})
           |> api.read(opts) do
      changes =
        strategy.monitor_fields
        |> Stream.map(&to_string/1)
        |> Stream.map(&{&1, Map.get(token_record.extra_data, &1)})
        |> Stream.reject(&is_nil(elem(&1, 1)))
        |> Map.new()

      {:ok, changes}
    else
      _ -> :error
    end
  end
end