diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/account/api_key_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/account/api_key_controller.ex
new file mode 100644
index 0000000000..8f49af2d2f
--- /dev/null
+++ b/apps/block_scout_web/lib/block_scout_web/controllers/account/api_key_controller.ex
@@ -0,0 +1,68 @@
+defmodule BlockScoutWeb.Account.ApiKeyController do
+ use BlockScoutWeb, :controller
+
+ alias Explorer.Account.Api.Key, as: ApiKey
+
+ import BlockScoutWeb.Account.AuthController, only: [authenticate!: 1]
+
+ def new(conn, _params) do
+ authenticate!(conn)
+
+ render(conn, "form.html", method: :create, api_key: empty_api_key())
+ end
+
+ def create(conn, %{"key" => api_key}) do
+ current_user = authenticate!(conn)
+
+ case ApiKey.create_api_key_changeset_and_insert(%ApiKey{}, %{name: api_key["name"], identity_id: current_user.id}) do
+ {:ok, _} ->
+ redirect(conn, to: api_key_path(conn, :index))
+
+ {:error, invalid_api_key} ->
+ render(conn, "form.html", method: :create, api_key: invalid_api_key)
+ end
+ end
+
+ def create(conn, _) do
+ redirect(conn, to: api_key_path(conn, :index))
+ end
+
+ def index(conn, _params) do
+ current_user = authenticate!(conn)
+
+ render(conn, "index.html", api_keys: ApiKey.get_api_keys_by_user_id(current_user.id))
+ end
+
+ def edit(conn, %{"id" => api_key}) do
+ current_user = authenticate!(conn)
+
+ case ApiKey.api_key_by_api_key_and_identity_id(api_key, current_user.id) do
+ nil ->
+ conn
+ |> put_status(:not_found)
+ |> put_view(BlockScoutWeb.PageNotFoundView)
+ |> render("index.html", token: nil)
+
+ %ApiKey{} = api_key ->
+ render(conn, "form.html", method: :update, api_key: ApiKey.changeset(api_key))
+ end
+ end
+
+ def update(conn, %{"id" => api_key, "key" => %{"api_key" => api_key, "name" => name}}) do
+ current_user = authenticate!(conn)
+
+ ApiKey.update_name_api_key(name, current_user.id, api_key)
+
+ redirect(conn, to: api_key_path(conn, :index))
+ end
+
+ def delete(conn, %{"id" => api_key}) do
+ current_user = authenticate!(conn)
+
+ ApiKey.delete_api_key(current_user.id, api_key)
+
+ redirect(conn, to: api_key_path(conn, :index))
+ end
+
+ defp empty_api_key, do: ApiKey.changeset()
+end
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/account/api_key/form.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/account/api_key/form.html.eex
new file mode 100644
index 0000000000..f34f47cbc6
--- /dev/null
+++ b/apps/block_scout_web/lib/block_scout_web/templates/account/api_key/form.html.eex
@@ -0,0 +1,34 @@
+
+
+ <%= render BlockScoutWeb.Account.CommonView, "_nav.html", conn: @conn %>
+
+
+
+
+
+ <% path = if @method == :update, do: api_key_path(@conn, @method, @api_key.data.api_key), else: api_key_path(@conn, @method) %>
+ <%= form_for @api_key, path, fn f -> %>
+ <%= if f.data.api_key do %>
+
+ <%= label f, :api_key, gettext("API key"), class: "control-label", style: "font-size: 14px" %>
+ <%= text_input f, :api_key, class: "form-control", placeholder: gettext("API key"), readonly: true %>
+ <%= error_tag f, :api_key %>
+
+ <% end %>
+
+ <%= label f, :name, gettext("Name"), class: "control-label", style: "font-size: 14px" %>
+ <%= text_input f, :name, class: "form-control", placeholder: gettext("Name this API key") %>
+ <%= error_tag f, :name %>
+
+
+
+ <% end %>
+
+
+
+
+
+
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/account/api_key/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/account/api_key/index.html.eex
new file mode 100644
index 0000000000..e31705f847
--- /dev/null
+++ b/apps/block_scout_web/lib/block_scout_web/templates/account/api_key/index.html.eex
@@ -0,0 +1,44 @@
+
+
+ <%= render BlockScoutWeb.Account.CommonView, "_nav.html", conn: @conn %>
+
+
+
+
+
+
+
+ <%= if @api_keys == [] do %>
+
+
+ <%= gettext "You don't have API keys yet" %>
+
+
+
+ <% else %>
+
+
+
+ <%= gettext "Name" %> |
+ <%= gettext "API key" %> |
+ |
+ |
+
+
+
+ <%= Enum.map(@api_keys, fn key ->
+ render("row.html", api_key: key, conn: @conn)
+ end) %>
+
+
+ <% end %>
+
+
+ <%= if Enum.count(@api_keys) < 3 do %>
+
<%= gettext "Add API key" %>
+ <% end %>
+
+
+
+
+
\ No newline at end of file
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/account/api_key/row.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/account/api_key/row.html.eex
new file mode 100644
index 0000000000..3481209f09
--- /dev/null
+++ b/apps/block_scout_web/lib/block_scout_web/templates/account/api_key/row.html.eex
@@ -0,0 +1,14 @@
+
+ <%= @api_key.name %> |
+
+ <%= @api_key.api_key %>
+ <%= render BlockScoutWeb.CommonComponentsView, "_btn_copy.html",
+ additional_classes: ["btn-copy-icon-small", "btn-copy-icon-custom", "btn-copy-icon-no-borders"], clipboard_text: @api_key.api_key, aria_label: gettext("Copy API key"), title: gettext("Copy API key"), style: "display: inline-block; vertical-align: text-bottom; position: initial; margin-top: 1px;" %>
+ |
+
+ <%= link gettext("Remove"), to: api_key_path(@conn, :delete, @api_key.api_key), method: :delete %>
+ |
+
+ <%= link gettext("Edit"), to: api_key_path(@conn, :edit, @api_key.api_key) %>
+ |
+
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/account/common/_nav.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/account/common/_nav.html.eex
index d632c0b169..0b4e839f9b 100644
--- a/apps/block_scout_web/lib/block_scout_web/templates/account/common/_nav.html.eex
+++ b/apps/block_scout_web/lib/block_scout_web/templates/account/common/_nav.html.eex
@@ -12,5 +12,8 @@
Transaction Tags
+
+ <%= gettext "API keys" %>
+
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/layout/_topnav.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/layout/_topnav.html.eex
index 8709d502ec..26de812222 100644
--- a/apps/block_scout_web/lib/block_scout_web/templates/layout/_topnav.html.eex
+++ b/apps/block_scout_web/lib/block_scout_web/templates/layout/_topnav.html.eex
@@ -187,6 +187,7 @@
Watch list
Address Tags
Transaction Tags
+ <%= gettext "API keys" %>
Sign out
diff --git a/apps/block_scout_web/lib/block_scout_web/views/access_helpers.ex b/apps/block_scout_web/lib/block_scout_web/views/access_helpers.ex
index c89ac10435..c3cbaeedef 100644
--- a/apps/block_scout_web/lib/block_scout_web/views/access_helpers.ex
+++ b/apps/block_scout_web/lib/block_scout_web/views/access_helpers.ex
@@ -8,6 +8,7 @@ defmodule BlockScoutWeb.AccessHelpers do
alias BlockScoutWeb.API.APILogger
alias BlockScoutWeb.API.RPC.RPCView
alias BlockScoutWeb.WebRouter.Helpers
+ alias Explorer.Account.Api.Key, as: ApiKey
alias Plug.Conn
alias RemoteIp
@@ -81,11 +82,18 @@ defmodule BlockScoutWeb.AccessHelpers do
ip = remote_ip_from_headers || remote_ip
ip_string = to_string(:inet_parse.ntoa(ip))
+ plan = get_plan(conn.query_params)
+
cond do
conn.query_params && Map.has_key?(conn.query_params, "apikey") &&
Map.get(conn.query_params, "apikey") == static_api_key ->
rate_limit_by_key(static_api_key, api_rate_limit_by_key)
+ conn.query_params && Map.has_key?(conn.query_params, "apikey") && !is_nil(plan) ->
+ conn.query_params
+ |> Map.get("apikey")
+ |> rate_limit_by_key(plan.max_req_per_second)
+
Enum.member?(api_rate_limit_whitelisted_ips(), ip_string) ->
rate_limit_by_ip(ip_string, api_rate_limit_by_ip)
@@ -95,6 +103,18 @@ defmodule BlockScoutWeb.AccessHelpers do
end
end
+ defp get_plan(query_params) do
+ with true <- query_params && Map.has_key?(query_params, "apikey"),
+ {:ok, casted_api_key} <- ApiKey.cast_api_key(Map.get(query_params, "apikey")),
+ api_key <- ApiKey.api_key_with_plan_by_api_key(casted_api_key),
+ true <- !is_nil(api_key) do
+ api_key.identity.plan
+ else
+ _ ->
+ nil
+ end
+ end
+
defp rate_limit_by_key(api_key, api_rate_limit_by_key) do
case Hammer.check_rate("api-#{api_key}", 1_000, api_rate_limit_by_key) do
{:allow, _count} ->
diff --git a/apps/block_scout_web/lib/block_scout_web/views/account/api_key_view.ex b/apps/block_scout_web/lib/block_scout_web/views/account/api_key_view.ex
new file mode 100644
index 0000000000..14b112e96d
--- /dev/null
+++ b/apps/block_scout_web/lib/block_scout_web/views/account/api_key_view.ex
@@ -0,0 +1,3 @@
+defmodule BlockScoutWeb.Account.ApiKeyView do
+ use BlockScoutWeb, :view
+end
diff --git a/apps/block_scout_web/lib/block_scout_web/web_router.ex b/apps/block_scout_web/lib/block_scout_web/web_router.ex
index c1db477448..9b3c1e23cb 100644
--- a/apps/block_scout_web/lib/block_scout_web/web_router.ex
+++ b/apps/block_scout_web/lib/block_scout_web/web_router.ex
@@ -50,6 +50,11 @@ defmodule BlockScoutWeb.WebRouter do
only: [:new, :create, :edit, :update, :delete],
as: :watchlist_address
)
+
+ resources("/api_key", Account.ApiKeyController,
+ only: [:new, :create, :edit, :update, :delete, :index],
+ as: :api_key
+ )
end
# Disallows Iframes (write routes)
diff --git a/apps/block_scout_web/priv/gettext/default.pot b/apps/block_scout_web/priv/gettext/default.pot
index ca936a1129..d3f83464aa 100644
--- a/apps/block_scout_web/priv/gettext/default.pot
+++ b/apps/block_scout_web/priv/gettext/default.pot
@@ -141,6 +141,11 @@ msgstr ""
msgid "Add"
msgstr ""
+#: lib/block_scout_web/templates/account/api_key/index.html.eex:38
+#, elixir-autogen, elixir-format
+msgid "Add API key"
+msgstr ""
+
#: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:16
#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:20
#: lib/block_scout_web/views/address_view.ex:104
@@ -583,6 +588,12 @@ msgstr ""
msgid "Copy ABI"
msgstr ""
+#: lib/block_scout_web/templates/account/api_key/row.html.eex:6
+#: lib/block_scout_web/templates/account/api_key/row.html.eex:6
+#, elixir-autogen, elixir-format
+msgid "Copy API key"
+msgstr ""
+
#: lib/block_scout_web/templates/account/tag_address/row.html.eex:7
#: lib/block_scout_web/templates/account/tag_address/row.html.eex:7
#: lib/block_scout_web/templates/account/tag_transaction/row.html.eex:10
@@ -889,6 +900,11 @@ msgstr ""
msgid "Easy Cowboy! This block does not exist yet!"
msgstr ""
+#: lib/block_scout_web/templates/account/api_key/row.html.eex:12
+#, elixir-autogen, elixir-format
+msgid "Edit"
+msgstr ""
+
#: lib/block_scout_web/templates/transaction/_emission_reward_tile.html.eex:5
#, elixir-autogen, elixir-format
msgid "Emission Contract"
@@ -1447,6 +1463,8 @@ msgstr ""
msgid "N/A bytes"
msgstr ""
+#: lib/block_scout_web/templates/account/api_key/form.html.eex:19
+#: lib/block_scout_web/templates/account/api_key/index.html.eex:22
#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:52
#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:59
#: lib/block_scout_web/templates/log/_data_decoded_view.html.eex:4
@@ -1455,6 +1473,11 @@ msgstr ""
msgid "Name"
msgstr ""
+#: lib/block_scout_web/templates/account/api_key/form.html.eex:20
+#, elixir-autogen, elixir-format
+msgid "Name this API key"
+msgstr ""
+
#: lib/block_scout_web/templates/address_token/overview.html.eex:44
#, elixir-autogen, elixir-format
msgid "Net Worth"
@@ -1707,6 +1730,11 @@ msgstr ""
msgid "Records"
msgstr ""
+#: lib/block_scout_web/templates/account/api_key/row.html.eex:9
+#, elixir-autogen, elixir-format
+msgid "Remove"
+msgstr ""
+
#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:155
#, elixir-autogen, elixir-format
msgid "Request URL"
@@ -1755,6 +1783,11 @@ msgstr ""
msgid "Run"
msgstr ""
+#: lib/block_scout_web/templates/account/api_key/form.html.eex:26
+#, elixir-autogen, elixir-format
+msgid "Save"
+msgstr ""
+
#: lib/block_scout_web/templates/address_logs/index.html.eex:16
#: lib/block_scout_web/templates/layout/_search.html.eex:34
#, elixir-autogen, elixir-format
@@ -2675,6 +2708,11 @@ msgstr ""
msgid "Yes"
msgstr ""
+#: lib/block_scout_web/templates/account/api_key/index.html.eex:14
+#, elixir-autogen, elixir-format
+msgid "You don't have API keys yet"
+msgstr ""
+
#: lib/block_scout_web/templates/address/overview.html.eex:111
#, elixir-autogen, elixir-format
msgid "at"
diff --git a/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po b/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
index c979e80a7a..53d903f8e0 100644
--- a/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
+++ b/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
@@ -141,6 +141,11 @@ msgstr ""
msgid "Add"
msgstr ""
+#: lib/block_scout_web/templates/account/api_key/index.html.eex:38
+#, elixir-autogen, elixir-format
+msgid "Add API key"
+msgstr ""
+
#: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:16
#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:20
#: lib/block_scout_web/views/address_view.ex:104
@@ -583,6 +588,12 @@ msgstr ""
msgid "Copy ABI"
msgstr ""
+#: lib/block_scout_web/templates/account/api_key/row.html.eex:6
+#: lib/block_scout_web/templates/account/api_key/row.html.eex:6
+#, elixir-autogen, elixir-format
+msgid "Copy API key"
+msgstr ""
+
#: lib/block_scout_web/templates/account/tag_address/row.html.eex:7
#: lib/block_scout_web/templates/account/tag_address/row.html.eex:7
#: lib/block_scout_web/templates/account/tag_transaction/row.html.eex:10
@@ -889,6 +900,11 @@ msgstr ""
msgid "Easy Cowboy! This block does not exist yet!"
msgstr ""
+#: lib/block_scout_web/templates/account/api_key/row.html.eex:12
+#, elixir-autogen, elixir-format
+msgid "Edit"
+msgstr ""
+
#: lib/block_scout_web/templates/transaction/_emission_reward_tile.html.eex:5
#, elixir-autogen, elixir-format
msgid "Emission Contract"
@@ -1447,6 +1463,8 @@ msgstr ""
msgid "N/A bytes"
msgstr ""
+#: lib/block_scout_web/templates/account/api_key/form.html.eex:19
+#: lib/block_scout_web/templates/account/api_key/index.html.eex:22
#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:52
#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:59
#: lib/block_scout_web/templates/log/_data_decoded_view.html.eex:4
@@ -1455,6 +1473,11 @@ msgstr ""
msgid "Name"
msgstr ""
+#: lib/block_scout_web/templates/account/api_key/form.html.eex:20
+#, elixir-autogen, elixir-format
+msgid "Name this API key"
+msgstr ""
+
#: lib/block_scout_web/templates/address_token/overview.html.eex:44
#, elixir-autogen, elixir-format
msgid "Net Worth"
@@ -1707,6 +1730,11 @@ msgstr ""
msgid "Records"
msgstr ""
+#: lib/block_scout_web/templates/account/api_key/row.html.eex:9
+#, elixir-autogen, elixir-format
+msgid "Remove"
+msgstr ""
+
#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:155
#, elixir-autogen, elixir-format
msgid "Request URL"
@@ -1755,6 +1783,11 @@ msgstr ""
msgid "Run"
msgstr ""
+#: lib/block_scout_web/templates/account/api_key/form.html.eex:26
+#, elixir-autogen, elixir-format
+msgid "Save"
+msgstr ""
+
#: lib/block_scout_web/templates/address_logs/index.html.eex:16
#: lib/block_scout_web/templates/layout/_search.html.eex:34
#, elixir-autogen, elixir-format
@@ -2675,6 +2708,11 @@ msgstr ""
msgid "Yes"
msgstr ""
+#: lib/block_scout_web/templates/account/api_key/index.html.eex:14
+#, elixir-autogen, elixir-format
+msgid "You don't have API keys yet"
+msgstr ""
+
#: lib/block_scout_web/templates/address/overview.html.eex:111
#, elixir-autogen, elixir-format
msgid "at"
diff --git a/apps/explorer/lib/explorer/accounts/api/key.ex b/apps/explorer/lib/explorer/accounts/api/key.ex
new file mode 100644
index 0000000000..fdb789aa8d
--- /dev/null
+++ b/apps/explorer/lib/explorer/accounts/api/key.ex
@@ -0,0 +1,113 @@
+defmodule Explorer.Account.Api.Key do
+ @moduledoc """
+ Module is responsible for schema for API keys, keys is used to track number of requests to the API endpoints
+ """
+ use Explorer.Schema
+
+ alias Explorer.Accounts.Identity
+ alias Ecto.{Changeset, UUID}
+ alias Explorer.Repo
+
+ import Ecto.Changeset
+
+ @max_key_per_account 3
+
+ @primary_key false
+ schema "account_api_keys" do
+ field(:name, :string)
+ field(:api_key, UUID, primary_key: true)
+ belongs_to(:identity, Identity)
+
+ timestamps()
+ end
+
+ @attrs ~w(api_key name identity_id)a
+
+ def changeset do
+ %__MODULE__{}
+ |> cast(%{}, @attrs)
+ end
+
+ def changeset(%__MODULE__{} = api_key, attrs \\ %{}) do
+ api_key
+ |> cast(attrs, @attrs)
+ |> validate_required(@attrs)
+ end
+
+ def create_api_key_changeset_and_insert(%__MODULE__{} = api_key \\ %__MODULE__{}, attrs \\ %{}) do
+ api_key
+ |> cast(attrs, @attrs)
+ |> put_change(:api_key, generate_api_key())
+ |> validate_required(@attrs)
+ |> unique_constraint(:api_key)
+ |> api_key_count_constraint()
+ |> Repo.insert()
+ end
+
+ def api_key_count_constraint(%Changeset{changes: %{identity_id: identity_id}} = api_key) do
+ if identity_id
+ |> api_keys_by_identity_id_query()
+ |> limit(@max_key_per_account)
+ |> Repo.aggregate(:count, :api_key) == @max_key_per_account do
+ api_key
+ |> add_error(:name, "Max #{@max_key_per_account} keys per account")
+ else
+ api_key
+ end
+ end
+
+ def generate_api_key do
+ UUID.generate()
+ end
+
+ def api_keys_by_identity_id_query(id) do
+ __MODULE__
+ |> where([api_key], api_key.identity_id == ^id)
+ end
+
+ def api_key_by_api_key_and_identity_id_query(api_key, identity_id) do
+ __MODULE__
+ |> where([api_key], api_key.identity_id == ^identity_id and api_key.api_key == ^api_key)
+ end
+
+ def api_key_by_api_key_and_identity_id(api_key, identity_id) do
+ api_key
+ |> api_key_by_api_key_and_identity_id_query(identity_id)
+ |> Repo.one()
+ end
+
+ def update_name_api_key(new_name, identity_id, api_key) do
+ api_key
+ |> api_key_by_api_key_and_identity_id_query(identity_id)
+ |> update([key], set: [name: ^new_name, updated_at: fragment("NOW()")])
+ |> Repo.update_all([])
+ end
+
+ def delete_api_key(identity_id, api_key) do
+ api_key
+ |> api_key_by_api_key_and_identity_id_query(identity_id)
+ |> Repo.delete_all()
+ end
+
+ def to_string(api_key_value) do
+ api_key_value
+ |> Base.encode64()
+ end
+
+ def get_api_keys_by_user_id(id) do
+ id
+ |> api_keys_by_identity_id_query()
+ |> Repo.all()
+ end
+
+ def api_key_with_plan_by_api_key(api_key) do
+ __MODULE__
+ |> where([api_key], api_key.api_key == ^api_key)
+ |> Repo.one()
+ |> Repo.preload(identity: :plan)
+ end
+
+ def cast_api_key(api_key) do
+ UUID.cast(api_key)
+ end
+end
diff --git a/apps/explorer/lib/explorer/accounts/api/plan.ex b/apps/explorer/lib/explorer/accounts/api/plan.ex
new file mode 100644
index 0000000000..b89b33bc9c
--- /dev/null
+++ b/apps/explorer/lib/explorer/accounts/api/plan.ex
@@ -0,0 +1,13 @@
+defmodule Explorer.Account.Api.Plan do
+ @moduledoc """
+ Module is responsible for schema for API plans, each plan contains its name and maximum number of requests per second
+ """
+ use Explorer.Schema
+
+ schema "account_api_plans" do
+ field(:name, :string)
+ field(:max_req_per_second, :integer)
+
+ timestamps()
+ end
+end
diff --git a/apps/explorer/lib/explorer/accounts/identity.ex b/apps/explorer/lib/explorer/accounts/identity.ex
index 202d159607..cb1c69886a 100644
--- a/apps/explorer/lib/explorer/accounts/identity.ex
+++ b/apps/explorer/lib/explorer/accounts/identity.ex
@@ -5,6 +5,7 @@ defmodule Explorer.Accounts.Identity do
use Ecto.Schema
import Ecto.Changeset
+ alias Explorer.Account.Api.Plan
alias Explorer.Accounts.{TagAddress, Watchlist}
schema "account_identities" do
@@ -13,6 +14,7 @@ defmodule Explorer.Accounts.Identity do
field(:name, :string)
has_many(:tag_addresses, TagAddress)
has_many(:watchlists, Watchlist)
+ belongs_to(:plan, Plan)
timestamps()
end
diff --git a/apps/explorer/priv/repo/migrations/20220407134152_add_api_keys_and_plans_tables.exs b/apps/explorer/priv/repo/migrations/20220407134152_add_api_keys_and_plans_tables.exs
new file mode 100644
index 0000000000..94362b20a0
--- /dev/null
+++ b/apps/explorer/priv/repo/migrations/20220407134152_add_api_keys_and_plans_tables.exs
@@ -0,0 +1,34 @@
+defmodule Explorer.Repo.Migrations.AddApiKeysAndPlansTables do
+ use Ecto.Migration
+
+ def change do
+ create table(:account_api_plans, primary_key: false) do
+ add(:id, :serial, null: false, primary_key: true)
+ add(:max_req_per_second, :smallint)
+ add(:name, :string)
+
+ timestamps()
+ end
+
+ create(unique_index(:account_api_plans, [:id]))
+
+ execute(
+ "INSERT INTO account_api_plans (id, max_req_per_second, name, inserted_at, updated_at) VALUES (1, 10, 'Free Plan', NOW(), NOW());"
+ )
+
+ create table(:account_api_keys, primary_key: false) do
+ add(:identity_id, references(:account_identities, column: :id, on_delete: :delete_all), null: false)
+ add(:name, :string)
+ add(:api_key, :uuid, null: false, primary_key: true)
+
+ timestamps()
+ end
+
+ alter table(:account_identities) do
+ add(:plan_id, references(:account_api_plans, column: :id), default: 1)
+ end
+
+ create(unique_index(:account_api_keys, [:api_key]))
+ create(index(:account_api_keys, [:identity_id]))
+ end
+end