Search code examples
ruby-on-railsruby-on-rails-4devisedevise-confirmable

Storing unconfirmed email column on the sign up


I'm using Devise with Confirmable module enabled.

I realised that unconfirmed email column is only used when the user already has a confirmed email and wants to change it.

I have the following issue:

  • The user "A" signed up with a email that belongs to user "B". The user "A" will never get success on confirm his account.
  • After that, the user "B" try to sign up with your own email (already used by user "A" before). The user "B" can't sign up because your email is being used by someone else.

I think that on sign up page, the email should be stored in unconfirmed email column to avoid the behavior above.

There is a workaround to prevent this?


Solution

  • It is very interesting question. I think that the simplest way (but not sure that it is the best way) to prevent this problem is just to change email uniqueness validation and make it work only if email was confirmed.

    To do this you should disallow validatable module in your User model and implement validations manually.

    You can copy all default validations from here https://github.com/plataformatec/devise/blob/master/lib/devise/models/validatable.rb, but validates_uniqueness_of. Then implement your own email uniqueness validation:

     class User < ActiveRecord::Base
    
       # do not include :validatable module here
       devise :confirmable, :database_authenticatable, :registerable, ...  
    
       # your own validations
       ...
       validates_uniqueness_of :email, allow_blank: true, if: lambda { |u| u.email_changed? && u.confirmed? }
       ...
    

    Edited:

    My solution is not right. First of all, email can't be confirmed if it just changed. Even if my solution would work and allow user to register with existing unconfirmed email, when the user would try to confirm his email he would fail anyway.

    The right solution is:

    1) Add validation to prevent registration if email exists and confirmed.

    2) Redefine #confirme! method to don't confirm email if it exists and confirmed

    3) (not neсessary) Redefine #after_confirmation method to remove all other unconfirmed accounts with this email

    class User < ActiveRecord::Base
    
      # do not include :validatable module here
      devise :confirmable, :database_authenticatable, :registerable  # , ...
    
      validate :confirmed_email_uniqueness
    
      scope :with_confirmed_email, -> { where.not(confirmed_at: nil) }
      scope :with_unconfirmed_email, -> { where(confirmed_at: nil) }
    
      def confirm!
        return false if confirmed_email_exists?  # or add validation error, or raise some exception
        super
      end
    
      private
    
      def confirmed_email_uniqueness
        errors.add(:email, "already exists") if email_changed? && confirmed_email_exists?
      end
    
      def confirmed_email_exists?
        User.with_confirmed_email.where(email: self.email).exists?
      end
    
      protected
    
      def after_confirmation
        User.with_unconfirmed_email.where(email: self.email).destroy_all
        super
      end
    
      ...