Iteration J4: Adding a Sidebar, More Administration

Let’s start with adding links to various administration functions to the sidebar in the layout and have them show up only if a :user_id is in the session:

 <!DOCTYPE html>
 <html>
  <head>
  <title>Pragprog Books Online Store</title>
 <%=​ csrf_meta_tags ​%>
 
 <%=​ stylesheet_link_tag ​'application'​, ​media: ​​'all'​,
 'data-turbolinks-track'​: ​'reload'​ ​%>
 <%=​ javascript_pack_tag ​'application'​,
 'data-turbolinks-track'​: ​'reload'​ ​%>
  </head>
 
  <body>
  <header class=​"main"​>
 <%=​ image_tag ​'logo.svg'​, ​alt: ​​'The Pragmatic Bookshelf'​ ​%>
  <h1>​<%=​ @page_title ​%>​</h1>
  </header>
  <section class=​"content"​>
  <nav class=​"side_nav"​>
 
  <div id=​"cart"​ class=​"carts"​>
 
 <%=​ render_if @cart && @cart.​line_items​.​any?​, @cart ​%>
  </div>
 
  <ul>
  <li><a href=​"/"​>Home</a></li>
  <li><a href=​"/questions"​>Questions</a></li>
  <li><a href=​"/news"​>News</a></li>
  <li><a href=​"/contact"​>Contact</a></li>
  </ul>
»
»<%​ ​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​ ​%>
  </nav>
  <main class=​'​​<%=​ controller.​controller_name​ ​%>​​'​>
 <%=​ ​yield​ ​%>
  </main>
  </section>
  </body>
 </html>

We should also add some light styling. Let’s add this to the end of app/assets/stylesheets/application.scss:

 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 it’s all starting to come together. We can log in, and by clicking a link in the sidebar, we can see a list of users. Let’s see if we can break something.

Would the Last Admin to Leave…

We bring up the user list screen that looks something like the following screenshot; then we click the Destroy link next to dave to delete that user. Sure enough, our user is removed. But to our surprise, we’re then presented with the login screen instead. We just deleted the only administrative user from the system. When the next request came in, the authentication failed, so the application refused to let us in. We have to log in again before using any administrative functions.

images/r_3_user_list.png

But now we have an embarrassing problem: there are no administrative users in the database, so we can’t log in.

Fortunately, we can quickly add a user to the database from the command line. If you invoke the rails console command, Rails invokes Ruby’s irb utility, but it does so in the context of your Rails application. That means you can interact with your application’s code by typing Ruby statements and looking at the values they return.

We can use this to invoke our user model directly, having it add a user into the database for us:

 depot>​​ ​​bin/rails​​ ​​console
 Loading development environment.
 >>​​ ​​User.create(name:​​ ​​'dave'​​,​​ ​​password:​​ ​​'secret'​​,​​ ​​password_confirmation:​​ ​​'secret'​​)
 => ​#<User:0x2933060 @attributes={...} ... >
 >>​​ ​​User.count
 => 1

The >> sequences are prompts. After the first, we call the User class to create a new user, and after the second, we call it again to show that we do indeed have a single user in our database. After each command we enter, rails console displays the value returned by the code (in the first case, it’s the model object, and in the second case, it’s the count).

Panic over. We can now log back in to the application. But how can we stop this from happening again? We have several ways. For example, we could write code that prevents you from deleting your own user. That doesn’t quite work: in theory, A could delete B at just the same time that B deletes A. Let’s try a different approach. We’ll delete the user inside a database transaction. Transactions provide an all-or-nothing proposition, stating that each work unit performed in a database must either complete in its entirety or none of them will have any effect whatsoever. If no users are left after we’ve deleted the user, we’ll roll the transaction back, restoring the user we just deleted.

To do this, we’ll use an Active Record hook method. We’ve already seen one of these: the validate hook is called by Active Record to validate an object’s state. It turns out that Active Record defines sixteen or so hook methods, each called at a particular point in an object’s life cycle. We’ll use the after_destroy hook, which is called after the SQL delete is executed. If a method by this name is publicly visible, it’ll conveniently be called in the same transaction as the delete—so if it raises an exception, the transaction will be rolled back. The hook method looks like this:

 after_destroy ​:ensure_an_admin_remains
 
 class​ Error < StandardError
 end
 
 private
 def​ ​ensure_an_admin_remains
 if​ User.​count​.​zero?
 raise​ Error.​new​ ​"Can't delete last user"
 end
 end

The key concept is the use of an exception to indicate an error when the user is deleted. This exception serves two purposes. First, because it’s raised inside a transaction, it causes an automatic rollback. By raising the exception if the users table is empty after the deletion, we undo the delete and restore that last user.

Second, the exception signals the error back to the controller, where we use a rescue_from block to handle it and report the error to the user in the notice. If you want only to abort the transaction but not otherwise signal an exception, raise an ActiveRecord::Rollback exception instead, because this is the only exception that won’t be passed on by ActiveRecord::Base.transaction:

 def​ ​destroy
  @user.​destroy
  respond_to ​do​ |format|
  format.​html​ { redirect_to users_url,
 notice: ​​'"User #{@user.name} deleted"'​ }
  format.​json​ { head ​:no_content​ }
 end
 end
 
»rescue_from ​'User::Error'​ ​do​ |exception|
» redirect_to users_url, ​notice: ​exception.​message
»end

This code still has a potential timing issue: it’s still possible for two administrators each to delete the last two users if their timing is right. Fixing this would require more database wizardry than we have space for here.

In fact, the login system described in this chapter is rudimentary. Most applications these days use a plugin to do this.

A number of plugins are available that provide ready-made solutions that not only are more comprehensive than the authentication logic shown here but generally require less code and effort on your part to use. Devise[79] is a common and popular gem that does this.

What We Just Did

By the end of this iteration, we’ve done the following:

  • We used has_secure_password to store an encrypted version of the password into the database.

  • We controlled access to the administration functions using before action callbacks to invoke an authorize method.

  • We used rails console to interact directly with a model (and dig us out of a hole after we deleted the last user).

  • We used a transaction to help prevent deletion of the last user.

Playtime

Here’s some stuff to try on your own:

  • Modify the user update function to require and validate the current password before allowing a user’s password to be changed.

  • The system test in test/system/users_test.rb was generated by the scaffolding generator we used at the start of the chapter. Those tests don’t pass. See if you can get them to pass without breaking the other system tests. You’ll recall we created the module AuthenticationHelpers and included it in all of the system tests by default, so you might need to change the code to not do that, so that you can properly test the login functionality.

    When the system is freshly installed on a new machine, no administrators are defined in the database, and hence no administrator can log on. But, if no administrator can log on, then no one can create an administrative user.

    Change the code so that if no administrator is defined in the database, any username works to log on (allowing you to quickly create a real administrator).

  • Experiment with rails console. Try creating products, orders, and line items. Watch for the return value when you save a model object—when validation fails, you’ll see false returned. Find out why by examining the errors:

     >>​​ ​​prd​​ ​​=​​ ​​Product.new
     => ​#<Product id: nil, title: nil, description: nil, image_url:
     nil, created_at: nil, updated_at: nil, price:
     #<BigDecimal:246aa1c,'0.0',4(8)>>
     >>​​ ​​prd.save
     => false
     >>​​ ​​prd.errors.full_messages
     => ["Image url must be a URL for a GIF, JPG, or PNG image",
      "Image url can't be blank", "Price should be at least 0.01",
      "Title can't be blank", "Description can't be blank"]
  • Look up the authenticate_or_request_with_http_basic method and utilize it in your :authorize callback if the request.format is not Mime[:HTML]. Test that it works by accessing an Atom feed:

     curl --silent --user dave:secret
      http://localhost:3000/products/2/who_bought.atom
  • We’ve gotten our tests working by performing a login, but we haven’t yet written tests that verify that access to sensitive data requires login. Write at least one test that verifies this by calling logout and then attempting to fetch or update some data that requires authentication.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.135.205.146