lib/ash_authentication/token_resource/transformer.ex

defmodule AshAuthentication.TokenResource.Transformer do
  @moduledoc """
  The token resource transformer.

  Sets up the default schema and actions for the token resource.
  """

  use Spark.Dsl.Transformer
  require Ash.Expr
  alias Ash.{Resource, Type}
  alias AshAuthentication.{TokenResource, TokenResource.Info}
  alias Spark.Dsl.Transformer

  import AshAuthentication.Utils
  import AshAuthentication.Validations
  import AshAuthentication.Validations.Action
  import AshAuthentication.Validations.Attribute

  @doc false
  @impl true
  @spec after?(any) :: boolean()
  def after?(Resource.Transformers.ValidatePrimaryActions), do: true
  def after?(_), do: false

  @doc false
  @impl true
  @spec before?(any) :: boolean
  def before?(Resource.Transformers.CachePrimaryKey), do: true
  def before?(Resource.Transformers.DefaultAccept), do: true
  def before?(_), do: false

  @doc false
  @impl true
  @spec transform(map) ::
          :ok | {:ok, map} | {:error, term} | {:warn, map, String.t() | [String.t()]} | :halt
  def transform(dsl_state) do
    with {:ok, dsl_state} <-
           maybe_build_attribute(dsl_state, :jti, :string,
             primary_key?: true,
             allow_nil?: false,
             sensitive?: true,
             writable?: true
           ),
         :ok <- validate_jti_field(dsl_state),
         {:ok, dsl_state} <-
           maybe_build_attribute(dsl_state, :subject, :string, allow_nil?: false, writable?: true),
         :ok <- validate_subject_field(dsl_state),
         {:ok, dsl_state} <-
           maybe_build_attribute(dsl_state, :expires_at, :utc_datetime,
             allow_nil?: false,
             writable?: true
           ),
         :ok <- validate_expires_at_field(dsl_state),
         {:ok, dsl_state} <-
           maybe_build_attribute(dsl_state, :purpose, :string,
             allow_nil?: false,
             writable?: true
           ),
         :ok <- validate_purpose_field(dsl_state),
         {:ok, dsl_state} <-
           maybe_build_attribute(dsl_state, :extra_data, :map,
             allow_nil?: true,
             writable?: true
           ),
         {:ok, dsl_state} <-
           maybe_build_attribute(dsl_state, :created_at, :utc_datetime_usec,
             allow_nil?: false,
             private?: true,
             default: &DateTime.utc_now/0
           ),
         {:ok, dsl_state} <-
           maybe_build_attribute(dsl_state, :updated_at, :utc_datetime_usec,
             allow_nil?: false,
             private?: true,
             default: &DateTime.utc_now/0,
             update_default: &DateTime.utc_now/0
           ),
         :ok <- validate_extra_data_field(dsl_state),
         {:ok, expunge_expired_action_name} <- Info.token_expunge_expired_action_name(dsl_state),
         {:ok, dsl_state} <-
           maybe_build_action(
             dsl_state,
             expunge_expired_action_name,
             &build_expunge_expired_action(&1, expunge_expired_action_name)
           ),
         :ok <- validate_expunge_expired_action(dsl_state, expunge_expired_action_name),
         {:ok, read_expired_action_name} <- Info.token_read_expired_action_name(dsl_state),
         {:ok, dsl_state} <-
           maybe_build_action(
             dsl_state,
             read_expired_action_name,
             &build_read_expired_action(&1, read_expired_action_name)
           ),
         :ok <- validate_read_expired_action(dsl_state, read_expired_action_name),
         {:ok, revoke_token_action_name} <-
           Info.token_revocation_revoke_token_action_name(dsl_state),
         {:ok, dsl_state} <-
           maybe_build_action(
             dsl_state,
             revoke_token_action_name,
             &build_revoke_token_action(&1, revoke_token_action_name)
           ),
         :ok <- validate_revoke_token_action(dsl_state, revoke_token_action_name),
         {:ok, is_revoked_action_name} <- Info.token_revocation_is_revoked_action_name(dsl_state),
         {:ok, dsl_state} <-
           maybe_build_action(
             dsl_state,
             is_revoked_action_name,
             &build_is_revoked_action(&1, is_revoked_action_name)
           ),
         :ok <- validate_is_revoked_action(dsl_state, is_revoked_action_name),
         {:ok, get_confirmation_changes_action_name} <-
           Info.token_confirmation_get_changes_action_name(dsl_state),
         {:ok, dsl_state} <-
           maybe_build_action(
             dsl_state,
             get_confirmation_changes_action_name,
             &build_get_confirmation_changes_action(&1, get_confirmation_changes_action_name)
           ),
         :ok <-
           validate_get_confirmation_changes_action(
             dsl_state,
             get_confirmation_changes_action_name
           ),
         {:ok, store_confirmation_changes_action_name} <-
           Info.token_confirmation_store_changes_action_name(dsl_state),
         {:ok, dsl_state} <-
           maybe_build_action(
             dsl_state,
             store_confirmation_changes_action_name,
             &build_store_confirmation_changes_action(&1, store_confirmation_changes_action_name)
           ),
         :ok <-
           validate_store_confirmation_changes_action(
             dsl_state,
             store_confirmation_changes_action_name
           ),
         {:ok, store_token_action_name} <-
           Info.token_store_token_action_name(dsl_state),
         {:ok, dsl_state} <-
           maybe_build_action(
             dsl_state,
             store_token_action_name,
             &build_store_token_action(&1, store_token_action_name)
           ),
         :ok <- validate_store_token_action(dsl_state, store_token_action_name),
         {:ok, get_token_action_name} <- Info.token_get_token_action_name(dsl_state),
         {:ok, dsl_state} <-
           maybe_build_action(
             dsl_state,
             get_token_action_name,
             &build_get_token_action(&1, get_token_action_name)
           ),
         :ok <- validate_get_token_action(dsl_state, get_token_action_name) do
      {:ok, dsl_state}
    end
  end

  defp validate_subject_field(dsl_state) do
    with {:ok, resource} <- persisted_option(dsl_state, :module),
         {:ok, attribute} <- find_attribute(dsl_state, :subject),
         :ok <- validate_attribute_option(attribute, resource, :type, [Type.String, :string]) do
      validate_attribute_option(attribute, resource, :writable?, [true])
    end
  end

  defp validate_get_token_action(dsl_state, action_name) do
    with {:ok, action} <- validate_action_exists(dsl_state, action_name),
         :ok <-
           validate_action_argument_option(action, :token, :type, [Ash.Type.String, :string]),
         :ok <- validate_action_argument_option(action, :token, :allow_nil?, [true]),
         :ok <- validate_action_argument_option(action, :token, :sensitive?, [true]),
         :ok <- validate_action_argument_option(action, :jti, :type, [Ash.Type.String, :string]),
         :ok <- validate_action_argument_option(action, :jti, :allow_nil?, [true]),
         :ok <-
           validate_action_argument_option(action, :purpose, :type, [Ash.Type.String, :string]) do
      validate_action_has_preparation(action, TokenResource.GetTokenPreparation)
    end
  end

  defp build_get_token_action(_dsl_state, action_name) do
    arguments = [
      Transformer.build_entity!(Resource.Dsl, [:actions, :read], :argument,
        name: :token,
        type: :string,
        sensitive?: true
      ),
      Transformer.build_entity!(Resource.Dsl, [:actions, :read], :argument,
        name: :jti,
        type: :string,
        sensitive?: false
      ),
      Transformer.build_entity!(Resource.Dsl, [:actions, :read], :argument,
        name: :purpose,
        type: :string,
        sensitive?: false
      )
    ]

    preparations = [
      Transformer.build_entity!(Resource.Dsl, [:actions, :read], :prepare,
        preparation: TokenResource.GetTokenPreparation
      )
    ]

    Transformer.build_entity(Resource.Dsl, [:actions], :read,
      name: action_name,
      arguments: arguments,
      preparations: preparations,
      get?: true
    )
  end

  defp validate_store_token_action(dsl_state, action_name) do
    with {:ok, action} <- validate_action_exists(dsl_state, action_name),
         :ok <- validate_token_argument(action) do
      validate_action_has_change(action, TokenResource.StoreTokenChange)
    end
  end

  defp build_store_token_action(_dsl_state, action_name) do
    arguments = [
      Transformer.build_entity!(Resource.Dsl, [:actions, :create], :argument,
        name: :token,
        type: :string,
        allow_nil?: false,
        sensitive?: true
      )
    ]

    changes = [
      Transformer.build_entity!(Resource.Dsl, [:actions, :create], :change,
        change: TokenResource.StoreTokenChange
      )
    ]

    Transformer.build_entity(Resource.Dsl, [:actions], :create,
      name: action_name,
      arguments: arguments,
      changes: changes,
      accept: [:extra_data, :purpose]
    )
  end

  defp build_store_confirmation_changes_action(_dsl_state, action_name) do
    arguments = [
      Transformer.build_entity!(Resource.Dsl, [:actions, :create], :argument,
        name: :token,
        type: :string,
        allow_nil?: false,
        sensitive?: true
      )
    ]

    changes = [
      Transformer.build_entity!(Resource.Dsl, [:actions, :create], :change,
        change: TokenResource.StoreConfirmationChangesChange
      )
    ]

    Transformer.build_entity(Resource.Dsl, [:actions], :create,
      name: action_name,
      arguments: arguments,
      changes: changes,
      accept: [:extra_data, :purpose]
    )
  end

  defp validate_store_confirmation_changes_action(dsl_state, action_name) do
    with {:ok, action} <- validate_action_exists(dsl_state, action_name),
         :ok <- validate_token_argument(action) do
      validate_action_has_change(action, TokenResource.StoreConfirmationChangesChange)
    end
  end

  defp build_get_confirmation_changes_action(_dsl_state, action_name) do
    arguments = [
      Transformer.build_entity!(Resource.Dsl, [:actions, :read], :argument,
        name: :jti,
        type: :string,
        allow_nil?: false,
        sensitive?: true
      )
    ]

    preparations = [
      Transformer.build_entity!(Resource.Dsl, [:actions, :read], :prepare,
        preparation: TokenResource.GetConfirmationChangesPreparation
      )
    ]

    Transformer.build_entity(Resource.Dsl, [:actions], :read,
      name: action_name,
      arguments: arguments,
      preparations: preparations,
      get?: true
    )
  end

  defp validate_get_confirmation_changes_action(dsl_state, action_name) do
    with {:ok, action} <- validate_action_exists(dsl_state, action_name),
         :ok <- validate_action_argument_option(action, :jti, :type, [Ash.Type.String, :string]),
         :ok <-
           validate_action_has_preparation(
             action,
             TokenResource.GetConfirmationChangesPreparation
           ) do
      validate_field_in_values(action, :type, [:read])
    end
  end

  defp build_read_expired_action(_dsl_state, action_name) do
    import Ash.Filter.TemplateHelpers

    Transformer.build_entity(Resource.Dsl, [:actions], :read,
      name: action_name,
      filter: expr(expires_at < now())
    )
  end

  defp validate_read_expired_action(dsl_state, action_name) do
    with {:ok, action} <- validate_action_exists(dsl_state, action_name) do
      validate_field_in_values(action, :type, [:read])
    end
  end

  defp validate_is_revoked_action(dsl_state, action_name) do
    with {:ok, action} <- validate_action_exists(dsl_state, action_name),
         :ok <-
           validate_action_argument_option(action, :token, :type, [Ash.Type.String, :string]),
         :ok <- validate_action_argument_option(action, :jti, :type, [Ash.Type.String, :string]),
         :ok <- validate_action_has_preparation(action, TokenResource.IsRevokedPreparation) do
      validate_field_in_values(action, :type, [:read])
    end
  end

  defp build_is_revoked_action(_dsl_state, action_name) do
    arguments = [
      Transformer.build_entity!(Resource.Dsl, [:actions, :read], :argument,
        name: :token,
        type: :string,
        allow_nil?: true,
        sensitive?: true
      ),
      Transformer.build_entity!(Resource.Dsl, [:actions, :read], :argument,
        name: :jti,
        type: :string,
        allow_nil?: true,
        sensitive?: true
      )
    ]

    preparations = [
      Transformer.build_entity!(Resource.Dsl, [:actions, :read], :prepare,
        preparation: TokenResource.IsRevokedPreparation
      )
    ]

    Transformer.build_entity(Resource.Dsl, [:actions], :read,
      name: action_name,
      get?: true,
      preparations: preparations,
      arguments: arguments
    )
  end

  defp validate_token_argument(action) do
    with :ok <-
           validate_action_argument_option(action, :token, :type, [Ash.Type.String, :string]),
         :ok <- validate_action_argument_option(action, :token, :allow_nil?, [false]) do
      validate_action_argument_option(action, :token, :sensitive?, [true])
    end
  end

  defp validate_revoke_token_action(dsl_state, revoke_token_action_name) do
    with {:ok, action} <- validate_action_exists(dsl_state, revoke_token_action_name),
         :ok <- validate_token_argument(action) do
      validate_action_has_change(action, TokenResource.RevokeTokenChange)
    end
  end

  defp build_revoke_token_action(_dsl_state, action_name) do
    arguments = [
      Transformer.build_entity!(Resource.Dsl, [:actions, :create], :argument,
        name: :token,
        type: :string,
        allow_nil?: false,
        sensitive?: true
      )
    ]

    changes = [
      Transformer.build_entity!(Resource.Dsl, [:actions, :create], :change,
        change: TokenResource.RevokeTokenChange
      )
    ]

    Transformer.build_entity(Resource.Dsl, [:actions], :create,
      name: action_name,
      arguments: arguments,
      changes: changes,
      accept: [:extra_data]
    )
  end

  defp validate_expunge_expired_action(dsl_state, action_name) do
    with {:ok, action} <- validate_action_exists(dsl_state, action_name) do
      validate_field_in_values(action, :type, [:destroy])
    end
  end

  defp build_expunge_expired_action(_dsl_state, action_name),
    do: Transformer.build_entity(Resource.Dsl, [:actions], :destroy, name: action_name)

  defp validate_jti_field(dsl_state) do
    with {:ok, resource} <- persisted_option(dsl_state, :module),
         {:ok, attribute} <- find_attribute(dsl_state, :jti),
         :ok <- validate_attribute_option(attribute, resource, :type, [Type.String, :string]),
         :ok <- validate_attribute_option(attribute, resource, :allow_nil?, [false]),
         :ok <- validate_attribute_option(attribute, resource, :sensitive?, [true]),
         :ok <- validate_attribute_option(attribute, resource, :writable?, [true]),
         :ok <- validate_attribute_option(attribute, resource, :primary_key?, [true]) do
      validate_attribute_option(attribute, resource, :private?, [false])
    end
  end

  defp validate_expires_at_field(dsl_state) do
    with {:ok, resource} <- persisted_option(dsl_state, :module),
         {:ok, attribute} <- find_attribute(dsl_state, :expires_at),
         :ok <-
           validate_attribute_option(attribute, resource, :type, [Type.UtcDatetime, :utc_datetime]),
         :ok <- validate_attribute_option(attribute, resource, :allow_nil?, [false]) do
      validate_attribute_option(attribute, resource, :writable?, [true])
    end
  end

  defp validate_purpose_field(dsl_state) do
    with {:ok, resource} <- persisted_option(dsl_state, :module),
         {:ok, attribute} <- find_attribute(dsl_state, :purpose),
         :ok <- validate_attribute_option(attribute, resource, :type, [Type.String, :string]),
         :ok <- validate_attribute_option(attribute, resource, :allow_nil?, [false]) do
      validate_attribute_option(attribute, resource, :writable?, [true])
    end
  end

  defp validate_extra_data_field(dsl_state) do
    with {:ok, resource} <- persisted_option(dsl_state, :module),
         {:ok, attribute} <- find_attribute(dsl_state, :extra_data),
         :ok <- validate_attribute_option(attribute, resource, :type, [Type.Map, :map]),
         :ok <- validate_attribute_option(attribute, resource, :allow_nil?, [true]) do
      validate_attribute_option(attribute, resource, :writable?, [true])
    end
  end
end