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. Optionally, second param is a map with the key `apiId` which sends the apiId 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" }}` iex> UnkeyElixirSdk.verify_key("xyz_AS5HDkXXPot2MMoPHD8jnL", %{"apiId"} => "api_sASDSsgeegd") `%{"valid" => true, "ownerId" => "chronark", "meta" => %{ "hello" => "world" }}` """ @spec verify_key(binary, map()) :: map() def verify_key(key, opts \\ %{}) when is_binary(key) do [{_m, pid}] = :ets.lookup(:pid_store, "pid") GenServer.call(pid, {:verify_key, key, opts}, :infinity) end @doc """ Delete an api key for your users Returns :ok ## Examples iex> UnkeyElixirSdk.delete_key("key_cm9vdCBvZiBnb29kXa") :ok """ @spec delete_key(binary) :: :ok def delete_key(key) when is_binary(key) do [{_m, pid}] = :ets.lookup(:pid_store, "pid") GenServer.call(pid, {:delete_key, key}, :infinity) end @doc """ Updates the `remaining` value for a specified key. Takes in a map of the shape: %{ "keyId": "key_123", "op": "increment", "value": 1 } Where "op" is "increment" | "decrement" | "set" and value is the value you want to increase by or nil (unlimited) Returns a map with the updated "remaining" value. ## Examples iex> UnkeyElixirSdk.update_remaining(%{ "keyId": "key_123", "op": "increment", "value": 1 }) %{remaining: 100} """ @spec update_remaining(map()) :: :ok def update_remaining(opts) when is_map(opts) do [{_m, pid}] = :ets.lookup(:pid_store, "pid") GenServer.call(pid, {:update_remaining, opts}, :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}createKey", 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({:update_remaining, opts}, _from, state) do validate_params(opts) body = opts |> Jason.encode!() case HTTPoison.post("#{state.base_url}updateRemaining", 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({:delete_key, key_id}, _from, state) do body = %{"keyId" => key_id} |> Jason.encode!() case HTTPoison.post("#{state.base_url}deleteKey", 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({:update_key, key_id, opts}, _from, state) do body = %{ "keyId" => key_id, "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.post("#{state.base_url}updateKey", 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, opts}, _from, state) do body = %{"key" => key} |> Map.merge(opts) |> Jason.encode!() case HTTPoison.post("#{state.base_url}verifyKey", 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 defp validate_params(params) do # Runtime checks to ensure op is valid and value is a number or nil valid_ops = ~w(increment decrement set)a op = String.to_atom(params["op"]) if !Enum.member?(valid_ops, op) do raise ArgumentError, "Invalid operation '#{op}', expected one of: #{inspect(valid_ops)}" end value = params["value"] unless is_nil(value) || is_integer(value) do raise ArgumentError, "Invalid value '#{value}', expected an integer or nil" end end end