This post is part of a series of reviews on the book Agile Web Development With Rails. Check out the Introduction post for a full table of contents along with some initial notes.

In this task we’ll learn how to create a simple administration system that will allow restricted access to product maintenance (what we created in task A), orders (task E) and a new user management system.

Let’s start with the user management system that will allow us to create, edit, view and delete users in our online shop. These users will have only a login and a password, which will be hashed by the SH1 digest and then saved into the database. We’ll use a generator and specify the fields we need, and by doing that the migration will be already filled with the columns we specified. At your application directory, open your command prompt and type this in:

ruby script/generate scaffold user name:string hashed_password:string salt:string

And to make the database base changes happen:

rake db:migrate

Open your database and checkout the new users table with all the fields we specified. As we move on to the user model, notice that this will get a little more complicated due to the fact that we need to work with a plain text password in the application, but with a hashed version in the database, and knowing that complication, it’s better to work out the model step by step, so first, let’s put in some validation:

require 'digest/sha1'
  
class User < ActiveRecord::Base
  
  validates_presence_of :name
  validates_uniqueness_of :name

  attr_accessor :password_confirmation
  validates_confirmation_of :password

  validate :password_non_blank
  
  private

  def password_non_blank
    errors.add_to_base("Missing password") if hashed_password.blank?
  end
end

You should be familiar with the presence and uniqueness validation, we’ve used before, but there’s a new one named validates_confirmation_of that will basically check if 2 fields are matching, in this case, the password field with a ‘Confirm Password’ field (this is pretty common). Lastly we validate the existence of a password, but notice we check for its hashed version, not itself, this is because the hashed version is the one being stored in the database, thus, what matters to validation. The require method includes the SHA1 digest library, allowing us to use all its methods, so let’s get to that by adding a new private class method to the user model that will encrypt the plain text password into a hashed result:

def self.encrypted_password(password,salt)
  string_to_hash = password + "secretkey" + salt
  Digest::SHA1.hexdigest(string_to_hash)
end

The method above is taking a plain text password (which the user inputs) and a random value named salt, which is used to concatenate to the password and stored in the database making the hashed value even more unpredictable. Notice the “secretkey”, which makes the hashed value even richer, and is only available to people who have access to the Rails code. A digest is then applied to that whole string, turning it into a 160-bit hash such as “588aa91d21e71d0e2e3f6febfe8e90e1819ae97f”.
The salt was mentioned but a way to create it wasn’t, so we’ll be doing that by adding another private class method:

def create_new_salt
  self.salt = self.object_id.to_s + rand.to_s
end

Notice that by writing self.salt, we’re avoiding that Ruby treats salt as a local variable, which would be of no effect to us since it would just get discarded and collected by the garbage collector, by doing this we’re telling it to use the salt= accessor method instead, which updates its value, in this case, we’re updating with the object’s id (by using an accessor method that returns the object’s current id) concatenated with a random string.

Now that we have the hashing process all set up, we need to make it so a plain text password sent by an user input will be treated and sent into the database, to do that, we’ll use a so called ‘virtual attribute’, which is something we can manipulate but doesn’t go into the database. Of course we can’t use Rails’ built-in accessor methods for that virtual attribute, because that would make it go into the database, instead, we’ll hard code our own accessors into the user model:

def password
  @password
end

def password=(pwd)
  @password = pwd
  return if pwd.blank?
  create_new_salt
  self.hashed_password = User.encrypted_password(self.password,self.salt)
end

Last thing we need to is create a User class method that checks if the right login/password combination has been supplied and returns that object if it has.

def self.authenticate(name,password)
  user = self.find_by_name(name)
  if user
    expected_password = encrypted_password(password,user.salt)
    if user.hashed_password != expected_password
      user = nil
    end
  end
  user
end

Notice the find_by_name method, whenever an undefined method that starts with find_by_ is called, Rails looks for the last part of the string, in this case, name, and begins a search on the class database table using that part as a string, in this case, it searches all users by their name using the name passed as a parameter.

Let’s put everything we did to the user model so far in a single chunk of code:

require 'digest/sha1'

class User < ActiveRecord::Base

  validates_presence_of :name
  validates_uniqueness_of :name

  attr_accessor :password_confirmation
  validates_confirmation_of :password

  validate :password_non_blank

  # 'password' is a virtual attribute

  def password
    @password
  end

  def password=(pwd)
    @password = pwd
    return if pwd.blank?
    create_new_salt
    self.hashed_password = User.encrypted_password(self.password,self.salt)
  end

  def self.authenticate(name,password)
    user = self.find_by_name(name)
    if user
      expected_password = encrypted_password(password,user.salt)
      if user.hashed_password != expected_password
        user = nil
      end
    end
    user
  end

  private

  def password_non_blank
    errors.add_to_base("Missing password") if hashed_password.blank?
  end

  def self.encrypted_password(password,salt)
    string_to_hash = password + "wibble" + salt
    Digest::SHA1.hexdigest(string_to_hash)
  end

  def create_new_salt
    self.salt = self.object_id.to_s + rand.to_s
  end

end

Moving on to the administration itself, let’s edit the user controller file that was generated by scaffold, first, the index method:

def index
    @users = User.find(:all, :order => :name)

    respond_to do |format|
      format.html # index.html.erb
      format.xml  { render :xml => @users }
    end
  end

All we did was alter the first second line of code to return all users to the @users instance variable. Moving on to the create method:

def create
    @user = User.new(params[:user])

    respond_to do |format|
      if @user.save
        flash[:notice] = "User #{@user.name} was successfully created."
        format.html { redirect_to(:action => :index) }
        format.xml  { render :xml => @user, :status => :created, :location => @user }
      else
        format.html { render :action => "new" }
        format.xml  { render :xml => @user.errors, :status => :unprocessable_entity }
      end
    end
  end

This time we added a flash notice and an index redirection, because it really doesn’t make sense to redirect to the edit page. Finally, the update method:

def update
    @user = User.find(params[:id])

    respond_to do |format|
      if @user.update_attributes(params[:user])
        flash[:notice] = "User #{@user.name} was successfully updated."
        format.html { redirect_to(:action => :index) }
        format.xml  { head :ok }
      else
        format.html { render :action => "edit" }
        format.xml  { render :xml => @user.errors, :status => :unprocessable_entity }
      end
    end
  end

Same flash notice and redirection as the create method.

We have our model and our controller ready, now we did to edit some view files, let’s start by the index.html.erb (inside the user folder, of course)

<h1>Listing users</h1>

<table>
  <tr>
    <th>Name</th>
  </tr>

<% @users.each do |user| %>
  <tr>
    <td><%=h user.name %></td>
    <td><%= link_to 'Show', user %></td>
    <td><%= link_to 'Edit', edit_user_path(user) %></td>
    <td><%= link_to 'Destroy', user, :confirm => 'Are you sure?', :method => :delete %></td>
  </tr>
<% end %>
</table>

<br />

<%= link_to 'New user', new_user_path %>

The scaffold generates viewing of all the table’s attributes, which in this case is a bad thing because the password and salt information do not need to show, so we removed all the data related to that in the code above, and with that same line of though, we’ll now edit the new.html.erb file:

<div class="depot-form">
  <%= error_messages_for :user%>
  <% form_for(@user) do |f| %>
  <fieldset>
    <legend>Enter User Details</legend>
    <p>
      <label for="user_name">Name:</label>
      <%=  f.text_field:name, :size=>40 %>
    </p>
    <p>
      <label for="user_password">Password:</label>
      <%=  f.password_field :password, :size=>40%>
    </p>
    <p>
      <label for="user_password_confirmation">Confirm:</label>
      <%=  f.password_field :password_confirmation, :size=>40 %>
    </p>
    <p>
      <%=f.submit "Add User", :class => "submit" %>
    </p>
  </fieldset>
  <% end %>
</div>

<%= link_to 'Back', users_path %>

Notice we removed the salt and hashed password information again and replaced it with a common password field and a confirmation field. As seen on Task A, we need to add our custom stylesheet to the stylesheet helper on the user layout file (it comes with the default scaffold stylesheet):

<!DOCTYPE htmlPUBLIC "-//W3C//DTDXHTML1.0Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="content-type" content="text/html;charset=UTF-8"/>
<title>Users:<%= controller.action_name %></title>
<%= stylesheet_link_tag'scaffold','depot' %>
</head>

All done, now we have a user management system with some design on it, you should add a user now, we will need it for the next step which will be the creation of a login system, which will be the primal basis for our administration system, so let’s get to it. First thing we need to do is generate a controller with some specific actions (at the command propmt in your application directory):

rubyscript/generate controller admin login logout index

With all files created, let’s edit the login method in the recently created admin controller:

def login
    session[:user_id] = nil
    if request.post?
      user = User.authenticate(params[:name], params[:password])
      if user
        session[:user_id] = user.id;
        redirect_to(:action => "index")
      else
        flash.now[:notice] = "Invalid user/password combination"
      end
    end
  end

Notice the request.post? condition, this is a way to know whether the page is being executed through a GET request (by some URL) or through a POST request (through a form), this way the action knows that it should only execute the login logic case the user has already submitted the form, if he hasn’t, the login view file will be parsed. The login logic is pretty simple, the authenticate method is called using the name and password passed into the request by the login form and returns a user case there is one (or nil case there isn’t), then a session variable is set using the user id and a redirection is made.
Having the controller all set, it’s time to edit the login view file (that will get parsed case the login action is accessed by some URL or redirection):

<div class="depot-form">
  <% form_tag do %>
  <fieldset>
    <legend>Please Log In</legend>
    <p>
      <label for="name">Name:</label>
      <%= text_field_tag :name, params[:name] %>
    </p>
    <p>
      <label for="password">Password:</label>
      <%= password_field_tag :password, params[:password]%>
    </p>
    <p>
      <%= submit_tag "Login" %>
    </p>
  </fieldset>
  <% end %>
</div>

Since there is no ‘admin’ model, we have to hard code the form using different helpers. This time, instead of form_for, we’re using form_tag, which creates a simple form tag that envelops the Ruby block. Then we use field_tags, which need a name and a value, which, in this case, are being filled in directly with parameters passed by the params hash. Okay, so the user logs in, then what? Then an index page shows, one which we will edit right now: (index.html.erb in the admin controller)

<h1>Welcome</h1>
It's <%= Time.now %>
We have<%= pluralize(@total_orders, "order") %>.

Pluralize helper just checks if it should say ‘order’ or ‘orders’, and its using a instance variable which we didn’t define yet. Going back to the controller, let’s add the until-now undefined @total_orders variable to the index method:

def index
  @total_orders = Order.count
end

Order.count returns the number of rows in the orders table. Now that we have our first administration page working with our login system, it’s time to actually make logging in necessary to anyone who tries to access administrating pages (which is the whole point). This sounds much, but it’s just one line of code (and a method). But what would be the perfect place to put that in, no use putting in the admin controller because the product maintenance would be accessible, and what about everything else related to administration that isn’t in the admin controller? As you can see, this requires global attention, and when it comes to globalizing actions, the best way to do it is by editing the application controller, which is the ‘mother’ of all controllers (all controllers heir all methods from the application controller). Knowing the right place to mess with (application_controller.rb), code this in:

class ApplicationController < ActionController::Base

  before_filter :authorize, :except => :login
  
  helper :all
  protect_from_forgery

  protected

  def authorize
    unless User.find_by_id(session[:user_id])
      flash[:notice] = "Please log in"
      redirect_to :controller => :admin, :action => :login
    end
  end

end

We’re telling Rails that we want to execute the authorize method before rendering any page, with the exception of the login action, obviously (you don’t have to be authorized to access the login page), redirecting the user to the login page and flashing a message pointing that he needs to log in first. But we don’t want the log in page to be forced on the user in the store page, for example, so what do we do? We could just add a bunch of exceptions, but that wouldn’t look nice and would need maintenance every time something new was added to the application, so it’s better to just override the authorize method on every controller that doesn’t need authorization, an example of this can be shown by adding few lines of code to the store controller:

protected

def authorize
end

Now we have our logic all set for the administration system, so we need to make the layout a little bit more consistant, and we’ll do that by adding the same sidebar as the one in the store layout to all pages, with a few options added for logged in administrators. This way, we will have a single layout for the whole application, needo, huh? Although a big change, this requires only a single line of coded added to the mother of controllers, the application controller:

class ApplicationController < ActionController::Base

  layout "store"

  before_filter :authorize, :except => :login
  
  helper :all
  protect_from_forgery

  protected

  def authorize
    unless User.find_by_id(session[:user_id])
      flash[:notice] = "Please log in"
      redirect_to :controller => :admin, :action => :login
    end
  end

end

With one line of code we made the whole application include the store layout, which is prettier than anything we’ve done so far. Make sure you delete the ‘users’ and the ‘products’ layout, by simple file deletion or by the command prompt: (rm command for Linux users, erase command for Windows users)

rm app/views/layouts/products.html.erb
rm app/views/layouts/users.html.erb

Now that the store layout is global, we need to make a few changes to it. First, a condition is needed to check if there is a @cart instance variable before executing the code relative to the shopping cart, we didn’t need this before because all the store actions instantiated the @cart object, but other controllers won’t. Next we will add a condition to check if there is someone logged in (an administrator, of course) and show some administrative options on the sidebar. All this can be done with this code (in the store layout file):

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" >
<html>
<head>
<title>Pragprog Books Online Store</title>
<%= stylesheet_link_tag "depot" , :media => "all" %>
<%= javascript_include_tag :defaults %>
</head>
<body id="store">
<div id="banner">
<%= image_tag("logo.png" ) %>
<%= @page_title || "Pragmatic Bookshelf" %>
</div>
<div id="columns"> </div>
<div id="side">
  <div id="cart">
    <% if @cart %>
      <% hidden_div_if(@cart.items.empty?, :id => "cart" ) do %>
      <%= render(:partial => "cart", :object => @cart) %>
      <% end %>
    <% end %>
  </div>
  <a href="http://www....">Home</a><br />
  <a href="http://www..../faq">Questions</a><br />
  <a href="http://www..../news">News</a><br />
  <a href="http://www..../contact">Contact</a><br />
  <%if session[:user_id]%>
    <br/>
    <%= link_to 'Orders',  :controller => 'orders' %><br/>
    <%= link_to 'Products',:controller => 'products' %><br/>
    <%= link_to 'Users',   :controller => 'users' %><br/>
    <br/>
    <%=link_to 'Logout',   :controller => :admin, :action => 'logout'%>
  <% end %>
</div>
<div id="main">
  <% if flash[:notice] -%>
  <div id="notice"> <%=flash[:notice]%> </div>
  <%  end -%>
<%= yield :layout %>
</div>
</body>
</html>

Next we’ll be adding a few security measures so that the last administrator cannot be deleted making the administration panel unaccessible. Rails has methods called ‘hook’ methods, we already used the ‘validate’ hook that validates an object state, and now it’s time to use a hook that will raise an exception on a database line deletion, making it rollback case the hook returns true. To do this we only need to add a method to the user model:

def after_destroy
  if User.count.zero?
    raise "Can't delete last user"
  end
end

Now we need to rescue that exception and show it to the user, and since it is risen when we order the deletion of a user, we’ll need to rescue in on the destroy action of the user controller:

def destroy
  @user = User.find(params[:id])
  begin
    flash[:notice] = "User #{@user.name} deleted"
    @user.destroy
  rescue Exception => e
    flash[:notice] = e.message
  end
  
  respond_to do |format|
    format.html { redirect_to(users_url) }
    format.xml  { head :ok }
  end
end

There we go, now an error message will pop case the last user is issued to deletion. Wait, we almost forgot about one thing: the log out action, we created a link to it on the sidebar but didn’t set the action itself, so let’s get to that (on the admin controller):

def logout
  session[:user_id] = nil
  flash[:notice] = "Logged out"
  redirect_to(:action=> "login")
end

Session clearing, flash notice and a redirection, nothing we haven’t mastered already.

Now we’re all set on the administration system, the user can log in and out, add users, and know that it is all being restricted. But, before we finish this task, let’s apply the filter concept we learned in the store controller. Remember all those @cart = find_cart assignments? They’re almost in every store action, which is ugly because it’s duplicated code, and thanks to filters, we can avoid that by making use of one. First we need to change the find_cart method to assign an instance variable (instead of returning something that would get assigned to an instance variable):

def find_cart
  @cart=(session[:cart]||=Cart.new)
end

And lastly, add the filter to the beggining of the controller (with an exception of the empty_cart method which clears out the instance variable):

before_filter:find_cart,:except=>:empty_cart

Now the find_cart method is executed everywhere in the store controller and you can remove all the @cart = find_cart assignments from it.

Our application is coming together, we’re almost done! See you next task.