validates_confirmation_of :email

Wednesday 18 August 2010

devise gem provides powerful rails authentication out of the box

I decided that it was time to switch over to authlogic now that I'm a bit more comfortable with ruby and rails, but I couldn't get past an annoying bug where my User.password virtual attribute couldn't be found, despite ensuring that I had the spelling correct for the authlogic "magic" column names, and confirming over and over that I defined attr_accessible :password

Then I stumbled upon a relatively new gem called devise, which comes with modules to satisfy all common project requirements such as automatic account locking, session timeouts, forgotten passwords and more.

After struggling with authlogic for an entire day, it was nice to have devise up and running in a matter of minutes, and see all the aforementioned functionality working out of the box.

One thing that I haven't been able to find online is a convenient reference of the attributes and methods that devise adds to your user model, so I compiled a crude one. It is not categorized by module, but it should be pretty evident by the names:

devise attributes:

confirmation_sent_at
confirmed_at
current_sign_in_ip
failed_attempts
last_sign_in_at
last_sign_in_ip
locked_at
locked_out_at
sign_in_count
unlock_token

devise methods:

access_locked?
active?
confirmation_required?
confirmed?
if_access_locked
lock!
lock_access!
locking_enabled?
lock_expired?
resend_unlock_token
reset_password!
send_reset_password_instructions
timedout?
unless_confirmed
unlock_access!

Wednesday 7 April 2010

github project howto: fork, clone, modify, request merge

I use the searchlogic gem by binarylogic for report search filters and it works great, but unfortunately it doesn't support the rails datetime_select helpers. The issue has existed on github for many months, and one user even offered a nice gist to fix the problem. Many months later and it still has not found its way into searchlogic master. So I figured its time to give back to the open source community by making it as easy as possible for Ben to incorporate the fix.

1. Fork the project on github http://help.github.com/forking/

2. Clone your fork.
2.1 If you have never done this before, you'll have to upload a private ssh key to github first. See http://help.github.com/mac-key-setup/
2.2 Then you can clone your newly-forked version of the project:

git clone git@github.com:philrosenstein/searchlogic.git

3. Run the tests. In my case I provided a relative path to spec in my rails project.

../path/to/script/spec ./spec/search_spec.rb -c

4. Make the changes locally. Using your favorite editor of course.

5. Run the tests again to make sure you haven't broken anything. You should also add new test cases if you have added functionality.

6. Commit the changes.

7. Request a merge from the project owner. If you are confident in your changes and they will make the project better for others, this is where you give back to the community that your projects have benefited so much from!

Wednesday 11 November 2009

Add a Lost Password feature to restful_authentication

This post describes how to add a lost password feature to restful_authentication. It is based off the instructions found here, adding some extra robustness and fixes for updates in the authentication plugin.

app/controllers/users_controller.rb

   
before_filter :login_required, :except => [:new, :create, :activate, :lost_password, :reset_password]
before_filter :get_partial_user_from_session, :only => [:new, :lost_password]
after_filter :save_partial_user_in_session, :only => [:new, :lost_password]
require_role "admin", :for_all_except => [:new, :create, :activate, :edit, :update, :lost_password, :reset_password]

def lost_password
case request.method
when :post
@user.attributes = params['user']
if valid_for_attributes(@user, ["email","email_confirmation"])
user = User.find_by_email(params[:user][:email])
user.create_password_reset_code if user
flash[:notice] = "Reset code sent to #{params[:user][:email]}"
redirect_back_or_default('/')
else
flash[:error] = "Please enter a valid email address"
end
when :get
@user = User.new
@user.confirming_email = true
@user.updating_email = false
end
end

def reset_password
@user = User.find_by_password_reset_code(params[:reset_code]) unless params[:reset_code].nil?
if !@user
flash[:error] = "Reset password token invalid, please contact support."
redirect_to('/')
return
else
@user.crypted_password = nil
end
if request.post?
if @user.update_attributes(:password => params[:user][:password], :password_confirmation => params[:user][:password_confirmation])
#self.current_user = @user
@user.delete_password_reset_code
flash[:notice] = "Password updated successfully for #{@user.email} - You may now log in using your new password."
redirect_back_or_default('/')
else
render :action => :reset_password
end
end
end

# Might be a good addition to AR::Base
def valid_for_attributes( model, attributes )
unless model.valid?
errors = model.errors
our_errors = Array.new
errors.each { |attr,error|
if attributes.include? attr
our_errors << [attr,error]
end
}
errors.clear
our_errors.each { |attr,error| errors.add(attr,error) }
return false unless errors.empty?
end
return true
end


app/models/user.rb

 
validates_uniqueness_of :email, :if => :updating_email?

validates_presence_of :email_confirmation, :if => :confirming_email
validates_confirmation_of :email, :if => :confirming_email

attr_accessible :login, :email, :email_confirmation

attr_accessor :updating_email, :confirming_email # allows us to control when email uniqueness and confirmation are validated (respectively)

def create_password_reset_code
@password_reset = true
self.password_reset_code = Digest::SHA1.hexdigest( Time.now.to_s.split(//).sort_by {rand}.join )
self.save(false)
end
def password_recently_reset?
@password_reset
end
def delete_password_reset_code
self.password_reset_code = nil
self.save(false)
end

protected
def updating_email?
# validate_uniqueness_of email unless specifically set to false
if updating_email == false
return false
else
return true
end
end


app/models/user_mailer.rb

 
def password_reset_notification(user)
setup_email(user)
@subject = 'Link to reset your password'
@body[:url] = "http://www.yourapp.com/reset_password/#{user.password_reset_code}"
end



app/models/user_observer.rb

 
def after_save(user)
UserMailer.deliver_password_reset_notification(user) if user.password_recently_reset?
end


app/views/sessions/new.html.haml

 = link_to " > Lost Password?", lost_password_path


config/routes.rb

   
map.lost_password '/lost_password', :controller => 'users', :action => 'lost_password'
map.reset_password 'reset_password/:reset_code', :controller => 'users', :action => 'reset_password'


users migration file:

       t.string    :password_reset_code,       :limit => 40


app/views/user_mailer/password_reset_notification.erb

Request to reset password received for <%= @user.login %>

Visit this url to choose a new password:

<%= @url %>

(Your password will remain the same if no action is taken)


app/views/users/lost_password.html.haml

%h2 Lost Password

%p
Please enter the email address that you registered with. Once you click on the Send button we will send you an email that allows you to change your password.

= error_messages_for :user, :header_message => "Please review the following errors:", :message => ""

- form_for :user do |f|
%table
%tr
%td{:align => "right"}
Email Address
%td
= f.text_field :email
%tr
%td{:align => "right"}
Retype Email Address
%td
= f.text_field :email_confirmation
%tr
%td
&nbsp;
%td
= submit_tag 'Submit'


app/views/users/reset_password.html.haml

= error_messages_for :user

%h2 Choose a new password

- form_for :user do |f|

%div
New Password:
= f.password_field :password
%div
Confirm Password:
= f.password_field :password_confirmation
%div
= submit_tag 'Change Password'