Skip to content

Commit

Permalink
feat: added support for OAuth2 (#87)
Browse files Browse the repository at this point in the history
  • Loading branch information
anthonator authored May 20, 2024
1 parent 435f84f commit 3971260
Show file tree
Hide file tree
Showing 8 changed files with 106 additions and 18 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ request and how it should behave.

### Options

- `:api_key` Public or private Klaviyo API key
- `:access_token` - OAuth2 access token
- `:api_key` - Public or private Klaviyo API key
- `:client` - HTTP client adapter used to make the request. Defaults to
`Klaviyo.HTTP.Hackney`.
- `:client_opts` - Configuration options passed to the client adapter
Expand Down
2 changes: 1 addition & 1 deletion lib/klaviyo.ex
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ defmodule Klaviyo do
RequestOperation.t(),
keyword
) :: response_t
def send(operation, opts) do
def send(operation, opts \\ []) do
opts = Opts.new(opts)

request = Request.new(operation, opts)
Expand Down
45 changes: 45 additions & 0 deletions lib/klaviyo/oauth2.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
defmodule Klaviyo.OAuth2 do
alias Klaviyo.RequestOperation

@spec authorize_url(String.t(), Enum.t()) :: String.t()
def authorize_url(url \\ "https://www.klaviyo.com/oauth/authorize", params) do
params =
params
|> put_in([:response_type], "code")
|> put_in([:code_challenge_method], "S256")

url
|> URI.parse()
|> Map.put(:query, URI.encode_query(params))
|> URI.to_string()
end

@spec code_challenge(String.t()) :: String.t()
def code_challenge(code_verifier) do
digest = :crypto.hash(:sha256, code_verifier)

Base.encode64(digest, padding: false)
end

@spec code_verifier(pos_integer) :: String.t()
def code_verifier(length \\ 128) do
symbols = Enum.concat([?0..?9, ?a..?z, ?A..?Z, ["_", ".", "-", "~"]])

Stream.repeatedly(fn -> Enum.random(symbols) end)
|> Enum.take(length)
|> List.to_string()
end

@spec get_token(String.t(), String.t(), Enum.t()) :: RequestOperation.t()
def get_token(client_id, client_secret, params) do
basic = Base.encode64("#{client_id}:#{client_secret}")

%RequestOperation{
body: params,
encoding: :www_form,
headers: [{"authorization", "Basic #{basic}"}],
method: :post,
path: "/oauth/token"
}
end
end
4 changes: 3 additions & 1 deletion lib/klaviyo/opts.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ defmodule Klaviyo.Opts do

@type t ::
%__MODULE__{
access_token: String.t(),
api_key: String.t(),
client: module,
client_opts: keyword,
Expand All @@ -21,7 +22,8 @@ defmodule Klaviyo.Opts do
revision: String.t()
}

defstruct api_key: nil,
defstruct access_token: nil,
api_key: nil,
client: HTTP.Hackney,
client_opts: [],
headers: [],
Expand Down
57 changes: 48 additions & 9 deletions lib/klaviyo/request.ex
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ defmodule Klaviyo.Request do
"""
@spec new(RequestOperation.t(), Opts.t()) :: t
def new(operation, opts) do
body = opts.json_codec.encode!(Enum.into(operation.body, %{}))
headers = opts.headers
body = encode_body(operation.body, operation.encoding, opts)
headers = opts.headers ++ operation.headers
method = operation.method
url = RequestOperation.to_url(operation, opts)

Expand All @@ -33,9 +33,39 @@ defmodule Klaviyo.Request do
|> Map.put(:headers, headers)
|> Map.put(:method, method)
|> Map.put(:url, url)
|> put_header("authorization", "Klaviyo-API-Key #{opts.api_key}")
|> put_header("content-type", "application/json")
|> put_header("content-type", encoding(operation.encoding))
|> put_header("revision", opts.revision)
|> put_new_header("authorization", authentication_token(opts))
end

def authentication_token(%{access_token: access_token}) when not is_nil(access_token) do
"Bearer #{access_token}"
end

def authentication_token(%{api_key: api_key}) when not is_nil(api_key) do
"Klaviyo-API-Key #{api_key}"
end

def authentication_token(_) do
nil
end

@spec encode_body(Enum.t(), atom, Opts.t()) :: String.t()
def encode_body(body, :json, opts) do
opts.json_codec.encode!(Enum.into(body, %{}))
end

def encode_body(body, :www_form, _opts) do
URI.encode_query(body, :www_form)
end

@spec encoding(atom) :: String.t()
def encoding(:json) do
"application/json"
end

def encoding(:www_form) do
"application/x-www-form-urlencoded"
end

@doc """
Expand All @@ -44,12 +74,21 @@ defmodule Klaviyo.Request do
"""
@spec put_header(t, String.t(), String.t()) :: t
def put_header(request, key, value) do
headers =
request.headers
|> Enum.into(%{})
|> Map.put(key, value)
|> Enum.into([])
header = {key, value}

headers = request.headers ++ [header]

%{request | headers: headers}
end

@spec put_new_header(t, String.t(), String.t()) :: t
def put_new_header(request, key, value) do
has_header = Enum.any?(request.headers, fn {name, _} -> name == key end)

if has_header do
request
else
put_header(request, key, value)
end
end
end
7 changes: 4 additions & 3 deletions lib/klaviyo/request_operation.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,19 @@ defmodule Klaviyo.RequestOperation do
expected by an endpoint.
"""

alias Klaviyo.{HTTP}
alias Klaviyo.HTTP

@type t ::
%__MODULE__{
body: Enum.t(),
encoding: :json,
encoding: :json | :www_form,
headers: HTTP.headers_t(),
method: HTTP.method_t(),
query: Enum.t(),
path: String.t()
}

defstruct body: [], encoding: :json, method: nil, query: [], path: nil
defstruct body: [], encoding: :json, headers: [], method: nil, query: [], path: nil

@doc """
Builds a URL string.
Expand Down
2 changes: 1 addition & 1 deletion lib/klaviyo/response.ex
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ defmodule Klaviyo.Response do
defp do_decode(response, opts) do
content_type = HTTP.Response.get_header(response, "content-type")

if content_type != nil && content_type =~ "application/vnd.api+json" do
if content_type != nil && Regex.match?(~r/(\/|\+)json/, content_type) do
opts.json_codec.decode!(response.body)
else
response.body
Expand Down
4 changes: 2 additions & 2 deletions test/klaviyo/request_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ defmodule Klaviyo.RequestTest do
opts = %Opts{api_key: api_key, host: host, port: port, revision: revision}

headers = [
{"authorization", "Klaviyo-API-Key #{api_key}"},
{"content-type", "application/json"},
{"revision", revision}
{"revision", revision},
{"authorization", "Klaviyo-API-Key #{api_key}"}
]

url = RequestOperation.to_url(operation, opts)
Expand Down

0 comments on commit 3971260

Please sign in to comment.