defmodule UnkeyElixirSdk do
use GenServer
require Logger
alias HTTPoison
alias Jason
@moduledoc """
Documentation for `UnkeyElixirSdk`.
"""
# Client
@doc """
Start the GenServer
Returns {:ok, pid}
## Examples
iex> UnkeyElixirSdk.start_link(%{token: "yourtoken"})
`{:ok, pid}`
iex> UnkeyElixirSdk.start_link(%{token: "yourtoken", base_url: "theunkeybaseurl"})
`{:ok, pid}`
"""
@spec start_link(map) :: {:ok, pid}
def start_link(default) when is_map(default) do
:ets.new(:pid_store, [:set, :public, :named_table])
if map_size(default) === 0 do
handle_error(
"You need to specify at least the token in either the supervisor or via the start_link function i.e. start_link(%{token: 'mytoken'})"
)
end
{:ok, pid} = GenServer.start_link(__MODULE__, default)
# save the PID to a store so the user does not need to alwayd supply it
:ets.insert(:pid_store, {"pid", pid})
{:ok, pid}
end
@doc """
Creates an API key for your users
Returns a map with the key
`%{"keyId" => "key_cm9vdCBvZiBnb29kXa", "key" => "xyz_AS5HDkXXPot2MMoPHD8jnL"}`
## Examples
iex> UnkeyElixirSdk.create_key(%{"apiId" => "myapiid"})
%{"keyId" => "key_cm9vdCBvZiBnb29kXa", "key" => "xyz_AS5HDkXXPot2MMoPHD8jnL"}
iex> `UnkeyElixirSdk.create_key(%{
"apiId" => "myapiid",
"prefix" => "xyz",
"byteLength" => 16,
"ownerId" => "glamboyosa",
"meta" => %{
"hello" => "world"
},
"expires" => 1_686_941_966_471,
"ratelimit" => %{
"type" => "fast",
"limit" => 10,
"refillRate" => 1,
"refillInterval" => 1000
},
"remaining" => 5
})`
%{"keyId" => "key_cm9vdCBvZiBnb29kXa", "key" => "xyz_AS5HDkXXPot2MMoPHD8jnL"}
"""
@spec create_key(map) :: map()
def create_key(opts) when is_map(opts) do
if(is_nil(Map.get(opts, "apiId"))) do
handle_error("You need to specify at least the apiId in the form %{apiId: 'yourapiId'}")
end
[{_m, pid}] = :ets.lookup(:pid_store, "pid")
GenServer.call(pid, {:create_key, opts}, 6000)
end
@doc """
Verify a key from your users. You only need to send the api key from your user.
Returns a map with whether the key is valid or not. Optionally sends `ownerId` and `meta`
## Examples
iex> UnkeyElixirSdk.verify_key("xyz_AS5HDkXXPot2MMoPHD8jnL")
`%{"valid" => true,
"ownerId" => "chronark",
"meta" => %{
"hello" => "world"
}}`
"""
@spec verify_key(binary) :: map()
def verify_key(key) when is_binary(key) do
[{_m, pid}] = :ets.lookup(:pid_store, "pid")
GenServer.call(pid, {:verify_key, key}, :infinity)
end
@doc """
Delete an api key for your users
Returns :ok
## Examples
iex> UnkeyElixirSdk.revoke_key("key_cm9vdCBvZiBnb29kXa")
:ok
"""
@spec revoke_key(binary) :: :ok
def revoke_key(key) when is_binary(key) do
[{_m, pid}] = :ets.lookup(:pid_store, "pid")
GenServer.call(pid, {:revoke_key, key}, :infinity)
end
@doc """
Updates the configuration of a key
Takes in a `key_id` argument and a map whose members are optional
but must have at most 1 member present.
```
%{
"name" => "my_new_key",
"ownerId" => "still_glamboyosa",
"meta" => %{
"hello" => "world"
},
"expires" => 1_686_941_966_471,
"ratelimit" => %{
"type" => "fast",
"limit" => 15,
"refillRate" => 2,
"refillInterval" => 500
},
"remaining" => 3
}
```
Returns :ok
## Examples
```
iex> UnkeyElixirSdk.update_key("key_cm9vdCBvZiBnb29kXa", %{
"name" => "my_new_key",
"ownerId" => "still_glamboyosa",
"meta" => %{
"hello" => "world"
},
"expires" => 1_686_941_966_471,
"ratelimit" => %{
"type" => "fast",
"limit" => 15,
"refillRate" => 2,
"refillInterval" => 500
},
"remaining" => 3
})
```
:ok
"""
@spec update_key(binary(), map()) :: :ok
def update_key(key_id, opts) when is_map(opts) and is_binary(key_id) do
[{_m, pid}] = :ets.lookup(:pid_store, "pid")
GenServer.call(pid, {:update_key, key_id, opts}, :infinity)
end
# Server (callbacks)
@impl true
def init(elements) do
case Map.get(elements, :base_url) do
nil ->
base_url = "https://api.unkey.dev/v1/keys"
elements = Map.put(elements, :base_url, base_url)
{:ok, elements}
_ ->
base_url = "https://api.unkey.dev/v1/keys"
elements = Map.put_new(elements, :base_url, base_url)
{:ok, elements}
end
end
@impl true
def handle_call({:create_key, opts}, _from, state) do
body = opts |> Jason.encode!()
case HTTPoison.post(state.base_url, body, headers(state.token)) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
{:reply, Map.new(Jason.decode!(body)), state}
{:ok, %HTTPoison.Response{status_code: 404}} ->
handle_error("Not found :(")
{:ok, %HTTPoison.Response{status_code: 401}} ->
handle_error("Unauthorised")
{:error, %HTTPoison.Error{reason: reason}} ->
IO.inspect(reason)
handle_error(to_string(reason))
{:ok, %HTTPoison.Response{body: body}} ->
handle_error(to_string(body))
_ ->
handle_error(to_string("Something went wrong"))
end
end
@impl true
def handle_call({:revoke_key, key_id}, _from, state) do
case HTTPoison.delete("#{state.base_url}/#{key_id}", headers(state.token)) do
{:ok, %HTTPoison.Response{status_code: 200}} ->
{:reply, :ok, state}
{:ok, %HTTPoison.Response{status_code: 404}} ->
handle_error("Not found :(")
{:ok, %HTTPoison.Response{status_code: 401}} ->
handle_error("Unauthorised")
{:error, %HTTPoison.Error{reason: reason}} ->
IO.inspect(reason)
handle_error(to_string(reason))
{:ok, %HTTPoison.Response{body: body}} ->
handle_error(to_string(body))
_ ->
handle_error(to_string("Something went wrong"))
end
end
@impl true
def handle_call({:update_key, key_id, opts}, _from, state) do
body =
%{
"name" => :undefined,
"ownerId" => :undefined,
"meta" => :undefined,
"expires" => :undefined,
"ratelimit" => :undefined,
"remaining" => :undefined
}
|> Map.merge(opts)
|> Map.filter(&(elem(&1, 1) !== :undefined))
|> Jason.encode!()
case HTTPoison.put("#{state.base_url}/#{key_id}", body, headers(state.token)) do
{:ok, %HTTPoison.Response{status_code: 200}} ->
{:reply, :ok, state}
{:ok, %HTTPoison.Response{status_code: 404}} ->
handle_error("Not found :(")
{:ok, %HTTPoison.Response{status_code: 401}} ->
handle_error("Unauthorised")
{:error, %HTTPoison.Error{reason: reason}} ->
IO.inspect(reason)
handle_error(to_string(reason))
{:ok, %HTTPoison.Response{body: body}} ->
handle_error(to_string(body))
_ ->
handle_error(to_string("Something went wrong"))
end
end
@impl true
def handle_call({:verify_key, key}, _from, state) do
body =
%{"key" => key}
|> Jason.encode!()
case HTTPoison.post("#{state.base_url}/verify", body, headers(state.token)) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
{:reply, Jason.decode!(body), state}
{:ok, %HTTPoison.Response{status_code: 404}} ->
handle_error("Not found :(")
{:ok, %HTTPoison.Response{status_code: 401}} ->
handle_error("Unauthorised")
{:error, %HTTPoison.Error{reason: reason}} ->
IO.inspect(reason)
handle_error(to_string(reason))
{:ok, %HTTPoison.Response{body: body}} ->
handle_error(to_string(body))
_ ->
handle_error(to_string("Something went wrong"))
end
end
defp handle_error(error_message) when is_binary(error_message) do
try do
throw(error_message)
catch
err ->
log_error("Error Message #{err}")
end
end
defp log_error(input) when is_binary(input) do
IO.puts(input)
end
defp headers(token) do
[{"Authorization", "Bearer #{token}"}, {"Content-Type", "application/json; charset=UTF-8"}]
end
end