Skip to content

Commit

Permalink
Merge remote-tracking branch 'heimidal/updates' into reconfirm
Browse files Browse the repository at this point in the history
Conflicts:
	lib/devise/models/confirmable.rb
	test/support/helpers.rb
  • Loading branch information
josevalim committed Dec 4, 2011
2 parents b303429 + 7f754ca commit 6d681c5
Show file tree
Hide file tree
Showing 15 changed files with 346 additions and 17 deletions.
3 changes: 2 additions & 1 deletion app/controllers/devise/registrations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ def update
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?
flash_key = :update_needs_confirmation if Devise.reconfirmable && resource.unconfirmed_email?
set_flash_message :notice, flash_key || :updated if is_navigational_format?
sign_in resource_name, resource, :bypass => true
respond_with resource, :location => after_update_path_for(resource)
else
Expand Down
2 changes: 1 addition & 1 deletion app/views/devise/mailer/confirmation_instructions.html.erb
Original file line number Diff line number Diff line change
@@ -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>
1 change: 1 addition & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
3 changes: 3 additions & 0 deletions lib/devise.rb
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,9 @@ module Strategies
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
Expand Down
92 changes: 82 additions & 10 deletions lib/devise/models/confirmable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ module Models
# 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
#
Expand All @@ -24,18 +29,50 @@ module Models
module Confirmable
extend ActiveSupport::Concern

# email uniqueness validation in unconfirmed_email column, works only if unconfirmed_email is defined on record
class ConfirmableValidator < ActiveModel::Validator
def validate(record)
if unconfirmed_email_defined?(record) && email_exists_in_unconfirmed_emails?(record)
record.errors.add(:email, :taken)
end
end

protected
def unconfirmed_email_defined?(record)
record.respond_to?(:unconfirmed_email)
end

def email_exists_in_unconfirmed_emails?(record)
count = record.class.where(:unconfirmed_email => record.email).count
expected_count = record.new_record? ? 0 : 1

count > expected_count
end
end

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 => :email_change_confirmation_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

Expand All @@ -46,6 +83,7 @@ def confirmed?

# Send confirmation instructions by email
def send_confirmation_instructions
@email_change_confirmation_required = false
generate_confirmation_token! if self.confirmation_token.nil?
self.devise_mailer.confirmation_instructions(self).deliver
end
Expand Down Expand Up @@ -74,6 +112,14 @@ def skip_confirmation!
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.
Expand Down Expand Up @@ -104,10 +150,10 @@ def confirmation_period_valid?
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? && (self.class.reconfirmable ? unconfirmed_email.blank? : true)
yield
else
self.errors.add(:email, :already_confirmed)
Expand All @@ -118,7 +164,6 @@ def unless_confirmed
# 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
Expand All @@ -132,13 +177,32 @@ def after_password_reset
confirm! unless confirmed?
end

def postpone_email_change_until_confirmation
@email_change_confirmation_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 email_change_confirmation_required?
self.class.reconfirmable && @email_change_confirmation_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
Expand All @@ -158,7 +222,15 @@ def confirmation_token
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
Expand Down
1 change: 1 addition & 0 deletions lib/devise/models/validatable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ def self.included(base)
validates_presence_of :email, :if => :email_required?
validates_uniqueness_of :email, :case_sensitive => (case_insensitive_keys != false), :allow_blank => true, :if => :email_changed?
validates_format_of :email, :with => email_regexp, :allow_blank => true, :if => :email_changed?
validates_with Devise::Models::Confirmable::ConfirmableValidator

validates_presence_of :password, :if => :password_required?
validates_confirmation_of :password, :if => :password_required?
Expand Down
5 changes: 5 additions & 0 deletions lib/devise/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ def confirmable
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
Expand Down
6 changes: 6 additions & 0 deletions lib/generators/templates/devise.rb
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@
# 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 ]

Expand Down
61 changes: 61 additions & 0 deletions test/integration/confirmable_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -201,3 +201,64 @@ def resend_confirmation
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 => '[email protected]')
ActionMailer::Base.deliveries.clear

visit new_user_session_path
click_link "Didn't receive confirmation instructions?"

fill_in 'email', :with => user.unconfirmed_email
click_button 'Resend confirmation instructions'

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'
assert_equal 1, ActionMailer::Base.deliveries.size
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 => '[email protected]')
assert '[email protected]', 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?
end

test 'user email should be unique also within unconfirmed_email' do
user = create_user(:confirm => true)
user.update_attributes(:email => '[email protected]')
assert '[email protected]', user.unconfirmed_email

get new_user_registration_path

fill_in 'email', :with => '[email protected]'
fill_in 'password', :with => 'new_user123'
fill_in 'password confirmation', :with => 'new_user123'
click_button 'Sign up'

assert_have_selector '#error_explanation'
assert_contain /Email.*already.*taken/
end
end
42 changes: 42 additions & 0 deletions test/integration/registerable_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -291,3 +291,45 @@ def user_sign_up
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 => '[email protected]'
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 "[email protected]", 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

Loading

0 comments on commit 6d681c5

Please sign in to comment.