-
Notifications
You must be signed in to change notification settings - Fork 5.5k
How To: Override confirmations so users can pick their own passwords as part of confirmation activation
Some websites allow the creation of an account providing only a username/email, leaving out the password. 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
.
The website may provide a few ways 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/initializers/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/initializers/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.
To use Rails 3/4 and Devise 3.1+, you must do the same as in older versions, but with an updated confirmations_controller, because Devise 3.1 introduced encrypted confirmation_tokens.
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::ConfirmationsController
# 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? and @confirmable.password_match?
do_confirm
else
do_show
@confirmable.errors.clear #so that we wont render :new
end
else
@confirmable.errors.add(:email, :password_already_set)
end
end
if !@confirmable.errors.empty?
self.resource = @confirmable
render 'devise/confirmations/new' #Change this if you don'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
unless @confirmable.errors.empty?
self.resource = @confirmable
render 'devise/confirmations/new' #Change this if you don'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
original_token = params[:confirmation_token]
confirmation_token = Devise.token_generator.digest(User, :confirmation_token, original_token)
@confirmable = User.find_or_initialize_with_error_by(:confirmation_token, original_token)
@requires_password = true
self.resource = @confirmable
render 'devise/confirmations/show' #Change this if you don'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
# 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
# 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
Since Devise 2.0 helper methods for your migrations are no longer included. Instead, it explicitly lists the database fields.
class AddUserConfirmable < ActiveRecord::Migration
def self.up
add_column :users, :confirmation_token, :string
add_column :users, :confirmed_at, :datetime
add_column :users, :confirmation_sent_at, :datetime
# add_column :users, :unconfirmed_email, :string # Only if using reconfirmable
add_index :users, :confirmation_token, :unique => true
User.update_all({:confirmed_at => DateTime.now, :confirmation_sent_at => DateTime.now})
end
def self.down
remove_column :users, [:confirmed_at, :confirmation_token, :confirmation_sent_at]
end
end
If not using reconfirmable, update the configuration in config/intializers/devise.rb
config.reconfirmable = false
To use Rails 3/4 and Devise 2.x-3.0, you must do the same as it was to Devise 1.2.x (see below), with a few exceptions.
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::ConfirmationsController
# 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? and @confirmable.password_match?
do_confirm
else
do_show
@confirmable.errors.clear #so that we wont render :new
end
else
@confirmable.errors.add(:email, :password_already_set)
end
end
if !@confirmable.errors.empty?
self.resource = @confirmable
render 'devise/confirmations/new' #Change this if you don'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
unless @confirmable.errors.empty?
self.resource = @confirmable
render 'devise/confirmations/new' #Change this if you don'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 don'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
# 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
# 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
Since Devise 2.0 helper methods for your migrations are no longer included. Instead, it explicitly lists the database fields.
class AddUserConfirmable < ActiveRecord::Migration
def self.up
add_column :users, :confirmation_token, :string
add_column :users, :confirmed_at, :datetime
add_column :users, :confirmation_sent_at, :datetime
# add_column :users, :unconfirmed_email, :string # Only if using reconfirmable
add_index :users, :confirmation_token, :unique => true
User.update_all({:confirmed_at => DateTime.now, :confirmation_sent_at => DateTime.now})
end
def self.down
remove_column :users, [:confirmed_at, :confirmation_token, :confirmation_sent_at]
end
end
If not using reconfirmable, update the configuration in config/intializers/devise.rb
config.reconfirmable = false
describe ConfirmationsController do
it "should be a child of Devise::ConfirmationsController" do
controller.class.superclass.should eq Devise::ConfirmationsController
end
end
# app/controllers/confirmations_controller.rb
class ConfirmationsController < Devise::ConfirmationsController
# 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_already_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?
self.resource = @confirmable
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
# 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 %>
Rails 4 change put to patch
<%= form_for resource, :as => resource_name, :url => update_user_confirmation_path, :html => {:method => 'patch'}, :id => 'activation-form' do |f| %>
as :user do
match '/user/confirmation' => 'confirmations#update', :via => :put, :as => :update_user_confirmation
end
devise_for :users, :controllers => { :confirmations => "confirmations" }
Rails 4 change put to patch
as :user do
patch '/user/confirmation' => 'confirmations#update', :via => :patch, :as => :update_user_confirmation
end
devise_for :users, :controllers => { :confirmations => "confirmations" }
# 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
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
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
# 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_already_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
# 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 %>
map.update_user_confirmation '/user/confirmation', :controller => 'confirmations', :action => 'update', :conditions => { :method => :put }
# 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