diff --git a/app/controllers/api_keys_controller.rb b/app/controllers/api_keys_controller.rb new file mode 100644 index 000000000..94ddfb448 --- /dev/null +++ b/app/controllers/api_keys_controller.rb @@ -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 diff --git a/app/controllers/maintenance/user/api_keys_controller.rb b/app/controllers/maintenance/user/api_keys_controller.rb deleted file mode 100644 index bb90d1c42..000000000 --- a/app/controllers/maintenance/user/api_keys_controller.rb +++ /dev/null @@ -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 diff --git a/app/helpers/icon_helper.rb b/app/helpers/icon_helper.rb index 356c73fe9..aa6f2f63f 100644 --- a/app/helpers/icon_helper.rb +++ b/app/helpers/icon_helper.rb @@ -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 diff --git a/app/javascript/src/styles/common/main_layout.scss b/app/javascript/src/styles/common/main_layout.scss index 4a9a0c367..d3eaf4edf 100644 --- a/app/javascript/src/styles/common/main_layout.scss +++ b/app/javascript/src/styles/common/main_layout.scss @@ -55,3 +55,18 @@ footer#page-footer { } } } + +/* A container for the main

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; + } +} diff --git a/app/models/api_key.rb b/app/models/api_key.rb index a08998db9..869051d4f 100644 --- a/app/models/api_key.rb +++ b/app/models/api_key.rb @@ -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 diff --git a/app/policies/api_key_policy.rb b/app/policies/api_key_policy.rb new file mode 100644 index 000000000..d43083d81 --- /dev/null +++ b/app/policies/api_key_policy.rb @@ -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 diff --git a/app/views/api_keys/_secondary_links.html.erb b/app/views/api_keys/_secondary_links.html.erb new file mode 100644 index 000000000..d98882116 --- /dev/null +++ b/app/views/api_keys/_secondary_links.html.erb @@ -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 %> diff --git a/app/views/api_keys/index.html.erb b/app/views/api_keys/index.html.erb new file mode 100644 index 000000000..73b1c4ee6 --- /dev/null +++ b/app/views/api_keys/index.html.erb @@ -0,0 +1,63 @@ +<%= render "secondary_links" %> + +
+
+
+

API Keys

+ + <%= link_to user_api_keys_path(CurrentUser.user.id), class: "button-primary", method: :post do %> + <%= plus_icon %> Add + <% end %> +
+ + <% if params[:user_id].present? %> +
+

An API key is used to give programs access to your <%= Danbooru.config.canonical_app_name %> account.

+ +

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.

+ +

Your API key is like your password. 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.

+ +

Example usage: + + <% 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 %> + +

+ +

See the <%= link_to_wiki "API documentation", "help:api" %> to learn more.

+
+ <% 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 %> +
+
diff --git a/app/views/maintenance/user/api_keys/show.html.erb b/app/views/maintenance/user/api_keys/show.html.erb deleted file mode 100644 index 940ddfff9..000000000 --- a/app/views/maintenance/user/api_keys/show.html.erb +++ /dev/null @@ -1,13 +0,0 @@ -<% page_title "API Key" %> - -
-
-

API Key

-

You must re-enter your password to view or change your API key.

- - <%= 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 %> -
-
diff --git a/app/views/maintenance/user/api_keys/update.js.erb b/app/views/maintenance/user/api_keys/update.js.erb deleted file mode 100644 index 13095d77e..000000000 --- a/app/views/maintenance/user/api_keys/update.js.erb +++ /dev/null @@ -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 %> diff --git a/app/views/maintenance/user/api_keys/view.html.erb b/app/views/maintenance/user/api_keys/view.html.erb deleted file mode 100644 index 72af6dc33..000000000 --- a/app/views/maintenance/user/api_keys/view.html.erb +++ /dev/null @@ -1,30 +0,0 @@ -<% page_title "API Key" %> - -
-
-

API Key

- - - - - - - - - - - - - - - - - - - -
API KeyCreatedUpdatedActions
<%= @api_key.key %><%= compact_time @api_key.created_at %><%= compact_time @api_key.updated_at %> - <%= 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 } %> -
-
-
diff --git a/app/views/users/_statistics.html.erb b/app/views/users/_statistics.html.erb index 147c67e3b..6fffa7552 100644 --- a/app/views/users/_statistics.html.erb +++ b/app/views/users/_statistics.html.erb @@ -261,7 +261,7 @@ API Key - <%= 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" %>) diff --git a/config/routes.rb b/config/routes.rb index a9fca44eb..4812a498d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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 diff --git a/test/factories/api_key.rb b/test/factories/api_key.rb new file mode 100644 index 000000000..6df1623fd --- /dev/null +++ b/test/factories/api_key.rb @@ -0,0 +1,5 @@ +FactoryBot.define do + factory(:api_key) do + user + end +end diff --git a/test/functional/api_keys_controller_test.rb b/test/functional/api_keys_controller_test.rb new file mode 100644 index 000000000..38be2a8cb --- /dev/null +++ b/test/functional/api_keys_controller_test.rb @@ -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 diff --git a/test/functional/application_controller_test.rb b/test/functional/application_controller_test.rb index 62685f78f..9792d42ed 100644 --- a/test/functional/application_controller_test.rb +++ b/test/functional/application_controller_test.rb @@ -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 diff --git a/test/functional/maintenance/user/api_keys_controller_test.rb b/test/functional/maintenance/user/api_keys_controller_test.rb deleted file mode 100644 index a47ec34b1..000000000 --- a/test/functional/maintenance/user/api_keys_controller_test.rb +++ /dev/null @@ -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 diff --git a/test/unit/api_key_test.rb b/test/unit/api_key_test.rb index cdc246e68..98e237f1e 100644 --- a/test/unit/api_key_test.rb +++ b/test/unit/api_key_test.rb @@ -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