Created
July 15, 2011 01:49
-
-
Save rke/1083882 to your computer and use it in GitHub Desktop.
email change notification for devise based on https://github.com/Mandaryn's solution
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module Devise | |
module Models | |
# This just allows valid_password to be called if encrypted_password is blank. | |
# The code exists in devise-1.3 and later, this just backports it to out 1.2.1 | |
module DatabaseAuthenticatable | |
def valid_password?(password) | |
return false if self.encrypted_password.blank? | |
bcrypt = ::BCrypt::Password.new(self.encrypted_password) | |
password = ::BCrypt::Engine.hash_secret("#{password}#{self.class.pepper}", bcrypt.salt) | |
Devise.secure_compare(password, self.encrypted_password) | |
end | |
end | |
# We add a whole bunch of handle_asynchronously to the emailers to prevent blocking the UI | |
module Recoverable | |
handle_asynchronously :send_reset_password_instructions | |
end | |
module Lockable | |
handle_asynchronously :send_unlock_instructions | |
end | |
module Confirmable | |
# But the biggest change (and also a backport) is the sending of confirmation email when email | |
# addresses change. The old email remains valid until the new one is confirmed. | |
# 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) | |
query = record.class | |
unless record.new_record? | |
if record.respond_to?(:_id) | |
query = query.where(:_id => {'$ne' => record._id}) | |
else | |
query = query.where('id <> ?', record.id) | |
end | |
end | |
query = query.where(:unconfirmed_email => record.email) | |
query.exists? | |
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, :if => :postpone_email_change? | |
after_update :send_confirmation_instructions, :if => :email_change_confirmation_required? | |
end | |
# Confirm a user by setting it's confirmed_at to actual time. If the user | |
# is already confirmed, add an error to email field. If the user is invalid | |
# add errors | |
def confirm! | |
unless_confirmed do | |
@devise_models_confirmable_confirming = true | |
self.confirmation_token = nil | |
self.confirmed_at = Time.now | |
self.email = unconfirmed_email if unconfirmed_email.present? | |
self.unconfirmed_email = nil | |
# Need to disable validation or it complains that it needs a current_password to change the email. | |
self.save(:validate => false) | |
end | |
end | |
def postpone_email_change? | |
email_changed? && !@devise_models_confirmable_confirming | |
# was email_changed? && email != unconfirmed_email_was, but I had to comment the second condition, otherwise setting an email the second time would set it without requiring confirmation. | |
end | |
# Send confirmation instructions by email but check unconfirmed email changed to avoid triple emails | |
def send_confirmation_instructions | |
if unconfirmed_email.present? | |
if unconfirmed_email_changed? | |
send_confirmation_instructions_later | |
end | |
else | |
if email_changed? | |
send_confirmation_instructions_later | |
end | |
end | |
end | |
# Send confirmation instructions by email | |
def send_confirmation_instructions_later | |
@email_change_confirmation_required = false | |
generate_confirmation_token! if self.confirmation_token.nil? | |
if unconfirmed_email.present? | |
if self.respond_to?(:return_unconfirmed_email_as_email=) | |
self.return_unconfirmed_email_as_email = true | |
end | |
::Devise.mailer.confirmation_instructions(self).deliver | |
if self.respond_to?(:return_unconfirmed_email_as_email=) | |
self.return_unconfirmed_email_as_email = false | |
end | |
else | |
::Devise.mailer.confirmation_instructions(self).deliver | |
end | |
end | |
# we have to put this down here because we are overwriting send_confirmation_instructions above | |
handle_asynchronously :send_confirmation_instructions_later | |
protected | |
# 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? && unconfirmed_email.blank? | |
yield | |
else | |
self.errors.add(:email, :already_confirmed) | |
false | |
end | |
end | |
# Generates a new random token for confirmation, and stores the time | |
# this token is being generated | |
def generate_confirmation_token | |
self.confirmation_token = self.class.confirmation_token | |
self.confirmation_sent_at = Time.now.utc | |
end | |
def postpone_email_change | |
@email_change_confirmation_required = true | |
# I added the 'if' to fix this problem: | |
# An original email update gets put into email, but a second update got put into | |
# unconfirmed_email. What we want is to use the email field until that one is confirmed | |
# and start using unconfirmed_email only after that. | |
if confirmed? | |
self.unconfirmed_email = self.email | |
self.email = self.email_was | |
end | |
end | |
def email_change_confirmation_required? | |
@email_change_confirmation_required | |
end | |
module ClassMethods | |
# Attempt to find a user by it's email. If a record is found, send new | |
# confirmation instructions to it. If not try searching the 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_by_unconfirmed_email_with_errors(attributes) | |
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 | |
# 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 | |
end | |
end | |
module Validatable | |
def self.included(base) | |
base.extend ClassMethods | |
assert_validations_api!(base) | |
base.class_eval do | |
validates_presence_of :email, :if => :email_required? | |
validates_uniqueness_of :email, :scope => authentication_keys[1..-1], | |
:case_sensitive => (case_insensitive_keys != false), :allow_blank => true | |
validates_format_of :email, :with => email_regexp, :allow_blank => true | |
validates_with Devise::Models::Confirmable::ConfirmableValidator | |
with_options :if => :password_required? do |v| | |
v.validates_presence_of :password | |
v.validates_confirmation_of :password | |
v.validates_length_of :password, :within => password_length, :allow_blank => true | |
end | |
end | |
end | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment