From 1274e9fa6341cc9774251e90d93f5f280e881db2 Mon Sep 17 00:00:00 2001 From: Kyle Drake Date: Sat, 13 May 2017 18:18:34 -0500 Subject: [PATCH] API key support --- app/api.rb | 29 +++++++++++++++++---------- app/settings.rb | 11 ++++++++++ migrations/103_sites_api_key.rb | 9 +++++++++ models/site.rb | 12 ++++++----- tests/api_tests.rb | 20 ++++++++++++++++++ views/settings/site.erb | 6 +++++- views/settings/site/_api_key_form.erb | 4 ++++ views/settings/site/api_key.erb | 19 ++++++++++++++++++ 8 files changed, 93 insertions(+), 17 deletions(-) create mode 100644 migrations/103_sites_api_key.rb create mode 100644 views/settings/site/_api_key_form.erb create mode 100644 views/settings/site/api_key.erb diff --git a/app/api.rb b/app/api.rb index 02352ad9..2bd5a152 100644 --- a/app/api.rb +++ b/app/api.rb @@ -148,24 +148,31 @@ def init_api_credentials auth = request.env['HTTP_AUTHORIZATION'] begin - user, pass = Base64.decode64(auth.match(/Basic (.+)/)[1]).split(':') + if bearer_match = auth.match(/^Bearer (.+)/) + api_key = bearer_match.captures.first + api_error_invalid_auth if api_key.nil? || api_key.empty? + else + user, pass = Base64.decode64(auth.match(/Basic (.+)/)[1]).split(':') + end rescue api_error_invalid_auth end - if Site.valid_login? user, pass - site = Site[username: user] - - if site.nil? || site.is_banned - api_error_invalid_auth - end - - DB['update sites set api_calls=api_calls+1 where id=?', site.id].first - - session[:id] = site.id + if defined?(api_key) && !api_key.blank? + site = Site[api_key: api_key] + elsif defined?(user) && defined?(pass) + site = Site.get_site_from_login user, pass else api_error_invalid_auth end + + if site.nil? || site.is_banned || site.is_deleted + api_error_invalid_auth + end + + DB['update sites set api_calls=api_calls+1 where id=?', site.id].first + + session[:id] = site.id end def api_success(message_or_obj) diff --git a/app/settings.rb b/app/settings.rb index f05d8e98..e8599730 100644 --- a/app/settings.rb +++ b/app/settings.rb @@ -168,6 +168,17 @@ post '/settings/:username/custom_domain' do end end +post '/settings/:username/generate_api_key' do + require_login + require_ownership_for_settings + is_new = current_site.api_key.nil? + current_site.generate_api_key! + + msg = is_new ? "New API key has been generated." : "API key has been regenerated." + flash[:success] = msg + redirect "/settings/#{current_site.username}#api_key" +end + post '/settings/change_password' do require_login diff --git a/migrations/103_sites_api_key.rb b/migrations/103_sites_api_key.rb new file mode 100644 index 00000000..b0cf0c64 --- /dev/null +++ b/migrations/103_sites_api_key.rb @@ -0,0 +1,9 @@ +Sequel.migration do + up { + DB.add_column :sites, :api_key, :text + } + + down { + DB.drop_column :sites, :api_key + } +end diff --git a/models/site.rb b/models/site.rb index 95a7359e..d59b5b9e 100644 --- a/models/site.rb +++ b/models/site.rb @@ -288,11 +288,8 @@ class Site < Sequel::Model def get_site_from_login(username_or_email, plaintext) site = get_with_identifier username_or_email - - return false if site.nil? - return false if site.is_deleted - return false if site.is_banned - site.valid_password?(plaintext) ? site : nil + return nil if site.nil? || site.is_deleted || site.is_banned || !site.valid_password?(plaintext) + site end def bcrypt_cost @@ -1505,6 +1502,11 @@ class Site < Sequel::Model true end + def generate_api_key! + self.api_key = SecureRandom.hex(16) + save_changes validate: false + end + private def store_file(path, uploaded, opts={}) diff --git a/tests/api_tests.rb b/tests/api_tests.rb index 3f1669ea..d878de43 100644 --- a/tests/api_tests.rb +++ b/tests/api_tests.rb @@ -217,6 +217,26 @@ describe 'api upload' do res[:error_type].must_equal 'missing_files' end + it 'succeeds with valid api key' do + create_site + @site.api_key.must_equal nil + @site.generate_api_key! + @site.reload.api_key.wont_equal nil + header 'Authorization', "Bearer #{@site.api_key}" + post '/api/upload', 'test.jpg' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') + res[:result].must_equal 'success' + site_file_exists?('test.jpg').must_equal true + end + + it 'fails with bad api key' do + create_site + @site.generate_api_key! + header 'Authorization', "Bearer zerocool" + post '/api/upload', 'test.jpg' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') + res[:result].must_equal 'error' + res[:error_type].must_equal 'invalid_auth' + end + =begin # Getting too slow to run this test it 'fails with too many files' do diff --git a/views/settings/site.erb b/views/settings/site.erb index d1614157..5885bc71 100644 --- a/views/settings/site.erb +++ b/views/settings/site.erb @@ -29,6 +29,8 @@
  • Change Site (User) Name
  • Tipping
  • +
  • API Key
  • + <% if @site.admin_nsfw != true %>
  • 18+
  • <% end %> @@ -53,7 +55,9 @@
    <%== erb :'settings/site/tipping' %>
    - +
    + <%== erb :'settings/site/api_key' %> +
    <% if @site.admin_nsfw != true %>
    <%== erb :'settings/site/nsfw' %> diff --git a/views/settings/site/_api_key_form.erb b/views/settings/site/_api_key_form.erb new file mode 100644 index 00000000..6da4b930 --- /dev/null +++ b/views/settings/site/_api_key_form.erb @@ -0,0 +1,4 @@ +
    + <%== csrf_token_input_html %> + +
    diff --git a/views/settings/site/api_key.erb b/views/settings/site/api_key.erb new file mode 100644 index 00000000..769803c6 --- /dev/null +++ b/views/settings/site/api_key.erb @@ -0,0 +1,19 @@ +

    API Key

    + +

    + An API Key can be used with the Neocities API to allow for changes to your site without requiring login credentials. This API key allows full write access to your site. Keep it secret, keep it safe. +

    + +<% if @site.api_key %> +

    Your API key:
    <%= @site.api_key %>

    + +

    + If you need to regenerate the key for some reason, you can do that below. + Keep in mind that this will make the old key no longer function. +

    + + <%== erb :'settings/site/_api_key_form', layout: false %> +<% else %> +

    You haven't yet generated an API key, click the button to create one:

    + <%== erb :'settings/site/_api_key_form', layout: false %> +<% end %>