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)
|
||||
icon_tag("fas fa-link", **options)
|
||||
end
|
||||
|
||||
def plus_icon(**options)
|
||||
icon_tag("fas fa-plus", **options)
|
||||
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
|
||||
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
|
||||
|
||||
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>
|
||||
<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>
|
||||
|
||||
@@ -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
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
@@ -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
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user