-
Notifications
You must be signed in to change notification settings - Fork 0
How To: Override confirmations so users can pick their own passwords as part of confirmation activation
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.
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.
# 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
# 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 %>
as :user do
match '/user/confirmation' => 'confirmations#update', :via => :put, :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 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
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_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
# 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