diff --git a/app/models/expiring_token.rb b/app/models/expiring_token.rb new file mode 100644 index 0000000..8dc2c84 --- /dev/null +++ b/app/models/expiring_token.rb @@ -0,0 +1,28 @@ +class ExpiringToken < ActiveRecord::Base + + belongs_to :user + + validates :user, :token, :expires_at, presence: true + + scope :active, -> { where(used_at: nil).where('expires_at > ?', Time.now) } + + def self.create_for_user(user) + create!( + user: user, + token: SecureRandom.urlsafe_base64, + expires_at: 15.minutes.from_now + ) + end + + def self.active_for_token(token) + active.where(token: token).first + end + + def use! + raise "token #{token} already used at #{used_at}" if used_at + + self.used_at = Time.now + save! + end + +end diff --git a/app/models/user.rb b/app/models/user.rb index e39b56b..0e70f21 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -9,6 +9,7 @@ class User < ActiveRecord::Base has_many :api_tokens, :dependent => :destroy has_many :asciicasts, :dependent => :destroy has_many :comments, :dependent => :destroy + has_many :expiring_tokens, dependent: :destroy validates :username, uniqueness: { case_sensitive: false }, format: { with: USERNAME_FORMAT }, diff --git a/db/migrate/20141004124152_create_auth_codes.rb b/db/migrate/20141004124152_create_auth_codes.rb new file mode 100644 index 0000000..8a85638 --- /dev/null +++ b/db/migrate/20141004124152_create_auth_codes.rb @@ -0,0 +1,13 @@ +class CreateAuthCodes < ActiveRecord::Migration + def change + create_table :auth_codes do |t| + t.references :user, index: true, null: false + t.string :code, null: false + t.datetime :expires_at, null: false + + t.timestamps + end + + add_index :auth_codes, [:code, :expires_at] + end +end diff --git a/db/migrate/20141004165807_add_used_at_to_auth_codes.rb b/db/migrate/20141004165807_add_used_at_to_auth_codes.rb new file mode 100644 index 0000000..d09ae64 --- /dev/null +++ b/db/migrate/20141004165807_add_used_at_to_auth_codes.rb @@ -0,0 +1,5 @@ +class AddUsedAtToAuthCodes < ActiveRecord::Migration + def change + add_column :auth_codes, :used_at, :datetime + end +end diff --git a/db/migrate/20141004165838_remove_updated_at_from_auth_codes.rb b/db/migrate/20141004165838_remove_updated_at_from_auth_codes.rb new file mode 100644 index 0000000..f7f4de6 --- /dev/null +++ b/db/migrate/20141004165838_remove_updated_at_from_auth_codes.rb @@ -0,0 +1,5 @@ +class RemoveUpdatedAtFromAuthCodes < ActiveRecord::Migration + def change + remove_column :auth_codes, :updated_at + end +end diff --git a/db/migrate/20141004170135_update_auth_codes_index.rb b/db/migrate/20141004170135_update_auth_codes_index.rb new file mode 100644 index 0000000..f39fa2d --- /dev/null +++ b/db/migrate/20141004170135_update_auth_codes_index.rb @@ -0,0 +1,6 @@ +class UpdateAuthCodesIndex < ActiveRecord::Migration + def change + remove_index :auth_codes, name: "index_auth_codes_on_code_and_expires_at" + add_index :auth_codes, [:used_at, :expires_at, :code] + end +end diff --git a/db/migrate/20141005100915_rename_auth_codes_to_temporary_tokens.rb b/db/migrate/20141005100915_rename_auth_codes_to_temporary_tokens.rb new file mode 100644 index 0000000..4b8865c --- /dev/null +++ b/db/migrate/20141005100915_rename_auth_codes_to_temporary_tokens.rb @@ -0,0 +1,5 @@ +class RenameAuthCodesToTemporaryTokens < ActiveRecord::Migration + def change + rename_table :auth_codes, :temporary_tokens + end +end diff --git a/db/migrate/20141005101420_rename_temporary_tokens_code_to_token.rb b/db/migrate/20141005101420_rename_temporary_tokens_code_to_token.rb new file mode 100644 index 0000000..03c6927 --- /dev/null +++ b/db/migrate/20141005101420_rename_temporary_tokens_code_to_token.rb @@ -0,0 +1,5 @@ +class RenameTemporaryTokensCodeToToken < ActiveRecord::Migration + def change + rename_column :temporary_tokens, :code, :token + end +end diff --git a/db/migrate/20141005103646_rename_temporary_tokens_to_expiring_tokens.rb b/db/migrate/20141005103646_rename_temporary_tokens_to_expiring_tokens.rb new file mode 100644 index 0000000..eba3434 --- /dev/null +++ b/db/migrate/20141005103646_rename_temporary_tokens_to_expiring_tokens.rb @@ -0,0 +1,5 @@ +class RenameTemporaryTokensToExpiringTokens < ActiveRecord::Migration + def change + rename_table :temporary_tokens, :expiring_tokens + end +end diff --git a/db/schema.rb b/db/schema.rb index 243a961..4130d3d 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -72,6 +72,17 @@ ActiveRecord::Schema.define(version: 20141005152615) do add_index "comments", ["asciicast_id"], name: "index_comments_on_asciicast_id", using: :btree add_index "comments", ["user_id"], name: "index_comments_on_user_id", using: :btree + create_table "expiring_tokens", force: true do |t| + t.integer "user_id", null: false + t.string "token", null: false + t.datetime "expires_at", null: false + t.datetime "created_at" + t.datetime "used_at" + end + + add_index "expiring_tokens", ["used_at", "expires_at", "token"], name: "index_expiring_tokens_on_used_at_and_expires_at_and_token", using: :btree + add_index "expiring_tokens", ["user_id"], name: "index_expiring_tokens_on_user_id", using: :btree + create_table "likes", force: true do |t| t.integer "asciicast_id", null: false t.integer "user_id", null: false diff --git a/spec/factories/expiring_tokens.rb b/spec/factories/expiring_tokens.rb new file mode 100644 index 0000000..aa71144 --- /dev/null +++ b/spec/factories/expiring_tokens.rb @@ -0,0 +1,17 @@ +# Read about factories at http://github.com/thoughtbot/factory_girl + +FactoryGirl.define do + factory :expiring_token do + association :user + sequence(:token) { |n| "token-#{n}" } + expires_at { 10.minutes.from_now } + end + + factory :used_expiring_token, parent: :expiring_token do + used_at { 1.minute.ago } + end + + factory :expired_expiring_token, parent: :expiring_token do + expires_at { 1.minute.ago } + end +end diff --git a/spec/models/expiring_token_spec.rb b/spec/models/expiring_token_spec.rb new file mode 100644 index 0000000..e5b439b --- /dev/null +++ b/spec/models/expiring_token_spec.rb @@ -0,0 +1,47 @@ +require 'rails_helper' + +RSpec.describe ExpiringToken, :type => :model do + + it { should validate_presence_of(:user) } + it { should validate_presence_of(:token) } + it { should validate_presence_of(:expires_at) } + + describe '.create_for_user' do + it 'creates expiring token with generated token and expiration time in the future' do + user = create(:user) + + expiring_token = ExpiringToken.create_for_user(user) + + expect(expiring_token.user).to eq(user) + expect(expiring_token.token.size).to eq(22) + expect(expiring_token.expires_at).to be > Time.now + end + end + + describe '.active_for_token' do + it 'returns not used and not expired expiring token matching given token' do + used_expiring_token = create(:used_expiring_token) + expired_expiring_token = create(:expired_expiring_token) + good_expiring_token = create(:expiring_token) + + expect(ExpiringToken.active_for_token(used_expiring_token.token)).to be(nil) + expect(ExpiringToken.active_for_token(expired_expiring_token.token)).to be(nil) + expect(ExpiringToken.active_for_token(good_expiring_token.token)).to eq(good_expiring_token) + end + end + + describe '#use!' do + it 'sets used_at to the current time and saves the record' do + expiring_token = create(:expiring_token) + now = Time.now + + Timecop.freeze(now) do + expiring_token.use! + end + + expect(expiring_token.used_at).to eq(now) + expect(expiring_token).to_not be_changed + end + end + +end