diff --git a/app/controllers/api_keys_controller.rb b/app/controllers/api_keys_controller.rb
new file mode 100644
index 000000000..2e6596f7e
--- /dev/null
+++ b/app/controllers/api_keys_controller.rb
@@ -0,0 +1,17 @@
+class ApiKeysController < ApplicationController
+ before_filter :gold_only
+
+ def new
+ @api_key = ApiKey.new(:user_id => CurrentUser.user.id)
+ end
+
+ def create
+ @api_key = ApiKey.generate!(CurrentUser.user)
+
+ if @api_key.errors.empty?
+ redirect_to user_path(CurrentUser.user), :notice => "API key generated"
+ else
+ render :action => "new"
+ end
+ end
+end
diff --git a/app/logical/session_loader.rb b/app/logical/session_loader.rb
index 9de3cab00..3040743a5 100644
--- a/app/logical/session_loader.rb
+++ b/app/logical/session_loader.rb
@@ -55,18 +55,18 @@ private
end
def authenticate_api_key(name, api_key)
- CurrentUser.user = User.authenticate_cookie_hash(name, api_key)
CurrentUser.ip_addr = request.remote_ip
+ CurrentUser.user = User.authenticate_api_key(name, api_key)
end
def authenticate_legacy_api_key(name, password_hash)
- CurrentUser.user = User.authenticate_hash(name, password_hash)
CurrentUser.ip_addr = request.remote_ip
+ CurrentUser.user = User.authenticate_hash(name, password_hash)
end
def load_session_user
- CurrentUser.user = User.find_by_id(session[:user_id])
CurrentUser.ip_addr = request.remote_ip
+ CurrentUser.user = User.find_by_id(session[:user_id])
end
def load_cookie_user
diff --git a/app/models/api_key.rb b/app/models/api_key.rb
new file mode 100644
index 000000000..58ac9ba8a
--- /dev/null
+++ b/app/models/api_key.rb
@@ -0,0 +1,10 @@
+class ApiKey < ActiveRecord::Base
+ belongs_to :user
+ validates_uniqueness_of :user_id
+ validates_uniqueness_of :key
+ attr_accessible :user_id, :key
+
+ def self.generate!(user)
+ create(:user_id => user.id, :key => SecureRandom.urlsafe_base64(32))
+ end
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index 281761464..c68a4b2dd 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -59,6 +59,7 @@ class User < ActiveRecord::Base
has_many :posts, :foreign_key => "uploader_id"
has_many :bans, lambda {order("bans.id desc")}
has_one :recent_ban, lambda {order("bans.id desc")}, :class_name => "Ban"
+ has_one :api_key
has_many :subscriptions, lambda {order("tag_subscriptions.name")}, :class_name => "TagSubscription", :foreign_key => "creator_id"
has_many :note_versions, :foreign_key => "updater_id"
has_many :dmails, lambda {order("dmails.id desc")}, :foreign_key => "owner_id"
@@ -192,6 +193,15 @@ class User < ActiveRecord::Base
authenticate_hash(name, sha1(pass))
end
+ def authenticate_api_key(name, api_key)
+ key = ApiKey.where(:key => api_key).first
+ return nil if key.nil?
+ user = find_by_name(name)
+ return nil if user.nil?
+ return user if key.user_id == user.id
+ nil
+ end
+
def authenticate_hash(name, hash)
user = find_by_name(name)
if user && user.bcrypt_password == hash
@@ -531,9 +541,9 @@ class User < ActiveRecord::Base
end
def api_hourly_limit
- if is_platinum?
+ if is_platinum? && api_key.present?
20_000
- elsif is_gold?
+ elsif is_gold? && api_key.present?
10_000
else
3_000
diff --git a/app/views/api_keys/new.html.erb b/app/views/api_keys/new.html.erb
new file mode 100644
index 000000000..5fd33dd4f
--- /dev/null
+++ b/app/views/api_keys/new.html.erb
@@ -0,0 +1,19 @@
+
+
+
New API Key
+
+
You can generate a new API key to authenticate against <%= Danbooru.config.app_name %>.
+
+ <%= error_messages_for :api_key %>
+
+ <%= simple_form_for(@api_key) do |f| %>
+ <%= submit_tag "Generate" %>
+ <% end %>
+
+
+
+<%= render "users/secondary_links" %>
+
+<% content_for(:page_title) do %>
+ New API Key - <%= Danbooru.config.app_name %>
+<% end %>
diff --git a/app/views/users/_statistics.html.erb b/app/views/users/_statistics.html.erb
index 732c1c2ea..e5895f249 100644
--- a/app/views/users/_statistics.html.erb
+++ b/app/views/users/_statistics.html.erb
@@ -131,7 +131,13 @@
<% if CurrentUser.user.id == user.id %>
| API Key |
- <%= CurrentUser.user.bcrypt_cookie_password_hash %> |
+
+ <% if CurrentUser.user.api_key %>
+ <%= CurrentUser.user.api_key.key %>
+ <% else %>
+ <%= link_to "Generate key", new_api_key_path %>
+ <% end %>
+ |
<% end %>
diff --git a/config/routes.rb b/config/routes.rb
index 10c61566b..04e78e39d 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -59,6 +59,7 @@ Rails.application.routes.draw do
resources :advertisements do
resources :hits, :controller => "advertisement_hits", :only => [:create]
end
+ resources :api_keys, :only => [:new, :create]
resources :artists do
member do
put :revert
diff --git a/db/migrate/20140722225753_create_api_keys.rb b/db/migrate/20140722225753_create_api_keys.rb
new file mode 100644
index 000000000..a9f161658
--- /dev/null
+++ b/db/migrate/20140722225753_create_api_keys.rb
@@ -0,0 +1,13 @@
+class CreateApiKeys < ActiveRecord::Migration
+ def change
+ create_table :api_keys do |t|
+ t.integer :user_id, :null => false
+ t.string :key, :null => false
+
+ t.timestamps
+ end
+
+ add_index :api_keys, :user_id, :unique => true
+ add_index :api_keys, :key, :unique => true
+ end
+end
diff --git a/db/structure.sql b/db/structure.sql
index 660244f05..c2f6f6fa9 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -515,6 +515,38 @@ CREATE SEQUENCE amazon_backups_id_seq
ALTER SEQUENCE amazon_backups_id_seq OWNED BY amazon_backups.id;
+--
+-- Name: api_keys; Type: TABLE; Schema: public; Owner: -; Tablespace:
+--
+
+CREATE TABLE api_keys (
+ id integer NOT NULL,
+ user_id integer NOT NULL,
+ key character varying(255) NOT NULL,
+ created_at timestamp without time zone,
+ updated_at timestamp without time zone
+);
+
+
+--
+-- Name: api_keys_id_seq; Type: SEQUENCE; Schema: public; Owner: -
+--
+
+CREATE SEQUENCE api_keys_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+
+--
+-- Name: api_keys_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
+--
+
+ALTER SEQUENCE api_keys_id_seq OWNED BY api_keys.id;
+
+
--
-- Name: artist_commentaries; Type: TABLE; Schema: public; Owner: -; Tablespace:
--
@@ -3124,6 +3156,13 @@ ALTER TABLE ONLY advertisements ALTER COLUMN id SET DEFAULT nextval('advertiseme
ALTER TABLE ONLY amazon_backups ALTER COLUMN id SET DEFAULT nextval('amazon_backups_id_seq'::regclass);
+--
+-- Name: id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY api_keys ALTER COLUMN id SET DEFAULT nextval('api_keys_id_seq'::regclass);
+
+
--
-- Name: id; Type: DEFAULT; Schema: public; Owner: -
--
@@ -4149,6 +4188,14 @@ ALTER TABLE ONLY amazon_backups
ADD CONSTRAINT amazon_backups_pkey PRIMARY KEY (id);
+--
+-- Name: api_keys_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace:
+--
+
+ALTER TABLE ONLY api_keys
+ ADD CONSTRAINT api_keys_pkey PRIMARY KEY (id);
+
+
--
-- Name: artist_commentaries_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace:
--
@@ -4514,6 +4561,20 @@ CREATE INDEX index_advertisement_hits_on_created_at ON advertisement_hits USING
CREATE INDEX index_advertisements_on_ad_type ON advertisements USING btree (ad_type);
+--
+-- Name: index_api_keys_on_key; Type: INDEX; Schema: public; Owner: -; Tablespace:
+--
+
+CREATE UNIQUE INDEX index_api_keys_on_key ON api_keys USING btree (key);
+
+
+--
+-- Name: index_api_keys_on_user_id; Type: INDEX; Schema: public; Owner: -; Tablespace:
+--
+
+CREATE UNIQUE INDEX index_api_keys_on_user_id ON api_keys USING btree (user_id);
+
+
--
-- Name: index_artist_commentaries_on_post_id; Type: INDEX; Schema: public; Owner: -; Tablespace:
--
@@ -6131,13 +6192,6 @@ CREATE INDEX index_forum_posts_on_topic_id ON forum_posts USING btree (topic_id)
CREATE INDEX index_forum_topic_visits_on_forum_topic_id ON forum_topic_visits USING btree (forum_topic_id);
---
--- Name: index_forum_topic_visits_on_last_read_at; Type: INDEX; Schema: public; Owner: -; Tablespace:
---
-
-CREATE INDEX index_forum_topic_visits_on_last_read_at ON forum_topic_visits USING btree (last_read_at);
-
-
--
-- Name: index_forum_topic_visits_on_user_id; Type: INDEX; Schema: public; Owner: -; Tablespace:
--
@@ -6936,3 +6990,5 @@ INSERT INTO schema_migrations (version) VALUES ('20140613004559');
INSERT INTO schema_migrations (version) VALUES ('20140701224800');
+INSERT INTO schema_migrations (version) VALUES ('20140722225753');
+
diff --git a/test/functional/api_keys_controller_test.rb b/test/functional/api_keys_controller_test.rb
new file mode 100644
index 000000000..7238ec572
--- /dev/null
+++ b/test/functional/api_keys_controller_test.rb
@@ -0,0 +1,36 @@
+require 'test_helper'
+
+class ApiKeysControllerTest < ActionController::TestCase
+ context "An api keys controller" do
+ setup do
+ @user = FactoryGirl.create(:gold_user)
+ end
+
+ context "#new" do
+ should "render" do
+ get :new, {}, {:user_id => @user.id}
+ assert_response :success
+ end
+ end
+
+ context "#create" do
+ should "succeed" do
+ assert_difference("ApiKey.count", 1) do
+ post :create, {}, {:user_id => @user.id}
+ end
+ end
+
+ context "when an api key already exists" do
+ setup do
+ ApiKey.generate!(@user)
+ end
+
+ should "not create another api key" do
+ assert_difference("ApiKey.count", 0) do
+ post :create, {}, {:user_id => @user.id}
+ end
+ end
+ end
+ end
+ end
+end
\ No newline at end of file
diff --git a/test/functional/posts_controller_test.rb b/test/functional/posts_controller_test.rb
index c1d1df1bf..0c4ae7629 100644
--- a/test/functional/posts_controller_test.rb
+++ b/test/functional/posts_controller_test.rb
@@ -19,15 +19,16 @@ class PostsControllerTest < ActionController::TestCase
context "passing the api limit" do
setup do
User.any_instance.stubs(:api_hourly_limit).returns(5)
+ ApiKey.generate!(@user)
end
should "work" do
CurrentUser.user.api_hourly_limit.times do
- get :index, {:format => "json", :login => @user.name, :api_key => @user.bcrypt_cookie_password_hash}
+ get :index, {:format => "json", :login => @user.name, :api_key => @user.api_key.key}
assert_response :success
end
- get :index, {:format => "json", :login => @user.name, :api_key => @user.bcrypt_cookie_password_hash}
+ get :index, {:format => "json", :login => @user.name, :api_key => @user.api_key.key}
assert_response 421
end
end
diff --git a/test/unit/api_key_test.rb b/test/unit/api_key_test.rb
new file mode 100644
index 000000000..e681cd115
--- /dev/null
+++ b/test/unit/api_key_test.rb
@@ -0,0 +1,23 @@
+require 'test_helper'
+
+class ApiKeyTest < ActiveSupport::TestCase
+ context "in all cases a user" do
+ setup do
+ @user = FactoryGirl.create(:user, :name => "abcdef")
+ @api_key = ApiKey.generate!(@user)
+ @user.name.mb_chars.downcase
+ end
+
+ should "authenticate via api key" do
+ assert_not_nil(User.authenticate_api_key(@user.name, @api_key.key))
+ end
+
+ should "not authenticate with the wrong api key" do
+ assert_nil(User.authenticate_api_key(@user.name, "xxx"))
+ end
+
+ should "not authenticate with the wrong name" do
+ assert_nil(User.authenticate_api_key("xxx", @api_key.key))
+ end
+ end
+end