devise-coupling



devise-coupling

0 0


devise-coupling

Presentation about devise & coupling it to User class and tests

On Github paneq / devise-coupling

Devise & User

robert @pankowecki

@arkency

We use devise (everything?) wrong and someone must speak about it ;)

Agenda

wrong

wrong

wrong

What's devise?

flexible authentication solution for Rails based on Warden

composed of 10 modules

What do we do with Devise?

We put it in User?

class User
  devise :database_authenticatable, :registerable,
    :recoverable, :rememberable, :trackable, :validatable,
    :token_authenticatable, :lockable, :timeoutable, :omniauthable,
    :remember_for => 6.months
end

Fact #1

User class has a tendency for becoming God-object in monolithic rails apps

Oh god...

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

Fact #2

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?"

Opinion #1

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

Opinion #2: Wrong modularity

Fact #3

# ==> 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

WHY?

will increase the performance of your test suite dramatically !!!

We are all fools!

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)

How many of your tests does actually care for the user password?

or encrypted password

Can't we build/create user without password and be happy?

maybe just in Invoicing, Inventory or Digital Product moduls tests?

One more thing

or two...

GUI

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.

From wiki

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

From devise code

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

Maybe separate view for updating credentials wouldn't be that bad?

So what am I trying to say ... ?

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.

Authentication knows which user was authenticated

User knows nothing about possible authentication methods such as password for WEB, token per mobile device etc.

This is probably what you have...

This is what you might consider having...

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? 😉

Read more on this topic

Did you like this presentation? Share it with your friends!

Share on twitter

Share on Facebook

Share on Google+

Share on LinkedIn

People also liked reading from us

• 3 ways to do eager loading (preloading) in Rails 3 & 4

• Value Objects and Aggregates in Active Record

• Developers Oriented Project Management

Subscribe to our Arkency newsletter to get more content like this

Thank you