What does it mean to add login support for administrators of our store?
We need to provide a form that allows them to enter a username and password.
Once they’re logged in, we need to record that fact somehow for the rest of the session (or until they log out).
We need to restrict access to the administrative parts of the application, allowing only people who are logged in to administer the store.
We could put all of the logic into a single controller, but it makes more sense to split it into two: a session controller to support logging in and out and a controller to welcome administrators:
| depot> bin/rails generate controller Sessions new create destroy |
| depot> bin/rails generate controller Admin index |
The SessionsController#create action will need to record something in session to say that an administrator is logged in. Let’s have it store the ID of that person’s User object using the key :user_id. The login code looks like this:
| def create |
» | user = User.find_by(name: params[:name]) |
» | if user.try(:authenticate, params[:password]) |
» | session[:user_id] = user.id |
» | redirect_to admin_url |
» | else |
» | redirect_to login_url, alert: "Invalid user/password combination" |
» | end |
| end |
This code makes use of the Rails try method, which checks to see if a variable has a value of nil before trying to call the method. If you’re using Ruby 2.3, you can use the version of this that’s built into the language instead:
| if user&.authenticate(params[:password]) |
We’re also doing something else new here: using a form that isn’t directly associated with a model object. To see how that works, let’s look at the template for the sessions#new action:
| <section class="depot_form"> |
| <% if flash[:alert] %> |
| <aside class="notice"><%= flash[:alert] %></aside> |
| <% end %> |
| |
| <%= form_tag do %> |
| <h2>Please Log In</h2> |
| <div class="field"> |
| <%= label_tag :name, 'Name:' %> |
| <%= text_field_tag :name, params[:name] %> |
| </div> |
| |
| <div class="field"> |
| <%= label_tag :password, 'Password:' %> |
| <%= password_field_tag :password, params[:password] %> |
| </div> |
| |
| <div class="actions"> |
| <%= submit_tag "Login" %> |
| </div> |
| <% end %> |
| </section> |
This form is different from ones you saw earlier. Rather than using form_with, it uses form_tag, which simply builds a regular HTML <form>. Inside that form, it uses text_field_tag and password_field_tag, two helpers that create HTML <input> tags. Each helper takes two parameters. The first is the name to give to the field, and the second is the value with which to populate the field. This style of form allows us to associate values in the params structure directly with form fields—no model object is required. In our case, we choose to use the params object directly in the form. An alternative would be to have the controller set instance variables.
We also make use of the label_tag helpers to create HTML <label> tags. This helper also accepts two parameters. The first contains the name of the field, and the second contains the label to be displayed.
See the figure. Note how the value of the form field is communicated between the controller and the view via the params hash: the view gets the value to display in the field from params[:name], and when the user submits the form, the new field value is made available to the controller the same way.
If the user successfully logs in, we store the ID of the user record in the session data. We’ll use the presence of that value in the session as a flag to indicate that an administrative user is logged in.
As you might expect, the controller actions for logging out are much shorter:
| def destroy |
» | session[:user_id] = nil |
» | redirect_to store_index_url, notice: "Logged out" |
| end |
Finally, it’s about time to add the index page—the first screen that administrators see when they log in. Let’s make it useful. We’ll have it display the total number of orders in our store. Create the template in the index.html.erb file in the app/views/admin directory. (This template uses the pluralize helper, which in this case generates the order or orders string, depending on the cardinality of its first parameter.)
| <h1>Welcome</h1> |
| |
| <p> |
| It's <%= Time.now %>. |
| We have <%= pluralize(@total_orders, "order") %>. |
| </p> |
The index action sets up the count:
| class AdminController < ApplicationController |
| def index |
» | @total_orders = Order.count |
| end |
| end |
We have one more task to do before we can use this. Whereas previously we relied on the scaffolding generator to create our model and routes for us, this time we simply generated a controller because there’s no database-backed model for this controller. Unfortunately, without the scaffolding conventions to guide it, Rails has no way of knowing which actions are to respond to GET requests, which are to respond to POST requests, and so on, for this controller. We need to provide this information by editing our config/routes.rb file:
| Rails.application.routes.draw do |
» | get 'admin' => 'admin#index' |
| |
» | controller :sessions do |
» | get 'login' => :new |
» | post 'login' => :create |
» | delete 'logout' => :destroy |
» | end |
| |
| resources :users |
| resources :orders |
| resources :line_items |
| resources :carts |
| root 'store#index', as: 'store_index' |
| |
| resources :products do |
| get :who_bought, on: :member |
| end |
| |
| # For details on the DSL available within this file, see |
| # http://guides.rubyonrails.org/routing.html |
| end |
We’ve touched this before, when we added a root statement in Iteration C1: Creating the Catalog Listing. What the generate command will add to this file are fairly generic get statements for each of the actions specified. You can (and should) delete the routes provided for sessions/new, sessions/create, and sessions/destroy.
In the case of admin, we’ll shorten the URL that the user has to enter (by removing the /index part) and map it to the full action. In the case of session actions, we’ll completely change the URL (replacing things like session/create with simply login) as well as tailor the HTTP action that we’ll match. Note that login is mapped to both the new and create actions, the difference being whether the request was an HTTP GET or HTTP POST.
We also make use of a shortcut: wrapping the session route declarations in a block and passing it to a controller class method. This saves us a bit of typing as well as makes the routes easier to read. We’ll describe all you can do in this file in Dispatching Requests to Controllers.
With these routes in place, we can experience the joy of logging in as an administrator. See the following screenshot.
We need to replace the functional tests in the session controller to match what was implemented. First, change the admin controller test to get the admin URL:
| require 'test_helper' |
| |
| class AdminControllerTest < ActionDispatch::IntegrationTest |
| test "should get index" do |
» | get admin_url |
| assert_response :success |
| end |
| |
| end |
Then we implement several tests for both successful and failed login attempts:
| require 'test_helper' |
| |
| class SessionsControllerTest < ActionDispatch::IntegrationTest |
| test "should prompt for login" do |
| get login_url |
| assert_response :success |
| end |
| |
| test "should login" do |
| dave = users(:one) |
| post login_url, params: { name: dave.name, password: 'secret' } |
| assert_redirected_to admin_url |
| assert_equal dave.id, session[:user_id] |
| end |
| |
| test "should fail login" do |
| dave = users(:one) |
| post login_url, params: { name: dave.name, password: 'wrong' } |
| assert_redirected_to login_url |
| end |
| |
| test "should logout" do |
| delete logout_url |
| assert_redirected_to store_index_url |
| end |
| |
| end |
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).