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 => &quot;admin&quot; else redirect_to :action => &quot;index&quot; end else flash[:notice] = &quot;Sorry, no user was found with that username/password combination.&quot; 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 &quot;Login&quot; %> <% 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.