Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Agile Web Development With Rails, 1st Edition (2005).pdf
Скачиваний:
34
Добавлен:
17.08.2013
Размер:
7.99 Mб
Скачать

Chapter 11

Task F: Administrivia

We have a happy customer—in a very short time we’ve jointly put together a basic shopping cart that she can start showing to customers. There’s just one more change that she’d like to see. Right now, anyone can access the administrative functions. She’d like us to add a basic user administration system that would force you to log in to get into the administration parts of the site.

We’re happy to do that, as it gives us a chance to play with callback hooks and filters, and it lets us tidy up the application somewhat.

Chatting with our customer, it seems as if we don’t need a particularly sophisticated security system for our application. We just need to recognize a number of people based on user names and passwords. Once recognized, these folks can use all of the administration functions.

11.1 Iteration F1: Adding Users

Let’s start by creating a simple database table to hold the user names and hashed passwords for our administrators.1

File 57

create table users (

 

 

id

int

not null auto_increment,

 

name

varchar(100)

not null,

 

hashed_password char(40)

null,

 

primary key (id)

 

 

 

);

 

 

We’ll create the Rails model too.

1Rather than store passwords in plain text, we’ll feed them through an SHA1 digest, resulting in a 160-bit hash. We check a user’s password by digesting the value they give us and comparing that hashed value with the one in the database.

Prepared exclusively for Rida Al Barazi

ITERATION F1: ADDING USERS 119

depot> ruby script/generate model User exists app/models/

exists test/unit/ exists test/fixtures/

create app/models/user.rb create test/unit/user_test.rb create test/fixtures/users.yml

Now we need some way to create the users in this table. In fact, it’s likely that we’ll be adding a number of functions related to users: login, list, delete, add, and so on. Let’s keep things tidy by putting them into their own controller. At this point, we could invoke the Rails scaffolding generator that we used when we work on product maintenance, but this time let’s do it by hand.2 That way, we’ll get to try out some new techniques. So, we’ll generate our controller (Login) along with a method for each of the actions we want.

depot> ruby script/generate controller Login add_user login logout \

 

delete_user list_users

exists

app/controllers/

exists

app/helpers/

create

app/views/login

exists

test/functional/

create

app/controllers/login_controller.rb

create test/functional/login_controller_test.rb

create

app/helpers/login_helper.rb

create

app/views/login/login.rhtml

create

app/views/login/add_user.rhtml

create

app/views/login/delete_user.rhtml

create

app/views/login/list_users.rhtml

We know how to create new rows in a database table; we create an action, put a form into a view, and invoke the model to save data away. But to make this chapter just a tad more interesting, let’s create users using a slightly different style in the controller.

In the automatically generated scaffold code that we used to maintain the products table, the edit action set up a form to edit product data. When that form was completed by the user, it was routed back to a separate save action in the controller. Two separate methods cooperated to get the job done.

In contrast, our user creation code will use just one action, add_user( ). Inside this method we’ll detect whether we’re being called to display the initial (empty) form or whether we’re being called to save away the data in a completed form. We’ll do this by looking at the HTTP method of the

2In fact, we probably wouldn’t use scaffolds at all. You can download Rails code generators which will write user management code for you. Search the Rails wiki (wiki.rubyonrails.com) for login generator. The Salted Hash version is the most secure from brute-force attacks.

Prepared exclusively for Rida Al Barazi

Report erratum

 

ITERATION F1: ADDING USERS

120

 

incoming request. If it has no associated data, it will come in as a GET

 

 

request. If instead it contains form data, we’ll see a POST. Inside a Rails

 

 

controller, the request information is available in the attribute request. We

 

 

can check the request type using the methods get?( ) and post?( ). Here’s

 

 

the code for the add_user( ) action in the file login_controller.rb. (Note that

 

 

we added the admin layout to this new controller—let’s make the screen

 

 

layouts consistent across all administration functions.)

 

File 52

class LoginController < ApplicationController

 

 

layout "admin"

 

 

def add_user

 

 

if request.get?

 

 

@user = User.new

 

 

else

 

 

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

 

 

if @user.save

 

 

redirect_to_index("User #{@user.name} created")

 

 

end

 

 

end

 

 

end

 

 

# . . .

 

 

If the incoming request is a GET, the add_user( ) method knows that there

 

 

is no existing form data, so it creates a new User object for the view to use.

 

 

If the request is not a GET, the method assumes that POST data is present.

 

 

It loads up a User object with data from the form and attempts to save it

 

 

away. If the save is successful, it redirects to the index page; otherwise it

 

 

displays its own view again, allowing the user to correct any errors.

 

 

To get this action to do anything useful, we’ll need to create a view for

 

 

it. This is the template add_user.rhtml in app/views/login. Note that the

 

 

form_tag needs no parameters, as it defaults to submitting the form back

 

 

to the action and controller that rendered the template.

 

File 55

<% @page_title = "Add a User" -%>

 

 

<%= error_messages_for 'user' %>

 

<%= form_tag %>

<table> <tr>

<td>User name:</td>

<td><%= text_field("user", "name") %></td>

</tr> <tr>

<td>Password:</td>

<td><%= password_field("user", "password") %></td>

</tr>

<tr> <td></td>

<td><input type="submit" value=" ADD USER " /></td>

</tr> </table>

<%= end_form_tag %>

Prepared exclusively for Rida Al Barazi

Report erratum

ITERATION F1: ADDING USERS 121

 

What’s less straightforward is our user model. In the database, the user’s

 

password is stored as a 40-character hashed string, but on the form the

 

user types it in plain text. The user model needs to have a split personal-

 

ity, maintaining the plain-text password when dealing with form data but

 

switching to deal with a hashed password when writing to the database.

 

Because the User class is an Active Record model, it knows about the

 

columns in the users table—it will have a hashed_password attribute auto-

 

matically. But there’s no plain-text password in the database, so we’ll use

 

Ruby’s attr_accessor to create a read/write attribute in the model.

File 54

class User < ActiveRecord::Base

 

attr_accessor :password

 

We need to ensure that the hashed password gets set from the value in the

 

plain-text attribute before the model data gets written to the database. We

 

can use the hook facility built into Active Record to do just that.

 

Active Record defines a large number of callback hooks that are invoked

 

at various points in the life of a model object. Callbacks run, for example,

 

before a model is validated, before a row is saved, after a new row has been

 

created, and so on. In our case, we can use the before and after creation

 

callbacks to manage the password.

 

Before the user row is saved, we use the before_create( ) hook to take a

 

plain-text password and apply the SHA1 hash function to it, storing the

 

result in the hashed_password attribute. That way, the hashed_password

 

column in the database will be set to the hashed value of the plain-text

 

password just before the model is written out.

 

After the row is saved, we use the after_create( ) hook to clear out the plain-

 

text password field. This is because the user object will eventually get

 

stored in session data, and we don’t want these passwords to be lying

 

around on disk for folks to see.

 

There are a number of ways of defining hook methods. Here, we’ll simply

 

define methods with the same name as the callbacks (before_create( ) and

 

after_create( )). Later, on page 126, we’ll see how we can do it declaratively.

 

Here’s the code for this password manipulation.

File 54

require "digest/sha1"

 

class User < ActiveRecord::Base

 

attr_accessor :password

 

attr_accessible :name, :password

 

def before_create

 

self.hashed_password = User.hash_password(self.password)

 

end

attr_accessor

page 472

Prepared exclusively for Rida Al Barazi

Report erratum

 

ITERATION F1: ADDING USERS

122

 

def after_create

 

 

@password = nil

 

 

end

 

 

private

 

 

def self.hash_password(password)

 

 

Digest::SHA1.hexdigest(password)

 

 

end

 

 

end

 

 

Add a couple of validations, and the work on the user model is done (for

 

 

now).

 

File 54

class User < ActiveRecord::Base

 

attr_accessor :password

attr_accessible :name, :password

validates_uniqueness_of :name validates_presence_of :name, :password

The add_user( ) method in the login controller calls the redirect_to_index( ) method. We’d previously defined this in the store controller on page 91, so it isn’t accessible in the login controller. To make the redirection method accessible across multiple controllers we need to move it out of the store controller and into the file application.rb in the app/controllers directory. This file defines class ApplicationController, which is the parent of all the controller classes in our application. Methods defined here are available in all these controllers.

File 51

class ApplicationController < ActionController::Base

 

model :cart

 

model :line_item

 

private

 

def redirect_to_index(msg = nil)

 

flash[:notice] = msg if msg

redirect_to(:action => 'index') end

end

That’s it: we can now add users to our database. Let’s try it. Navigate to http://localhost:3000/login/add_user, and you should see this stunning example of page design.

Prepared exclusively for Rida Al Barazi

Report erratum