diff --git a/CHANGELOG.md b/CHANGELOG.md
index d00c262e4..65af90f62 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,8 @@
### API Changes
* You can now have multiple API keys.
+* You can now see when your API keys were last used, how many times they've
+ been used, and which IP address last used them.
* API keys can be restricted to only work with certain IPs or certain API
endpoints.
* If you're an app or script developer, and you have an app that requests API
diff --git a/app/logical/session_loader.rb b/app/logical/session_loader.rb
index 9ae708fc4..57b27dcdc 100644
--- a/app/logical/session_loader.rb
+++ b/app/logical/session_loader.rb
@@ -90,6 +90,7 @@ class SessionLoader
def authenticate_api_key(name, key)
user, api_key = User.find_by_name(name)&.authenticate_api_key(key)
raise AuthenticationFailure if user.blank?
+ update_api_key(api_key)
raise User::PrivilegeError if !api_key.has_permission?(request.remote_ip, request.params[:controller], request.params[:action])
CurrentUser.user = user
end
@@ -117,6 +118,11 @@ class SessionLoader
CurrentUser.user.update_attribute(:last_ip_addr, @request.remote_ip)
end
+ def update_api_key(api_key)
+ api_key.increment!(:uses, touch: :last_used_at)
+ api_key.update!(last_ip_address: request.remote_ip)
+ end
+
def set_time_zone
Time.zone = CurrentUser.user.time_zone
end
diff --git a/app/views/api_keys/index.html.erb b/app/views/api_keys/index.html.erb
index 2167be418..074209874 100644
--- a/app/views/api_keys/index.html.erb
+++ b/app/views/api_keys/index.html.erb
@@ -51,14 +51,22 @@
<%= safe_join(api_key.permitted_ip_addresses, "
".html_safe).presence || "All" %>
<% end %>
- <% if !params[:user_id].present? %>
- <% t.column "User" do |api_key| %>
- <%= link_to_user api_key.user %>
+ <% t.column :uses %>
+
+ <% t.column "Last Used" do |api_key| %>
+ <%= time_ago_in_words_tagged api_key.last_used_at %>
+
+ <% if api_key.last_ip_address.present? %>
+
by <%= api_key.last_ip_address %>
<% end %>
<% end %>
<% t.column "Created" do |api_key| %>
<%= time_ago_in_words_tagged api_key.created_at %>
+
+ <% if !params[:user_id].present? %>
+
by <%= link_to_user api_key.user %>
+ <% end %>
<% end %>
<% t.column column: "control" do |api_key| %>
diff --git a/test/functional/application_controller_test.rb b/test/functional/application_controller_test.rb
index 3c9de79e0..bd96c4a24 100644
--- a/test/functional/application_controller_test.rb
+++ b/test/functional/application_controller_test.rb
@@ -52,14 +52,20 @@ class ApplicationControllerTest < ActionDispatch::IntegrationTest
should "succeed for api key matches" do
basic_auth_string = "Basic #{::Base64.encode64("#{@user.name}:#{@api_key.key}")}"
get edit_user_path(@user), headers: { HTTP_AUTHORIZATION: basic_auth_string }
+
assert_response :success
+ assert_equal(1, @api_key.reload.uses)
+ assert_not_nil(@api_key.reload.last_used_at)
end
should "succeed when the user has multiple api keys" do
@api_key2 = create(:api_key, user: @user)
basic_auth_string = "Basic #{::Base64.encode64("#{@user.name}:#{@api_key2.key}")}"
get edit_user_path(@user), headers: { HTTP_AUTHORIZATION: basic_auth_string }
+
assert_response :success
+ assert_equal(1, @api_key2.reload.uses)
+ assert_not_nil(@api_key2.reload.last_used_at)
end
should "fail for api key mismatches" do
@@ -80,13 +86,19 @@ class ApplicationControllerTest < ActionDispatch::IntegrationTest
context "using the api_key parameter" do
should "succeed for api key matches" do
get edit_user_path(@user), params: { login: @user.name, api_key: @api_key.key }
+
assert_response :success
+ assert_equal(1, @api_key.reload.uses)
+ assert_not_nil(@api_key.reload.last_used_at)
end
should "succeed when the user has multiple api keys" do
@api_key2 = create(:api_key, user: @user)
get edit_user_path(@user), params: { login: @user.name, api_key: @api_key2.key }
+
assert_response :success
+ assert_equal(1, @api_key2.reload.uses)
+ assert_not_nil(@api_key2.reload.last_used_at)
end
should "fail for api key mismatches" do
@@ -135,6 +147,10 @@ class ApplicationControllerTest < ActionDispatch::IntegrationTest
ActionDispatch::Request.any_instance.stubs(:remote_ip).returns("2600:dead:beef::1")
get posts_path, params: { login: @api_key.user.name, api_key: @api_key.key }
assert_response 403
+
+ assert_equal(6, @api_key.reload.uses)
+ assert_equal("2600:dead:beef::1", @api_key.reload.last_ip_address.to_s)
+ assert_not_nil(@api_key.reload.last_used_at)
end
should "restrict requests to the permitted endpoints" do
@@ -152,6 +168,10 @@ class ApplicationControllerTest < ActionDispatch::IntegrationTest
put post_path(@post), params: { login: @api_key.user.name, api_key: @api_key.key, post: { rating: "s" }}
assert_response 403
+
+ assert_equal(4, @api_key.reload.uses)
+ assert_equal("127.0.0.1", @api_key.reload.last_ip_address.to_s)
+ assert_not_nil(@api_key.reload.last_used_at)
end
end