From 78dff9121e793906460b0c27ba5a19eb764b3499 Mon Sep 17 00:00:00 2001 From: tomomichi Date: Mon, 14 Mar 2016 22:55:29 +0900 Subject: [PATCH 01/11] Add magic login feature --- .../sorcery/templates/initializer.rb | 51 +++++++ .../templates/migration/magic_login.rb | 9 ++ lib/sorcery.rb | 1 + lib/sorcery/model/submodules/magic_login.rb | 128 ++++++++++++++++++ 4 files changed, 189 insertions(+) create mode 100644 lib/generators/sorcery/templates/migration/magic_login.rb create mode 100644 lib/sorcery/model/submodules/magic_login.rb diff --git a/lib/generators/sorcery/templates/initializer.rb b/lib/generators/sorcery/templates/initializer.rb index 8a736c3f..237e1f56 100644 --- a/lib/generators/sorcery/templates/initializer.rb +++ b/lib/generators/sorcery/templates/initializer.rb @@ -381,6 +381,57 @@ # user.reset_password_time_between_emails = + # -- magic_login -- + # magic login code attribute name. + # Default: `:magic_login_token` + # + # user.magic_login_token_attribute_name = + + + # expires at attribute name. + # Default: `:magic_login_token_expires_at` + # + # user.magic_login_token_expires_at_attribute_name = + + + # when was email sent, used for hammering protection. + # Default: `:magic_login_email_sent_at` + # + # user.magic_login_email_sent_at_attribute_name = + + + # mailer class. Needed. + # Default: `nil` + # + # user.magic_login_mailer = + + + # magic login email method on your mailer class. + # Default: `:magic_login_email` + # + # user.magic_login_email_method_name = + + + # when true sorcery will not automatically + # email magic login details and allow you to + # manually handle how and when email is sent + # Default: `false` + # + # user.magic_login_mailer_disabled = + + + # how many seconds before the request expires. nil for never expires. + # Default: `nil` + # + # user.magic_login_expiration_period = + + + # hammering protection, how long in seconds to wait before allowing another email to be sent. + # Default: `5 * 60` + # + # user.magic_login_time_between_emails = + + # -- brute_force_protection -- # Failed logins attribute name. # Default: `:failed_logins_count` diff --git a/lib/generators/sorcery/templates/migration/magic_login.rb b/lib/generators/sorcery/templates/migration/magic_login.rb new file mode 100644 index 00000000..a1a4082f --- /dev/null +++ b/lib/generators/sorcery/templates/migration/magic_login.rb @@ -0,0 +1,9 @@ +class SorceryMagicLogin < ActiveRecord::Migration + def change + add_column :<%= model_class_name.tableize %>, :magic_login_token, :string, :default => nil + add_column :<%= model_class_name.tableize %>, :magic_login_token_expires_at, :datetime, :default => nil + add_column :<%= model_class_name.tableize %>, :magic_login_email_sent_at, :datetime, :default => nil + + add_index :<%= model_class_name.tableize %>, :magic_login_token + end +end diff --git a/lib/sorcery.rb b/lib/sorcery.rb index e9e90d8e..a4c2e8ff 100644 --- a/lib/sorcery.rb +++ b/lib/sorcery.rb @@ -20,6 +20,7 @@ module Submodules require 'sorcery/model/submodules/activity_logging' require 'sorcery/model/submodules/brute_force_protection' require 'sorcery/model/submodules/external' + require 'sorcery/model/submodules/magic_login' end end diff --git a/lib/sorcery/model/submodules/magic_login.rb b/lib/sorcery/model/submodules/magic_login.rb new file mode 100644 index 00000000..1efe7f1a --- /dev/null +++ b/lib/sorcery/model/submodules/magic_login.rb @@ -0,0 +1,128 @@ +module Sorcery + module Model + module Submodules + # This submodule adds the ability to login via email without password. + # When the user requests an email is sent to him with a url. + # The url includes a token, which is also saved with the user's record in the db. + # The token has configurable expiration. + # When the user clicks the url in the email, providing the token has not yet expired, + # he will be able to login. + # + # When using this submodule, supplying a mailer is mandatory. + module MagicLogin + def self.included(base) + base.sorcery_config.class_eval do + attr_accessor :magic_login_token_attribute_name, # magic login code attribute name. + :magic_login_token_expires_at_attribute_name, # expires at attribute name. + :magic_login_email_sent_at_attribute_name, # when was email sent, used for hammering + # protection. + + :magic_login_mailer, # mailer class. Needed. + + :magic_login_mailer_disabled, # when true sorcery will not automatically + # email magic login details and allow you to + # manually handle how and when email is sent + + :magic_login_email_method_name, # magic login email method on your + # mailer class. + + :magic_login_expiration_period, # how many seconds before the request + # expires. nil for never expires. + + :magic_login_time_between_emails # hammering protection, how long to wait + # before allowing another email to be sent. + + end + + base.sorcery_config.instance_eval do + @defaults.merge!(:@magic_login_token_attribute_name => :magic_login_token, + :@magic_login_token_expires_at_attribute_name => :magic_login_token_expires_at, + :@magic_login_email_sent_at_attribute_name => :magic_login_email_sent_at, + :@magic_login_mailer => nil, + :@magic_login_mailer_disabled => false, + :@magic_login_email_method_name => :magic_login_email, + :@magic_login_expiration_period => 15 * 60, + :@magic_login_time_between_emails => 5 * 60 ) + + reset! + end + + base.extend(ClassMethods) + + base.sorcery_config.after_config << :validate_mailer_defined + base.sorcery_config.after_config << :define_magic_login_fields + + base.send(:include, InstanceMethods) + + end + + module ClassMethods + # Find user by token, also checks for expiration. + # Returns the user if token found and is valid. + def load_from_magic_login_token(token) + token_attr_name = @sorcery_config.magic_login_token_attribute_name + token_expiration_date_attr = @sorcery_config.magic_login_token_expires_at_attribute_name + load_from_token(token, token_attr_name, token_expiration_date_attr) + end + + protected + + # This submodule requires the developer to define his own mailer class to be used by it + # when magic_login_mailer_disabled is false + def validate_mailer_defined + msg = "To use magic_login submodule, you must define a mailer (config.magic_login_mailer = YourMailerClass)." + raise ArgumentError, msg if @sorcery_config.magic_login_mailer == nil and @sorcery_config.magic_login_mailer_disabled == false + end + + def define_magic_login_fields + sorcery_adapter.define_field sorcery_config.magic_login_token_attribute_name, String + sorcery_adapter.define_field sorcery_config.magic_login_token_expires_at_attribute_name, Time + sorcery_adapter.define_field sorcery_config.magic_login_email_sent_at_attribute_name, Time + end + + end + + module InstanceMethods + # generates a reset code with expiration + def generate_magic_login_token! + config = sorcery_config + attributes = {config.magic_login_token_attribute_name => TemporaryToken.generate_random_token, + config.magic_login_email_sent_at_attribute_name => Time.now.in_time_zone} + attributes[config.magic_login_token_expires_at_attribute_name] = Time.now.in_time_zone + config.magic_login_expiration_period if config.magic_login_expiration_period + + self.sorcery_adapter.update_attributes(attributes) + end + + # generates a magic login code with expiration and sends an email to the user. + def deliver_magic_login_instructions! + mail = false + config = sorcery_config + # hammering protection + return false if config.magic_login_time_between_emails.present? && self.send(config.magic_login_email_sent_at_attribute_name) && self.send(config.magic_login_email_sent_at_attribute_name) > config.magic_login_time_between_emails.seconds.ago.utc + self.class.sorcery_adapter.transaction do + generate_magic_login_token! + mail = send_magic_login_email! unless config.magic_login_mailer_disabled + end + mail + end + + # Clears the token. + def clear_magic_login_token! + config = sorcery_config + self.sorcery_adapter.update_attributes({ + config.magic_login_token_attribute_name => nil, + config.magic_login_token_expires_at_attribute_name => nil + }) + end + + protected + + def send_magic_login_email! + generic_send_email(:magic_login_email_method_name, :magic_login_mailer) + end + end + + end + end + end +end From fbd9dd32342753852427b285d7cb20f16faf3b09 Mon Sep 17 00:00:00 2001 From: Josh Buker Date: Wed, 19 Oct 2016 14:18:51 -0700 Subject: [PATCH 02/11] Use `.nil?` instead of `== nil` --- lib/sorcery/model/submodules/magic_login.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/sorcery/model/submodules/magic_login.rb b/lib/sorcery/model/submodules/magic_login.rb index 1efe7f1a..d414bde1 100644 --- a/lib/sorcery/model/submodules/magic_login.rb +++ b/lib/sorcery/model/submodules/magic_login.rb @@ -71,7 +71,7 @@ def load_from_magic_login_token(token) # when magic_login_mailer_disabled is false def validate_mailer_defined msg = "To use magic_login submodule, you must define a mailer (config.magic_login_mailer = YourMailerClass)." - raise ArgumentError, msg if @sorcery_config.magic_login_mailer == nil and @sorcery_config.magic_login_mailer_disabled == false + raise ArgumentError, msg if @sorcery_config.magic_login_mailer.nil? and @sorcery_config.magic_login_mailer_disabled == false end def define_magic_login_fields From 675e69607d42f282aef81a155e962c44a56eeed5 Mon Sep 17 00:00:00 2001 From: Yusuke Ebihara Date: Mon, 25 Sep 2017 00:38:48 +0900 Subject: [PATCH 03/11] Prepare for setting up database - create a migration file for spec - create a spec to be run and a shared_expmple file --- spec/active_record/user_magic_login_spec.rb | 15 +++++++++++++++ .../20170924151831_add_magic_login_to_users.rb | 17 +++++++++++++++++ .../user_magic_login_shared_examples.rb | 4 ++++ 3 files changed, 36 insertions(+) create mode 100644 spec/active_record/user_magic_login_spec.rb create mode 100644 spec/rails_app/db/migrate/magic_login/20170924151831_add_magic_login_to_users.rb create mode 100644 spec/shared_examples/user_magic_login_shared_examples.rb diff --git a/spec/active_record/user_magic_login_spec.rb b/spec/active_record/user_magic_login_spec.rb new file mode 100644 index 00000000..ea2e86aa --- /dev/null +++ b/spec/active_record/user_magic_login_spec.rb @@ -0,0 +1,15 @@ +require 'spec_helper' +require 'shared_examples/user_magic_login_shared_examples' + +describe User, 'with magic_login submodule', active_record: true do + before(:all) do + ActiveRecord::Migrator.migrate("#{Rails.root}/db/migrate/magic_login") + User.reset_column_information + end + + after(:all) do + ActiveRecord::Migrator.rollback("#{Rails.root}/db/migrate/magic_login") + end + + it_behaves_like 'magic_login_model' +end diff --git a/spec/rails_app/db/migrate/magic_login/20170924151831_add_magic_login_to_users.rb b/spec/rails_app/db/migrate/magic_login/20170924151831_add_magic_login_to_users.rb new file mode 100644 index 00000000..33196a6c --- /dev/null +++ b/spec/rails_app/db/migrate/magic_login/20170924151831_add_magic_login_to_users.rb @@ -0,0 +1,17 @@ +class AddMagicLoginToUsers < ActiveRecord::CompatibleLegacyMigration.migration_class + def self.up + add_column :users, :magic_login_token, :string, default: nil + add_column :users, :magic_login_token_expires_at, :datetime, default: nil + add_column :users, :magic_login_email_sent_at, :datetime, default: nil + + add_index :users, :magic_login_token + end + + def self.down + remove_index :users, :magic_login_token + + remove_column :users, :magic_login_token + remove_column :users, :magic_login_token_expires_at + remove_column :users, :magic_login_email_sent_at + end +end diff --git a/spec/shared_examples/user_magic_login_shared_examples.rb b/spec/shared_examples/user_magic_login_shared_examples.rb new file mode 100644 index 00000000..09268c90 --- /dev/null +++ b/spec/shared_examples/user_magic_login_shared_examples.rb @@ -0,0 +1,4 @@ +shared_examples_for 'magic_login_model' do + it "test" do + end +end \ No newline at end of file From 4406b15355d3a601bf2dea7d294ea58bc3442c46 Mon Sep 17 00:00:00 2001 From: Yusuke Ebihara Date: Sun, 1 Oct 2017 14:46:02 +0900 Subject: [PATCH 04/11] Add configration tests change the default of `@magic_login_mailer_disabled` into true because the default breaks the tests --- .../sorcery/templates/initializer.rb | 2 +- lib/sorcery/model/submodules/magic_login.rb | 2 +- .../user_magic_login_shared_examples.rb | 63 ++++++++++++++++++- 3 files changed, 62 insertions(+), 5 deletions(-) diff --git a/lib/generators/sorcery/templates/initializer.rb b/lib/generators/sorcery/templates/initializer.rb index 879450de..6c7f2e17 100644 --- a/lib/generators/sorcery/templates/initializer.rb +++ b/lib/generators/sorcery/templates/initializer.rb @@ -381,7 +381,7 @@ # when true sorcery will not automatically # email magic login details and allow you to # manually handle how and when email is sent - # Default: `false` + # Default: `true` # # user.magic_login_mailer_disabled = diff --git a/lib/sorcery/model/submodules/magic_login.rb b/lib/sorcery/model/submodules/magic_login.rb index d414bde1..51f711a5 100644 --- a/lib/sorcery/model/submodules/magic_login.rb +++ b/lib/sorcery/model/submodules/magic_login.rb @@ -39,7 +39,7 @@ def self.included(base) :@magic_login_token_expires_at_attribute_name => :magic_login_token_expires_at, :@magic_login_email_sent_at_attribute_name => :magic_login_email_sent_at, :@magic_login_mailer => nil, - :@magic_login_mailer_disabled => false, + :@magic_login_mailer_disabled => true, :@magic_login_email_method_name => :magic_login_email, :@magic_login_expiration_period => 15 * 60, :@magic_login_time_between_emails => 5 * 60 ) diff --git a/spec/shared_examples/user_magic_login_shared_examples.rb b/spec/shared_examples/user_magic_login_shared_examples.rb index 09268c90..38a520c5 100644 --- a/spec/shared_examples/user_magic_login_shared_examples.rb +++ b/spec/shared_examples/user_magic_login_shared_examples.rb @@ -1,4 +1,61 @@ -shared_examples_for 'magic_login_model' do - it "test" do +shared_examples_for "magic_login_model" do + let(:user) {create_new_user} + before(:each) do + User.sorcery_adapter.delete_all end -end \ No newline at end of file + + context 'loaded plugin configuration' do + let(:config) {User.sorcery_config} + + before(:all) do + sorcery_reload!([:magic_login]) + end + + after(:each) do + User.sorcery_config.reset! + end + + describe "enables configuration options" do + it do + sorcery_model_property_set(:magic_login_token_attribute_name, :test_magic_login_token) + expect(config.magic_login_token_attribute_name).to eq :test_magic_login_token + end + + it do + sorcery_model_property_set(:magic_login_token_expires_at_attribute_name, :test_magic_login_token_expires_at) + expect(config.magic_login_token_expires_at_attribute_name).to eq :test_magic_login_token_expires_at + end + + it do + sorcery_model_property_set(:magic_login_email_sent_at_attribute_name, :test_magic_login_email_sent_at) + expect(config.magic_login_email_sent_at_attribute_name).to eq :test_magic_login_email_sent_at + end + + it do + TestMailerClass = Class.new # need a mailer class to test + sorcery_model_property_set(:magic_login_mailer, TestMailerClass) + expect(config.magic_login_mailer).to eq TestMailerClass + end + + it do + sorcery_model_property_set(:magic_login_mailer_disabled, false) + expect(config.magic_login_mailer_disabled).to eq false + end + + it do + sorcery_model_property_set(:magic_login_email_method_name, :test_magic_login_email) + expect(config.magic_login_email_method_name).to eq :test_magic_login_email + end + + it do + sorcery_model_property_set(:magic_login_expiration_period, 100000000) + expect(config.magic_login_expiration_period).to eq 100000000 + end + + it do + sorcery_model_property_set(:magic_login_time_between_emails, 100000000) + expect(config.magic_login_time_between_emails).to eq 100000000 + end + end + end +end From ecb5e8122055e0d8ad77baef77a380283f49c007 Mon Sep 17 00:00:00 2001 From: Yusuke Ebihara Date: Sun, 1 Oct 2017 14:48:40 +0900 Subject: [PATCH 05/11] Change the configuration key, magic_login_mailer into magic_login_mailer_class --- lib/generators/sorcery/templates/initializer.rb | 2 +- lib/sorcery/model/submodules/magic_login.rb | 10 +++++----- .../user_magic_login_shared_examples.rb | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/generators/sorcery/templates/initializer.rb b/lib/generators/sorcery/templates/initializer.rb index 6c7f2e17..e25e861a 100644 --- a/lib/generators/sorcery/templates/initializer.rb +++ b/lib/generators/sorcery/templates/initializer.rb @@ -369,7 +369,7 @@ # mailer class. Needed. # Default: `nil` # - # user.magic_login_mailer = + # user.magic_login_mailer_class = # magic login email method on your mailer class. diff --git a/lib/sorcery/model/submodules/magic_login.rb b/lib/sorcery/model/submodules/magic_login.rb index 51f711a5..c1653b4a 100644 --- a/lib/sorcery/model/submodules/magic_login.rb +++ b/lib/sorcery/model/submodules/magic_login.rb @@ -17,7 +17,7 @@ def self.included(base) :magic_login_email_sent_at_attribute_name, # when was email sent, used for hammering # protection. - :magic_login_mailer, # mailer class. Needed. + :magic_login_mailer_class, # mailer class. Needed. :magic_login_mailer_disabled, # when true sorcery will not automatically # email magic login details and allow you to @@ -38,7 +38,7 @@ def self.included(base) @defaults.merge!(:@magic_login_token_attribute_name => :magic_login_token, :@magic_login_token_expires_at_attribute_name => :magic_login_token_expires_at, :@magic_login_email_sent_at_attribute_name => :magic_login_email_sent_at, - :@magic_login_mailer => nil, + :@magic_login_mailer_class => nil, :@magic_login_mailer_disabled => true, :@magic_login_email_method_name => :magic_login_email, :@magic_login_expiration_period => 15 * 60, @@ -70,8 +70,8 @@ def load_from_magic_login_token(token) # This submodule requires the developer to define his own mailer class to be used by it # when magic_login_mailer_disabled is false def validate_mailer_defined - msg = "To use magic_login submodule, you must define a mailer (config.magic_login_mailer = YourMailerClass)." - raise ArgumentError, msg if @sorcery_config.magic_login_mailer.nil? and @sorcery_config.magic_login_mailer_disabled == false + msg = "To use magic_login submodule, you must define a mailer (config.magic_login_mailer_class = YourMailerClass)." + raise ArgumentError, msg if @sorcery_config.magic_login_mailer_class.nil? and @sorcery_config.magic_login_mailer_disabled == false end def define_magic_login_fields @@ -118,7 +118,7 @@ def clear_magic_login_token! protected def send_magic_login_email! - generic_send_email(:magic_login_email_method_name, :magic_login_mailer) + generic_send_email(:magic_login_email_method_name, :magic_login_mailer_class) end end diff --git a/spec/shared_examples/user_magic_login_shared_examples.rb b/spec/shared_examples/user_magic_login_shared_examples.rb index 38a520c5..e0095e37 100644 --- a/spec/shared_examples/user_magic_login_shared_examples.rb +++ b/spec/shared_examples/user_magic_login_shared_examples.rb @@ -33,8 +33,8 @@ it do TestMailerClass = Class.new # need a mailer class to test - sorcery_model_property_set(:magic_login_mailer, TestMailerClass) - expect(config.magic_login_mailer).to eq TestMailerClass + sorcery_model_property_set(:magic_login_mailer_class, TestMailerClass) + expect(config.magic_login_mailer_class).to eq TestMailerClass end it do From 0ed9a42f7a723a6d72ceedd15044f499296fab3e Mon Sep 17 00:00:00 2001 From: Yusuke Ebihara Date: Tue, 3 Oct 2017 00:36:21 +0900 Subject: [PATCH 06/11] Add specs of `.generate_magic_login_token` --- .../user_magic_login_shared_examples.rb | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/spec/shared_examples/user_magic_login_shared_examples.rb b/spec/shared_examples/user_magic_login_shared_examples.rb index e0095e37..a06c85e6 100644 --- a/spec/shared_examples/user_magic_login_shared_examples.rb +++ b/spec/shared_examples/user_magic_login_shared_examples.rb @@ -57,5 +57,25 @@ expect(config.magic_login_time_between_emails).to eq 100000000 end end + + describe "#generate_magic_login_token!" do + context "magic_login_token is nil" do + it do + token_before = user.magic_login_token + user.generate_magic_login_token! + expect(user.magic_login_token).not_to eq token_before + expect(user.magic_login_token_expires_at).not_to be_nil + expect(user.magic_login_email_sent_at).not_to be_nil + end + end + + context "magic_login_token is not nil" do + it "changes `user.magic_login_token`" do + token_before = user.magic_login_token + user.generate_magic_login_token! + expect(user.magic_login_token).not_to eq token_before + end + end + end end end From e41f618e1412284ffae7e4d285e22ac50cc45e27 Mon Sep 17 00:00:00 2001 From: Yusuke Ebihara Date: Fri, 6 Oct 2017 00:53:00 +0900 Subject: [PATCH 07/11] Add specs of `.clear_magic_login_token` --- .../user_magic_login_shared_examples.rb | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/spec/shared_examples/user_magic_login_shared_examples.rb b/spec/shared_examples/user_magic_login_shared_examples.rb index a06c85e6..974f76c3 100644 --- a/spec/shared_examples/user_magic_login_shared_examples.rb +++ b/spec/shared_examples/user_magic_login_shared_examples.rb @@ -77,5 +77,17 @@ end end end + + describe "#clear_magic_login_token!" do + it "makes magic_login_token_attribute_name and magic_login_token_expires_at_attribute_name nil" do + user.magic_login_token = "test_token" + user.magic_login_token_expires_at = Time.now + + user.clear_magic_login_token! + + expect(user.magic_login_token).to eq nil + expect(user.magic_login_token_expires_at).to eq nil + end + end end end From 49b329755414d9ef7540a20db0fb66e260968380 Mon Sep 17 00:00:00 2001 From: Yusuke Ebihara Date: Mon, 16 Oct 2017 01:12:32 +0900 Subject: [PATCH 08/11] Add faliure case specs of `.magic_login_email` --- lib/sorcery/model/submodules/magic_login.rb | 4 ++- .../user_magic_login_shared_examples.rb | 33 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/lib/sorcery/model/submodules/magic_login.rb b/lib/sorcery/model/submodules/magic_login.rb index c1653b4a..a4270fc0 100644 --- a/lib/sorcery/model/submodules/magic_login.rb +++ b/lib/sorcery/model/submodules/magic_login.rb @@ -98,7 +98,9 @@ def deliver_magic_login_instructions! mail = false config = sorcery_config # hammering protection - return false if config.magic_login_time_between_emails.present? && self.send(config.magic_login_email_sent_at_attribute_name) && self.send(config.magic_login_email_sent_at_attribute_name) > config.magic_login_time_between_emails.seconds.ago.utc + return false if !config.magic_login_time_between_emails.nil? && + self.send(config.magic_login_email_sent_at_attribute_name) && + self.send(config.magic_login_email_sent_at_attribute_name) > config.magic_login_time_between_emails.seconds.ago self.class.sorcery_adapter.transaction do generate_magic_login_token! mail = send_magic_login_email! unless config.magic_login_mailer_disabled diff --git a/spec/shared_examples/user_magic_login_shared_examples.rb b/spec/shared_examples/user_magic_login_shared_examples.rb index 974f76c3..398295e5 100644 --- a/spec/shared_examples/user_magic_login_shared_examples.rb +++ b/spec/shared_examples/user_magic_login_shared_examples.rb @@ -78,6 +78,39 @@ end end + describe "#deliver_magic_login_instructions!" do + context "failure" do + context "magic_login_time_between_emails is nil" do + it "returns false" do + sorcery_model_property_set(:magic_login_time_between_emails, nil) + expect(user.deliver_magic_login_instructions!).to eq false + end + end + + context "magic_login_email_sent_at is nil" do + it "returns false" do + user.send(:"#{config.magic_login_email_sent_at_attribute_name}=", nil) + expect(user.deliver_magic_login_instructions!).to eq false + end + end + + context "now is before magic_login_email_sent_at plus the interval" do + it "returns false" do + user.send(:"#{config.magic_login_email_sent_at_attribute_name}=", DateTime.now) + sorcery_model_property_set(:magic_login_time_between_emails, 30*60) + expect(user.deliver_magic_login_instructions!).to eq false + end + end + + context "magic_login_mailer_disabled is true" do + it "returns false" do + sorcery_model_property_set(:magic_login_mailer_disabled, true) + expect(user.deliver_magic_login_instructions!).to eq false + end + end + end + end + describe "#clear_magic_login_token!" do it "makes magic_login_token_attribute_name and magic_login_token_expires_at_attribute_name nil" do user.magic_login_token = "test_token" From 6aba7d95460bedeaf619473079ed87865b5bd357 Mon Sep 17 00:00:00 2001 From: Yusuke Ebihara Date: Fri, 20 Oct 2017 01:01:47 +0900 Subject: [PATCH 09/11] Add success case specs of `.magic_login_email` --- lib/sorcery/model/submodules/magic_login.rb | 102 +++++++++--------- spec/rails_app/app/mailers/sorcery_mailer.rb | 7 ++ .../sorcery_mailer/magic_login_email.html.erb | 13 +++ .../sorcery_mailer/magic_login_email.text.erb | 6 ++ .../user_magic_login_shared_examples.rb | 20 ++++ 5 files changed, 99 insertions(+), 49 deletions(-) create mode 100644 spec/rails_app/app/views/sorcery_mailer/magic_login_email.html.erb create mode 100644 spec/rails_app/app/views/sorcery_mailer/magic_login_email.text.erb diff --git a/lib/sorcery/model/submodules/magic_login.rb b/lib/sorcery/model/submodules/magic_login.rb index a4270fc0..b1e534cc 100644 --- a/lib/sorcery/model/submodules/magic_login.rb +++ b/lib/sorcery/model/submodules/magic_login.rb @@ -12,50 +12,50 @@ module Submodules module MagicLogin def self.included(base) base.sorcery_config.class_eval do - attr_accessor :magic_login_token_attribute_name, # magic login code attribute name. - :magic_login_token_expires_at_attribute_name, # expires at attribute name. - :magic_login_email_sent_at_attribute_name, # when was email sent, used for hammering - # protection. - - :magic_login_mailer_class, # mailer class. Needed. - - :magic_login_mailer_disabled, # when true sorcery will not automatically - # email magic login details and allow you to - # manually handle how and when email is sent - - :magic_login_email_method_name, # magic login email method on your - # mailer class. - - :magic_login_expiration_period, # how many seconds before the request - # expires. nil for never expires. - - :magic_login_time_between_emails # hammering protection, how long to wait - # before allowing another email to be sent. - + attr_accessor :magic_login_token_attribute_name, # magic login code attribute name. + :magic_login_token_expires_at_attribute_name, # expires at attribute name. + :magic_login_email_sent_at_attribute_name, # when was email sent, used for hammering + # protection. + + :magic_login_mailer_class, # mailer class. Needed. + + :magic_login_mailer_disabled, # when true sorcery will not automatically + # email magic login details and allow you to + # manually handle how and when email is sent + + :magic_login_email_method_name, # magic login email method on your + # mailer class. + + :magic_login_expiration_period, # how many seconds before the request + # expires. nil for never expires. + + :magic_login_time_between_emails # hammering protection, how long to wait + # before allowing another email to be sent. + end - + base.sorcery_config.instance_eval do - @defaults.merge!(:@magic_login_token_attribute_name => :magic_login_token, + @defaults.merge!(:@magic_login_token_attribute_name => :magic_login_token, :@magic_login_token_expires_at_attribute_name => :magic_login_token_expires_at, - :@magic_login_email_sent_at_attribute_name => :magic_login_email_sent_at, - :@magic_login_mailer_class => nil, - :@magic_login_mailer_disabled => true, - :@magic_login_email_method_name => :magic_login_email, - :@magic_login_expiration_period => 15 * 60, - :@magic_login_time_between_emails => 5 * 60 ) - + :@magic_login_email_sent_at_attribute_name => :magic_login_email_sent_at, + :@magic_login_mailer_class => nil, + :@magic_login_mailer_disabled => true, + :@magic_login_email_method_name => :magic_login_email, + :@magic_login_expiration_period => 15 * 60, + :@magic_login_time_between_emails => 5 * 60) + reset! end - + base.extend(ClassMethods) - + base.sorcery_config.after_config << :validate_mailer_defined base.sorcery_config.after_config << :define_magic_login_fields - + base.send(:include, InstanceMethods) - + end - + module ClassMethods # Find user by token, also checks for expiration. # Returns the user if token found and is valid. @@ -64,24 +64,24 @@ def load_from_magic_login_token(token) token_expiration_date_attr = @sorcery_config.magic_login_token_expires_at_attribute_name load_from_token(token, token_attr_name, token_expiration_date_attr) end - + protected - + # This submodule requires the developer to define his own mailer class to be used by it # when magic_login_mailer_disabled is false def validate_mailer_defined msg = "To use magic_login submodule, you must define a mailer (config.magic_login_mailer_class = YourMailerClass)." raise ArgumentError, msg if @sorcery_config.magic_login_mailer_class.nil? and @sorcery_config.magic_login_mailer_disabled == false end - + def define_magic_login_fields sorcery_adapter.define_field sorcery_config.magic_login_token_attribute_name, String sorcery_adapter.define_field sorcery_config.magic_login_token_expires_at_attribute_name, Time sorcery_adapter.define_field sorcery_config.magic_login_email_sent_at_attribute_name, Time end - + end - + module InstanceMethods # generates a reset code with expiration def generate_magic_login_token! @@ -89,10 +89,10 @@ def generate_magic_login_token! attributes = {config.magic_login_token_attribute_name => TemporaryToken.generate_random_token, config.magic_login_email_sent_at_attribute_name => Time.now.in_time_zone} attributes[config.magic_login_token_expires_at_attribute_name] = Time.now.in_time_zone + config.magic_login_expiration_period if config.magic_login_expiration_period - + self.sorcery_adapter.update_attributes(attributes) end - + # generates a magic login code with expiration and sends an email to the user. def deliver_magic_login_instructions! mail = false @@ -101,29 +101,33 @@ def deliver_magic_login_instructions! return false if !config.magic_login_time_between_emails.nil? && self.send(config.magic_login_email_sent_at_attribute_name) && self.send(config.magic_login_email_sent_at_attribute_name) > config.magic_login_time_between_emails.seconds.ago + self.class.sorcery_adapter.transaction do generate_magic_login_token! - mail = send_magic_login_email! unless config.magic_login_mailer_disabled + unless config.magic_login_mailer_disabled + send_magic_login_email! + mail = true + end end mail end - + # Clears the token. def clear_magic_login_token! config = sorcery_config self.sorcery_adapter.update_attributes({ - config.magic_login_token_attribute_name => nil, - config.magic_login_token_expires_at_attribute_name => nil - }) + config.magic_login_token_attribute_name => nil, + config.magic_login_token_expires_at_attribute_name => nil + }) end - + protected - + def send_magic_login_email! generic_send_email(:magic_login_email_method_name, :magic_login_mailer_class) end end - + end end end diff --git a/spec/rails_app/app/mailers/sorcery_mailer.rb b/spec/rails_app/app/mailers/sorcery_mailer.rb index 4a415d43..bf68d59e 100644 --- a/spec/rails_app/app/mailers/sorcery_mailer.rb +++ b/spec/rails_app/app/mailers/sorcery_mailer.rb @@ -28,4 +28,11 @@ def send_unlock_token_email(user) mail(to: user.email, subject: 'Your account has been locked due to many wrong logins') end + + def magic_login_email(user) + @user = user + @url = 'http://example.com/login' + mail(to: user.email, + subject: 'Magic Login') + end end diff --git a/spec/rails_app/app/views/sorcery_mailer/magic_login_email.html.erb b/spec/rails_app/app/views/sorcery_mailer/magic_login_email.html.erb new file mode 100644 index 00000000..c414ab48 --- /dev/null +++ b/spec/rails_app/app/views/sorcery_mailer/magic_login_email.html.erb @@ -0,0 +1,13 @@ + + + + + + +

Hello, <%= @user.username %>

+

+ To login without a password, just follow this link: <%= @url %>. +

+

Have a great day!

+ + \ No newline at end of file diff --git a/spec/rails_app/app/views/sorcery_mailer/magic_login_email.text.erb b/spec/rails_app/app/views/sorcery_mailer/magic_login_email.text.erb new file mode 100644 index 00000000..ee11bcb9 --- /dev/null +++ b/spec/rails_app/app/views/sorcery_mailer/magic_login_email.text.erb @@ -0,0 +1,6 @@ +Hello, <%= @user.username %> +=============================================== + +To login without a password, just follow this link: <%= @url %>. + +Have a great day! \ No newline at end of file diff --git a/spec/shared_examples/user_magic_login_shared_examples.rb b/spec/shared_examples/user_magic_login_shared_examples.rb index 398295e5..e0c8b5e3 100644 --- a/spec/shared_examples/user_magic_login_shared_examples.rb +++ b/spec/shared_examples/user_magic_login_shared_examples.rb @@ -79,6 +79,26 @@ end describe "#deliver_magic_login_instructions!" do + context "success" do + before do + sorcery_model_property_set(:magic_login_time_between_emails, 30*60) + sorcery_model_property_set(:magic_login_mailer_disabled, false) + Timecop.travel(10.days.ago) do + user.send(:"#{config.magic_login_email_sent_at_attribute_name}=", DateTime.now) + end + sorcery_model_property_set(:magic_login_mailer_class, ::SorceryMailer) + end + + it do + user.deliver_magic_login_instructions! + expect(ActionMailer::Base.deliveries.size).to eq 1 + end + + it do + expect(user.deliver_magic_login_instructions!).to eq true + end + end + context "failure" do context "magic_login_time_between_emails is nil" do it "returns false" do From 3896d8a93264718e39b1e2b3b8195e61a628f2ec Mon Sep 17 00:00:00 2001 From: Yusuke Ebihara Date: Mon, 23 Oct 2017 23:37:55 +0900 Subject: [PATCH 10/11] Refactoring: split the success case of the `.generate_magic_login_token` spec into two --- .../user_magic_login_shared_examples.rb | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/spec/shared_examples/user_magic_login_shared_examples.rb b/spec/shared_examples/user_magic_login_shared_examples.rb index e0c8b5e3..758a2526 100644 --- a/spec/shared_examples/user_magic_login_shared_examples.rb +++ b/spec/shared_examples/user_magic_login_shared_examples.rb @@ -16,7 +16,7 @@ end describe "enables configuration options" do - it do + it do sorcery_model_property_set(:magic_login_token_attribute_name, :test_magic_login_token) expect(config.magic_login_token_attribute_name).to eq :test_magic_login_token end @@ -60,13 +60,17 @@ describe "#generate_magic_login_token!" do context "magic_login_token is nil" do - it do - token_before = user.magic_login_token + it "magic_login_token_expires_at and magic_login_email_sent_at aren't nil " do user.generate_magic_login_token! - expect(user.magic_login_token).not_to eq token_before expect(user.magic_login_token_expires_at).not_to be_nil expect(user.magic_login_email_sent_at).not_to be_nil end + + it "magic_login_token is different from the one before" do + token_before = user.magic_login_token + user.generate_magic_login_token! + expect(user.magic_login_token).not_to eq token_before + end end context "magic_login_token is not nil" do From ceac23f01d82ba2062eccc812cadf019cb5ad65c Mon Sep 17 00:00:00 2001 From: Yusuke Ebihara Date: Wed, 29 Nov 2017 11:02:11 +0900 Subject: [PATCH 11/11] Fix posix compliance offence: No newline at end of file --- .../app/views/sorcery_mailer/magic_login_email.html.erb | 2 +- .../app/views/sorcery_mailer/magic_login_email.text.erb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/rails_app/app/views/sorcery_mailer/magic_login_email.html.erb b/spec/rails_app/app/views/sorcery_mailer/magic_login_email.html.erb index c414ab48..cf8243f3 100644 --- a/spec/rails_app/app/views/sorcery_mailer/magic_login_email.html.erb +++ b/spec/rails_app/app/views/sorcery_mailer/magic_login_email.html.erb @@ -10,4 +10,4 @@

Have a great day!

- \ No newline at end of file + diff --git a/spec/rails_app/app/views/sorcery_mailer/magic_login_email.text.erb b/spec/rails_app/app/views/sorcery_mailer/magic_login_email.text.erb index ee11bcb9..64be0dd2 100644 --- a/spec/rails_app/app/views/sorcery_mailer/magic_login_email.text.erb +++ b/spec/rails_app/app/views/sorcery_mailer/magic_login_email.text.erb @@ -3,4 +3,4 @@ Hello, <%= @user.username %> To login without a password, just follow this link: <%= @url %>. -Have a great day! \ No newline at end of file +Have a great day!