Skip to content

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

chikamichi edited this page Feb 10, 2012 · 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/confirmation link is sent by e-mail to the newly registered user. Following the link leads to a page where the new user must pick a password to confirm the account.

Here's how to add this functionality to your website, by overriding Devise's ConfirmationsController.

As a service preview

The website may provide a few days to confirm the account and, in the mean time, allow the "pending" user to use all of the website features, as a "service preview"; or it could limit the features scope. This could prove useful for SaaS, for instance. If the confirmation is not performed within a certain time range, the account is disabled somehow.

If you do want to allow for a "service preview", in config/initializer/devise.rb, set confirm_within (Devise < 2.0) or allow_unconfirmed_access_for (Devise 2.0+) config key to a value like 2.days or anything suitable for your requirements.

If you do not want to allow for a "service preview", make sure that confirm_within or allow_unconfirmed_access_for is set to 0 in config/initializer/devise.rb. This is to prevent people from signing in. If you manage several scopes with Devise, you may set confirm_within or allow_unconfirmed_access_for per model, as an option to the devise instruction. Thus you could require admins to confirm their account, but allows 2.days free-sign up for users.

For Rails 3 & Devise 2.0.0

To use Rails 3 and Devise 2.0.0, you must do the same as it was to Devise 1.2.x (see below), with a few exceptions.

1) Change render_with_scope method to render on ConfirmationsController

Devise::Controllers::ScopedViews::render_with_scope was removed in version 2.0.0. One solution is to call render inside your controller, passing the path of the view, as usual.

# 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 'devise/confirmations/new' #Change this if you doens't have the views on default path
    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 'devise/confirmations/new' #Change this if you doens't have the views on default path 
    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 'devise/confirmations/show' #Change this if you doens't have the views on default path
  end

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

2) Change only_if_unconfirmed method on your model

Devise::Models:unless_confirmed method doesn't exist in Devise 2.0.0 anymore. Instead you should use pending_any_confirmation.

def only_if_unconfirmed
  pending_any_confirmation {yield}
end

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 user 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