Web Application Development Practical: Advanced Ruby on Rails - User authenticationThis practical is continuous from previous practical, by providing a login facility based on user accounts. Users will log in with a unique username and password, as in most commercial and community web sites. Before doing this practical you should have finished previous practical. .
Step 1) Enter the cookbook directory of the application. we’re going to need a controller for creating users and so we’ll create a Users Controller and give it a new action. rails g controller users new In the above “rails g” is a short form for “rails generate”. Step 2) We’ll need a User model to go with the controller in order to store the users’ email addresses and passwords. For obvious reasons we should never store passwords as plain text so instead we’ll store a password hash and a salt. rails g model user email:string password_hash:string password_salt:string Step 3) Now we’ve created the model we’ll migrate the database to create the users table. rake db:migrate Step 4)Next we’ll write the code in the app/controllers/user_controller.rb file for the new and create actions in the UsersController. class UsersController < ApplicationController def new @user = User.new end # POST users def create @user = User.new(params[:user]) if @user.save redirect_to :sign_up, :notice => "Signed up!" else render "new" end end end Step 5) We’ll write that “new” template now. In the folder cookbook/app/views/users, find a file named new.html.erb. Rewrite this file, and make it have a form that has email, password and password_confirmation fields along with some code for showing any validation errors.
Our User model doesn’t have password or password_confirmation attributes but we’ll be making accessor methods inside the User model to handle these. Step 6)We’ll make a couple of changes to the routes file next. The controller generator has generated the following route. get "users/new" We’ll change this route in file config/routes.rb to /sign_up, have it point to users#new and give it a name of "sign_up". We’ll also create a root route that points to the signup form. Don't forget to delete index.html in the cookbook/public folder. Finally we’ll add a users resource so that the create action works. get "sign_up" => "users#new", :as => "sign_up" root :to => "users#new" resources :users
Visit http://localhost:3000/sign_up, and try to sign up as a new user. We will get some errors. This is because we have a password field on the form for a User but no matching field in the database and therefore no password attribute in the User model. You may get different errors depending on which version of rails you are using, in this case don't worry and go ahead. In file app/models/user.rb, we’ll create that attribute in the model now along with an attribute to handle the password_confirmation field. We also append these two attributes in the attr_accessible section. For password_confirmation we can use validates_confirmation_of which will also check that the password and the validation match. Now it is also a good idea to add some other validation to the form to check for the presence of the email address and password and also the uniqueness of the password. class User < ActiveRecord::Base attr_accessible :email, :password_hash, :password_salt, :password, :password_confirmation attr_accessor :password,:password_confirmation validates_confirmation_of :password validates_presence_of :password, :on => :create validates_presence_of :email validates_uniqueness_of :email end When we created the User model we created password_hash and password_salt fields in the database to hold the encrypted version of the password. When the form is submitted we’ll have to encrypt the value in the password field and store the resulting hash and salt in those two fields. A good way to encrypt passwords is to use bcrypt and we’ll use the bcrypt-ruby gem to do this in our application. First we’ll add a reference (see the last line of the following statements) to the gem in the Gemfile (in the cookbook folder). source 'http://rubygems.org' gem 'rails', '3.2.8' gem 'sqlite3' gem 'bcrypt-ruby', :require => 'bcrypt' Then run the bundle install command to make sure that the gem is installed. You need to restart the web server after the installation. (You may have some problems when installing this gem, for instance, if you are using a mac, make sure you installed xCode, which must include the command line tool in order to compile the package) Step 7) Next we’ll modify the User model ( app/models/user.rb) so that it encrypts the password before it’s saved. We’ll do this by using a before_save callback that will calls a method called encrypt_password that we’ll write soon. This method will check that the password is present and if it is it will generate the salt and hash using two BCrypt::Engine methods, generate_salt and hash_secret. class User < ActiveRecord::Base
attr_accessible :email, :password_hash, :password_salt, :password, :password_confirmation
attr_accessor :password,:password_confirmation
before_save :encrypt_password
validates_confirmation_of :password
validates_presence_of :password, :on => :create
validates_presence_of :email
validates_uniqueness_of :email
def encrypt_password
if password.present?
self.password_salt = BCrypt::Engine.generate_salt
self.password_hash = BCrypt::Engine.hash_secret(password, password_salt)
end
end
end Now when a user signs up the password_hash and password_salt will be stored in the database. If we visit the signup form now it works and if we fill in the form correctly we’ll be redirected to the homepage. Logging InStep 8) We’re halfway there now. Users can sign up but they can’t yet sign in. We’ll fix that now, adding a new method called login, process_login and my_account as follow: class UsersController < ApplicationController def new @user = User.new end def create @user = User.new(params[:user]) if @user.save redirect_to :log_in, :notice => "Signed up!" else render "new" end end def login end def process_login user = User.authenticate(params[:email], params[:password]) if user session[:user_id] = user.id redirect_to :my_account, :notice => "Logged in!" else flash.now.alert = "Invalid email or password" render "login" end end def my_account if session[:user_id] != nil @sessName = User.find(session[:user_id]).email else @sessName = "Guest" end end end In the folder app/views/users/, create a file named login.html.erb. Inside this file we’ll create the form for signing in.
Then we create very simple view for my_account:
We’ll need to make some changes to the routing here( config/routes.rb), too. get "log_in" => "users#login", :as => "log_in" get "my_account" => "users#my_account", :as => "my_account" get "sign_up" => "users#new", :as => "sign_up" root :to => "users#new" resources :users do post 'process_login', :on => :collection end
def self.authenticate(email, password) user = find_by_email(email) if user && user.password_hash == BCrypt::Engine.hash_secret(password, user.password_salt) user else nil end end Before we test this out we’ll modify the application’s layout file ( app/views/layouts/application.html. erb) so that the flash messages are shown. <!DOCTYPEhtml> <html> <head> <title>Cookbook</title> <%=stylesheet_link_tag:all%> <%=javascript_include_tag:defaults%> <%=csrf_meta_tag%> </head> <body> <%flash.eachdo|name,msg|%> <%=content_tag:div,msg,:id=>"flash#{name}"%> <%end%> <%=yield%> ></body> </html> If we try to log in with an invalid username or password then we’ll see the login form again and now we’ll see the flash message telling us that our login was invalid. Logging OutStep 10) This all works but we’ll need a way to log out as well. The first thing we’ll do to implement this is to adding a new route. get "log_in" => "users#login", :as => "log_in" get "my_account" => "users#my_account", :as => "my_account" get "log_out" => "users#logout", :as => "log_out" get "sign_up" => "users#new", :as => "sign_up" root :to => "users#new" resources :users do post 'process_login', :on => :collection end This route points to the UsersController’s logout action. The logout action will be defined in app/controllers/users_controller.rb. This action will log the user out by removing the user_id session variable and then redirecting to the home page. Now if you try to use the my_account action without being logged in, you'll be redirected to the login page. def logout session[:user_id] = nil redirect_to :log_in , :notice => "Logged out!" end We can try this out by visiting /log_out. When we do we’ll be redirected back to the home page and we’ll see the “Logged out!” flash message. There's just one missing piece: you can visit the my_account action even if you're not logged in. Becuase we don't have a way to close off an action to unauthenticated users. Step 11) Edit app/controllers/application_controller. rb to define the three actions: class ApplicationController < ActionController::Base protected def login_required return true if User.find_by_id(session[:user_id]) access_denied return false end def access_denied flash[:error] = 'Oops. You need to login before you can view that page.' redirect_to :log_in end end
You can prohibit unauthenticated users from using a specific action or controller by passing the symbol for the login_required method into before_filter. Here's how to protect the my_account action defined in app/controllers/user_controller.rb: class UserController < ApplicationController before_filter :login_required, :only => :my_account ......
Step 12) Next, try to add a restriction to your cookbook application like unauthenticated users are not allow to add a new recipe or delete a recipe. CallbacksCallbacks are methods that can be called before and/or after our named methods such as 'index' or 'show' are run in the controllers. Eighteen of the twenty callback methods are paired to be before and after our named methods. With the other two, one is available after instances are initialised, and the other is run after a finder method has been called. Step 13) We'll add a callback to the app/model/recipe.rb file, as callbacks always go into the app/model directory. Add this code below to the recipe.rb file. This code will add a record to the development.log file under cookbook/log telling us that a recipe was added. after_create do |recipe| logger.info "recipe created: #{recipe.title} #{recipe.description}" end This is ok as far as it goes for our application. In other callbacks, though, there is more potential if we wanted to clean up submissions before they were processed, or if we needed to log details afterwards. Notice, how little actual (were there any?) SQL we wrote for our database driven site.
|