Registrazione in due step con Devise

[** UPDATE: 24/05/2012 **]: Grazie a Kevin Triplettora c’è una pagina sul wiki di Devise che fa riferimento a questo post combinando anche i vari commenti e contributi ricevuti. Visitate la pagina, soprattutto se utilizzate Rails 3 e Devise 2: https://github.com/plataformatec/devise/wiki/How-To:-Two-Step-Confirmation

Nei miei progetti in Ruby on Rails generalemente utilizzo Devise come gem per l’autenticazione degli utenti.

Nell’ultima applicazione sviluppata avevo necessità di personalizzare Devise in modo che gli utenti potessero registrarsi fornendo solo l’indirizzo email.

La password di accesso doveva essere impostata nello step di conferma dell’account.

Dopo alcuni test, sono giunto alla seguente soluzione.

1. Per prima cosa ho dovuto sovrascrivere il ConfirmationsController. Quindi nel file routes.rb ho impostato devise perchè utilizzase il mio controller custom (il mio model degli utenti si chiamava Account).

Ho dovuto sovrascrivere anche il RegistrationsController per personalizzare la pagina di registrazione:

1
2
3
4
5
</p>
<p>devise_for :accounts, :controllers =&gt; {:confirmations =&gt; "confirmations", :registrations =&gt; "registrations"} do</p>
<p>put "confirm_account", :to =&gt; "confirmations#confirm_account"</p>
<p>end</p>
<p>

Come vedete ho anche aggiunto un metodo custom, confirm_account che userò nel quarto step, come vedremo tra poco.

2. A questo punto devo fare in modo di evitare la validazione della password di devise.

Per questo scopo ho scritto un initializer chiamato devise_customization.rb in /config/initalizers/ come segue:

1
2
3
4
</p>
<p>module Devise</p>
<p>module Models</p>
<p>module Validatable

1
2
3
4
def password_required?</p>
<p>false</p>
<p>end</p>
<p>

1
2
3
4
 end</p>
<p>end</p>
<p>end</p>
<p>

In questo modo ho sovrascritto il modulo validatable di devise skippando la validazione della password.

3. Il passo successivo è la personalizzazione delle view.

Poichè utilizzo i miei ConfirmationsController e RegistrationsController custom ho copiato le view standard di devise sotto /views/confirmations/ per le confirmations e sotto /views/registrations/ per le registrations.

Le ho poi modificate come segue per adattarle al mio workflow di registrazione.

La view /registrations/new è diventata cosi (utilizzo haml per i layout)

1
2
3
4
5
6
7
8
9
10
11
12
</p>
<p>= form_for(resource, :as =&gt; resource_name, :url =&gt; account_registration_path(@account)) do |f|</p>
<p>= devise_error_messages!</p>
<p>%p</p>
<p>= f.label :email</p>
<p>= f.email_field :email</p>
<p>%p.clearfix</p>
<p>= f.submit 'Signup'</p>
<p>= link_to 'Home', root_url</p>
<p>%br/</p>
<p>= render :partial =&gt; 'devise/shared/links'</p>
<p>

In questa view ho semplicemente rimosso i campi password e password_confirmation.

La view /confirmations/show è invece diventata:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
</p>
<p>= form_for(resource, :url =&gt; confirm_account_path) do |f|</p>
<p>= devise_error_messages!</p>
<p>%p</p>
<p>= f.label :email</p>
<p>= @account.email</p>
<p>= f.hidden_field :confirmation_token</p>
<p>%p</p>
<p>= f.label :password</p>
<p>%br/</p>
<p>= f.password_field :password</p>
<p>%p</p>
<p>= f.label :password_confirmation</p>
<p>%br/</p>
<p>= f.password_field :password_confirmation</p>
<p>%p.clearfix</p>
<p>= f.submit 'Confirm Account'</p>
<p>= link_to 'Home', root_url</p>
<p>%br/</p>
<p>= render :partial =&gt; 'devise/shared/links'</p>
<p>

4. Ok, ora che ho definito le mie view è tempo di scrivere il nuovo metodo di conferma dell’account nel ConfirmationsController. Posso invece lasciare inalterato il RegistrationsController.

1
2
3
4
5
6
7
8
</p>
<p>class ConfirmationsController &lt; Devise::ConfirmationsController</p>
<p>def show</p>
<p>@account = Account.find_by_confirmation_token(params[:confirmation_token])</p>
<p>if !@account.present?</p>
<p>render_with_scope :new</p>
<p>end</p>
<p>end

1
2
3
4
5
6
7
8
9
10
11
def confirm_account</p>
<p>@account = Account.find(params[:account][:confirmation_token])</p>
<p>if @account.update_attributes(params[:account]) and @account.password_match?</p>
<p>@account = Account.confirm_by_token(@account.confirmation_token)</p>
<p>set_flash_message :notice, :confirmed</p>
<p>sign_in_and_redirect("account", @account)</p>
<p>else</p>
<p>render :action =&gt; "show"</p>
<p>end</p>
<p>end</p>
<p>

1
2
end</p>
<p>

Il metodo show semplicemente si occupa di trovare l’account in base al token ricevuto e poi renderizzza la view show.

Il punto chiave è il metodo confirm_account dove viene trovato l’account, eseguito l’update dei suoi attributes e se il metodo password_match? ritorna true allora l’account viene confermato chiamando il metodo standard di devise confirm_by_token

5. L’ultima cosa che rimane da fare è definire il metodo password_match? all’interno del model Account.

1
2
3
</p>
<p>class Account &lt; ActiveRecord::Base</p>
<p>...

1
2
3
4
5
6
def password_match?</p>
<p>self.errors[:password] &lt;&lt; 'password not match' if password != password_confirmation</p>
<p>self.errors[:password] &lt;&lt; 'you must provide a password' if password.blank?</p>
<p>password == password_confirmation and !password.blank?</p>
<p>end</p>
<p>

1
2
end</p>
<p>

E questo è tutto!

Ora potete riavviare il server rails e registrarvi nella vostra applicazione scegliendo la password solo in fase di conferma dell’account.

Tags: , , , , , , ,


About Claudio

Claudio Marai is a co-founder of DevInterface.

After graduating in Computer Science has contributed to develop complex web applications based on Java/J2EE and desktop applications with the. NET framework for the Ministry of Justice and ultimately for the banking ambit.

The passion for web in recent years has led him to be interested in more modern frameworks such as Ruby on Rails and Django, and to a development approach based on agile methodologies such as eXtreme Programming and SCRUM.

About DevInterface

We are an information and communication technology agency. Our mission is to provide web application development, design services and communication strategies. We specialize in building web applications with modern and efficient frameworks.

Related Post

32 Responses to “Registrazione in due step con Devise”

  1. Thanks for sharing your technique. It would make a great addition (or link) on the Devise wiki How To-s at github.

  2. kris scrive:

    Passing the user id as a hidden field is a bad idea. It would be trivial to change that, submit the form and get access to another account.

  3. Claudio scrive:

    Hello Kris, thanks for reporting. In fact should be used the the cofirmation_token in place of the id.

    I’ve fixed the example code.

  4. Kevin Brown scrive:

    Claudio, I’ve been working on this for about two weeks and I’m just stuck. Everything I have looks exactly like you have showed except my model is called ‘user’. I’m getting a `no route matches ‘confirm_account’` message. I tried to change ‘put’ to ‘match’ in my routes.rb, but that threw a lot more errors (at least it was finding it, though…maybe?)

    I’d really like to get this working. Do you have any advice?

  5. Claudio scrive:

    Hi Kevin, I don’t know what cause the error.
    Have you try to see the routes with rake routes?
    If you want post your code so I can try to find the problem.

  6. Kevin Brown scrive:

    Got it worked out. You can see the details here: http://stackoverflow.com/questions/6588865/rails-devise-two-step-confirmation-route-error

    I’m a newbie, so please excuse my poor understanding of things. :)

  7. Fred Schoeneman scrive:

    Thanks for this! I had just decided that asking for a password at signup, and then requiring confirmation, makes more sense for my project. I will let you know how it works out!

  8. Fred Schoeneman scrive:

    Claudio,

    I think there’s a missing “if” statement in code you provide in the account model for “def password_match?”

    1
    2
    3
    4
    5
    def password_match?
        self.errors[:password] << 'password not match'  password != password_confirmation
        self.errors[:password] << 'you must provide a password' if password.blank?
        password == password_confirmation and !password.blank?
      end

    this works for me:

    1
    2
    3
    4
    5
    def password_match?
        self.errors[:password] << 'password not match' if password != password_confirmation
        self.errors[:password] << 'you must provide a password' if password.blank?
        password == password_confirmation and !password.blank?
      end

    Thanks again for this blog post. You really helped me out.

  9. Claudio scrive:

    I added the “if”.

    Thank you Fred for your bug fixing :-)

  10. Naveed scrive:

    Hi

    I used your method in my rails 3 app with webrick and nginx but its taking too much time for user registration and user confirmations although i used the same code you provided.

    Regards

  11. Greg scrive:

    I believe you also need to change Account.find to Account.find_by_confirmation_token in the confirm_account action of the controller.

  12. Nick scrive:

    I appreciate you taking the time to write this up. It is very close to what I need for my site and it is helping me tremendously. I followed your instruction and I ran into an error. (I am sure I just did something wrong but I was hoping you might be able to help if you have time). It works all the way up to the entering the password part. Once I submit the password it gives and error.

    ActiveRecord::RecordNotFound in ConfirmationsController#confirm_admin
    Couldn’t find Admin with id=WjqDa7hz2Y9bUh5EKxBf
    Rails.root: Websites/Development/Rails/appignani
    app/controllers/confirmations_controller.rb:10:in `confirm_admin’

    It should not be looking for ID and as far as the code I can see and entered it is not. Here is the confirm_admin code:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
     def confirm_admin
        @admin = Admin.find(params[:admin][:confirmation_token])
        if @admin.update_attributes(params[:admin]) and @admin.password_match?
          @admin = Admin.confirm_by_token(@admin.confirmation_token)
          set_flash_message :notice, :confirmed      
          sign_in_and_redirect("admin", @admin)
        else
          render :action => "show"
        end
      end

    I appreciate any help you can give on this. Thank you.

  13. Nick scrive:

    I figured it out. It is what the last post before mine suggested.

  14. brad phelan scrive:

    Awsome tip. Took me 10 minutes after reading your blog to have it working for myself.

  15. Shane scrive:

    I was looking to implement something similar when I ran across this post. I was considering using the built-in token_authenticatable module in Devise to provide access to a newly created account where the user has not set a password. I was going to set the password to something random and force a change when the user confirms the account. I was going to allow access to confirmation and password changing via the token_authentication. I am glad I ran across this because I don’t think I need to use the token_authenticatable module, I can just use the confirmation token! I think I will still just set the password to something random for a non-confirmed user since I don’t want to override the password validation methods in Devise as I have other models that need it.

    Anyways, thanks for the write, lots of good ideas in here!

  16. Shane scrive:

    Works great!

    Step #4 under confirm_account method, Account.find should be Account.find_by_confirmation_token

  17. Shane scrive:

    For my case I found that I could simply override password_required in my user model like so:

    1
    2
    3
      def password_required?
        confirmed?
      end

    This allows the user to confirm their account without setting a password only when their account has not yet been confirmed. After confirmation, the password is required.

  18. Aayush Kumar scrive:

    This is exactly what I wanted – thanks for writing it up! :)

    I had some trouble setting this up on my end however – I get a SQLite3 Constraint Exception error with this. More details here: http://stackoverflow.com/questions/10442510/rails-two-step-signup-with-devise-errors

    I was wondering if anyone else faced similar problems setting this up. I would really appreciate any help! :)

  19. Kevin Triplett scrive:

    Excellent post, thanks! I have multiple Devise resources and I want each resources to be able to use this feature. So here’s what I did…

    In my confirmation controller:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    class ConfirmationsController < Devise::ConfirmationsController
      def show
        self.resource = resource_class.find_by_confirmation_token(params[:confirmation_token])
        if !resource.present?
          render_with_scope :new
        end
      end

      def confirm
        self.resource = resource_class.find_by_confirmation_token(params[resource_name][:confirmation_token])
        if resource.present? && resource.update_attributes(params[resource_name]) && resource.password_match?
          resource = resource_class.confirm_by_token(resource.confirmation_token)
          set_flash_message :notice, :confirmed
          sign_in_and_redirect(resource_name.to_s, resource)
        else
          render :action => "show"
        end
      end
    end

    Very subtle difference, but now any Devise resource can use this controller. (I added the check for present? in case someone tries to submit the form a second time.) Oh — you also have to change the routing configuration (which note that the new version of Devise now deprecates passing a block to #device_for:

    1
    2
    3
    4
    5
    6
    7
    8
    9
      devise_for :clients, :controllers => {:confirmations => 'confirmations'}
      devise_for :admins,  :controllers => {:confirmations => 'confirmations'}

      devise_scope :clients do
        put "confirm", :to => "confirmations#confirm"
      end
      devise_scope :admins do
        put "confirm", :to => "confirmations#confirm"
      end

    Thanks again, Claudio — and if you have time, ha ha, please change above your Account.find to Account.find_by_confirmation_token above. Just in case someone is too lazy and they don’t read these comments. Again, thanks for documenting this sweet feature!

    Also, thanks to Shane, I made my model methods and grammatized the error messages:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
      def password_required?
        super if confirmed?
      end

      def password_match?
        if password_confirmation.blank?
          self.errors[:password_confirmation] << "cannot be blank"
        elsif password != password_confirmation
          self.errors[:password_confirmation] << "does not match password"
        end
        if password.blank?
          self.errors[:password] << "cannot be blank"
        end
        password == password_confirmation && !password.blank?
      end
  20. Kevin Triplett scrive:

    Serves me right for posting late at night! It worked for my clients model but devise gets confused with the same routing information for multiple resources. So here’s what works with multiple Devise resources:

    In my routes.rb:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
      devise_for :clients, :controllers => {:sessions => 'sessions', :confirmations => 'confirmations'}
      devise_for :admins, :controllers => {:sessions => 'sessions', :confirmations => 'confirmations'}
      devise_for :super_users, :controllers => {:sessions => 'sessions', :confirmations => 'confirmations'}

      devise_scope :client do
        match "/client/confirm" => "confirmations#confirm", :as => :client_confirm
      end
      devise_scope :admin do
        match "/admin/confirm" => "confirmations#confirm", :as => :admin_confirm
      end
      devise_scope :super_user do
        match "/super_user/confirm" => "confirmations#confirm", :as => :super_user_confirm
      end

    (Notice the singular v. plural of the resource names. I’m sure put v. match would also work.)

    And here’s the hack I had to do in order to use the same view for each resource (I use the semantic forms gem):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    - confirm_path = send("#{resource_name}_confirm_path")
    %h2 You're almost done! Now create a password to securely access your account.
    = semantic_form_for(resource, :as => resource_name, :url => confirm_path) do |form|
      = devise_error_messages!
      = form.inputs do
        = form.input :email, :input_html => { :disabled => true }
        = form.input :password
        = form.input :password_confirmation
        = form.input :confirmation_token, :as => :hidden
      = form.actions do
        = form.action :submit, :label => '
    Confirm Account'

    I disabled the email field so the client can see their email address but not change it. Sorry for the mis-post! Carry on.

  21. Kevin Triplett scrive:

    One more thing: in my confirmations controller, I had to reference the token this way:


    resource_class.confirm_by_token(params[resource_name][:confirmation_token])

    Otherwise, it raised an exception for me. YMMV

  22. Kevin Triplett scrive:

    Me again, but with sleep I noticed that if the user changes their email address then they are sent a confirmation email and, after clicking the confirmation link, the user is presented with the same confirmation screen asking them to enter in their new password. Whoops. The client is not expecting this behavior so here is how I modified my controller code:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    class ConfirmationsController < Devise::ConfirmationsController
      before_filter :top_nav_for_public

      def show
        self.resource = resource_class.find_by_confirmation_token(params[:confirmation_token])
        debugger
        if resource.present? && resource.confirmed?
          self.resource = resource_class.confirm_by_token(params[:confirmation_token])
          set_flash_message :notice, :confirmed
          sign_in_and_redirect(resource_name, resource)
        else
          render_with_scope :new
        end
      end

      def confirm
        debugger
        self.resource = resource_class.find_by_confirmation_token(params[resource_name][:confirmation_token])
        if resource.present? && resource.update_attributes(params[resource_name]) && resource.password_match?
          self.resource = resource_class.confirm_by_token(params[resource_name][:confirmation_token])
          set_flash_message :notice, :confirmed
          sign_in_and_redirect(resource_name.to_s, resource)
        else
          render :action => "show"
        end
      end
    end

    I also added self.resource= statements, which I forgot to do in the earlier code. Again, great blog post Claudio!

  23. Kevin Triplett scrive:

    Whoops — sorry, take out the two debugger statements and you have the correct code.

  24. Kevin Triplett scrive:

    Dang, so Devise deprecated and then removed #render_with_scope, but that’s okay, it made #show simpler:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    class ConfirmationsController < Devise::ConfirmationsController
      before_filter :top_nav_for_public

      def show
        self.resource = resource_class.find_by_confirmation_token(params[:confirmation_token])
        if resource.present? && resource.confirmed?
          self.resource = resource_class.confirm_by_token(params[:confirmation_token])
          set_flash_message :notice, :confirmed
          sign_in_and_redirect(resource_name, resource)
        end
      end

      def confirm
        self.resource = resource_class.find_by_confirmation_token(params[resource_name][:confirmation_token])
        if resource.present? && resource.update_attributes(params[resource_name]) && resource.password_match?
          self.resource = resource_class.confirm_by_token(params[resource_name][:confirmation_token])
          set_flash_message :notice, :confirmed
          sign_in_and_redirect(resource_name, resource)
        else
          render :action => "show"
        end
      end
    end

    Much nicer!

  25. Kevin Triplett scrive:

    I’ve created a Devise wiki page referencing this post and combining the comments. Please check this wiki page, especially if you’re using Rails 3 and Devise 2:

    https://github.com/plataformatec/devise/wiki/How-To:-Two-Step-Confirmation

    Feel free to add your examples for Rails 2 and Devise 1. Thanks again.

  26. Claudio scrive:

    Thank you Kevin for your big contribution! I’ve updated the post with a reference to the Devise wiki page.

  27. John Mathis scrive:

    Claudio, thanks for the article. Kevin, thanks for the Wiki page. The URL is access-controlled, though available in Google cache. Could you please open up the Wiki page.

  28. Kevin Triplett scrive:

    The wiki page got renamed, the new URL is https://github.com/plataformatec/devise/wiki/How-To:-Email-only-sign-up — the page should be open now and accessible by everyone. Thanks, Mario, for the shout out! :)

  29. Kevin Triplett scrive:

    Sorry, I meant Claudio (I have to stop playing my Nintendo)

  30. Claudio scrive:

    Thank you Kevin,
    I’ve updated the reference.

  31. Adam scrive:

    After the form on the confirmations show page is submitted, how can I customize the redirect for that form?

  32. Kaworu scrive:

    find_by_confirmation_token(nil).update_attributes is dangerous

Leave a Reply

Insert code beetween <code lang="ruby"> and </code>

Copyright 2012 DevInterface s.n.c.

DevInterface Blog is proudly powered by WordPress