Skip to content

How To: Override confirmations so users can pick their own passwords as part of confirmation activation

chikamichi edited this page Oct 4, 2011 · 44 revisions

How To: Override confirmations so users can pick their own passwords as part of confirmation activation

Some websites allow to create an account providing only a username/email, dropping the password out. The sign up step is thus reduced to the bare minimum. At sign up time, an activation link is sent by e-mail. Following the link leads to a page where the new user must pick a password, at confirmation time.

The website may provide a few days to confirm the account and allow the "pending" user to use the website features in the mean time, as a "service preview". This could prove useful for SaaS, for instance.

Here's how to add this functionality to your website by overriding Devise's confirmations controller.

Before you start

If you don't want to allow for a "service preview", make sure that confirm_within is set to the default value of 0 in config/initializer/devise.rb. Setting your password while confirming doesn't make any sense if people can sign in before they've confirmed in this workflow. If you manage several scopes with Devise, you may set confirm_within per model, as an option to the devise instruction, so you could require admins to confirm their account but allows 2.days for users.

For Rails 3 & Devise 1.2x

1) Override ConfirmationsController

# app/controllers/confirmations_controller.rb
class ConfirmationsController < Devise::PasswordsController
  # Remove the first skip_before_filter (:require_no_authentication) if you
  # don't want to enable logged users to access the confirmation page.
  skip_before_filter :require_no_authentication
  skip_before_filter :authenticate_user!

  # PUT /resource/confirmation
  def update
    with_unconfirmed_confirmable do
      if @confirmable.has_no_password?
        @confirmable.attempt_set_password(params[:user])
        if @confirmable.valid?
          do_confirm
        else
          do_show
          @confirmable.errors.clear #so that we wont render :new
        end
      else
        self.class.add_error_on(self, :email, :password_allready_set)
      end
    end

    if !@confirmable.errors.empty?
      render_with_scope :new
    end
  end

  # GET /resource/confirmation?confirmation_token=abcdef
  def show
    with_unconfirmed_confirmable do
      if @confirmable.has_no_password?
        do_show
      else
        do_confirm
      end
    end
    if !@confirmable.errors.empty?
      render_with_scope :new
    end
  end
  
  protected

  def with_unconfirmed_confirmable
    @confirmable = User.find_or_initialize_with_error_by(:confirmation_token, params[:confirmation_token])
    if !@confirmable.new_record?
      @confirmable.only_if_unconfirmed {yield}
    end
  end

  def do_show
    @confirmation_token = params[:confirmation_token]
    @requires_password = true
    self.resource = @confirmable
    render_with_scope :show
  end

  def do_confirm
    @confirmable.confirm!
    set_flash_message :notice, :confirmed
    sign_in_and_redirect(resource_name, @confirmable)
  end
end

2) Add a custom confirmations view

# app/views/confirmations/show.html.erb
<h2>Account Activation</h2>

<%= form_for resource, :as => resource_name, :url => update_user_confirmation_path, :html => {:method => 'put'}, :id => 'activation-form' do |f| %>
  <%= devise_error_messages! %>
  <fieldset>
    <legend>Account Activation<% if resource.user_name %> for <%= resource.user_name %><% end %></legend>

  <% if @requires_password %>
      <p><%= f.label :password,'Choose a Password:' %> <%= f.password_field :password %></p>
      <p><%= f.label :password_confirmation,'Password Confirmation:' %> <%= f.password_field :password_confirmation %></p>
  <% end %>
  <%= hidden_field_tag :confirmation_token,@confirmation_token %>
  <p><%= f.submit "Activate" %></p>
  </fieldset>
<% end %>

3) Update config/routes.rb

  as :user do
      match '/user/confirmation' => 'confirmations#update', :via => :put, :as => :update_user_confirmation
  end
  devise_for :users, :controllers => { :confirmations => "confirmations" }

4) Add these methods to your user model:

  # new function to set the password without knowing the current password used in our confirmation controller. 
  def attempt_set_password(params)
    p = {}
    p[:password] = params[:password]
    p[:password_confirmation] = params[:password_confirmation]
    update_attributes(p)
  end
  # new function to return whether a password has been set
  def has_no_password?
    self.encrypted_password.blank?
  end
  
  # new function to provide access to protected method unless_confirmed
  def only_if_unconfirmed
    unless_confirmed {yield}
  end

5) If you are using the Validatable strategy

You will also need to add the following to your using model:

def password_required?
    # Password is required if it is being set, but not for new records
    if !persisted? 
      false
    else
      !password.nil? || !password_confirmation.nil?
    end
  end

6) If you are adding confirmation to an existing dataset

You can grandfather older accounts (no confirmation required) with the following migration:

class AddUserConfirmable < ActiveRecord::Migration
  def self.up
    change_table :users do |u|
      u.confirmable
    end

    User.update_all({:confirmed_at => DateTime.now, :confirmation_token => "Grandfathered Account", :confirmation_sent_at => DateTime.now})
  end

  def self.down
    remove_column :users, [:confirmed_at, :confirmation_token, :confirmation_sent_at]
  end
end

For Rail 2.3 & Devise 1.0.9

Override ConfirmationsController

# app/controllers/confirmations_controller.rb
class ConfirmationsController < ApplicationController
  include Devise::Controllers::InternalHelpers

  # GET /resource/confirmation/new
  def new
    build_resource
    render_with_scope :new
  end

  # POST /resource/confirmation
  def create
    self.resource = resource_class.send_confirmation_instructions(params[resource_name])

    if resource.errors.empty?
      set_flash_message :notice, :send_instructions
      redirect_to new_session_path(resource_name)
    else
      render_with_scope :new
    end
  end

  # PUT /resource/confirmation
  def update
    with_unconfirmed_confirmable do
      if @confirmable.has_no_password?
        @confirmable.attempt_set_password(params[:user])
        if @confirmable.valid?
          do_confirm
        else
          do_show
          @confirmable.errors.clear #so that we wont render :new
        end
      else
        self.class.add_error_on(self, :email, :password_allready_set)
      end
    end

    if !@confirmable.errors.empty?
      render_with_scope :new
    end
  end

  # GET /resource/confirmation?confirmation_token=abcdef
  def show
    with_unconfirmed_confirmable do
      if @confirmable.has_no_password?
        do_show
      else
        do_confirm
      end
    end
    if !@confirmable.errors.empty?
      render_with_scope :new
    end
  end
  
  protected

  def with_unconfirmed_confirmable
    @confirmable = User.find_or_initialize_with_error_by(:confirmation_token, params[:confirmation_token])
    if !@confirmable.new_record?
      @confirmable.only_if_unconfirmed {yield}
    end
  end

  def do_show
    @confirmation_token = params[:confirmation_token]
    @requires_password = true
    self.resource = @confirmable
    render_with_scope :show
  end

  def do_confirm
    @confirmable.confirm!
    set_flash_message :notice, :confirmed
    sign_in_and_redirect(resource_name, @confirmable)
  end
end

2/ Add a custom confirmations view

# app/views/confirmations/show.html.erb
<h2>Account Activation</h2>
<% form_for resource_name, resource, :url => update_user_confirmation_path, :html => {:method => 'put'}, :id => 'activation-form' do |f| %>
  <%= f.error_messages %>
  <fieldset>
    <legend>Account Activation<% if resource.user_name %> for <%= resource.user_name %><% end %></legend>

  <% if @requires_password %>
      <p><%= f.label :password,'Choose a Password:' %> <%= f.password_field :password %></p>
      <p><%= f.label :password_confirmation,'Password Confirmation:' %> <%= f.password_field :password_confirmation %></p>
  <% end %>
  <%= hidden_field_tag :confirmation_token,@confirmation_token %>
  <p><%= f.submit "Activate" %></p>
  </fieldset>
<% end %>

3) Update config/routes.rb

map.update_user_confirmation '/user/confirmation', :controller => 'confirmations', :action => 'update', :conditions => { :method => :put }

4) Add some methods to your User/scope/whatever model

  # new function to set the password without knowing the current password used in our confirmation controller. 
  def attempt_set_password(params)
    p = {}
    p[:password] = params[:password]
    p[:password_confirmation] = params[:password_confirmation]
    update_attributes(p)
  end

  # new function to return whether a password has been set
  def has_no_password?
    self.encrypted_password.blank?
  end
  
  # new function to provide access to protected method unless_confirmed
  def only_if_unconfirmed
    unless_confirmed {yield}
  end
Clone this wiki locally