The Login Page (Ruby on Rails) Part 1

Introduction

This entry picks up where the last one left off and is (as always) based on an entry at the Building Browsergames blog: The Login Page (PHP)

Right off the bat we modify the user model to have two new attributes, one to mark the user as an admin and the other to note the date and time of the last login. Rather than hand writing some SQL to do this, we’ll use the Rails migration capability instead:

> ruby script/generate migration AddAdminFlagAndLastLoginToUser admin:boolean last_login:timestamp
As with the last time we saw a migration created (i.e. when we created the user model), the date and time that appears as part of the migration name will be different for you than in this example:

exists db/migrate
create db/migrate/20080713194325_add_admin_flag_and_last_login_to_user.rb
If we look at the migration file it created we will see that Rails was smart enough to figure out from the name of the migration (somethingsomethingToUser) that we wanted to perform our changes to the users table and we told it what we wanted added so it is almost perfect already:

class AddAdminFlagAndLastLoginToUser < ActiveRecord::Migration
  def self.up
    add_column :users, :admin, :boolean
    add_column :users, :last_login, :timestamp
  end
 
  def self.down
    remove_column :users, :last_login
    remove_column :users, :admin
  end
end

You just need to change the line that ads the new admin boolean to read:

add_column :users, :admin, :boolean, :default => false
Now we run this new migration the same way as we did the last time:

> rake db:migrate
As with the last page we had, we need a combination of a controller action for login and a page in the views to display take input and display errors. Since we already have an Account controller, we’ll use it again and just add a new login method to it. Add the following methods into your app/controllers/account_controller.rb:

def admin
end
 
def login
  if request.post? and params[:user]
    @user = User.new(params[:user])
 
    user = User.find_by_username(@user.username)
 
    # If we found a user with that username and the password provided matches
    # the password on file for that user, we can login the user.
    if user and user.password_matches?(@user.password)
      session[:user_id] = user.id
      user.last_login = Time.now
      user.save
 
      if user.admin?
        redirect_to :action => "admin"
      else
        redirect_to :action => "index"
      end
    else
      flash[:notice] = "Sorry, no user was found with that username/password combination."
    end
  end
end

Because we have to test passwords against user’s who have salt added to their passwords, we put the password testing into the User class so the details of password comparison don’t pollute controllers or views. It is as easy as adding the function we referred to in the login code above to the app/models/user.rb:

def password_matches?(password_to_match)
  self.password == Digest::MD5.hexdigest(self.salt + password_to_match)
end

Note: You must add this code above the line that says “private” in the user.rb.

Then we add two new views. One is a destination just for administrators after they have logged in and the other is to take input for login. The first one will go into app/views/account/admin.html.erb:

<h1>Welcome To The Admin Homepage!</h1>

The other will go in the file app/views/account/login.html.erb:

<% if flash[:notice] %>
  <%= h flash[:notice] %>
<% end %>
 
<% form_for :user, @user do |f| %>
  <%= error_messages_for :user %>
 
  Username: <%= f.text_field :username %><br/>
  Password: <%= f.password_field :password %><br/>
  <%= submit_tag "Login" %>
<% end %>

You’ll notice that the flash notice display is the same as what we saw before in the index.html.erb. We’ll get rid of that repetition in a later installment because there’s no reason to repeat the same code over and over. The form_for is similar to the one we used in registration.html.erb and the login function we added to account_controller.rb is similar in some ways to the registration function. You should start noticing a pattern to how this is done in Rails at this point. The controller acts as glue between the view (which displays information and gathers user input) and the models (which handle saving and loading data and have all the rules for changing that data). Methods in the controller are usually paired with a view file they they automatically transition to as soon as they are done with the controller code. The view then has access to any variables which were created in that method and which had an @ at the beginning of the name. Those are instance variables and are the convenient communication method from the controller to the view.

One thing you might notice is different between the PHP version of this code and the Rails code is that I’m only keeping the user ID in the session rather than the username and an authenticated flag. The simple presence of the user ID alone in the session tells us that the user has authenticated, so there’s really no need for the flag. As for the choice of ID vs. username, that’s really more personal preference though I can say that if you want your user to be able to change his/her username then it would be better if everything keyed off the ID. This is really a minor difference between the implementations and you could go either way in your code.

Extra Credit

Use the console to load up a user you register using the web page and change the admin account on the flag from false to true. Then login and confirm that you did indeed get sent to the new admin page instead of the regular index page.
Read the blog entry Cross Site Scripting: What it is, and how to prevent it. It’s a pretty good primer on another way malicious users can try to exploit or ruin your game. If you look at the view code above you can see that I’ve used the function “h” when I’m displaying the flash[:notice]. It is the Rails equivalent to the functions mentioned in the blog entry that Perl and PHP use for HTML encoding.