- •Introduction
- •Rails Is Agile
- •Finding Your Way Around
- •Acknowledgments
- •Getting Started
- •Models, Views, and Controllers
- •Installing Rails
- •Installing on Windows
- •Installing on Mac OS X
- •Installing on Unix/Linux
- •Rails and Databases
- •Keeping Up-to-Date
- •Rails and ISPs
- •Creating a New Application
- •Hello, Rails!
- •Linking Pages Together
- •What We Just Did
- •Building an Application
- •The Depot Application
- •Incremental Development
- •What Depot Does
- •Task A: Product Maintenance
- •Iteration A1: Get Something Running
- •Iteration A2: Add a Missing Column
- •Iteration A4: Prettier Listings
- •Task B: Catalog Display
- •Iteration B1: Create the Catalog Listing
- •Iteration B2: Add Page Decorations
- •Task C: Cart Creation
- •Sessions
- •More Tables, More Models
- •Iteration C1: Creating a Cart
- •Iteration C3: Finishing the Cart
- •Task D: Checkout!
- •Iteration D2: Show Cart Contents on Checkout
- •Task E: Shipping
- •Iteration E1: Basic Shipping
- •Task F: Administrivia
- •Iteration F1: Adding Users
- •Iteration F2: Logging In
- •Iteration F3: Limiting Access
- •Finishing Up
- •More Icing on the Cake
- •Task T: Testing
- •Tests Baked Right In
- •Testing Models
- •Testing Controllers
- •Using Mock Objects
- •Test-Driven Development
- •Running Tests with Rake
- •Performance Testing
- •The Rails Framework
- •Rails in Depth
- •Directory Structure
- •Naming Conventions
- •Active Support
- •Logging in Rails
- •Debugging Hints
- •Active Record Basics
- •Tables and Classes
- •Primary Keys and IDs
- •Connecting to the Database
- •Relationships between Tables
- •Transactions
- •More Active Record
- •Acts As
- •Aggregation
- •Single Table Inheritance
- •Validation
- •Callbacks
- •Advanced Attributes
- •Miscellany
- •Action Controller and Rails
- •Context and Dependencies
- •The Basics
- •Routing Requests
- •Action Methods
- •Caching, Part One
- •The Problem with GET Requests
- •Action View
- •Templates
- •Builder templates
- •RHTML Templates
- •Helpers
- •Formatting Helpers
- •Linking to Other Pages and Resources
- •Pagination
- •Form Helpers
- •Layouts and Components
- •Adding New Templating Systems
- •Introducing AJAX
- •The Rails Way
- •Advanced Techniques
- •Action Mailer
- •Sending E-mail
- •Receiving E-mail
- •Testing E-mail
- •Web Services on Rails
- •Dispatching Modes
- •Using Alternate Dispatching
- •Method Invocation Interception
- •Testing Web Services
- •Protocol Clients
- •Securing Your Rails Application
- •SQL Injection
- •Cross-Site Scripting (CSS/XSS)
- •Avoid Session Fixation Attacks
- •Creating Records Directly from Form Parameters
- •Knowing That It Works
- •Deployment and Scaling
- •Picking a Production Platform
- •A Trinity of Environments
- •Iterating in the Wild
- •Maintenance
- •Finding and Dealing with Bottlenecks
- •Case Studies: Rails Running Daily
- •Appendices
- •Introduction to Ruby
- •Ruby Names
- •Regular Expressions
- •Source Code
- •Cross-Reference of Code Samples
- •Resources
- •Index
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