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