We have several Rails apps with duplicated user information. If you're John Smith in one of the apps, we will create a new user with the same information in the others. That's crazy, single sign-on should be the obvious solution.
In order to get there we had to disconnect the authentication mechanism from our User entity. Once we have the same separation in our apps, pulling the authentication out into a service or API should be very easy.
Also, I like to keep the User model application specific. I would much rather have fields that are relevant to our app needs in our User model and not mix devise's fields into that entity.
Here is what I had to do to disconnect devise from our User model.
First of all, I had to generate a member table with the following migration:
class AddMembers < ActiveRecord::Migration def change create_table(:members) do |t| ## Database authenticatable t.integer :user_id t.string :email, :null => false, :default => "" t.string :encrypted_password, :null => false, :default => "" t.string :first_name, :default => "" t.string :last_name, :default => "" t.string :middle_name, :default => "" ## Recoverable t.string :reset_password_token t.datetime :reset_password_sent_at ## Rememberable t.datetime :remember_created_at ## Trackable t.integer :sign_in_count, :default => 0 t.datetime :current_sign_in_at t.datetime :last_sign_in_at t.string :current_sign_in_ip t.string :last_sign_in_ip ## Confirmable t.string :confirmation_token t.datetime :confirmed_at t.datetime :confirmation_sent_at # Only if using reconfirmable t.string :unconfirmed_email ## Lockable # Only if lock strategy is :failed_attempts t.integer :failed_attempts, :default => 0 t.string :unlock_token # Only if unlock strategy is :email or :both t.datetime :locked_at ## Token authenticatable t.string :authentication_token # Uncomment below if timestamps were not included # in your original model. t.timestamps end add_index :members, :email, :unique => true add_index :members, :reset_password_token, :unique => true add_index :members, :confirmation_token, :unique => true add_index :members, :unlock_token, :unique => true add_index :members, :authentication_token, :unique => true end def self.down # By default, we don't want to make any assumption # about how to roll back a migration when your # model already existed. Please edit below which fields # you would like to remove in this migration. raise ActiveRecord::IrreversibleMigration end end
Instead of putting all the Member related code into the User model, I created a concern that can be mixed into the User model. This module can be reused in all of the Rails apps to connect the Member entity to the User.
require 'active_support/concern' require 'active_support/core_ext/module' module ModelExtensions module UserDevised extend ActiveSupport::Concern included do has_one :member validates_associated :member after_save :save_member, :if => lambda {|u| u.member } delegate :last_sign_in_at, :password, :password=, :password_confirmation, :password_confirmation=, :to => :member before_validation do self.member.first_name = self.first_name self.member.middle_name = self.middle_name self.member.last_name = self.last_name self.member.email = self.email_address end end def initialize(*params) super(*params) self.build_member(*params) if member.nil? end def save_member self.member.save! end end end
I'd like to mention a couple of things about the module you see above. I am using one-to-one relationship between User and Member. Whenever I save a User, I save the Member as well by using the after_save
callback. Admins can overwrite the users' password, I delegate those fields from User to Member, so the Member entity remains hidden behind the User. When a new User is initialized, I create a Member entity as well as you see it in the initialize
method.
Devise strategies and overrides are defined in the Member model.
class Member < ActiveRecord::Base belongs_to :user # Include default devise modules. Others available are: # :token_authenticatable, :confirmable, # :lockable, :timeoutable and :omniauthable devise :database_authenticatable, :lockable, :timeoutable, :recoverable, :trackable, :validatable, :omniauthable # non-registerable :registerable, :rememberable validates :first_name, :last_name, :presence => true def active_for_authentication? # Comment out the below debug statement to view # the properties of the returned self model values. super && user.active? end def confirmed_account? (self.last_sign_in_at.nil? == false && self.reset_password_token.nil?) end end
I remember how hard it was to find information on the separation of the application specific User and the devise Member entity when I was working on this. I hope someone will find this code helpful, it would have helped me a lot.