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>

View File

@@ -53,12 +53,11 @@ Rails.application.routes.draw do
resource :count_fixes, only: [:new, :create]
resource :email_notification, :only => [:show, :destroy]
resource :deletion, :only => [:show, :destroy]
resource :api_key, :only => [:show, :view, :update, :destroy] do
post :view
end
end
end
resources :api_keys, only: [:create, :index, :destroy]
resources :artists do
member do
put :revert
@@ -247,9 +246,7 @@ Rails.application.routes.draw do
post :send_confirmation
end
resource :password, only: [:edit, :update]
resource :api_key, :only => [:show, :view, :update, :destroy], :controller => "maintenance/user/api_keys" do
post :view
end
resources :api_keys, only: [:create, :index, :destroy]
collection do
get :custom_style

View File

@@ -0,0 +1,5 @@
FactoryBot.define do
factory(:api_key) do
user
end
end

View File

@@ -0,0 +1,72 @@
require 'test_helper'
class ApiKeysControllerTest < ActionDispatch::IntegrationTest
context "An api keys controller" do
setup do
@user = create(:user)
end
context "#index action" do
setup do
@api_key = create(:api_key, user: @user)
end
should "let a user see their own API keys" do
get_auth user_api_keys_path(@user.id), @user
assert_response :success
assert_select "#api-key-#{@api_key.id}", count: 1
end
should "not let a user see API keys belonging to other users" do
get_auth user_api_keys_path(@user.id), create(:user)
assert_response :success
assert_select "#api-key-#{@api_key.id}", count: 0
end
should "let the owner see all API keys" do
get_auth user_api_keys_path(@user.id), create(:owner_user)
assert_response :success
assert_select "#api-key-#{@api_key.id}", count: 1
end
should "not return the key in the API" do
get_auth user_api_keys_path(@user.id), @user, as: :json
assert_response :success
assert_nil response.parsed_body.first["key"]
end
end
context "#create action" do
should "create a new API key" do
post_auth user_api_keys_path(@user.id), @user
assert_redirected_to user_api_keys_path(@user.id)
assert_equal(true, @user.api_key.present?)
end
end
context "#destroy" do
setup do
@api_key = create(:api_key, user: @user)
end
should "delete the user's API key" do
delete_auth api_key_path(@api_key.id), @user
assert_redirected_to user_api_keys_path(@user.id)
assert_nil(@user.reload.api_key)
end
should "not allow deleting another user's API key" do
delete_auth api_key_path(@api_key.id), create(:user)
assert_response 403
assert_not_nil(@user.reload.api_key)
end
end
end
end

View File

@@ -39,7 +39,7 @@ class ApplicationControllerTest < ActionDispatch::IntegrationTest
context "on api authentication" do
setup do
@user = create(:user, password: "password")
@api_key = ApiKey.generate!(@user)
@api_key = create(:api_key, user: @user)
ActionController::Base.allow_forgery_protection = true
end

View File

@@ -1,51 +0,0 @@
require 'test_helper'
module Maintenance
module User
class ApiKeysControllerTest < ActionDispatch::IntegrationTest
context "An api keys controller" do
setup do
@user = create(:gold_user, :password => "password")
ApiKey.generate!(@user)
end
context "#show" do
should "render" do
get_auth maintenance_user_api_key_path, @user, params: {user_id: @user.id}
assert_response :success
end
end
context "#view" do
context "with a correct password" do
should "succeed" do
post_auth view_maintenance_user_api_key_path(user_id: @user.id), @user, params: {user: {password: "password"}}
assert_response :success
end
should "not generate another API key if the user already has one" do
assert_difference("ApiKey.count", 0) do
post_auth view_maintenance_user_api_key_path(user_id: @user.id), @user, params: {user: {password: "password"}}
end
end
end
end
context "#update" do
should "regenerate the API key" do
old_key = @user.api_key
put_auth maintenance_user_api_key_path, @user, params: {user_id: @user.id, user: {password: "password"}}
assert_not_equal(old_key.key, @user.reload.api_key.key)
end
end
context "#destroy" do
should "delete the API key" do
delete_auth maintenance_user_api_key_path, @user, params: {user_id: @user.id, user: {password: "password"}}
assert_nil(@user.reload.api_key)
end
end
end
end
end
end

View File

@@ -3,14 +3,8 @@ require 'test_helper'
class ApiKeyTest < ActiveSupport::TestCase
context "in all cases a user" do
setup do
@user = FactoryBot.create(:gold_user, :name => "abcdef")
@api_key = ApiKey.generate!(@user)
end
should "regenerate the key" do
assert_changes(-> { @api_key.key }) do
@api_key.regenerate!
end
@user = create(:user)
@api_key = create(:api_key, user: @user)
end
should "generate a unique key" do