Devise Authentication Recover Password by Email

来源:互联网 发布:淘宝买家虚假交易 编辑:程序博客网 时间:2024/05/29 09:34

Devise Authentication Recover Password by Email (Code Kata)

[Updated: August 3, 2011]

Once in a while, you might get a long-running application problem which defeats all attempts to resolve. For example, perhaps your Devise-enabled application running on Heroku simply will not allow users to recover their passwords via email. Perhaps it even crashes when users attempt to recover their passwords.

And of course your application is sufficiently complex to derail simplistic debugging techniques.

There has to be a better way!

Instead of getting all bogged down in the gory details…

Working from a minimal known-good application provides three benefits:

  1. The working application can be compared to the non-working application to (hopefully) find the problem by comparison, and
  2. Problems in a minimal application are much easier to find and fix.
  3. It’s easier to get both the big picture on a tiny application, and understand how all the pieces fit together.

Sounds great!

Here’s the goal: A minimal Devise-enabled Rails 3.1 micro-application which allows users to recover passwords.

Here’s what we need to do:

  1. Generate a minimal Rails application
  2. Install Devise and generate a minimal model for user authentication
  3. Build a minimal set of views and controllers to handle routing
  4. Configure the mailer
  5. Test test test

Devise, minimal

First off, we’re doing this in Rails 3.1 and on Heroku’s Celadon Cedar stack. Heroku is making a concerted effort to ephemeralize their hosting by using as much off-the-shelf technology as possible, so if you are not using Heroku, it’s not difficult to replicate their stack for yourself.

You might want to read these articles first:

  • Rails on localhost Postgres
  • Migrating Rails from Bamboo to Cedar

Given you now have a minimal application running locally and on Heroku and using Postgres, let’s install Devise:

$ vi Gemfile  # add gem 'devise'$ bundle install$ rails generate devise:install

Follow the instructions after Devise installs itself, ensuring you have the environments url set correctly.

$ rails generate devise:install      create  config/initializers/devise.rb      create  config/locales/devise.en.yml===============================================================================Some setup you must do manually if you haven't yet:  1. Setup default url options for your specific environment. Here is an     example of development environment:       config.action_mailer.default_url_options = { :host => 'localhost:3000' }     This is a required Rails configuration. In production it must be the     actual host of your application  2. Ensure you have defined root_url to *something* in your config/routes.rb.     For example:       root :to => "home#index"  3. Ensure you have flash messages in app/views/layouts/application.html.erb.     For example:       <p class="notice"><%= notice %></p>       <p class="alert"><%= alert %></p>===============================================================================

Let Devise handle users

Next, create the Devise model:

$ rails generate devise User$ rake db:migrate # did you create the postgres role?$ rake db:test:prepare

Important: Devise inverts the “natural” order of user model development, where the User model is created first, stocked with various attributes (Name, URL, etc.), then the authentication is added to the User model. In Devise, the user authentication is the model. If you attempted to create a Devise User after creating the application User, the migration will fail.

Instead of adding a large number of attributes to the User model, constrain User to authentication, authorization and session management.

Bonus for Riding Rails readers: I haven’t seen this anywhere else, but it turns out that if you would like to add a user name to the default Devise model, it’s easy:

$ rails generate devise User name:string

Spiffy! Beats creating another migration to add :name.

Put all the other details into a Profile, where each user has_one :profile and each profile belongs_to :user.

If you want to be evil, keep the profiles for marketing purposes when users delete their accounts. The benefit to the user, of course, is that when they realize their `mistake’ and create a new account, their profile is ready and waiting for their return.

Moving right along…

More configuration

For the next few sections, we’re going to lean heavily on Daniel Kehoe’s superb tutorial on Rails with Devise. In fact, that tutorial is worth working through a few times itself. What we’re doing here is similar, but constrained to Rails 3.1, and we’ll be doing more testing.

In config/application.rb, add config.filter_parameters += [:password, :password_confirmation]

In app/models/user.rb, add

  attr_accessible :name, :email, :password, :password_confirmation, :remember_me  validates :name,  :presence => true, :uniqueness => true  validates :email, :presence => true, :uniqueness => true

Views

At this point, your application will not run “out-of-the-box” because it has no views. An easy way to get views is to Devise’s built-in views:

rails generate devise:views

This copies over Devise’s internal views so that you can modify them as you see fit. Go ahead and do that, then let’s take a look at the routes:

$ rake routes        new_user_session GET    /users/sign_in(.:format)       {:action=>"new", :controller=>"devise/sessions"}            user_session POST   /users/sign_in(.:format)       {:action=>"create", :controller=>"devise/sessions"}    destroy_user_session DELETE /users/sign_out(.:format)      {:action=>"destroy", :controller=>"devise/sessions"}           user_password POST   /users/password(.:format)      {:action=>"create", :controller=>"devise/passwords"}       new_user_password GET    /users/password/new(.:format)  {:action=>"new", :controller=>"devise/passwords"}      edit_user_password GET    /users/password/edit(.:format) {:action=>"edit", :controller=>"devise/passwords"}                         PUT    /users/password(.:format)      {:action=>"update", :controller=>"devise/passwords"}cancel_user_registration GET    /users/cancel(.:format)        {:action=>"cancel", :controller=>"devise/registrations"}       user_registration POST   /users(.:format)               {:action=>"create", :controller=>"devise/registrations"}   new_user_registration GET    /users/sign_up(.:format)       {:action=>"new", :controller=>"devise/registrations"}  edit_user_registration GET    /users/edit(.:format)          {:action=>"edit", :controller=>"devise/registrations"}                         PUT    /users(.:format)               {:action=>"update", :controller=>"devise/registrations"}                         DELETE /users(.:format)               {:action=>"destroy", :controller=>"devise/registrations"}                    root        /                              {:controller=>"users", :action=>"index"}

Yes, this code listing is truncated at the edge of the page. And yes, you should type this out in your terminal window to see the controllers. Narrow web page, wide terminal. Nice for a change, right? Think about it…

Let’s dig a little deeper into this routing.

Default routing in Devise

So far, we have installed a “default” Rails 3.1 application using Devise’s default configuration for authentication. Examining config/routes.rb, we find

Pgtest::Application.routes.draw do  devise_for :users..end

Assuming you’re running Rails on the default http://localhost:3000, hit these links and see what happens:

  • http://localhost:3000/routing error, No route matches [GET] “/”.
  • http://localhost:3000/usersrouting error, No route matches [GET] “/users”.
  • http://localhost:3000/users/sign_uprenders sign up page.
  • http://localhost:3000/users/sign_inrenders sign in page.
  • http://localhost:3000/users/sign_outrouting error, No route matches [GET] “/users/sign_out”.

This is as it should be, Devise doing the heavy lifting, while staying out of our application’s domain.

Further, note the routing errors come in two flavors:

  1. GET requests which are not now (but will later be) routed to a view, and
  2. POST, DELETE and PUT requests which are not supposed to render. These requests – HTTP verbs in RESTful routing – get redirected somewhere sensible after the action is performed.

Unfortunately, we are going to have to do more work before we can investigate retrieving forgotten passwords.

Minimal views

We need a few views to drive the application. Let’s start with signing up, click here: http://localhost:3000/users/sign_up

Put in your details, click Sign Up, and get for your effort:

undefined local variable or method `root_path' for #<Devise::RegistrationsController:0x00000100fe4880>

Ok, this is boring, back to Daniel’s tutorial to fill out the application skeleton.

In app/views/devise/registrations/edit.html.erb:

    <p><%= f.label :name %><br />    <%= f.text_field :name %></p>

In app/views/devise/registrations/new.html.erb:

    <p><%= f.label :name %><br />    <%= f.text_field :name %></p>

Now issue $ rails generate controller home index

In config/routes.rb replace:

get "home/index"

with

root :to => "home#index"

Add to controllers/home_controller.rb:

def index  @users = User.allend

Modify the file app/views/home/index.html.erb and add:

    <h3>Home</h3>    <% @users.each do |user| %>    <p>User: <%= user.name %> </p>    <% end %>

Create initialization file db/seeds.rb by adding:

puts 'SETTING UP DEFAULT USER LOGIN'user = User.create! :name => 'First User', :email => 'user@test.com', :password => 'please', :password_confirmation => 'please'puts 'New user created: ' << user.name

Whence:

$ rake db:seed

If you need to, you can run $ rake db:reset to recreate everything from scratch.

Controllers

$ rails generate controller users show

Note that “users” is plural when you create the controller.

Open app/controllers/users_controller.rb and add:

before_filter :authenticate_user!def show  @user = User.find(params[:id])end

The file config/routes.rb has already been modified to include:

get "users/show"

Remove that and change the routes to:

root :to => "home#index"devise_for :usersresources :users, :only => :show

Open app/views/users/show.html.erb and add:

    <p>      User: <%= @user.name %>    </p>

Now modify the file app/views/home/index.html.erb to look like this:

    <h3>Home</h3>    <% @users.each do |user| %>    <p>User: <%=link_to user.name, user %></p>    <% end %>

In the app/assets/stylesheets/application.css file:

ul.hmenu {  list-style: none;   margin: 0 0 2em;  padding: 0;}ul.hmenu li {  display: inline;  }#flash_notice, #flash_alert {  padding: 5px 8px;  margin: 10px 0;}#flash_notice {  background-color: #CFC;  border: solid 1px #6C6;}#flash_alert {  background-color: #FCC;  border: solid 1px #C66;}

Adding session management links.

We want our users to be able to sign in and sign out, so we’ll need to provide links. An easy way to do this is following the Devise documentation for signing in and out.

First, make a partials directory for convenience:

$ mkdir app/views/devise/menu/

Then open app/views/devise/menu/_login_items.html.erb and add:

 <% if user_signed_in? %>   <li>   <%= link_to('Logout', destroy_user_session_path, :method => 'delete') %>           </li> <% else %>   <li>   <%= link_to('Login', new_user_session_path)  %>     </li> <% end %>

Create the file app/views/devise/menu/_registration_items.html.erb and add:

 <% if user_signed_in? %>   <li>   <%= link_to('Edit account', edit_user_registration_path) %>  </li> <% else %>   <li>   <%= link_to('Sign up', new_user_registration_path)  %>   </li> <% end %>

Then use these partials in your app/views/layouts/application.html.erb file, like this:

 <body>     <ul class="hmenu">     <%= render 'devise/menu/registration_items' %>     <%= render 'devise/menu/login_items' %>   </ul>   <%- flash.each do |name, msg| -%>     <%= content_tag :div, msg, :id => "flash_#{name}" if msg.is_a?(String) %>   <%- end -%> <%= yield %> </body>

That’s it for the framework.

Getting email unscrewed

First configure Devise email in config/initializers/devise.rb:

  config.mailer_sender = "david.doolin@gmail.com"

If you continue to get an error message, did you reboot the server?

Localhost email testing

Do this in the shell you’re using to run the Rails server:

$ export GMAIL_SMTP_USER=username@gmail.com$ export GMAIL_SMTP_PASSWORD=yourpassword

Add the following to config/environments/development.rb:

config.action_mailer.default_url_options = { :host => 'localhost:3000' }config.action_mailer.delivery_method = :smtpconfig.action_mailer.perform_deliveries = trueconfig.action_mailer.raise_delivery_errors = trueconfig.action_mailer.default :charset => "utf-8"ActionMailer::Base.smtp_settings = {  :address => "smtp.gmail.com",  :port => 587,  :authentication => :plain,  :domain => ENV['GMAIL_SMTP_USER'],  :user_name => ENV['GMAIL_SMTP_USER'],  :password => ENV['GMAIL_SMTP_PASSWORD'],}

Remote host

As recommended, you do have your application running on Heroku, right?

$ heroku config:add GMAIL_SMTP_USER=username@gmail.com$ heroku config:add GMAIL_SMTP_PASSWORD=yourpassword

Add these lines to config/environments/production.rb:

config.action_mailer.default_url_options = { :host => 'herokuapp.com' }# ActionMailer Config# Setup for production - deliveries, no errors raisedconfig.action_mailer.delivery_method = :smtpconfig.action_mailer.perform_deliveries = trueconfig.action_mailer.raise_delivery_errors = trueconfig.action_mailer.default :charset => "utf-8"ActionMailer::Base.smtp_settings = {  :address => "smtp.gmail.com",  :port => 587,  :authentication => :plain,  :domain => ENV['GMAIL_SMTP_USER'],  :user_name => ENV['GMAIL_SMTP_USER'],  :password => ENV['GMAIL_SMTP_PASSWORD'],}

If these parameters look like they could be refactored, they probably could be. Good opportunity to extend and maintain this article.

At this point the application works for me. I can:

  1. Recover passwords using email from my localhost development server, and
  2. recover passwords from a production server running on Heroku.

Now, I’ll compare the configuration files of this application with the configuration files of the non-working application currently deployed on Heroku. It’s “the long way ’round,” but as a result, I also have a better understanding of how to use Devise effectively. If you work this whole article line-by-line, you will understand much better too.

Mission accomplished!

(Sort of. We didn’t do any testing…)

Testing

Technically, BDD/TDD requires the tests for all this functionality be written first, then the code written to pass the tests. This is all well and good when the programmer has a good grip on both the code which is to be written, and how to test that code.

When neither apply, as was the case for the author when this was first written, working code is developed in a “spike.”

Now that this code is working, a suite of relevant tests should be developed. In the best possible world, after the tests are developed, the entire spike should be trashed and all the code rewritten in red-green form. Since this is a code kata, either this article will be rewritten as test-first, or a companion article will be written to illustrate the test first principles in action. For now, let’s figure out what to test, and about how best to test it.

Actually, we’re at 1800 words, so let’s instead quickly recap and call this one finished. Watch another article in a series, or a rewrite of this to be more BDD/TDD oriented.

Recap

This was a longer piece of work than is comfortable in this format, with a lot of files being touched.

Here’s a list of the files which were modified or created manually:

  1. config/routes.rb
  2. config/application.rb
  3. config/initializers/devise.rb
  4. config/environments/development.rb
  5. config/environments/production.rb
  6. app/assets/stylesheets/application.css
  7. app/views/home/index.html.erb
  8. app/views/users/show.html.erb
  9. app/views/layouts/application.html.erb
  10. app/views/devise/registrations/new.html.erb
  11. app/views/devise/registrations/edit.html.erb
  12. app/views/devise/menu/_login_items.html.erb
  13. app/views/devise/menu/_registration_items.html.erb
  14. app/models/user.rb
  15. app/controllers/home_controller.rb
  16. app/controllers/users_controller.rb

(Please leave a comment if you find a file missing. Thanks.)

Here’s an interesting conundrum. It’s customary to start with a bottom-up approach in TDD by writing unit tests in a Fat Model Skinny Controller paradigm, without worrying overmuch about the view layer. Here, out of 16 files, there is only one model, and by itself it doesn’t much of anything interesting. We can (and will) write some model tests exercising the callbacks and validation, but that doesn’t much help test the behavior of the micro-application as a whole.

In this case, we’re working top down, which makes sense given the task at hand.

One last point: this example application isn’t minimal. It could be shortened by removing the name attribute and not adding the css or menu links.

From around the Rubysphere…

  • Here’s a great link: Devise with locking and confirmation
0 0