lib/sptfy/oauth.ex

defmodule Sptfy.OAuth do
  @moduledoc """
  This provides functions to obtain authorization with authorization code flow.

  https://developer.spotify.com/documentation/general/guides/authorization-guide/#authorization-code-flow
  """

  alias Sptfy.Object.{OAuthError, OAuthResponse}

  @type response :: {:ok, OAuthResponse.t()} | {:error, OAuthError.t()} | {:error, Mint.Types.error()}

  @doc """
  Returns a URL to request an authorization code.

      iex> Sptfy.OAuth.url("CLIENT_ID", "https://example.com/callback")
      ...> "https://accounts.spotify.com/authorize?client_id=CLIENT_ID&redirect_uri=https%3A%2F%2Fexample.com%2Fcallback&response_type=code&scope="

      iex> Sptfy.OAuth.url("CLIENT_ID", "https://example.com/callback", %{scope: ["streaming"]})
      ...> "https://accounts.spotify.com/authorize?client_id=CLIENT_ID&redirect_uri=https%3A%2F%2Fexample.com%2Fcallback&response_type=code&scope=streaming"
  """
  @spec url(client_id :: String.t(), redirect_uri :: String.t(), params :: map() | Keyword.t()) :: String.t()
  def url(client_id, redirect_uri, params \\ %{})

  def url(client_id, redirect_uri, params) when is_list(params) do
    url(client_id, redirect_uri, Enum.into(params, %{}))
  end

  def url(client_id, redirect_uri, params) when is_map(params) do
    scope = Map.get(params, :scope, []) |> Enum.join(",")

    endpoint = "https://accounts.spotify.com/authorize"
    query = Map.merge(params, %{response_type: "code", client_id: client_id, redirect_uri: redirect_uri, scope: scope})

    endpoint <> "?" <> URI.encode_query(query)
  end

  @doc """
  Requests access token and refresh token to the Spotify Accounts service.
  """
  @spec get_token(client_id :: String.t(), client_secret :: String.t(), code :: String.t(), redirect_uri :: String.t()) :: response()
  def get_token(client_id, client_secret, code, redirect_uri) do
    post(client_id, client_secret, grant_type: "authorization_code", code: code, redirect_uri: redirect_uri)
  end

  @doc """
  Exchanges a refresh token for new access token.
  """
  @spec refresh_token(client_id :: String.t(), client_secret :: String.t(), refresh_token :: String.t()) :: response()
  def refresh_token(client_id, client_secret, refresh_token) do
    post(client_id, client_secret, grant_type: "refresh_token", refresh_token: refresh_token)
  end

  @spec post(client_id :: String.t(), client_secret :: String.t(), body :: Keyword.t()) :: response()
  defp post(client_id, client_secret, body) do
    endpoint = "https://accounts.spotify.com/api/token"
    headers = headers(client_id, client_secret)
    body = URI.encode_query(body)

    Finch.build(:post, endpoint, headers, body)
    |> Finch.request(Sptfy.Finch)
    |> case do
      {:ok, response} -> handle_response(response)
      error -> error
    end
  end

  defp headers(client_id, client_secret) do
    [
      {"Authorization", "Basic " <> Base.encode64(client_id <> ":" <> client_secret)},
      {"Content-Type", "application/x-www-form-urlencoded"}
    ]
  end

  defp handle_response(%Finch.Response{status: status, body: body}) when status in 200..299 do
    {:ok, body} = Jason.decode(body)
    {:ok, OAuthResponse.new(body)}
  end

  defp handle_response(%Finch.Response{status: status, body: body}) when status in 400..499 do
    {:ok, body} = Jason.decode(body)
    {:error, OAuthError.new(body)}
  end
end