14

I'm looking for a solution to allow a user on my app to have more than 1 email. This should work similar to Facebook, LinkedIn and Quora. Where an account can have multiple emails, 1 as the primary.

Is there a turn-key solution for devise avaialble? I'm hoping to not have to write this from scratch given it's so common.

Ideas? Thanks

AnApprentice
  • 108,152
  • 195
  • 629
  • 1,012
  • Do you want to enable sign-in with those secondary emails too, for any user? – kiddorails Apr 29 '13 at 17:50
  • Yes, a user should only be able to sign in with any of their emails. Also an email should not be able to co-exist, unique emails per accounts across the entire system... Just like how Facebook handles this... – AnApprentice Apr 29 '13 at 17:51
  • I'm not aware of an existing solution to this, but if you decide to write it yourself, you might start with this, which is at least similar (multiple possible login values): https://github.com/plataformatec/devise/wiki/How-To:-Allow-users-to-sign-in-using-their-username-or-email-address – MrTheWalrus Apr 29 '13 at 18:05

7 Answers7

15

Hm...I'll suggest to create new model, You'll do something like this:

For example model would beUserEmail.

class UserEmail < ActiveRecord::Base
    belongs_to :user
end

class User < ActiveRecord::Base
    has_many :user_emails
end

and override devise's method to find record in User model:

def self.find_first_by_auth_conditions(warden_conditions)
  conditions = warden_conditions.dup
  if email = conditions.delete(:email)
    User.includes(:user_emails).where('user_emails.email = ?', email).first
  else
    super(warden_conditions)
  end

Read more about override here: https://github.com/plataformatec/devise/wiki/How-To:-Allow-users-to-sign-in-using-their-username-or-email-address

Bob
  • 2,081
  • 18
  • 25
  • This will fail (in a potentially insecure way) for other Devise lookups, such as finding a user by their confirmation token or unlock token. See my answer for a fix. – Brad May 15 '13 at 20:42
  • How do you deal with password recovery? what do I do if i want to let the user choose which email he want the link to be sent to? – Gady May 16 '13 at 14:22
  • As I know, to reset password user must fill his email in form. And you will deliver message if email exists. – Bob May 16 '13 at 18:13
8

Bob's answer is close but it has a potentially dangerous bug. The find_first_by_auth_conditions method should look like this:

def self.find_first_by_auth_conditions(warden_conditions)
  conditions = warden_conditions.dup
  if email = conditions.delete(:email)
    User.includes(:user_emails).where('user_emails.email = ?', email).first
  else
    super(warden_conditions)
  end
end

The email check and call to super is important, because Devise also uses this method to look up Users by confirmation_token, unlock_token, and reset_token. Without the else branch, any calls to this method that don't have an email parameter will either fail, or load up a User with a nil email.

Brad
  • 937
  • 1
  • 9
  • 23
4

I would suggest you to create a new model SecondaryEmail.
A User can has_many :secondary_emails, and each SecondaryEmail belongs_to :user.

You will have to add the validation of uniqueness for each email in SecondaryEmail, and further, will have to make sure that no new SecondaryEmail is already a primary email of any User.
Provide the interface, so that a User can add his secondary_emails, with those validations.

Next step will be overriding the SessionController of Devise.
Upon any login procedure, set up your login procedure for SecondaryEmail.where(:email => params[:email]) whenever an email is not found in User's primary emails. If it exists, authenticate with that user's password, else, user doesn't exist.

This is what I came up with so far. I would really love to know the experts' view and approach in this. :)

kiddorails
  • 12,961
  • 2
  • 32
  • 41
3

I came across this problem awhile ago - and have outlined the solution in my blog .

The steps are summarized below:

  • Email information has to be stored in a additional model, say Email. So a User has many related Email instances.
  • We would most likely want to designate one email as the default email which we can use for communication with the user.
  • The class method of the User class find_first_by_auth_conditions is used to find the user for provided credentials - so we will have to override that method to search using the Email model.
  • We will have to change the default views because they implicitly assume the presence of email method of user. Rather than rewriting the views entirely we can use add a proxy accessor for email that delegates to the default email for the user.
  • If a user is allowed to have multiple emails - we would also like to provide the user facility to add/remove emails in the account edit page.
  • If you need omniauth integration - then the stock implementation of User.from_omniauth outlined in the devise wiki will have to be modified to support multiple emails.

My implementation is available on Github.

lorefnon
  • 12,875
  • 6
  • 61
  • 93
1

The answers given here is pretty good, and should solve your problem. To answer your last question, i don't think you will find a gem for this, as it's too simple even though it's pretty common. Just make it from scratch as suggested, it should not be to much work :)

jokklan
  • 3,520
  • 17
  • 37
1

Bob's answer threw a SQL error for me in rails 4.1.5- the below change got it working, per http://guides.rubyonrails.org/active_record_querying.html#specifying-conditions-on-eager-loaded-associations:

def self.find_first_by_auth_conditions(warden_conditions)
  conditions = warden_conditions.dup
  if email = conditions.delete(:email)
    User.includes(:user_emails).where(user_emails: {email: email}).first
  else
    super(warden_conditions)
  end
end
Jake
  • 36
  • 2
0

You can use the devise-multi_email gem: https://github.com/allenwq/devise-multi_email

Replace devise :authenticatable with devise :multi_email_authenticatable, so that your models might look like:

class User < ActiveRecord::Base
  has_many :emails

  # Replace :database_authenticatable, with :multi_email_authenticatable
  devise :multi_email_authenticatable, :registerable
end

class Email < ActiveRecord::Base
  belongs_to :user
end

And if you want all the emails to be confirmable and recover your password from anyone of your emails, just

devise :multi_email_authenticatable, :multi_email_confirmable, :confirmable

Allen
  • 11
  • 5