Rails 3.1 gives us a really easy way to authenticate users in our app: http_basic_authenticate_with
. Using it, all we need to do is create a model for the user (certainly, User model :P) with an attribute called password_digest and some views feature for login and register users. After all, let's relax and let Rails do the hard work.
gem 'bcrypt-ruby', '~> 3.0.0'
First at all, an User model which we can generate by following:
rails g model user email:string password_digest:string
and then add the following methodo call to generated class:
has_secure_password
We certainly want validate presence for email and password fields when creating new users. In order to accomplish this task, let's deal with the validators in the User model class:
validates_uniqueness_of :email
validates_presence_of :email
validates_presence_of :password, :on => :create
After all, we may need some attr_accessible
in order to access user's attributes. Adding this, our final user model may look like this:
class User < ActiveRecord::Base
attr_accessible :email, :password, :password_confirmation
has_secure_password
validates_uniqueness_of :email
validates_presence_of :email
validates_presence_of :password, :on => :create
end
We'll need a controller that allows users creation and manipulation at all.
rails g controller users
Our new controller may look like this:
class UsersController < ApplicationController
def new
@user = User.new
end
def create
@user = User.new(params[:user])
if @user.save
redirect_to root_url, :notice => "Signed up!"
else
render "new"
end
end
end
Later we'll need a controller for willing users authenticate (login and logout). Authentications are handled by Sessions controller.
rails g controller sessions
At a first glance, our sessions controller can look just like that:
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by_email(params[:email])
if user && user.authenticate(params[:password])
session[:user_id] = user.id
redirect_to root_url, :notice => "Logged in!"
else
flash.now.alert = "Invalid email or password"
render "new"
end
end
def destroy
session[:user_id] = nil
redirect_to root_url, :notice => "Logged out!"
end
end
It's in sessions controller that magic happens. The has_secure_password
method that we've added in users model earlier gives us the power to call authenticate
method from an user object. We've got the power! authenticate
method will ensure that the given password matches the password_digest for that user and return true if it does - false otherwise.
In order to fetch the session's user anywhere in our application, we can make use of ApplicationController and append to it the following lines of code:
private
def current_user
@current_user ||= User.find(session[:user_id]) if session[:user_id]
end
helper_method :current_user
The call to helper_method
passing :current_user
as parameter does so that current_user
method can also be called from views - nice!
Located at app/views/users/new.html.erb
<h1>Sign Up</h1>
<%= form_for @user do |f| %>
<% if @user.errors.any? %>
<div class="error_messages">
<h2>Form is invalid</h2>
<ul>
<% for message in @user.errors.full_messages %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= f.label :email %>
<%= f.text_field :email %>
</div>
<div class="field">
<%= f.label :password %>
<%= f.password_field :password %>
</div>
<div class="field">
<%= f.label :password_confirmation %>
<%= f.password_field :password_confirmation %>
</div>
<div class="actions"><%= f.submit %></div>
<% end %>
Located at app/views/sessions/new.html.erb
<h1>Log in</h1>
<%= form_tag sessions_path do %>
<div class="field">
<%= label_tag :email %>
<%= text_field_tag :email, params[:email] %>
</div>
<div class="field">
<%= label_tag :password %>
<%= password_field_tag :password %>
</div>
<div class="actions"><%= submit_tag "Log in" %></div>
<% end %>
Ok. Now let's make some tests. Note that it's not at all a TDD approach, once we're not covering our models neither our controllers with unitary and functional tests.
We'll need the following gems:
gem "rspec-rails"
gem "factory_girl_rails"
gem "capybara"
gem "guard-rspec"
Short way, rspec stands for the test framework, factory girl will help us as an alternative for rails built-in fixtures mechanism, cabypara is the test framework for integration tests and guard-spec will run tests for us after changes occur.
rails g rspec:install
mkdir spec/support spec/models spec/routing
guard init
bundle exec guard
At this point, if it's all right, guard should be running all tests - I mean... guard must not have much work to do right now - besides anything else than run all specs and get no errors because there's no test. Enable capybara by adding the following line to spec/spec_helper.rb:
require 'capybara/rspec'
You may also want to remove the configuration line for config.fixture_path
, once we're not going to use fixtures.
We can generate an integration test by running rails g integration_test login
, which will create the spec/requests/logins_spec.rb file. Now, we need an user. Let's make use of our factory just by creating a spec/factories.rb file with the following lines:
Factory.define :user do |f|
f.sequence(:email) { |n| "foo#{n}@example.com" }
f.password "secret"
end
Now we have an user factory to play around, lets take a look at what could be the code for our login_spec.rb file:
require 'spec_helper'
describe "Logins" do
it "log in valid users" do
user = Factory(:user)
visit login_path
fill_in "Email", :with => user.email
fill_in "Password", :with => user.password
click_button "Login"
page.should have_content("successfully")
end
end
Thats a pretty simple integration test but it is one! After saving this, guard should run tests and warn about the failure. Let's see what fails:
1) Logins log in valid users
Failure/Error: user = Factory(:user)
ActiveRecord::StatementInvalid:
Could not find table 'users'
That's because I haven't run rake db:migrate in tests environment neither the rake db:test:prepare command. Ok, now that I did it, lets click enter in terminal which is running guard to force it running our tests. Another failure:
1) Logins log in valid users
Failure/Error: visit login_path
NameError:
undefined local variable or method `login_path'
The get "login" => "sessions#new", :as => "login"
line in config/routes.rb should solve the problem. Now we get another failure message related to routes:
1) Logins log in valid users
Failure/Error: visit login_path
ActionView::Template::Error:
undefined local variable or method `sessions_path'
That's pretty obvious. We did not created any route to our sessions controller. Let's do it now. resources :sessions
should handle this. Next failure:
1) Logins log in valid users
Failure/Error: click_button "Login"
Capybara::ElementNotFound:
no button with value or id or text 'Login' found
Great! Some broken test which failure is not related to routes. The complaint we received is about our log in view. There's no such login button. Inspecting our earlier views/sessions/new.html.erb, I found that submit_tag
is named "Log in" and not "Login" as we stated in our integration test. Let's change it and fix this problem. Next guard complaint:
1) Logins log in valid users
Failure/Error: click_button "Login"
NameError:
undefined local variable or method `root_url'
This is an easy one! We haven't configured a root route in our routes.rb. A simple root :to => 'sessions#new'
line in our routes.rb should solve this problem. Next complaint:
1) Logins log in valid users
Failure/Error: page.should have_content("successfully")
Uh. We'll need give a feedback message when user logs in. Let's take some lines into views/layouts/application.html.erb in order to show our flash message:
<% if flash[:error] %>
<p class='error'><%=h flash[:error] %></p>
<% end %>
<% if flash[:notice] %>
<p class='notice'><%=h flash[:notice] %></p>
<% end %>
Next... for sure! Let's modify the :notice message in our sessions controller to "Logged in successfully!". Do not forget to remove public/index.html file if your rails project is a brand new one. And here we are! Our first integration test run successfully.
Nice but you should add some details/help for logging out ... ie:
Logging Out
You'll also need to allow your users to logout. We already have a controller action, so let's wire it up.
In
config/routes.rb
add another route next tologin
:Now we can add the following link to our views or layouts: