On Github paneq / devise-coupling
robert @pankowecki
@arkency
flexible authentication solution for Rails based on Warden
composed of 10 modules
class User devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable, :token_authenticatable, :lockable, :timeoutable, :omniauthable, :remember_for => 6.months end
☹
User class has a tendency for becoming God-object in monolithic rails apps
wc -l app/models/user.rb 348 app/models/user.rb 528 app/models/user.rb 508 app/models/user.rb 762 app/models/user.rb
Adding devise does not help in counteracting Fact #1
Devise::Models.constants.sort.each {|c| Devise::Models.const_get(c).instance_methods.each{|m| puts "#{c}##{m}" } }; " Authenticatable#valid_for_authentication? Authenticatable#unauthenticated_message Authenticatable#active_for_authentication? Authenticatable#inactive_message Authenticatable#authenticatable_salt Authenticatable#serializable_hash Authenticatable#devise_mailer Authenticatable#send_devise_notification Authenticatable#downcase_keys Authenticatable#strip_whitespace Authenticatable#apply_to_attribute_or_variable Confirmable#confirm! Confirmable#confirmed? Confirmable#pending_reconfirmation? Confirmable#send_confirmation_instructions Confirmable#resend_confirmation_token Confirmable#active_for_authentication? Confirmable#inactive_message Confirmable#skip_confirmation! Confirmable#skip_confirmation_notification! Confirmable#skip_reconfirmation! Confirmable#send_on_create_confirmation_instructions Confirmable#confirmation_required? Confirmable#confirmation_period_valid? Confirmable#confirmation_period_expired? Confirmable#pending_any_confirmation Confirmable#generate_confirmation_token Confirmable#generate_confirmation_token! Confirmable#after_password_reset Confirmable#postpone_email_change_until_confirmation Confirmable#postpone_email_change? Confirmable#reconfirmation_required? Confirmable#send_confirmation_notification? Confirmable#distance_of_time_in_words Confirmable#time_ago_in_words Confirmable#distance_of_time_in_words_to_now Confirmable#date_select Confirmable#time_select Confirmable#datetime_select Confirmable#select_datetime Confirmable#select_date Confirmable#select_time Confirmable#select_second Confirmable#select_minute Confirmable#select_hour Confirmable#select_day Confirmable#select_month Confirmable#select_year Confirmable#time_tag DatabaseAuthenticatable#password= DatabaseAuthenticatable#valid_password? DatabaseAuthenticatable#clean_up_passwords DatabaseAuthenticatable#update_with_password DatabaseAuthenticatable#update_without_password DatabaseAuthenticatable#destroy_with_password DatabaseAuthenticatable#after_database_authentication DatabaseAuthenticatable#authenticatable_salt DatabaseAuthenticatable#password_digest Lockable#lock_strategy_enabled? Lockable#unlock_strategy_enabled? Lockable#lock_access! Lockable#unlock_access! Lockable#access_locked? Lockable#send_unlock_instructions Lockable#resend_unlock_token Lockable#active_for_authentication? Lockable#inactive_message Lockable#valid_for_authentication? Lockable#unauthenticated_message Lockable#attempts_exceeded? Lockable#generate_unlock_token Lockable#generate_unlock_token! Lockable#lock_expired? Lockable#if_access_locked Recoverable#reset_password! Recoverable#send_reset_password_instructions Recoverable#reset_password_period_valid? Recoverable#should_generate_reset_token? Recoverable#generate_reset_password_token Recoverable#generate_reset_password_token! Recoverable#clear_reset_password_token Recoverable#after_password_reset Rememberable#remember_me Rememberable#remember_me= Rememberable#extend_remember_period Rememberable#extend_remember_period= Rememberable#remember_me! Rememberable#forget_me! Rememberable#remember_expired? Rememberable#remember_expires_at Rememberable#rememberable_value Rememberable#rememberable_options Rememberable#generate_remember_token? Rememberable#generate_remember_timestamp? Timeoutable#timedout? Timeoutable#timeout_in TokenAuthenticatable#reset_authentication_token TokenAuthenticatable#reset_authentication_token! TokenAuthenticatable#ensure_authentication_token TokenAuthenticatable#ensure_authentication_token! TokenAuthenticatable#after_token_authentication TokenAuthenticatable#expire_auth_token_on_timeout Trackable#update_tracked_fields! Validatable#password_required? Validatable#email_required?"
Biggest rails apps problem: lack of modularity
class User # wannabe module A if anyone cares has_many :alphas has_many :betas has_many :gammas has_many :deltas # wannabe module B if we only had time to refactor someday has_many :sigmas has_many :epsilons has_many :lambdas # maybe on hackathon day in our company has_many :has_many # nope... # too many exception end
# ==> Configuration for :database_authenticatable # For bcrypt, this is the cost for hashing the password and defaults to 10. If # using other encryptors, it sets how many times you want the password re-encrypted. # # Limiting the stretches to just one in testing will increase the performance of # your test suite dramatically. However, it is STRONGLY RECOMMENDED to not use # a value less than 10 in other environments. config.stretches = Rails.env.test? ? 1 : 10
will increase the performance of your test suite dramatically !!!111oneoneone
will increase the performance of your test suite dramatically !!!
FactoryGirl.define do factory :user do email { generate(:email) } password { generate(:password) } end end
who coupled authentication concerns (like having password) to all our test suits that require User object (in domain sense)
or encrypted password
maybe just in Invoicing, Inventory or Digital Product moduls tests?
or two...
If you want users to update all information except the password itself, you can use update_without_password provided by Devise and then proceed to implement the views.
class RegistrationsController < Devise::RegistrationsController def update @user = User.find(current_user.id) successfully_updated = if needs_password?(@user, params) @user.update_with_password(devise_parameter_sanitizer.sanitize(:account_update)) else # remove the virtual current_password attribute # update_without_password doesn't know how to ignore it params[:user].delete(:current_password) @user.update_without_password(devise_parameter_sanitizer.sanitize(:account_update)) end if successfully_updated set_flash_message :notice, :updated # Sign in the user bypassing validation in case their password changed sign_in @user, :bypass => true redirect_to after_update_path_for(@user) else render "edit" end end private # check if we need password to update user data # ie if password or email was changed # extend this as needed def needs_password?(user, params) user.email != params[:user][:email] || params[:user][:password].present? end end
def update_with_password(params, *options) current_password = params.delete(:current_password) if params[:password].blank? params.delete(:password) params.delete(:password_confirmation) if params[:password_confirmation].blank? end result = if valid_password?(current_password) update_attributes(params, *options) else self.assign_attributes(params, *options) self.valid? self.errors.add(:current_password, current_password.blank? ? :blank : :invalid) false end clean_up_passwords result end def update_without_password(params, *options) params.delete(:password) params.delete(:password_confirmation) result = update_attributes(params, *options) clean_up_passwords result end
module Auth class UserCredentials devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable, :token_authenticatable, :lockable, :timeoutable, :omniauthable, :remember_for => 6.months end end
Authentication module does not sound so bad.
User knows nothing about possible authentication methods such as password for WEB, token per mobile device etc.
Maybe logins, passwords, tokens, emails (duplicated) belong to Auth module and the rest of your system couldn't care less about them?
Think about it next time you add devise to your User module, mkay? 😉
• 3 ways to do eager loading (preloading) in Rails 3 & 4