mirror of
https://github.com/heartcombo/devise.git
synced 2022-11-09 12:18:31 -05:00
Merge branch 'reconfirm'
This commit is contained in:
commit
03d9ebb56e
15 changed files with 320 additions and 20 deletions
|
@ -1,7 +1,12 @@
|
|||
== 1.6.0 (unreleased)
|
||||
|
||||
* enhancements
|
||||
* Add support for e-mail reconfirmation on change (by github.com/Mandaryn and github.com/heimidal)
|
||||
|
||||
== 1.5.2
|
||||
|
||||
* enhancements
|
||||
* Add support for rails 3.1 new mass assignment conventions (by github.com/kirs)
|
||||
* Add support for Rails 3.1 new mass assignment conventions (by github.com/kirs)
|
||||
* Add timeout_in method to Timeoutable, it can be overriden in a model (by github.com/lest)
|
||||
|
||||
* bug fix
|
||||
|
|
|
@ -41,7 +41,12 @@ class Devise::RegistrationsController < ApplicationController
|
|||
self.resource = resource_class.to_adapter.get!(send(:"current_#{resource_name}").to_key)
|
||||
|
||||
if resource.update_with_password(params[resource_name])
|
||||
set_flash_message :notice, :updated if is_navigational_format?
|
||||
if is_navigational_format?
|
||||
if resource.respond_to?(:pending_reconfirmation?) && resource.pending_reconfirmation?
|
||||
flash_key = :update_needs_confirmation
|
||||
end
|
||||
set_flash_message :notice, flash_key || :updated
|
||||
end
|
||||
sign_in resource_name, resource, :bypass => true
|
||||
respond_with resource, :location => after_update_path_for(resource)
|
||||
else
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<p>Welcome <%= @resource.email %>!</p>
|
||||
|
||||
<p>You can confirm your account through the link below:</p>
|
||||
<p>You can confirm your account email through the link below:</p>
|
||||
|
||||
<p><%= link_to 'Confirm my account', confirmation_url(@resource, :confirmation_token => @resource.confirmation_token) %></p>
|
||||
|
|
|
@ -37,6 +37,7 @@ en:
|
|||
signed_up: 'Welcome! You have signed up successfully.'
|
||||
inactive_signed_up: 'You have signed up successfully. However, we could not sign you in because your account is %{reason}.'
|
||||
updated: 'You updated your account successfully.'
|
||||
update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and click on the confirm link to finalize your new email address."
|
||||
destroyed: 'Bye! Your account was successfully cancelled. We hope to see you again soon.'
|
||||
reasons:
|
||||
inactive: 'inactive'
|
||||
|
|
|
@ -141,6 +141,9 @@ module Devise
|
|||
mattr_accessor :confirmation_keys
|
||||
@@confirmation_keys = [ :email ]
|
||||
|
||||
mattr_accessor :reconfirmable
|
||||
@@reconfirmable = false
|
||||
|
||||
# Time interval to timeout the user session without activity.
|
||||
mattr_accessor :timeout_in
|
||||
@@timeout_in = 30.minutes
|
||||
|
|
|
@ -14,6 +14,11 @@ module Devise
|
|||
# use this to let your user access some features of your application without
|
||||
# confirming the account, but blocking it after a certain period (ie 7 days).
|
||||
# By default confirm_within is zero, it means users always have to confirm to sign in.
|
||||
# * +reconfirmable+: requires any email changes to be confirmed (exactly the same way as
|
||||
# initial account confirmation) to be applied. Requires additional unconfirmed_email
|
||||
# db field to be setup (t.reconfirmable in migrations). Until confirmed new email is
|
||||
# stored in unconfirmed email column, and copied to email column on successful
|
||||
# confirmation.
|
||||
#
|
||||
# == Examples
|
||||
#
|
||||
|
@ -27,15 +32,26 @@ module Devise
|
|||
included do
|
||||
before_create :generate_confirmation_token, :if => :confirmation_required?
|
||||
after_create :send_confirmation_instructions, :if => :confirmation_required?
|
||||
before_update :postpone_email_change_until_confirmation, :if => :postpone_email_change?
|
||||
after_update :send_confirmation_instructions, :if => :reconfirmation_required?
|
||||
end
|
||||
|
||||
# Confirm a user by setting its confirmed_at to actual time. If the user
|
||||
# is already confirmed, add en error to email field
|
||||
# Confirm a user by setting it's confirmed_at to actual time. If the user
|
||||
# is already confirmed, add en error to email field. If the user is invalid
|
||||
# add errors
|
||||
def confirm!
|
||||
unless_confirmed do
|
||||
self.confirmation_token = nil
|
||||
self.confirmed_at = Time.now.utc
|
||||
save(:validate => false)
|
||||
|
||||
if self.class.reconfirmable
|
||||
@bypass_postpone = true
|
||||
self.email = unconfirmed_email if unconfirmed_email.present?
|
||||
self.unconfirmed_email = nil
|
||||
save
|
||||
else
|
||||
save(:validate => false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -44,8 +60,13 @@ module Devise
|
|||
!!confirmed_at
|
||||
end
|
||||
|
||||
def pending_reconfirmation?
|
||||
self.class.reconfirmable && unconfirmed_email.present?
|
||||
end
|
||||
|
||||
# Send confirmation instructions by email
|
||||
def send_confirmation_instructions
|
||||
@reconfirmation_required = false
|
||||
generate_confirmation_token! if self.confirmation_token.nil?
|
||||
self.devise_mailer.confirmation_instructions(self).deliver
|
||||
end
|
||||
|
@ -74,6 +95,14 @@ module Devise
|
|||
self.confirmed_at = Time.now.utc
|
||||
end
|
||||
|
||||
def headers_for(action)
|
||||
if action == :confirmation_instructions && respond_to?(:unconfirmed_email)
|
||||
{ :to => unconfirmed_email.present? ? unconfirmed_email : email }
|
||||
else
|
||||
{}
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# Callback to overwrite if confirmation is required or not.
|
||||
|
@ -104,10 +133,10 @@ module Devise
|
|||
confirmation_sent_at && confirmation_sent_at.utc >= self.class.confirm_within.ago
|
||||
end
|
||||
|
||||
# Checks whether the record is confirmed or not, yielding to the block
|
||||
# Checks whether the record is confirmed or not or a new email has been added, yielding to the block
|
||||
# if it's already confirmed, otherwise adds an error to email.
|
||||
def unless_confirmed
|
||||
unless confirmed?
|
||||
unless confirmed? && !pending_reconfirmation?
|
||||
yield
|
||||
else
|
||||
self.errors.add(:email, :already_confirmed)
|
||||
|
@ -118,7 +147,6 @@ module Devise
|
|||
# Generates a new random token for confirmation, and stores the time
|
||||
# this token is being generated
|
||||
def generate_confirmation_token
|
||||
self.confirmed_at = nil
|
||||
self.confirmation_token = self.class.confirmation_token
|
||||
self.confirmation_sent_at = Time.now.utc
|
||||
end
|
||||
|
@ -132,13 +160,32 @@ module Devise
|
|||
confirm! unless confirmed?
|
||||
end
|
||||
|
||||
def postpone_email_change_until_confirmation
|
||||
@reconfirmation_required = true
|
||||
self.unconfirmed_email = self.email
|
||||
self.email = self.email_was
|
||||
end
|
||||
|
||||
def postpone_email_change?
|
||||
postpone = self.class.reconfirmable && email_changed? && !@bypass_postpone
|
||||
@bypass_postpone = nil
|
||||
postpone
|
||||
end
|
||||
|
||||
def reconfirmation_required?
|
||||
self.class.reconfirmable && @reconfirmation_required
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
# Attempt to find a user by its email. If a record is found, send new
|
||||
# confirmation instructions to it. If not user is found, returns a new user
|
||||
# with an email not found error.
|
||||
# confirmation instructions to it. If not, try searching for a user by unconfirmed_email
|
||||
# field. If no user is found, returns a new user with an email not found error.
|
||||
# Options must contain the user email
|
||||
def send_confirmation_instructions(attributes={})
|
||||
confirmable = find_or_initialize_with_errors(confirmation_keys, attributes, :not_found)
|
||||
confirmable = find_by_unconfirmed_email_with_errors(attributes) if reconfirmable
|
||||
unless confirmable.try(:persisted?)
|
||||
confirmable = find_or_initialize_with_errors(confirmation_keys, attributes, :not_found)
|
||||
end
|
||||
confirmable.resend_confirmation_token if confirmable.persisted?
|
||||
confirmable
|
||||
end
|
||||
|
@ -158,7 +205,15 @@ module Devise
|
|||
generate_token(:confirmation_token)
|
||||
end
|
||||
|
||||
Devise::Models.config(self, :confirm_within, :confirmation_keys)
|
||||
# Find a record for confirmation by unconfirmed email field
|
||||
def find_by_unconfirmed_email_with_errors(attributes = {})
|
||||
unconfirmed_required_attributes = confirmation_keys.map { |k| k == :email ? :unconfirmed_email : k }
|
||||
unconfirmed_attributes = attributes.symbolize_keys
|
||||
unconfirmed_attributes[:unconfirmed_email] = unconfirmed_attributes.delete(:email)
|
||||
find_or_initialize_with_errors(unconfirmed_required_attributes, unconfirmed_attributes, :not_found)
|
||||
end
|
||||
|
||||
Devise::Models.config(self, :confirm_within, :confirmation_keys, :reconfirmable)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -23,7 +23,6 @@ module Devise
|
|||
# Checks whether the user session has expired based on configured time.
|
||||
def timedout?(last_access)
|
||||
return false if remember_exists_and_not_expired?
|
||||
|
||||
!timeout_in.nil? && last_access && last_access <= timeout_in.ago
|
||||
end
|
||||
|
||||
|
@ -35,7 +34,6 @@ module Devise
|
|||
|
||||
def remember_exists_and_not_expired?
|
||||
return false unless respond_to?(:remember_expired?)
|
||||
|
||||
remember_created_at && !remember_expired?
|
||||
end
|
||||
|
||||
|
|
|
@ -40,6 +40,11 @@ module Devise
|
|||
apply_devise_schema :confirmation_sent_at, DateTime
|
||||
end
|
||||
|
||||
# Creates unconfirmed_email
|
||||
def reconfirmable
|
||||
apply_devise_schema :unconfirmed_email, String
|
||||
end
|
||||
|
||||
# Creates reset_password_token and reset_password_sent_at.
|
||||
#
|
||||
# == Options
|
||||
|
|
|
@ -79,6 +79,12 @@ Devise.setup do |config|
|
|||
# the user cannot access the website without confirming his account.
|
||||
# config.confirm_within = 2.days
|
||||
|
||||
# If true, requires any email changes to be confirmed (exctly the same way as
|
||||
# initial account confirmation) to be applied. Requires additional unconfirmed_email
|
||||
# db field (see migrations). Until confirmed new email is stored in
|
||||
# unconfirmed email column, and copied to email column on successful confirmation.
|
||||
# config.reconfirmable = false
|
||||
|
||||
# Defines which key will be used when confirming an account
|
||||
# config.confirmation_keys = [ :email ]
|
||||
|
||||
|
|
|
@ -201,3 +201,62 @@ class ConfirmationTest < ActionController::IntegrationTest
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
class ConfirmationOnChangeTest < ConfirmationTest
|
||||
|
||||
def create_second_user(options={})
|
||||
@user = nil
|
||||
create_user(options)
|
||||
end
|
||||
|
||||
def setup
|
||||
add_unconfirmed_email_column
|
||||
Devise.reconfirmable = true
|
||||
end
|
||||
|
||||
def teardown
|
||||
remove_unconfirmed_email_column
|
||||
Devise.reconfirmable = false
|
||||
end
|
||||
|
||||
test 'user should be able to request a new confirmation after email changed' do
|
||||
user = create_user(:confirm => true)
|
||||
user.update_attributes(:email => 'new_test@example.com')
|
||||
|
||||
visit new_user_session_path
|
||||
click_link "Didn't receive confirmation instructions?"
|
||||
|
||||
fill_in 'email', :with => user.unconfirmed_email
|
||||
assert_difference "ActionMailer::Base.deliveries.size" do
|
||||
click_button 'Resend confirmation instructions'
|
||||
end
|
||||
|
||||
assert_current_url '/users/sign_in'
|
||||
assert_contain 'You will receive an email with instructions about how to confirm your account in a few minutes'
|
||||
end
|
||||
|
||||
test 'user with valid confirmation token should be able to confirm email after email changed' do
|
||||
user = create_user(:confirm => true)
|
||||
user.update_attributes(:email => 'new_test@example.com')
|
||||
assert 'new_test@example.com', user.unconfirmed_email
|
||||
visit_user_confirmation_with_token(user.confirmation_token)
|
||||
|
||||
assert_contain 'Your account was successfully confirmed.'
|
||||
assert_current_url '/'
|
||||
assert user.reload.confirmed?
|
||||
assert_not user.reload.pending_reconfirmation?
|
||||
end
|
||||
|
||||
test 'user email should be unique also within unconfirmed_email' do
|
||||
user = create_user(:confirm => true)
|
||||
user.update_attributes(:email => 'new_test@example.com')
|
||||
assert 'new_test@example.com', user.unconfirmed_email
|
||||
|
||||
create_second_user(:email => "new_test@example.com")
|
||||
|
||||
visit_user_confirmation_with_token(user.confirmation_token)
|
||||
assert_have_selector '#error_explanation'
|
||||
assert_contain /Email.*already.*taken/
|
||||
assert user.reload.pending_reconfirmation?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -291,3 +291,45 @@ class RegistrationTest < ActionController::IntegrationTest
|
|||
assert_equal User.count, 0
|
||||
end
|
||||
end
|
||||
|
||||
class ReconfirmableRegistrationTest < ActionController::IntegrationTest
|
||||
def setup
|
||||
add_unconfirmed_email_column
|
||||
Devise.reconfirmable = true
|
||||
end
|
||||
|
||||
def teardown
|
||||
remove_unconfirmed_email_column
|
||||
Devise.reconfirmable = false
|
||||
end
|
||||
|
||||
test 'a signed in user should see a more appropriate flash message when editing his account if reconfirmable is enabled' do
|
||||
sign_in_as_user
|
||||
get edit_user_registration_path
|
||||
|
||||
fill_in 'email', :with => 'user.new@example.com'
|
||||
fill_in 'current password', :with => '123456'
|
||||
click_button 'Update'
|
||||
|
||||
assert_current_url '/'
|
||||
assert_contain 'You updated your account successfully, but we need to verify your new email address. Please check your email and click on the confirm link to finalize your new email address.'
|
||||
|
||||
assert_equal "user.new@example.com", User.first.unconfirmed_email
|
||||
end
|
||||
|
||||
test 'A signed in user should not see a reconfirmation message if they did not change their password' do
|
||||
sign_in_as_user
|
||||
get edit_user_registration_path
|
||||
|
||||
fill_in 'password', :with => 'pas123'
|
||||
fill_in 'password confirmation', :with => 'pas123'
|
||||
fill_in 'current password', :with => '123456'
|
||||
click_button 'Update'
|
||||
|
||||
assert_current_url '/'
|
||||
assert_contain 'You updated your account successfully.'
|
||||
|
||||
assert User.first.valid_password?('pas123')
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -80,8 +80,8 @@ class ConfirmableTest < ActiveSupport::TestCase
|
|||
end
|
||||
|
||||
test 'should send confirmation instructions by email' do
|
||||
assert_email_sent do
|
||||
create_user
|
||||
assert_email_sent "mynewuser@example.com" do
|
||||
create_user :email => "mynewuser@example.com"
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -123,7 +123,7 @@ class ConfirmableTest < ActiveSupport::TestCase
|
|||
|
||||
test 'should send email instructions for the user confirm its email' do
|
||||
user = create_user
|
||||
assert_email_sent do
|
||||
assert_email_sent user.email do
|
||||
User.send_confirmation_instructions(:email => user.email)
|
||||
end
|
||||
end
|
||||
|
@ -236,3 +236,99 @@ class ConfirmableTest < ActiveSupport::TestCase
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
class ReconfirmableTest < ConfirmableTest
|
||||
def setup
|
||||
add_unconfirmed_email_column
|
||||
Devise.reconfirmable = true
|
||||
end
|
||||
|
||||
def teardown
|
||||
remove_unconfirmed_email_column
|
||||
Devise.reconfirmable = false
|
||||
end
|
||||
|
||||
def test_should_not_resend_email_instructions_if_the_user_change_his_email
|
||||
#behaves differently
|
||||
end
|
||||
|
||||
def test_should_not_reset_confirmation_status_or_token_when_updating_email
|
||||
#behaves differently
|
||||
end
|
||||
|
||||
test 'should generate confirmation token after changing email' do
|
||||
user = create_user
|
||||
assert user.confirm!
|
||||
assert_nil user.confirmation_token
|
||||
assert user.update_attributes(:email => 'new_test@example.com')
|
||||
assert_not_nil user.confirmation_token
|
||||
end
|
||||
|
||||
test 'should send confirmation instructions by email after changing email' do
|
||||
user = create_user
|
||||
assert user.confirm!
|
||||
assert_email_sent "new_test@example.com" do
|
||||
assert user.update_attributes(:email => 'new_test@example.com')
|
||||
end
|
||||
end
|
||||
|
||||
test 'should not send confirmation by email after changing password' do
|
||||
user = create_user
|
||||
assert user.confirm!
|
||||
assert_email_not_sent do
|
||||
assert user.update_attributes(:password => 'newpass', :password_confirmation => 'newpass')
|
||||
end
|
||||
end
|
||||
|
||||
test 'should stay confirmed when email is changed' do
|
||||
user = create_user
|
||||
assert user.confirm!
|
||||
assert user.update_attributes(:email => 'new_test@example.com')
|
||||
assert user.confirmed?
|
||||
end
|
||||
|
||||
test 'should update email only when it is confirmed' do
|
||||
user = create_user
|
||||
assert user.confirm!
|
||||
assert user.update_attributes(:email => 'new_test@example.com')
|
||||
assert_not_equal 'new_test@example.com', user.email
|
||||
assert user.confirm!
|
||||
assert_equal 'new_test@example.com', user.email
|
||||
end
|
||||
|
||||
test 'should not allow user to get past confirmation email by resubmitting their new address' do
|
||||
user = create_user
|
||||
assert user.confirm!
|
||||
assert user.update_attributes(:email => 'new_test@example.com')
|
||||
assert_not_equal 'new_test@example.com', user.email
|
||||
assert user.update_attributes(:email => 'new_test@example.com')
|
||||
assert_not_equal 'new_test@example.com', user.email
|
||||
end
|
||||
|
||||
test 'should find a user by send confirmation instructions with unconfirmed_email' do
|
||||
user = create_user
|
||||
assert user.confirm!
|
||||
assert user.update_attributes(:email => 'new_test@example.com')
|
||||
confirmation_user = User.send_confirmation_instructions(:email => user.unconfirmed_email)
|
||||
assert_equal confirmation_user, user
|
||||
end
|
||||
|
||||
test 'should return a new user if no email or unconfirmed_email was found' do
|
||||
confirmation_user = User.send_confirmation_instructions(:email => "invalid@email.com")
|
||||
assert_not confirmation_user.persisted?
|
||||
end
|
||||
|
||||
test 'should add error to new user email if no email or unconfirmed_email was found' do
|
||||
confirmation_user = User.send_confirmation_instructions(:email => "invalid@email.com")
|
||||
assert confirmation_user.errors[:email]
|
||||
assert_equal "not found", confirmation_user.errors[:email].join
|
||||
end
|
||||
|
||||
test 'should find user with email in unconfirmed_emails' do
|
||||
user = create_user
|
||||
user.unconfirmed_email = "new_test@email.com"
|
||||
assert user.save
|
||||
user = User.find_by_unconfirmed_email_with_errors(:email => "new_test@email.com")
|
||||
assert user.persisted?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -16,7 +16,7 @@ class SerializableTest < ActiveSupport::TestCase
|
|||
end
|
||||
|
||||
test 'should include unsafe keys on XML if a force_except is provided' do
|
||||
assert_no_match /email/, @user.to_xml(:force_except => :email)
|
||||
assert_no_match /<email/, @user.to_xml(:force_except => :email)
|
||||
assert_match /confirmation-token/, @user.to_xml(:force_except => :email)
|
||||
end
|
||||
|
||||
|
|
|
@ -14,8 +14,11 @@ class ActiveSupport::TestCase
|
|||
end
|
||||
alias :assert_present :assert_not_blank
|
||||
|
||||
def assert_email_sent(&block)
|
||||
def assert_email_sent(address = nil, &block)
|
||||
assert_difference('ActionMailer::Base.deliveries.size') { yield }
|
||||
if address.present?
|
||||
assert_equal address, ActionMailer::Base.deliveries.last['to'].to_s
|
||||
end
|
||||
end
|
||||
|
||||
def assert_email_not_sent(&block)
|
||||
|
|
|
@ -84,4 +84,26 @@ class ActiveSupport::TestCase
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: Get rid of this in favor of a real model with unconfirmed
|
||||
def add_unconfirmed_email_column
|
||||
if DEVISE_ORM == :active_record
|
||||
ActiveRecord::Base.connection.add_column(:users, :unconfirmed_email, :string)
|
||||
User.reset_column_information
|
||||
elsif DEVISE_ORM == :mongoid
|
||||
User.field(:unconfirmed_email, :type => String)
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: Get rid of this in favor of a real model with unconfirmed
|
||||
def remove_unconfirmed_email_column
|
||||
if DEVISE_ORM == :active_record
|
||||
ActiveRecord::Base.connection.remove_column(:users, :unconfirmed_email)
|
||||
User.reset_column_information
|
||||
elsif DEVISE_ORM == :mongoid
|
||||
User.fields.delete(:unconfirmed_email)
|
||||
User.send(:undefine_attribute_methods)
|
||||
User.send(:define_attribute_methods, User.fields.keys)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue