Skip to content

Instantly share code, notes, and snippets.

@rke
Created July 15, 2011 01:49
Show Gist options
  • Save rke/1083882 to your computer and use it in GitHub Desktop.
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
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