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