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:
26
app/controllers/api_keys_controller.rb
Normal file
26
app/controllers/api_keys_controller.rb
Normal 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
|
||||||
@@ -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
|
|
||||||
@@ -144,4 +144,8 @@ module IconHelper
|
|||||||
def link_icon(**options)
|
def link_icon(**options)
|
||||||
icon_tag("fas fa-link", **options)
|
icon_tag("fas fa-link", **options)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def plus_icon(**options)
|
||||||
|
icon_tag("fas fa-plus", **options)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,12 +4,17 @@ class ApiKey < ApplicationRecord
|
|||||||
validates_uniqueness_of :key
|
validates_uniqueness_of :key
|
||||||
has_secure_token :key
|
has_secure_token :key
|
||||||
|
|
||||||
def self.generate!(user)
|
def self.visible(user)
|
||||||
create(:user_id => user.id)
|
if user.is_owner?
|
||||||
|
all
|
||||||
|
else
|
||||||
|
where(user: user)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def regenerate!
|
def self.search(params)
|
||||||
regenerate_key
|
q = search_attributes(params, :id, :created_at, :updated_at, :key, :user)
|
||||||
save
|
q = q.apply_default_order(params)
|
||||||
|
q
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
17
app/policies/api_key_policy.rb
Normal file
17
app/policies/api_key_policy.rb
Normal 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
|
||||||
5
app/views/api_keys/_secondary_links.html.erb
Normal file
5
app/views/api_keys/_secondary_links.html.erb
Normal 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 %>
|
||||||
63
app/views/api_keys/index.html.erb
Normal file
63
app/views/api_keys/index.html.erb
Normal 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>
|
||||||
@@ -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>
|
|
||||||
@@ -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 %>
|
|
||||||
@@ -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>
|
|
||||||
@@ -261,7 +261,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th>API Key</th>
|
<th>API Key</th>
|
||||||
<td>
|
<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" %>)
|
(<%= link_to_wiki "help", "help:api" %>)
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -53,12 +53,11 @@ Rails.application.routes.draw do
|
|||||||
resource :count_fixes, only: [:new, :create]
|
resource :count_fixes, only: [:new, :create]
|
||||||
resource :email_notification, :only => [:show, :destroy]
|
resource :email_notification, :only => [:show, :destroy]
|
||||||
resource :deletion, :only => [:show, :destroy]
|
resource :deletion, :only => [:show, :destroy]
|
||||||
resource :api_key, :only => [:show, :view, :update, :destroy] do
|
|
||||||
post :view
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
resources :api_keys, only: [:create, :index, :destroy]
|
||||||
|
|
||||||
resources :artists do
|
resources :artists do
|
||||||
member do
|
member do
|
||||||
put :revert
|
put :revert
|
||||||
@@ -247,9 +246,7 @@ Rails.application.routes.draw do
|
|||||||
post :send_confirmation
|
post :send_confirmation
|
||||||
end
|
end
|
||||||
resource :password, only: [:edit, :update]
|
resource :password, only: [:edit, :update]
|
||||||
resource :api_key, :only => [:show, :view, :update, :destroy], :controller => "maintenance/user/api_keys" do
|
resources :api_keys, only: [:create, :index, :destroy]
|
||||||
post :view
|
|
||||||
end
|
|
||||||
|
|
||||||
collection do
|
collection do
|
||||||
get :custom_style
|
get :custom_style
|
||||||
|
|||||||
5
test/factories/api_key.rb
Normal file
5
test/factories/api_key.rb
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
FactoryBot.define do
|
||||||
|
factory(:api_key) do
|
||||||
|
user
|
||||||
|
end
|
||||||
|
end
|
||||||
72
test/functional/api_keys_controller_test.rb
Normal file
72
test/functional/api_keys_controller_test.rb
Normal 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
|
||||||
@@ -39,7 +39,7 @@ class ApplicationControllerTest < ActionDispatch::IntegrationTest
|
|||||||
context "on api authentication" do
|
context "on api authentication" do
|
||||||
setup do
|
setup do
|
||||||
@user = create(:user, password: "password")
|
@user = create(:user, password: "password")
|
||||||
@api_key = ApiKey.generate!(@user)
|
@api_key = create(:api_key, user: @user)
|
||||||
|
|
||||||
ActionController::Base.allow_forgery_protection = true
|
ActionController::Base.allow_forgery_protection = true
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -3,14 +3,8 @@ require 'test_helper'
|
|||||||
class ApiKeyTest < ActiveSupport::TestCase
|
class ApiKeyTest < ActiveSupport::TestCase
|
||||||
context "in all cases a user" do
|
context "in all cases a user" do
|
||||||
setup do
|
setup do
|
||||||
@user = FactoryBot.create(:gold_user, :name => "abcdef")
|
@user = create(:user)
|
||||||
@api_key = ApiKey.generate!(@user)
|
@api_key = create(:api_key, user: @user)
|
||||||
end
|
|
||||||
|
|
||||||
should "regenerate the key" do
|
|
||||||
assert_changes(-> { @api_key.key }) do
|
|
||||||
@api_key.regenerate!
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
should "generate a unique key" do
|
should "generate a unique key" do
|
||||||
|
|||||||
Reference in New Issue
Block a user