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

ITERATION F2: LOGGING IN 123

When we hit the Add User button, the application blows up, as we don’t yet have an index action defined. But we can check that the user data was created by looking in the database.

depot>

mysql depot_development

 

mysql>

select * from users;

 

+

----+

------+

------------------------------------------

+

| id |

name |

hashed _ password

|

+----

+------

+------------------------------------------

 

+

|

1 |

dave |

e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4 |

+----

+------

+------------------------------------------

 

+

1

row in set (0.00 sec)

 

11.2 Iteration F2: Logging In

What does it mean to add login support for administrators of our store?

• We need to provide a form that allows them to enter their user name and password.

• Once they are logged in, we need to record the fact somehow for the rest of their session (or until they log out).

• We need to restrict access to the administrative parts of the applica-

 

tion, allowing only people who are logged in to administer the store.

 

We’ll need a login( ) action in the login controller, and it will need to record

 

something in session to say that an administrator is logged in. Let’s have it

 

store the id of their User object using the key :user_id. The login code looks

 

like this.

File 52

def login

 

if request.get?

 

session[:user_id] = nil

 

@user = User.new

 

else

 

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

 

logged_in_user = @user.try_to_login

if logged_in_user

session[:user_id] = logged_in_user.id redirect_to(:action => "index")

else

flash[:notice] = "Invalid user/password combination" end

end end

This uses the same trick that we used with the add_user( ) method, handling both the initial request and the response in the same method. On the initial GET we allocate a new User object to provide default data to the form. We also clear out the user part of the session data; when you’ve reached the login action, you’re logged out until you successfully log in.

Prepared exclusively for Rida Al Barazi

Report erratum

 

ITERATION F2: LOGGING IN

124

 

If the login action receives POST data, it extracts it into a User object. It

 

 

invokes that object’s try_to_login( ) method. This returns a fresh User object

 

 

corresponding to the user’s row in the database, but only if the name and

 

 

hashed password match. The implementation, in the model file user.rb, is

 

 

straightforward.

 

File 54

def self.login(name, password)

 

 

hashed_password = hash_password(password || "")

 

 

find(:first,

 

 

:conditions => ["name = ? and hashed_password = ?",

 

 

name, hashed_password])

 

 

end

 

 

def try_to_login

 

 

User.login(self.name, self.password)

 

 

end

 

 

We also need a login view, login.rhtml. This is pretty much identical to

 

 

the add_user view, so let’s not clutter up the book by showing it here.

 

 

(Remember, a complete listing of the Depot application starts on page 486.)

 

 

Finally, it’s about time to add the index page, the first thing that admin-

 

 

istrators see when they log in. Let’s make it useful—we’ll have it display

 

 

the total number of orders in our store, along with the number pending

 

 

shipping. The view is in the file index.rhtml in the directory app/views/login.

 

File 56

<% @page_title = "Administer your Store" -%>

 

 

<h1>Depot Store Status</h1>

 

 

<p>

 

 

Total orders in system: <%= @total_orders %>

 

 

</p>

 

 

<p>

 

 

Orders pending shipping: <%= @pending_orders %>

 

 

</p>

 

 

The index( ) action sets up the statistics.

 

File 52

def index

 

 

@total_orders = Order.count

 

 

@pending_orders = Order.count_pending

 

 

end

 

 

And we need to add a class method to the Order model to return the count

 

 

of pending orders.

 

File 53

def self.count_pending

 

 

count("shipped_at is null")

 

 

end

 

Now we can experience the joy of logging in as an administrator.

Prepared exclusively for Rida Al Barazi

Report erratum

ITERATION F3: LIMITING ACCESS 125

We show our customer where we are, but she points out that we still haven’t controlled access to the administrative pages (which was, after all, the point of this exercise).

 

11.3 Iteration F3: Limiting Access

 

We want to prevent people without an administrative login from accessing

 

our site’s admin pages. It turns out that it’s easy to implement using the

 

Rails filter facility.

 

Rails filters allow you to intercept calls to action methods, adding your own

 

processing before they are invoked, after they return, or both. In our case,

 

we’ll use a before filter to intercept all calls to the actions in our admin

 

controller. The interceptor can check session[:user_id]. If set, the application

 

knows an administrator is logged in and the call can proceed. If it’s not

 

set, the interceptor can issue a redirect, in this case to our login page.

 

Where should we put this method? It could sit directly in the admin

 

controller, but, for reasons that will become apparent shortly, let’s put

 

it instead in the ApplicationController, the parent class of all our controllers.

 

This is in the file application.rb in the directory app/controllers.

File 59

def authorize

 

unless session[:user_id]

 

flash[:notice] = "Please log in"

 

redirect_to(:controller => "login", :action => "login")

 

end

 

end

 

This authorization method can be invoked before any actions in the admin-

 

istration controller by adding just one line.

Prepared exclusively for Rida Al Barazi

Report erratum

ITERATION F3: LIMITING ACCESS 126

File 58

class AdminController < ApplicationController

before_filter :authorize

# ...

 

We need to make a similar change to the login controller. Here, though, we

 

want to allow the login action to be invoked even if the user is not logged

 

in, so we exempt it from the check.

File 60

class LoginController < ApplicationController

 

before_filter :authorize, :except => :login

 

# . .

 

If you’re following along, delete your session file (because in it we’re already

 

logged in). Navigate to http://localhost:3000/admin/ship. The filter method

 

intercepts us on the way to the shipping screen and shows us the login

 

screen instead.

We show our customer and are rewarded with a big smile and a request. Could we add the user administration stuff to the menu on the sidebar and add the capability to list and delete administrative users? You betcha!

Adding a user list to the login controller is easy; in fact it’s so easy we won’t bother to show it here. Have a look at the source of the controller on page 490 and of the view on page 498. Note how we link the delete functionality to the list of users. Rather than have a delete screen that asks for a user name and then deletes that user, we simply add a delete link next to each name in the list of users.

Would the Last Admin to Leave...

The delete function does raise one interesting issue, though. We don’t want to delete all the administrative users from our system (because if we

Prepared exclusively for Rida Al Barazi

Report erratum

ITERATION F3: LIMITING ACCESS 127

 

did we wouldn’t be able to get back in without hacking the database). To

 

prevent this, we use a hook method in the User model, arranging for the

 

method dont_destroy_dave( ) to be called before a user is destroyed. This

 

method raises an exception if an attempt is made to delete the user with

 

the name dave (Dave seems to be a good name for the all-powerful user,

 

no?). We’ll take the opportunity to show the second way of defining call-

 

backs, using a class-level declaration (before_destroy), which references the

 

instance method that does the work.

File 61

before_destroy :dont_destroy_dave

 

def dont_destroy_dave

 

raise "Can't destroy dave" if self.name == 'dave'

 

end

raise

page 477

This exception is caught by the delete( ) action in the login controller, which reports an error back to the user.

File 60

def delete_user

 

id

= params[:id]

 

if

id && user = User.find(id)

 

 

begin

user.destroy

flash[:notice] = "User #{user.name} deleted" rescue

flash[:notice] = "Can't delete that user" end

end

redirect_to(:action => :list_users) end

rescue

page 477

 

Updating the Sidebar

 

Adding the extra administration functions to the sidebar is straightfoward.

 

We edit the layout admin.rhtml and follow the pattern we used when adding

 

the functions in the admin controller. However, there’s a twist. We can use

 

the fact that the session information is available to the views to determine

 

if the current session has a logged-in user. If not, we suppress the display

 

of the sidebar menu altogether.

File 62

<html>

<head>

<title>ADMINISTER Pragprog Books Online Store</title>

<%= stylesheet_link_tag "scaffold", "depot", "admin", :media => "all" %>

</head> <body>

<div id="banner">

<%= @page_title || "Administer Bookshelf" %>

</div>

<div id="columns"> <div id="side">

<% if session[:user_id] -%>

<%= link_to("Products", :controller => "admin", :action => "list") %><br />

Prepared exclusively for Rida Al Barazi

Report erratum

 

 

ITERATION F3: LIMITING ACCESS

128

 

<%= link_to("Shipping",

:controller => "admin",

 

 

 

:action => "ship") %><br />

 

 

<hr/>

 

 

 

<%= link_to("Add user",

:controller => "login",

 

 

 

:action => "add_user") %><br />

 

 

<%= link_to("List users", :controller => "login",

 

 

 

:action => "list_users") %><br />

 

 

<hr/>

 

 

 

<%= link_to("Log out",

:controller => "login",

 

 

 

:action => "logout") %>

 

 

<% end -%>

 

 

 

</div>

 

 

 

<div id="main">

 

 

 

<% if flash[:notice] -%>

 

 

 

<div id="notice"><%= flash[:notice] %></div>

 

 

<% end -%>

 

 

 

<%= @content_for_layout %>

 

 

 

</div>

 

 

 

</div>

 

 

 

</body>

 

 

 

</html>

 

 

 

Logging Out

 

 

 

Our administration layout has a logout option in the sidebar menu. Its

 

 

implementation in the login controller is trivial.

 

File 60

def logout

 

 

 

session[:user_id] = nil

 

 

 

flash[:notice] = "Logged out"

 

 

redirect_to(:action => "login") end

We call our customer over one last time, and she plays with the store application. She tries our new administration functions and checks out the buyer experience. She tries to feed bad data in. The application holds up beautifully. She smiles, and we’re almost done.

We’ve finished adding functionality, but before we leave for the day we have one last look through the code. We notice a slightly ugly piece of duplication in the store controller. Every action apart from index has to find the user’s cart in the session data. The line

@cart = find_cart

appears five times in the controller. Now that we know about filters we can fix this. We’ll change the find_cart( ) method to store its result directly into the @cart instance variable.

def find_cart

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

Prepared exclusively for Rida Al Barazi

Report erratum