Skip to content

Commit

Permalink
Merge pull request #30 from blockscout/np-api-keys-2
Browse files Browse the repository at this point in the history
API keys management
  • Loading branch information
nikitosing committed Apr 13, 2022
2 parents 5f976e4 + 3299420 commit 2c07d16
Show file tree
Hide file tree
Showing 15 changed files with 521 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -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_identity_id(current_user.id))
end

def edit(conn, %{"id" => api_key}) do
current_user = authenticate!(conn)

case ApiKey.api_key_by_value_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" => %{"value" => api_key, "name" => name}}) do
current_user = authenticate!(conn)

ApiKey.update_api_key_name(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
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<section class="container">
<div class="row">
<%= render BlockScoutWeb.Account.CommonView, "_nav.html", conn: @conn %>
<div class="col-sm-10">
<div class="card">
<div class="card-body">
<h1 class="card-title list-title-description header-account"><%=if @method == :update, do: gettext("Update"), else: gettext("Add") %> <%= gettext "API key"%></h1>
<div class="col-sm-10 card-body-account">
<% path = if @method == :update, do: api_key_path(@conn, @method, @api_key.data.value), else: api_key_path(@conn, @method) %>
<%= form_for @api_key, path, fn f -> %>
<%= if f.data.value do %>
<div class="form-group">
<%= label f, :value, gettext("API key"), class: "control-label", style: "font-size: 14px" %>
<%= text_input f, :value, class: "form-control", placeholder: gettext("API key"), readonly: true %>
<%= error_tag f, :value %>
</div>
<% end %>
<div class="form-group">
<%= 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 %>
</div>
<br>
<div class="form-group float-right form-input">
<a class="btn btn-line" href="<%= api_key_path(@conn, :index) %>"><%= gettext "Back to API keys (Cancel)"%></a>
<%= submit gettext("Save"), class: "button button-primary button-sm ml-3" %>
</div>
<% end %>
</div>
</div>
</div>
</div>
</div>
</section>
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<section class="container">
<div class="row">
<%= render BlockScoutWeb.Account.CommonView, "_nav.html", conn: @conn %>
<div class="col-md">
<div class="card">
<div class="card-body">
<h1 class="card-title list-title-description header-account"><%= gettext "Api keys" %> </h1>
<br>
<div class="col-sm">
<div class="mb-3 row o-flow-x">
<%= if @api_keys == [] do %>
<div style="min-width: 100%;">
<div class="tile tile-muted text-center" data-selector="empty-coin-balances-list">
<%= gettext "You don't have API keys yet" %>
</div>
</div>
<h2></h2>
<% else %>
<table class="table mb-3 table-watchlist">
<thead style="font-size: 14px; color: #6c757d" >
<tr>
<th scope="col"><%= gettext "Name" %></th>
<th scope="col"><%= gettext "API key" %></th>
<th scope="col"></th>
<th scope="col"></th>
</tr>
</thead>
<tbody style="font-size: 15px; color: #6c757d" >
<%= Enum.map(@api_keys, fn key ->
render("row.html", api_key: key, conn: @conn)
end) %>
</tbody>
</table>
<% end %>
</div>
</div>
<%= if Enum.count(@api_keys) < 3 do %>
<a class="button button-primary button-sm" href="<%= api_key_path(@conn, :new) %>"><%= gettext "Add API key" %></a>
<% end %>
</div>
</div>
</div>
</div>
</section>
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<tr>
<td><%= @api_key.name %></td>
<td>
<span><%= @api_key.value %></span>
<%= 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.value, aria_label: gettext("Copy API key"), title: gettext("Copy API key"), style: "display: inline-block; vertical-align: text-bottom; position: initial; margin-top: 1px;" %>
</td>
<td>
<%= link gettext("Remove"), to: api_key_path(@conn, :delete, @api_key.value), method: :delete %>
</td>
<td>
<%= link gettext("Edit"), to: api_key_path(@conn, :edit, @api_key.value) %>
</td>
</tr>
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,8 @@
<li class="nav-item">
<a class="dropdown-item fs-14" href="<%= tag_transaction_path(@conn, :index) %>">Transaction Tags</a>
</li>
<li class="nav-item">
<a class="dropdown-item fs-14" href="<%= api_key_path(@conn, :index) %>"><%= gettext "API keys" %></a>
</li>
</ul>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@
<a href="<%= watchlist_path(@conn, :show) %>" class= "dropdown-item">Watch list</a>
<a href="<%= tag_address_path(@conn, :index) %>" class= "dropdown-item">Address Tags</a>
<a href="<%= tag_transaction_path(@conn, :index) %>" class= "dropdown-item">Transaction Tags</a>
<a href="<%= api_key_path(@conn, :index) %>" class= "dropdown-item"><%= gettext "API keys" %></a>
<a href="<%= BlockScoutWeb.LayoutView.sign_out_link %>" class= "dropdown-item">Sign out</a>
</div>
</li>
Expand Down
31 changes: 29 additions & 2 deletions apps/block_scout_web/lib/block_scout_web/views/access_helpers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -81,11 +82,17 @@ 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 ->
check_api_key(conn) && get_api_key(conn) == static_api_key ->
rate_limit_by_key(static_api_key, api_rate_limit_by_key)

check_api_key(conn) && !is_nil(plan) ->
conn
|> get_api_key()
|> 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)

Expand All @@ -95,6 +102,26 @@ defmodule BlockScoutWeb.AccessHelpers do
end
end

defp check_api_key(conn) do
conn.query_params && Map.has_key?(conn.query_params, "apikey")
end

defp get_api_key(conn) do
Map.get(conn.query_params, "apikey")
end

defp get_plan(query_params) do
with true <- query_params && Map.has_key?(query_params, "apikey"),
api_key_value <- Map.get(query_params, "apikey"),
api_key <- ApiKey.api_key_with_plan_by_value(api_key_value),
false <- 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} ->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
defmodule BlockScoutWeb.Account.ApiKeyView do
use BlockScoutWeb, :view
end
5 changes: 5 additions & 0 deletions apps/block_scout_web/lib/block_scout_web/web_router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
70 changes: 68 additions & 2 deletions apps/block_scout_web/priv/gettext/default.pot
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ msgid "Actual gas amount used by the transaction."
msgstr ""

#, elixir-format
#: lib/block_scout_web/templates/account/api_key/form.html.eex:7
#: lib/block_scout_web/templates/layout/_footer.html.eex:44
msgid "Add"
msgstr ""
Expand Down Expand Up @@ -1647,7 +1648,8 @@ msgid "N/A bytes"
msgstr ""

#, elixir-format
#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:52
#: 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
#: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:21
msgid "Name"
Expand Down Expand Up @@ -2628,7 +2630,7 @@ msgstr ""
#: lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex:16 lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:5
#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:15 lib/block_scout_web/templates/transaction/_tabs.html.eex:4
#: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:7 lib/block_scout_web/views/address_view.ex:357
#: lib/block_scout_web/views/tokens/instance/overview_view.ex:194 lib/block_scout_web/views/tokens/overview_view.ex:41
#: lib/block_scout_web/views/tokens/instance/overview_view.ex:195 lib/block_scout_web/views/tokens/overview_view.ex:41
#: lib/block_scout_web/views/transaction_view.ex:512
msgid "Token Transfers"
msgstr ""
Expand Down Expand Up @@ -3343,3 +3345,67 @@ msgstr ""
#: lib/block_scout_web/templates/transaction/_tile.html.eex:11
msgid "Error in internal transactions"
msgstr ""

#, elixir-format
#: lib/block_scout_web/templates/account/api_key/form.html.eex:7
#: lib/block_scout_web/templates/account/api_key/form.html.eex:13 lib/block_scout_web/templates/account/api_key/form.html.eex:14
#: lib/block_scout_web/templates/account/api_key/index.html.eex:23
msgid "API key"
msgstr ""

#, elixir-format
#: lib/block_scout_web/templates/account/common/_nav.html.eex:16
#: lib/block_scout_web/templates/layout/_topnav.html.eex:219
msgid "API keys"
msgstr ""

#, elixir-format
#: lib/block_scout_web/templates/account/api_key/index.html.eex:38
msgid "Add API key"
msgstr ""

#, elixir-format
#: lib/block_scout_web/templates/account/api_key/index.html.eex:7
msgid "Api keys"
msgstr ""

#, elixir-format
#: lib/block_scout_web/templates/account/api_key/form.html.eex:25
msgid "Back to API keys (Cancel)"
msgstr ""

#, elixir-format
#: lib/block_scout_web/templates/account/api_key/row.html.eex:6
#: lib/block_scout_web/templates/account/api_key/row.html.eex:6
msgid "Copy API key"
msgstr ""

#, elixir-format
#: lib/block_scout_web/templates/account/api_key/row.html.eex:12
msgid "Edit"
msgstr ""

#, elixir-format
#: lib/block_scout_web/templates/account/api_key/form.html.eex:20
msgid "Name this API key"
msgstr ""

#, elixir-format
#: lib/block_scout_web/templates/account/api_key/row.html.eex:9
msgid "Remove"
msgstr ""

#, elixir-format
#: lib/block_scout_web/templates/account/api_key/form.html.eex:26
msgid "Save"
msgstr ""

#, elixir-format
#: lib/block_scout_web/templates/account/api_key/form.html.eex:7
msgid "Update"
msgstr ""

#, elixir-format
#: lib/block_scout_web/templates/account/api_key/index.html.eex:14
msgid "You don't have API keys yet"
msgstr ""

0 comments on commit 2c07d16

Please sign in to comment.