Skip to content

How To: Allow users to edit their password

tmaier edited this page Oct 7, 2011 · 54 revisions

We have two options to allow users to edit their password:

  • Use the registerable module, which will give you both sign up and edit user features
  • Handle your own passwords controller to allow users editing their password. (see Option 2)
  • Add this feature to your UsersController (see Option 3)

Option 2 - Rewrite PasswordsController

Example:

class PasswordsController < ApplicationController
  before_filter :authenticate_user!

  def edit
    @user = current_user
  end

  def update
    @user = current_user

    if @user.update_with_password(params[:user])
      sign_in(@user, :bypass => true)
      redirect_to root_path, :notice => "Password updated!"
    else
      render :edit
    end
  end
end

Overriding devise's password controller in your routes file like this:

devise_for :users, :controllers => {:passwords => "passwords"}

You will also need to add the following into routes, so that Rails knows what to do (at least I did, and I had a custom registration controller-Travis):

 resources :passwords

You will then need to create the appropriate views:

Create passwords/edit.html.erb in your view and add and modify the following to your needs:

# views/passwords/edit.html.erb
<h2>Change Password</h2>

<div class="form_div">
<%= form_for(@user, :as => @user, :url => password_path, :html => { :method => :put }) do |f| %>

  <p>
   <%= f.label :current_password %>
   <br />
   <%= f.password_field :current_password %></p>

  <p><%= f.label :password, "New password" %><br />
  <%= f.password_field :password %></p>

  <p><%= f.label :password_confirmation, "Confirm new password" %><br />
  <%= f.password_field :password_confirmation %></p>

  <p><%= f.submit "Change my password" %></p>
  <%end%>
 </div>

It is also crucial to have attr_accessible :password, :password_confirmation (in addition to others) in you model. Otherwise, password and confirmation validation will not take place.

I have: attr_accessible :email, :remember_me, :first_name, :last_name, :address_street, :address_city, :address_state, :address_zip, :address_country, :password, :password_confirmation

If you don't want to use update_with_password, which will require to enter the current password, see How To: Allow users to edit their account without providing a password.

Option 3 - Custom action in UsersController

The disadvantage of option 2 ist that it 'abuses' the PasswordsController for something it was not meant. The original use of PasswordsController is to handle forgotten passwords and how to reset them.

Instead you could introduce a custom action to UsersController.

This example uses the inherited_resources gem, the cancan gem for authentication and Ruby 1.9 syntax. It would also work without this gems but would probably require some changes.

# /app/controllers/users_controller.rb
class UsersController < InheritedResources::Base
  actions :show, :update
  custom_actions resource: :change_password
  load_and_authorize_resource

  def update
    if params[:user][:password]
      if @user.update_with_password(params[:user])
        sign_in(@user, bypass: true)
        redirect_to user_path(@user), notice: "Password updated!"
      else
        render :change_password
      end
    else
      super
    end
  end
end

We want to spec our new feature with a request spec using rspec and capybara.

# /spec/requests/users_spec.rb
require 'spec_helper'

describe "Users" do

  let(:user) {Fabricate(:user)}

  before(:each) do
    login_as user
    # logs in the user
    # see this gist for this method: https://gist.github.com/1053489
  end

  describe "GET /users/12345/change_password" do
    it "changes the password" do
      visit change_password_user_path(user)

      within("#content") do
        fill_in "Current password", with: user.password

        fill_in "Password", with: "8844889"
        fill_in "Password confirmation", with: "8844889"

        click_button "Change my password"
      end

      within("#content") do
        page.should have_content "Password updated!"
      end
    end

    it "fails when providing the wrong current password" do
      visit change_password_user_path(user)

      within("#content") do
        fill_in "Current password", with: "I'm the wrong password"

        fill_in "Password", with: "8844889"
        fill_in "Password confirmation", with: "8844889"

        click_button "Change my password"
      end

      within("#content") do
        page.should_not have_content "Password updated!"

        page.should have_content "Some errors were found, please take a look:"
        page.should have_content "is invalid"
      end
    end

    it "fails when providing different new passwords" do
      visit change_password_user_path(user)

      within("#content") do
        fill_in "Current password", with: user.password

        fill_in "Password", with: "8844889"
        fill_in "Password confirmation", with: "11113333435"

        click_button "Change my password"
      end

      within("#content") do
        page.should_not have_content "Password updated!"

        page.should have_content "Some errors were found, please take a look:"
        page.should have_content "doesn't match confirmation"
      end
    end

    it "fails when providing no new passwords" do
      old_password = user.password

      visit change_password_user_path(user)

      within("#content") do
        fill_in "Current password", with: user.password

        click_button "Change my password"
      end

      within("#content") do
        # This case is not really handled.
        # Nothing happens. Nothing gets changed.
        # But the user does not get any error message.
        #page.should_not have_content "Password updated!"

        user.reload
        user.password.should eq(old_password)
      end
    end
  end

You also need a /app/users/change_password.html.erb/ view. This one basically looks like the view from option 2. The main difference is the much simpler form_for arguments list. It is the same as for the default edit action and uses update. I used the following one.

# /app/views/users/change_password.html.haml
- title "Change Password"

= simple_form_for(@user, :html => { :method => :put }) do |f|
  = f.error_notification

  .inputs
    = f.input :current_password, hint: "We need your current password to confirm your changes.", required: true

    = f.input :password, required: true
    = f.input :password_confirmation, required: true

  .actions
    = f.button :submit, "Change my password"

Your routes.rb needs to handle the custom action. change your devise_forand users recources according to this snippet:

  devise_for :users
  resources :users, only: [:show, :edit, :update] do
    member do
      get :change_password
    end
  end

Clone this wiki locally