diff --git a/app/models/token.rb b/app/models/token.rb
index b98c6dbf3..d425e2b11 100644
--- a/app/models/token.rb
+++ b/app/models/token.rb
@@ -94,9 +94,15 @@ class Token < ApplicationRecord
# Returns the active user who owns the key for the given action
def self.find_active_user(action, key, validity_days=nil)
- user = find_user(action, key, validity_days)
- if user && user.active?
- user
+ token = find_token(action, key, validity_days)
+ if token
+ user = token.user
+ if user&.active?
+ if token.updated_on.nil? || token.updated_on <= 1.minute.ago
+ token.update_column(:updated_on, Time.now.utc)
+ end
+ user
+ end
end
end
diff --git a/app/views/my/_sidebar.html.erb b/app/views/my/_sidebar.html.erb
index 82a22cef2..8bae9ddc0 100644
--- a/app/views/my/_sidebar.html.erb
+++ b/app/views/my/_sidebar.html.erb
@@ -38,9 +38,14 @@
<%= javascript_tag("$('#api-access-key').hide();") %>
<% if @user.api_token %>
- <%= l(:label_api_access_key_created_on, distance_of_time_in_words(Time.now, @user.api_token.created_on)) %>
+ <%= l(:label_api_access_key_created_on, distance_of_time_in_words(Time.now, @user.api_token.created_on)) %>
+ <% if @user.api_token.updated_on > @user.api_token.created_on %>
+ <%= l(:label_api_access_key_last_used_on, distance_of_time_in_words(Time.now, @user.api_token.updated_on)) %>
+ <% else %>
+ <%= l(:label_api_access_key_never_used) %>
+ <% end %>
<% else %>
- <%= l(:label_missing_api_access_key) %>
+ <%= l(:label_missing_api_access_key) %>
<% end %>
(<%= link_to l(:button_reset), my_api_key_path, :method => :post %>)
diff --git a/config/locales/de.yml b/config/locales/de.yml
index 2b3a6042a..69f9ef211 100644
--- a/config/locales/de.yml
+++ b/config/locales/de.yml
@@ -434,6 +434,8 @@ de:
label_any_issues_not_in_project: irgendein Ticket nicht im Projekt
label_api_access_key: API-Zugriffsschlüssel
label_api_access_key_created_on: Der API-Zugriffsschlüssel wurde vor %{value} erstellt
+ label_api_access_key_last_used_on: "Zuletzt verwendet: vor %{value}"
+ label_api_access_key_never_used: Nie verwendet
label_applied_status: Zugewiesener Status
label_ascending: Aufsteigend
label_ask: Nachfragen
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 90e67bfd8..b97471e90 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -1035,6 +1035,8 @@ en:
label_api_access_key: API access key
label_missing_api_access_key: Missing an API access key
label_api_access_key_created_on: "API access key created %{value} ago"
+ label_api_access_key_last_used_on: "Last used: %{value} ago"
+ label_api_access_key_never_used: Never used
label_profile: Profile
label_subtask: Subtask
label_subtask_plural: Subtasks
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index 014a47072..74df46e58 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -921,6 +921,8 @@ fr:
label_api_access_key: Clé d'accès API
label_missing_api_access_key: Clé d'accès API manquante
label_api_access_key_created_on: Clé d'accès API créée il y a %{value}
+ label_api_access_key_last_used_on: "Dernier usage : il y a %{value}"
+ label_api_access_key_never_used: Jamais utilisée
label_profile: Profil
label_subtask_plural: Sous-tâches
label_project_copy_notifications: Envoyer les notifications durant la copie du projet
diff --git a/config/locales/ja.yml b/config/locales/ja.yml
index 51fc2c164..4cfa02716 100644
--- a/config/locales/ja.yml
+++ b/config/locales/ja.yml
@@ -806,6 +806,8 @@ ja:
label_api_access_key: APIアクセスキー
label_missing_api_access_key: APIアクセスキーが見つかりません
label_api_access_key_created_on: "APIアクセスキーは%{value}前に作成されました"
+ label_api_access_key_last_used_on: "最終使用:%{value}前"
+ label_api_access_key_never_used: 未使用
label_subtask_plural: 子チケット
label_project_copy_notifications: コピーしたチケットのメール通知を送信する
label_principal_search: "ユーザーまたはグループの検索:"
diff --git a/test/integration/api_test/authentication_test.rb b/test/integration/api_test/authentication_test.rb
index 4145fb969..b1b28d20c 100644
--- a/test/integration/api_test/authentication_test.rb
+++ b/test/integration/api_test/authentication_test.rb
@@ -161,4 +161,40 @@ class Redmine::ApiTest::AuthenticationTest < Redmine::ApiTest::Base
assert_response :success
assert_select 'h2', :text => "#{user.initials} #{user.name}"
end
+
+ def test_api_key_usage_via_header_should_update_updated_on
+ user = User.generate!
+ token = Token.create!(:user => user, :action => 'api', :updated_on => 2.minutes.ago)
+ updated = token.updated_on
+ get '/users/current.xml', :headers => {'X-Redmine-API-Key' => token.value}
+ assert_response :ok
+ assert token.reload.updated_on > updated
+ end
+
+ def test_api_key_usage_via_parameter_should_update_updated_on
+ user = User.generate!
+ token = Token.create!(:user => user, :action => 'api', :updated_on => 2.minutes.ago)
+ updated = token.updated_on
+ get "/users/current.xml?key=#{token.value}"
+ assert_response :ok
+ assert token.reload.updated_on > updated
+ end
+
+ def test_api_key_usage_via_basic_auth_should_update_updated_on
+ user = User.generate!
+ token = Token.create!(:user => user, :action => 'api', :updated_on => 2.minutes.ago)
+ updated = token.updated_on
+ get '/users/current.xml', :headers => credentials(token.value, 'X')
+ assert_response :ok
+ assert token.reload.updated_on > updated
+ end
+
+ def test_failed_api_auth_should_not_update_updated_on
+ user = User.generate!
+ token = Token.create!(:user => user, :action => 'api', :updated_on => 2.minutes.ago)
+ updated = token.updated_on
+ get '/users/current.xml', :headers => {'X-Redmine-API-Key' => 'wrong_key'}
+ assert_response :unauthorized
+ assert_equal updated.to_i, token.reload.updated_on.to_i
+ end
end
diff --git a/test/unit/token_test.rb b/test/unit/token_test.rb
index d40b3a2a1..754f0ad04 100644
--- a/test/unit/token_test.rb
+++ b/test/unit/token_test.rb
@@ -137,4 +137,25 @@ class TokenTest < ActiveSupport::TestCase
token = Token.create!(:user_id => 999, :action => 'api', :created_on => 2.days.ago)
assert_nil Token.find_token('api', token.value, 1)
end
+
+ def test_find_active_user_should_bump_updated_on_when_not_recently_updated
+ token = Token.create!(:user_id => 1, :action => 'api', :updated_on => 2.minutes.ago)
+ updated = token.updated_on
+ Token.find_active_user('api', token.value)
+ assert token.reload.updated_on > updated
+ end
+
+ def test_find_active_user_should_not_bump_updated_on_within_one_minute
+ token = Token.create!(:user_id => 1, :action => 'api', :updated_on => 1.second.ago)
+ updated = token.reload.updated_on
+ Token.find_active_user('api', token.value)
+ assert_equal updated.to_i, token.reload.updated_on.to_i
+ end
+
+ def test_find_active_user_should_bump_updated_on_for_feeds_token
+ token = Token.create!(:user_id => 1, :action => 'feeds', :updated_on => 2.minutes.ago)
+ updated = token.updated_on
+ Token.find_active_user('feeds', token.value)
+ assert token.reload.updated_on > updated
+ end
end