parent
96d3bbc63b
commit
464f260f13
@ -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 |
@ -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.api_key), else: api_key_path(@conn, @method) %> |
||||
<%= form_for @api_key, path, fn f -> %> |
||||
<%= if f.data.api_key do %> |
||||
<div class="form-group"> |
||||
<%= 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 %> |
||||
</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> |
@ -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> |
@ -0,0 +1,14 @@ |
||||
<tr> |
||||
<td><%= @api_key.name %></td> |
||||
<td> |
||||
<span><%= @api_key.api_key %></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.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;" %> |
||||
</td> |
||||
<td> |
||||
<%= link gettext("Remove"), to: api_key_path(@conn, :delete, @api_key.api_key), method: :delete %> |
||||
</td> |
||||
<td> |
||||
<%= link gettext("Edit"), to: api_key_path(@conn, :edit, @api_key.api_key) %> |
||||
</td> |
||||
</tr> |
@ -0,0 +1,3 @@ |
||||
defmodule BlockScoutWeb.Account.ApiKeyView do |
||||
use BlockScoutWeb, :view |
||||
end |
@ -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 |
@ -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 |
@ -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 |
Loading…
Reference in new issue