I have used the acts_as_authenticated plugin for quite some time. It works well, and there’s lots of documentation. I’ve been happy with it. Mostly. I say mostly because it has always bothered me that acts_as_authenticated generates unRESTful code, which is tainting my perfectly RESTful application.
So, I recently evaluated the restful_authentication plugin. The plugin works great, but it is lacking the same breadth of documentation. I needed to add the ability handle a lost password and to change a password. Here’s what I did:
1. Create a new Passwords controller.
The new/create actions equate to a lost password (user wants to get a new password). The edit/update actions equate to changing a password (user wants to update their password).
When a user forgets their password and requests a new one, both their login and email will be required. If this login and email pair exists, then a new, random password will be generated and emailed.
When a user desires a new password, they are required to enter their old password in addition to typing and confirming a new password. The old password is required for security sake.
class PasswordsController < ApplicationController
before_filter :login_from_cookie
before_filter :login_required, :except => [:create]
# Don't write passwords as plain text to the log files
filter_parameter_logging :old_password, :password, :password_confirmation
# GETs should be safe
verify :method => :post, :only => [:create], :redirect_to => { :controller => :site }
verify :method => :put, :only => [:update], :redirect_to => { :controller => :site }
# POST /passwords
# Forgot password
def create
respond_to do |format|
if user = User.find_by_email_and_login(params[:email], params[:login])
@new_password = random_password
user.password = user.password_confirmation = @new_password
user.save_without_validation
UserNotifier.deliver_new_password(user, @new_password)
format.html {
flash[:notice] = "We sent a new password to #{params[:email]}"
redirect_to signin_path
}
else
flash[:notice] = "We can't find that account. Try again."
format.html { render :action => "new" }
end
end
end
# GET /users/1/password/edit
# Changing password
def edit
@user = current_user
end
# PUT /users/1/password
# Changing password
def update
@user = current_user
old_password = params[:old_password]
@user.attributes = params[:user]
respond_to do |format|
if @user.authenticated?(old_password) && @user.save
format.html { redirect_to user_path(@user) }
else
format.html { render :action => 'edit' }
end
end
end
protected
def random_password( len = 20 )
chars = (("a".."z").to_a + ("1".."9").to_a )- %w(i o 0 1 l 0)
newpass = Array.new(len, '').collect{chars[rand(chars.size)]}.join
end
end
2. Here is the change password view (edit.html.erb):
<div class="form_container">
<%= error_messages_for :user %>
<% form_for(:user, :url => user_password_path(@user), :html => { :method => :put }) do |f| %>
<fieldset>
<div class="form_row">
<label for="password">Old password</label>
<%= password_field_tag :old_password %></div>
<div class="form_row">
<label for="password">New password</label>
<%= f.password_field :password %></div>
<div class="form_row">
<label for="password_confirmation">Retype the new password</label>
<%= f.password_field :password_confirmation %></div>
<div class="submit_row">
<%= f.submit "Update", :class => "submit" %> or <%= link_to 'Cancel', home_path %></div>
</fieldset>
<% end %></div>
3. Here is the new password view (new.html.erb):
<div class="form_container">
<% form_for(:password, :url => passwords_path) do |f| %>
<fieldset>
<div class="form_row">
<label for="login">Username</label>
<%= text_field_tag 'login' %></div>
<div class="form_row">
<label for="email">Email</label>
<%= text_field_tag 'email' %></div>
<div class="submit_row">
<%= submit_tag 'Send Password', :class => "button-to" %></div>
</fieldset>
<% end %></div>
4. Here’s the user notifier (emailer) code:
You’ll need a user_notifier.rb file and a view template (app/views/user_notifier/new_password.html.erb) which will just contain the text of the email you want to send.
app/models/user_notifier.rb:
class UserNotifier < ActionMailer::Base
def new_password(user, new_password)
setup_email(user)
@subject += 'Your new password'
@body[:new_password] = new_password
end
protected
def setup_email(user)
@recipients = "#{user.email}"
@from = "Support" "<support@yoursite.com>"
@subject = ""
@sent_on = Time.now
@body[:user] = user
end
end
app/views/user_notifier/new_password.html.erb:
We’re sorry to hear your lost your password. But, there’s no need to worry, because we’ve created a new, temporary password for you.
Your new password is: <%=h @new_password %>
You can change this password to something more memorable once you log into your account.
5. Here’s the modified routes.rb file:
map.resources :passwords
map.resources :users, :has_one => [:password]