lib/ex_oauth2_provider/applications/application.ex

defmodule ExOauth2Provider.Applications.Application do
  @moduledoc """
  Handles the Ecto schema for application.

  ## Usage

  Configure `lib/my_project/oauth_applications/oauth_application.ex` the following way:

      defmodule MyApp.OauthApplications.OauthApplication do
        use Ecto.Schema
        use ExOauth2Provider.Applications.Application

        schema "oauth_applications" do
          application_fields()

          timestamps()
        end
      end

  ## Application owner

  By default the application owner will be will be the `:resource_owner`
  configuration setting. You can override this by overriding the `:owner`
  belongs to association:

      defmodule MyApp.OauthApplications.OauthApplication do
        use Ecto.Schema
        use ExOauth2Provider.Applications.Application

        schema "oauth_applications" do
          belongs_to :owner, MyApp.Users.User

          application_fields()

          timestamps()
        end
      end
  """

  @type t :: Ecto.Schema.t()

  @doc false
  def attrs() do
    [
    {:name, :string, [], null: false},
    {:uid, :string, [], null: false},
    {:secret, :string, [default: ""], null: false},
    {:redirect_uri, :string, [], null: false},
    {:scopes, :string, [default: ""], null: false},
    ]
  end

  @doc false
  def assocs() do
    [
      {:belongs_to, :owner, :users},
      {:has_many, :access_tokens, :access_tokens, foreign_key: :application_id}
    ]
  end

  @doc false
  def indexes(), do: [{:uid, true}]

  @doc false
  defmacro __using__(config) do
    quote do
      use ExOauth2Provider.Schema, unquote(config)

      # For Phoenix integrations
      if Code.ensure_loaded?(Phoenix.Param), do: @derive {Phoenix.Param, key: :uid}

      import unquote(__MODULE__), only: [application_fields: 0]
    end
  end

  defmacro application_fields do
    quote do
      ExOauth2Provider.Schema.fields(unquote(__MODULE__))
    end
  end

  alias Ecto.Changeset
  alias ExOauth2Provider.{RedirectURI, Utils}
  alias ExOauth2Provider.Mixin.Scopes

  @spec changeset(Ecto.Schema.t(), map(), keyword()) :: Changeset.t()
  def changeset(application, params, config \\ []) do
    application
    |> maybe_new_application_changeset(params, config)
    |> Changeset.cast(params, [:name, :secret, :redirect_uri, :scopes])
    |> Changeset.validate_required([:name, :uid, :redirect_uri])
    |> validate_secret_not_nil()
    |> Scopes.validate_scopes(nil, config)
    |> validate_redirect_uri(config)
    |> Changeset.unique_constraint(:uid)
  end

  defp validate_secret_not_nil(changeset) do
    case Changeset.get_field(changeset, :secret) do
      nil -> Changeset.add_error(changeset, :secret, "can't be blank")
      _   -> changeset
    end
  end

  defp maybe_new_application_changeset(application, params, config) do
    case Ecto.get_meta(application, :state) do
      :built  -> new_application_changeset(application, params, config)
      :loaded -> application
    end
  end

  defp new_application_changeset(application, params, config) do
    application
    |> Changeset.cast(params, [:uid, :secret])
    |> put_uid()
    |> put_secret()
    |> Scopes.put_scopes(nil, config)
    |> Changeset.assoc_constraint(:owner)
  end

  defp validate_redirect_uri(changeset, config) do
    changeset
    |> Changeset.get_field(:redirect_uri)
    |> Kernel.||("")
    |> String.split()
    |> Enum.reduce(changeset, fn url, changeset ->
      url
      |> RedirectURI.validate(config)
      |> case do
         {:error, error} -> Changeset.add_error(changeset, :redirect_uri, error)
         {:ok, _}        -> changeset
       end
    end)
  end

  defp put_uid(%{changes: %{uid: _}} = changeset), do: changeset
  defp put_uid(%{} = changeset) do
    Changeset.change(changeset, %{uid: Utils.generate_token()})
  end

  defp put_secret(%{changes: %{secret: _}} = changeset), do: changeset
  defp put_secret(%{} = changeset) do
    Changeset.change(changeset, %{secret: Utils.generate_token()})
  end
end