OSDN Git Service

Added an API token for each User to use when making API requests. (#3920)
authorEric Davis <edavis@littlestreamsoftware.com>
Wed, 23 Dec 2009 06:27:28 +0000 (06:27 +0000)
committerEric Davis <edavis@littlestreamsoftware.com>
Wed, 23 Dec 2009 06:27:28 +0000 (06:27 +0000)
The API key will be displayed on My Account page with a link to reset or
generate a new one.  All existing users will have a token generated by the
migration.

git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/trunk@3217 e93f8b46-1217-0410-a6f0-8f06a7374b81

app/controllers/my_controller.rb
app/models/user.rb
app/views/my/_sidebar.rhtml
app/views/my/account.rhtml
config/locales/en.yml
db/migrate/20091221004949_add_api_keys_for_users.rb [new file with mode: 0644]
test/functional/my_controller_test.rb
test/unit/user_test.rb

index 64687d8..f686759 100644 (file)
@@ -108,6 +108,19 @@ class MyController < ApplicationController
     redirect_to :action => 'account'
   end
 
+  # Create a new API key
+  def reset_api_key
+    if request.post?
+      if User.current.api_token
+        User.current.api_token.destroy
+        User.current.reload
+      end
+      User.current.api_key
+      flash[:notice] = l(:notice_api_access_key_reseted)
+    end
+    redirect_to :action => 'account'
+  end
+
   # User's page layout configuration
   def page_layout
     @user = User.current
index 4cfa2b4..39fdb16 100644 (file)
@@ -39,6 +39,7 @@ class User < Principal
   has_many :changesets, :dependent => :nullify
   has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
   has_one :rss_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='feeds'"
+  has_one :api_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='api'"
   belongs_to :auth_source
   
   # Active non-anonymous users scope
@@ -192,6 +193,12 @@ class User < Principal
     token = self.rss_token || Token.create(:user => self, :action => 'feeds')
     token.value
   end
+
+  # Return user's API key (a 40 chars long string), used to access the API
+  def api_key
+    token = self.api_token || Token.create(:user => self, :action => 'api')
+    token.value
+  end
   
   # Return an array of project ids for which the user has explicitly turned mail notifications on
   def notified_projects_ids
@@ -210,6 +217,11 @@ class User < Principal
     token && token.user.active? ? token.user : nil
   end
   
+  def self.find_by_api_key(key)
+    token = Token.find_by_action_and_value('api', key)
+    token && token.user.active? ? token.user : nil
+  end
+  
   # Makes find_by_mail case-insensitive
   def self.find_by_mail(mail)
     find(:first, :conditions => ["LOWER(mail) = ?", mail.to_s.downcase])
index d30eacf..1f511bd 100644 (file)
@@ -2,7 +2,25 @@
 
 <p><%=l(:field_login)%>: <strong><%= @user.login %></strong><br />
 <%=l(:field_created_on)%>: <%= format_time(@user.created_on) %></p>
+
+
+<h4><%= l(:label_feeds_access_key) %></h4>
+
+<p>
 <% if @user.rss_token %>
-<p><%= l(:label_feeds_access_key_created_on, distance_of_time_in_words(Time.now, @user.rss_token.created_on)) %>
-(<%= link_to l(:button_reset), {:action => 'reset_rss_key'}, :method => :post %>)</p>
+<%= l(:label_feeds_access_key_created_on, distance_of_time_in_words(Time.now, @user.rss_token.created_on)) %>
+<% else %>
+<%= l(:label_missing_feeds_access_key) %>
+<% end %>
+(<%= link_to l(:button_reset), {:action => 'reset_rss_key'}, :method => :post %>)
+</p>
+
+<h4><%= l(:label_api_access_key) %></h4>
+<p>
+<% if @user.api_token %>
+<%= l(:label_api_access_key_created_on, distance_of_time_in_words(Time.now, @user.api_token.created_on)) %>
+<% else %>
+<%= l(:label_missing_api_access_key) %>
 <% end %>
+(<%= link_to l(:button_reset), {:action => 'reset_api_key'}, :method => :post %>)
+</p>
index 018414e..1b8347c 100644 (file)
 <p><%= pref_fields.select :comments_sorting, [[l(:label_chronological_order), 'asc'], [l(:label_reverse_chronological_order), 'desc']] %></p>
 <% end %>
 </div>
+
+<% if @user.api_token %>
+<h3><%=l(:label_api_access_key) %></h3>
+<div class="box">
+  <p>
+    <%= link_to_function(l(:text_show), "$('api-access-key').show();")%>
+    <pre id='api-access-key'><%= @user.api_key %></pre>
+  </p>
+  <%= javascript_tag("$('api-access-key').hide();") %>
+</div>
+<% end %>
+
 </div>
 <% end %>
 
index cab7ff4..d75897b 100644 (file)
@@ -142,6 +142,7 @@ en:
   notice_email_sent: "An email was sent to {{value}}"
   notice_email_error: "An error occurred while sending mail ({{value}})"
   notice_feeds_access_key_reseted: Your RSS access key was reset.
+  notice_api_access_key_reseted: Your API access key was reset.
   notice_failed_to_save_issues: "Failed to save {{count}} issue(s) on {{total}} selected: {{ids}}."
   notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit."
   notice_account_pending: "Your account was created and is now pending administrator approval."
@@ -668,6 +669,8 @@ en:
   label_language_based: Based on user's language
   label_sort_by: "Sort by {{value}}"
   label_send_test_email: Send a test email
+  label_feeds_access_key: RSS access key
+  label_missing_feeds_access_key: Missing a RSS access key
   label_feeds_access_key_created_on: "RSS access key created {{value}} ago"
   label_module_plural: Modules
   label_added_time_by: "Added by {{author}} {{age}} ago"
@@ -729,6 +732,9 @@ en:
   label_copy_target: Target
   label_copy_same_as_target: Same as target
   label_display_used_statuses_only: Only display statuses that are used by this tracker
+  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"
   
   button_login: Login
   button_submit: Submit
@@ -836,6 +842,7 @@ en:
   text_wiki_page_nullify_children: "Keep child pages as root pages"
   text_wiki_page_destroy_children: "Delete child pages and all their descendants"
   text_wiki_page_reassign_children: "Reassign child pages to this parent page"
+  text_show: Show
   
   default_role_manager: Manager
   default_role_developper: Developer
diff --git a/db/migrate/20091221004949_add_api_keys_for_users.rb b/db/migrate/20091221004949_add_api_keys_for_users.rb
new file mode 100644 (file)
index 0000000..36fc7e1
--- /dev/null
@@ -0,0 +1,13 @@
+class AddApiKeysForUsers < ActiveRecord::Migration
+  def self.up
+    say_with_time("Generating API keys for active users") do
+      User.active.all(:include => :api_token).each do |user|
+        user.api_key
+      end
+    end
+  end
+
+  def self.down
+    # No-op
+  end
+end
index b871807..877095d 100644 (file)
@@ -163,4 +163,38 @@ class MyControllerTest < ActionController::TestCase
       should_redirect_to('my account') {'/my/account' }
     end
   end
+
+  context "POST to reset_api_key" do
+    context "with an existing api_token" do
+      setup do
+        @previous_token_value = User.find(2).api_key # Will generate one if it's missing
+        post :reset_api_key
+      end
+
+      should "destroy the existing token" do
+        assert_not_equal @previous_token_value, User.find(2).api_key
+      end
+
+      should "create a new token" do
+        assert User.find(2).api_token
+      end
+
+      should_set_the_flash_to /reset/
+      should_redirect_to('my account') {'/my/account' }
+    end
+    
+    context "with no api_token" do
+      setup do
+        assert_nil User.find(2).api_token
+        post :reset_api_key
+      end
+
+      should "create a new token" do
+        assert User.find(2).api_token
+      end
+
+      should_set_the_flash_to /reset/
+      should_redirect_to('my account') {'/my/account' }
+    end
+  end
 end
index 2a49965..a94870d 100644 (file)
@@ -126,7 +126,9 @@ class UserTest < ActiveSupport::TestCase
     assert !anon.new_record?
     assert_kind_of AnonymousUser, anon
   end
-  
+
+  should_have_one :rss_token
+
   def test_rss_key
     assert_nil @jsmith.rss_token
     key = @jsmith.rss_key
@@ -135,7 +137,55 @@ class UserTest < ActiveSupport::TestCase
     @jsmith.reload
     assert_equal key, @jsmith.rss_key
   end
+
   
+  should_have_one :api_token
+
+  context "User#api_key" do
+    should "generate a new one if the user doesn't have one" do
+      user = User.generate_with_protected!(:api_token => nil)
+      assert_nil user.api_token
+
+      key = user.api_key
+      assert_equal 40, key.length
+      user.reload
+      assert_equal key, user.api_key
+    end
+
+    should "return the existing api token value" do
+      user = User.generate_with_protected!
+      token = Token.generate!(:action => 'api')
+      user.api_token = token
+      assert user.save
+      
+      assert_equal token.value, user.api_key
+    end
+  end
+
+  context "User#find_by_api_key" do
+    should "return nil if no matching key is found" do
+      assert_nil User.find_by_api_key('zzzzzzzzz')
+    end
+
+    should "return nil if the key is found for an inactive user" do
+      user = User.generate_with_protected!(:status => User::STATUS_LOCKED)
+      token = Token.generate!(:action => 'api')
+      user.api_token = token
+      user.save
+
+      assert_nil User.find_by_api_key(token.value)
+    end
+
+    should "return the user if the key is found for an active user" do
+      user = User.generate_with_protected!(:status => User::STATUS_ACTIVE)
+      token = Token.generate!(:action => 'api')
+      user.api_token = token
+      user.save
+      
+      assert_equal user, User.find_by_api_key(token.value)
+    end
+  end
+
   def test_roles_for_project
     # user with a role
     roles = @jsmith.roles_for_project(Project.find(1))