Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use the Devise gem in a Rails app where the "User" is split up between three models?

Say I have the following in a Rails 4 app:

class Person < ActiveRecord::Base
  has_many :email_addresses, as: :emailable
  has_one :user_account
end

class EmailAddress < ActiveRecord::Base
  belongs_to :emailable, polymorphic: true
  # There is an :address column
end

class UserAccount < ActiveRecord::Base
  belongs_to :person
end

A person can have multiple email addresses. A person can also have a user account. (I've moved this into it's own model because not all people would be users.) Any of the person's email addresses could be used as the "username" when logging in.

Given this, I would like to use the Devise gem. I see you can specify the model to which the authentication is applied. User is widely used, but I would be using UserAccount. However, Devise then expects the email (username) field to be in this model.

When a person registers a user account, there would actually be three associated records created (Person, EmailAddress, and UserAccount). I can't figure out how to to get Devise to work with this setup. Any ideas?

like image 234
robertwbradford Avatar asked Feb 26 '14 20:02

robertwbradford


People also ask

Does devise work with rails 7?

Our out-of-the box Devise setup is now working with Rails 7. Once again, if you'd like to refer to any of the code for this setup, or use the template wholesale for a new app, the code is available on GitHub, and you may also use it as a template repo to kick off your own Rails 7 devise projects.

What is resource in devise?

Resource is an abstraction name of instance of a user. It can be configured in devise settings to work with Admin model or any other. By default it's the first devise role declared in your routes devise :users # resource is instance of User class devise :admins # resource is instance of Admin class.


2 Answers

If you are using ActiveRecord,

First, add

attr_accessor :email

to user_account (i think this is the easiest way to deal with devise form)

next, you need to modify devise login procedure. Still, in your user_account, override the devise method such

  def self.find_for_database_authentication(warden_conditions)
    conditions = warden_conditions.dup
    if email = conditions.delete(:email)
      where(conditions.to_h).includes(:email_addresses).where(email_addresses: {email: email}).first
    else
      where(conditions.to_h).first
    end
  end

you may also need to define following to get work the code above

class UserAccount < ActiveRecord::Base
  belongs_to :person
  has_many :email_addresses, through: :person

That should just work, i have tested this using active record but if you are using mongoid, probably the solution will be different.

Note that, i have modified the code from devise's How To: Allow users to sign in using their username or email address documentation to get the solution.

like image 103
Alper Karapınar Avatar answered Oct 23 '22 17:10

Alper Karapınar


One option would be, to delegate the email method from your UserAccount to your email model and override the finder def self.find_first_by_auth_conditions(warden_conditions) used by the devise login procedure. I found a pretty nice blog post that describes this in depth and another stackoverflow answer that has the same approach. There is also a section in the docs about how to confirm a devise account with multiple emails.

As your setup is a bit more complicated, you could also maybe use EmailAddress as your primary devise model and delegate the password methods to the UserAccount.
This would potentially be useful if you have to confirm every email address with confirmable and not only the user account. This setup would you from overriding that finder, but you may run into other issues with the delegated password as is never tried that before.

like image 36
smallbutton Avatar answered Oct 23 '22 18:10

smallbutton