api keys: rework API key UI.

* Add an explanation of what an API key is and how to use it.
* Make it possible for the site owner to view all API keys.
* Remove the requirement to re-enter your password before you can view
  your API key (to be reworked).
* Move the API key controller from maintenance/user/api_keys_controller.rb
  to a top level controller.
This commit is contained in:
evazion
2021-02-14 02:50:03 -06:00
parent ae204df4ca
commit 37061f95a6
18 changed files with 224 additions and 167 deletions

View File

@@ -0,0 +1,26 @@
class ApiKeysController < ApplicationController
respond_to :html, :json, :xml
def create
@api_key = authorize ApiKey.new(user: CurrentUser.user)
@api_key.save
respond_with(@api_key, location: user_api_keys_path(CurrentUser.user.id))
end
def index
params[:search][:user_id] = params[:user_id] if params[:user_id].present?
@api_keys = authorize ApiKey.visible(CurrentUser.user).paginated_search(params, count_pages: true)
respond_with(@api_keys)
end
def show
@api_key = authorize ApiKey.find(params[:id])
respond_with(@api_key)
end
def destroy
@api_key = authorize ApiKey.find(params[:id])
@api_key.destroy
respond_with(@api_key, location: user_api_keys_path(CurrentUser.user.id))
end
end

View File

@@ -1,43 +0,0 @@
module Maintenance
module User
class ApiKeysController < ApplicationController
before_action :check_privilege
before_action :authenticate!, :except => [:show]
rescue_from ::SessionLoader::AuthenticationFailure, :with => :authentication_failed
respond_to :html, :json, :xml
def view
respond_with(CurrentUser.user, @api_key)
end
def update
@api_key.regenerate!
respond_with(CurrentUser.user, @api_key) { |format| format.js }
end
def destroy
@api_key.destroy
respond_with(CurrentUser.user, @api_key, location: CurrentUser.user)
end
protected
def check_privilege
raise ::User::PrivilegeError unless params[:user_id].to_i == CurrentUser.id
end
def authenticate!
if CurrentUser.user.authenticate_password(params[:user][:password])
@api_key = CurrentUser.user.api_key || ApiKey.generate!(CurrentUser.user)
@password = params[:user][:password]
else
raise ::SessionLoader::AuthenticationFailure
end
end
def authentication_failed
redirect_to(user_api_key_path(CurrentUser.user), :notice => "Password was incorrect.")
end
end
end
end

View File

@@ -144,4 +144,8 @@ module IconHelper
def link_icon(**options)
icon_tag("fas fa-link", **options)
end
def plus_icon(**options)
icon_tag("fas fa-plus", **options)
end
end

View File

@@ -55,3 +55,18 @@ footer#page-footer {
}
}
}
/* A container for the main <h1> tag, with optional right-aligned action buttons */
div.page-heading {
display: flex;
margin-bottom: 1em;
h1 {
flex-grow: 1;
line-height: 1em;
}
a {
align-self: center;
}
}

View File

@@ -4,12 +4,17 @@ class ApiKey < ApplicationRecord
validates_uniqueness_of :key
has_secure_token :key
def self.generate!(user)
create(:user_id => user.id)
def self.visible(user)
if user.is_owner?
all
else
where(user: user)
end
end
def regenerate!
regenerate_key
save
def self.search(params)
q = search_attributes(params, :id, :created_at, :updated_at, :key, :user)
q = q.apply_default_order(params)
q
end
end

View File

@@ -0,0 +1,17 @@
class ApiKeyPolicy < ApplicationPolicy
def create?
!user.is_anonymous?
end
def index?
!user.is_anonymous?
end
def destroy?
record.user == user
end
def api_attributes
super - [:key]
end
end

View File

@@ -0,0 +1,5 @@
<% content_for(:secondary_links) do %>
<%= subnav_link_to "Listing", user_api_keys_path(CurrentUser.user.id) %>
<%= subnav_link_to "New", user_api_keys_path(CurrentUser.user.id), method: :post %>
<%= subnav_link_to "Help", wiki_page_path("help:api") %>
<% end %>

View File

@@ -0,0 +1,63 @@
<%= render "secondary_links" %>
<div id="c-api-keys">
<div id="a-index" class="fixed-width-container">
<div class="page-heading">
<h1>API Keys</h1>
<%= link_to user_api_keys_path(CurrentUser.user.id), class: "button-primary", method: :post do %>
<%= plus_icon %> Add
<% end %>
</div>
<% if params[:user_id].present? %>
<div class="prose">
<p>An API key is used to give programs access to your <%= Danbooru.config.canonical_app_name %> account.</p>
<p>If you're a developer, you can use an API key to access the
<%= link_to_wiki "#{Danbooru.config.canonical_app_name} API", "help:api" %>. If you're not a
developer, you probably don't need an API key.</p>
<p><strong>Your API key is like your password</strong>. Anyone who has it has full access to
your account. Don't give your API key to apps or people you don't trust, and don't post your
API key in public locations.</p>
<p>Example usage:
<code>
<% if @api_keys.present? %>
<%= profile_url(format: "json", login: CurrentUser.user.name, api_key: @api_keys.first.key) %>
<% else %>
<%= profile_url(format: "json", login: CurrentUser.user.name, api_key: "your_api_key_goes_here") %>
<% end %>
</code>
</p>
<p>See the <%= link_to_wiki "API documentation", "help:api" %> to learn more.</p>
</div>
<% end %>
<% if params[:user_id].present? && !@api_keys.present? %>
<%= link_to "Create API key", user_api_keys_path(CurrentUser.user.id), method: :post %>
<% else %>
<%= table_for @api_keys, width: "100%", class: "striped autofit" do |t| %>
<% t.column :key, td: { class: "col-expand" } %>
<% if !params[:user_id].present? %>
<% t.column "User" do |api_key| %>
<%= link_to_user api_key.user %>
<% end %>
<% end %>
<% t.column "Created" do |api_key| %>
<%= time_ago_in_words_tagged api_key.created_at %>
<% end %>
<% t.column column: "control" do |api_key| %>
<%= link_to "Delete", api_key, method: :delete %>
<% end %>
<% end %>
<%= numbered_paginator(@api_keys) %>
<% end %>
</div>
</div>

View File

@@ -1,13 +0,0 @@
<% page_title "API Key" %>
<div id="c-maintenance-user-api-keys">
<div id="a-show">
<h1>API Key</h1>
<p>You must re-enter your password to view or change your API key.</p>
<%= edit_form_for CurrentUser.user, url: view_user_api_key_path(CurrentUser.user), method: :post do |f| %>
<%= f.input :password, :as => :password, :input_html => {:autocomplete => "off"} %>
<%= f.button :submit, "Submit" %>
<% end %>
</div>
</div>

View File

@@ -1,9 +0,0 @@
<% if @api_key.errors.any? %>
Danbooru.error("<%= j @api_key.errors.full_messages.join(', ') %>");
<% else %>
$("#api-key").text("<%= j @api_key.key %>");
$("#api-key-created").html("<%= j compact_time @api_key.created_at %>");
$("#api-key-updated").html("<%= j compact_time @api_key.updated_at %>");
Danbooru.notice("API key regenerated.");
<% end %>

View File

@@ -1,30 +0,0 @@
<% page_title "API Key" %>
<div id="c-maintenance-user-api-keys">
<div id="a-view">
<h1>API Key</h1>
<table class="striped" width="100%">
<thead>
<tr>
<th>API Key</th>
<th>Created</th>
<th>Updated</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr>
<td id="api-key"><code><%= @api_key.key %></code></td>
<td id="api-key-created"><%= compact_time @api_key.created_at %></td>
<td id="api-key-updated"><%= compact_time @api_key.updated_at %></td>
<td>
<%= button_to "Regenerate", user_api_key_path(CurrentUser.user), method: :put, params: { 'user[password]': @password }, remote: true %>
<%= button_to "Delete", user_api_key_path(CurrentUser.user), method: :delete, params: { 'user[password]': @password } %>
</td>
</tr>
</tbody>
</table>
</div>
</div>

View File

@@ -261,7 +261,7 @@
<tr>
<th>API Key</th>
<td>
<%= link_to (CurrentUser.api_key ? "View" : "Generate"), user_api_key_path(CurrentUser.user) %>
<%= link_to "View", user_api_keys_path(CurrentUser.user) %>
(<%= link_to_wiki "help", "help:api" %>)
</td>
</tr>