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
%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'
Thanks, philrosenstein.
ReplyDeleteFirst simple note while trying it:
Could't "updating_email?" just return "updating_email"?
Instead of:
if updating_email == false
return false
else
return true
end
Do (not even needed the return keyword):
updating_email
Regards.
I notice @password_reset is never set to false. Doesn't this cause the user_observer to mail out a notification on every save after a reset code is created?
ReplyDeleteThat's true!
ReplyDeleteThe email uniqueness validation is never skipped!
You are missing those two lines inside users_controller, lost_password method:
@user = User.new
@user.updating_email = false
def lost_password
case request.method
when :post
@user = User.new
@user.updating_email = false
@user.attributes = params['user']
......
And inside the user model you are missing an @ before updating_email variable:
def updating_email?
# validate_uniqueness_of email unless specifically set to false
if @updating_email == false
return false
else
return true
end
end
That was very helpful. Thanks!
ReplyDelete