Practical 8

0. Finish Practical Sheet 7.

1. User Accounts. Allowing users to log in and out requires another table containing the user names and associated passwords. The situation is complicated by the fact that passwords must not be stored literally. What’s stored is a digest hash value of a user’s password, in such a way that the original password cannot be computed from the stored data (in case the database gets stolen or otherwise compromised). Rails provides a data type digest for this purpose. Generate the scaffold for a User model as usual:

User  
name string
password digest

What is the full command?

2. Find the migration that has been generated as part of the User scaffold and check its contents. Does it look different from what you would have expected?

Then migrate the database (which command?).

3. Model. In the User class, add validation that ensures for each user that the name is present and unique, i.e., each user must have a name and no two users can have the same name. What is the ruby code for this?

Why can’t you test presence: true on the password field?

Note the command has_secure_password in the User model. It has been added as part of the scaffolding for the password component of type digest. It’s purpose is to manage a password confirmation field on the user form.

4. For the purpose of encrypting the passwords we use the bcrypt gem. Uncomment the corresponding line in the Gemfile. Then run

bundle install

and restart your server.

5. Controller. Some modifications are necessary in the users controller (which file?).

First, in the index method, replace @users = User.all by @users = User.order(:name), to have user names listed alphabetically.

6. Then, in the create and in the update methods, instead of @user, redirect to users_url after successfully creating or updating the user, and insert #{@user.name} into the :notice strings. Note that these strings now need to be delimited by double quotes, rather than single quotes, to enable string interpolation, a ruby feature that will replace #{@user.name} automatically by the user’s name.

7. Views. There is no notice yet, in the users index view. To display the notice about newly created or updated users, add the lines

<% if notice %>
  <aside id="notice"><%= notice %></aside>
<% end %>

to the top of the index view.

8. Modify the form partial (which file?) that is used to enter new users and to update existing users so that it looks as follows:

<div class="depot_form">

<%= form_with(model: @user, local: true) do |form| %>
  <% if @user.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@user.errors.count, "error") %>
        prohibited this user from being saved:</h2>
      <ul>
      <% @user.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <fieldset>
  <legend>Enter User Details</legend>

  <div class="field">
    <%= form.label :name, 'Name:' %>
    <%= form.text_field :name, size: 40 %>
  </div>

  <div class="field">
    <%= form.label :password, 'Password:' %>
    <%= form.password_field :password, size: 40 %>
  </div>


  <div class="field">
    <%= form.label :password_confirmation, 'Confirm:' %>
    <%= form.password_field :password_confirmation, size: 40 %>
  </div>

  <div class="actions">
    <%= form.submit %>
  </div>

  </fieldset>
<% end %>

</div>

Note how <legend> and <fieldset> tags have been added to improve the appearance of the form, and that the entire form is enclosed in a <div> tag with the class .depot_form that is defined in the style sheet.

9. Navigate to http://localhost:3000/users/new and create an account for yourself, and another one for a friend. The users index will just show a list of names. To see that password related information has arrived in the database, enter the following bit of SQL on the command line:

sqlite3 -line db/development.sqlite3 "select * from users"

Listing the password digests next to user name presumably would not look nice, nor be informative.

10. Test. We need to update the tests to reflect the changes to validations and redirections. The test "should create user" should read like this:

test "should create user" do
  assert_difference('User.count') do
    post users_url, params: { user: { name: 'sam',
      password: 'secret', password_confirmation: 'secret' } }
  end

  assert_redirected_to users_path
end

In a similar way, modify the assert_redirected_to line of the "should update user" test.

11. In the users.yml fixtures, change the names of the two users to conor and emma (in that order).

Note how some values in the fixtures are now computed dynamically in the form of embedded ruby. This is yet another effect of scaffolding a password component of type digest.

11a. If all works fine, commit the changes to your local git repository, and push them to the github cloud.

12. Authentication. In order to support logins of store administrators, the site needs

  • a login page, i.e., a form that allows users to enter their names and passwords,

  • a place to record that a user is logged in,

  • a way to restrict access to the administrative part of the store.

Let’s start by generating some actions in two controllers:

rails generate controller sessions new create destroy
rails generate controller admin index

13. The create method in the sessions controller, should record the user id in the session hash and redirect to the admin home page, if the correct password is provided,. Otherwise, it reloads the login page. Modify the method to look as follows:

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

(Here, admin_url and login_url are symbolic names for routes that still have to be defined.)

14. For the login page, we use a form that (unlike the forms we have used before) is not directly linked to a model object – it would not make sense here, would it? This is the sessions/new.html.erb view:

<section class="depot_form">
  <% if flash[:alert] %>
    <aside class="notice"><%= flash[:alert] %></aside>
  <% end %>
  <%= form_tag do %>
    <fieldset>
      <legend>Please Log In</legend>
      <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>
    </fieldset>
  <% end %>
</section>

Note how the form uses the helper methods form_tag and friends. Also note how it directly accesses and uses the params hash in order to communicate field values between the form and the server.

15. Logging out is easy: remove the user id from the session hash and send the browser back to the catalog page. Use the following two lines as body of the session controller’s destroy method:

session[:user_id] = nil
redirect_to store_url, notice: "Logged out"

16. Prepare the admin/index view to greet a freshly logged in administrator with useful information:

<h1>Welcome</h1>

<p>
  It's <%= Time.now %>.
  We have <%= pluralize(@total_orders, "order") %>.
</p>

17. For this to work, the instance variable @total_orders has to be provided by the admin controller. Add the line

@total_orders = Order.count

to its index method.

18. What’s still missing is routes connecting to the symbolic names like admin_url and login_url. Rails has automatically set up routes for admin/index, sessions/new, sessions/create and sessions/destroy in the routes.rb config file. Delete these four, and rename them to the more descriptive admin, login and logout, by inserting

get 'admin' => 'admin#index'

controller :sessions do
  get 'login' => :new
  post 'login' => :create
  delete 'logout' => :destroy
end

into the file instead.

Now you can use the login feature to prevent non admin visitors of the web site from accessing admin pages. Try and log in as an admin.

19. The functional tests in the admin and sessions controllers need to be updated in order to match the recent changes. In the admin controller, change the get request to

get admin_url

Modify the sessions controller test file to look like:

require 'test_helper'

class SessionsControllerTest < ActionController::TestCase
  test "should prompt for login" do
    get login_url
    assert_response :success
  end

  test "should login" do
    conor = users(:one)
    post login_url, params: { name: conor.name, password: 'secret' }
    assert_redirected_to admin_url
    assert_equal conor.id, session[:user_id]
  end

  test "should fail login" do
    conor = users(:one)
    post login_url, params: { name: conor.name, password: 'wrong' }
    assert_redirected_to login_url
  end

  test "should logout" do
    delete logout_url
    assert_redirected_to store_url
  end
end

19a. Run the tests. If all works well commit and push to github.

20. Restricting Access. Rails uses filters at various points in the life cycle of an action. One of these filters is before_action, called before the processing of the action has even started. This would be the right place to check if the current user is logged in as an admin. Define a function

protected

def authorize
  unless User.find_by(id: session[:user_id])
    redirect_to login_url, notice: "Please log in"
  end
end

in the (protected part of the) application controller, and as first line in the definition of the ApplicationController class, add the command

before_action :authorize

in order to use the new authorize function as before_action for every action in this application.

21. This is not exactly what you want, people should still be allowed to shop around and order without administrative privileges. So you need to undo the effect of before_action on those actions that should be generally accessible. Authentication should be disabled for these actions. As first line in the classes StoreController and SessionsController, add the command

skip_before_action :authorize

Access to the catalog and to the login page(!) should be unrestricted.

22. Other controllers need a more fine-tuned access control. Buyers are allowed to create, update and delete carts (but not to see a particular cart, or a list of carts). This is expressed by

skip_before_action :authorize, only: [:create, :update, :destroy]

as first line in the carts controller class definition.

23. They need to be able to create line items:

skip_before_action :authorize, only: :create

(in the line_items controller) and create orders through the new order form:

skip_before_action :authorize, only: [:new, :create]

(in the orders controller).

24. These changes make most tests fail, as now most actions redirect to the login page. This can be rectified in the test/test_helper.rb file by adding the methods:

# Add more helper methods to be used by all tests here...
def login_as(user)
  post login_url params: { name: user.name, password: 'secret' }
end

def logout
  delete logout_url
end

def setup
  login_as users(:one)
end

Now all the tests should pass again.

24a. If all works fine, commit the changes to your local git repository, and push them to the github cloud.

25. It would be convenient, to have an admin menu to the side bar (in the application.html.erb layout file), in such a way that it only appears when a user is logged in:

<% if session[:user_id] %>
  <nav class="logged_in_nav">
    <ul>
      <li><%= link_to 'Orders',   orders_path   %></li>
      <li><%= link_to 'Products', products_path %></li>
      <li><%= link_to 'Users',    users_path    %></li>
      <li><%= button_to 'Logout', logout_path, method: :delete   %></li>
    </ul>
  </nav>
<% end %>

For the styling of the class "logged_user_nav", add the following to the end of the application.scss stylefile:

nav.logged_in_nav {
  border-top: solid thin #bfb;
  padding: 0.354em 0;
  margin-top: 0.354em;
  input[type="submit"] {
    // Make the logout button look like a
    // link, so it matches the nav style
    background: none;
    border: none;
    color: #bfb;
    font-size: 1em;
    letter-spacing: 0.354em;
    margin: 0;
    padding: 0;
    text-transform: uppercase;
  }
  input[type="submit"]:hover {
    color: white;
  }
}

Now surf through your web site and experience the difference between the buyer’s and the seller’s view …

25a. If all works fine, commit the changes to your local git repository and push them to the github cloud.

Written on December 2, 2020 by CS424.