[pragmaticstudio] Ruby on Rails 6 - part 6

User Account Model

Exercises

Objective

Over the next few exercises we’ll incrementally create a basic user account and authentication system from scratch. For the purposes of this exercise, we’ll focus on getting the User model in good shape. Doing that involves the following tasks:

  • Generate a User resource that securely stores user passwords in a password_digest column
  • Install the bcrypt gem
  • Declare reasonable validations in the User model
  • Create a couple example users in the database using the Rails console

We hope we don’t need to belabor the point that passwords should never, ever be stored in the database as plain text . But just for good measure, go ahead and raise your right hand now and repeat aloud after us:

I, (state your name), being a professional in the craft of software development, do hereby promise to take responsibility for securing the passwords entrusted to me by users of my application. I will never, under any circumstance including project deadline pressures, store the plain-text version of a user’s password in my database or other storage medium. Furthermore, I will not hold Mike and Nicole liable for any wrongdoing on my part. And I won’t use totally obvious passwords such as ‘123456’ when creating my own account.

OK, then. We’re ready to make good on that promise!

1. Generate the User Resource

First we need to create a new (third) resource to represent a user with a name, e-mail address, and secure password. We’ll need a users database table, a User model, and ultimately a UsersController and conventional routes for interacting with users via the web. We’ll use the resource generator to make quick work of this.

  1. Start by generating a resource named user (singular) with name and email attributes of type string , and a password attribute of type digest .

[ANSWER]

rails g resource user name:string email:string password:digest
  1. We’ll look at everything that got generated as we go along. For now, open the generated migration file and you should see the following:
class CreateUsers < ActiveRecord::Migration[6.0]
  def change
    create_table :users do |t|
      t.string :name
      t.string :email
      t.string :password_digest

      t.timestamps
    end
  end
end

Notice that the generator translated the declaration password:digest into a password_digest column of type string in the migration file. That’s pretty smart!

  1. To actually create the users database table, you need to run the migration.
rails db:migrate
  1. The generator also gave us a User model in the app/models/user.rb file, so crack that file open and you should see the following:
class User < ApplicationRecord
  has_secure_password
end

Again, the generator is pretty clever! Notice that it added the has_secure_password line. It knew we wanted to store passwords securely because we used password:digest when running the generator.
As you’ll recall from the video, the built-in has_secure_password method in Rails helps us do the right thing when it comes to securely storing passwords. And when you read it out loud it makes sense: A user has a secure password.
In the Movie and Review models, we used similar declarations— belongs_to and has_many —to get functionality for managing model relationships. Using has_secure_password in a model adds functionality for securely storing passwords.

  1. The has_secure_password method depends on the bcrypt gem which implements a state-of-the-art secure hash algorithm used to encrypt passwords. Not all Rails applications need this gem, so it’s commented out in the default Gemfile .So you need to uncomment the following line in your Gemfile :
gem 'bcrypt', '~> 3.1.7'
  1. Then install the gem using
bundle install

And that’s all there is to it! With these changes in place, Rails has everything it needs to securely store passwords in the password_digest column of the users database table.

2. Declare User Validations

Next we need to add some reasonable validations to the generated User model to ensure that invalid user records can’t be stored in the database. The has_secure_password line automatically adds password-related validations, but we also need validations for the user’s name and email. Use built-in validations to enforce the following validation rules:

  1. A name must be present.

[ANSWER]

validates :name, presence: true
  1. An email must be present and formatted so that it has one or more non-whitespace characters on the left and right side of an @ sign.
validates :email, format: { with: /\S+@\S+/ }
  1. We don’t want two users in the database to have the same e-mail address. So make sure emails are unique regardless of whether they use upper or lower case characters.
    [ANSWER]
validates :email, presence: true,
                  format: { with: /\S+@\S+/ },
                  uniqueness: { case_sensitive: false }

3. Create Users in the Console

Now that we have a users database table and a User model with validations, let’s try creating some users in the database using the Rails console and see what has_secure_password gives us:

  1. Fire up a Rails console session.
rails c
  1. Then instantiate a new User object without a name, email, or password.

[ANSWER]

>> user = User.new

Now try to save the invalid User object to the database.

[ANSWER]

>> user.save

The result should be false . Remember, when you try to save (or create) a model object, its validations are automatically run. If a validation fails, a corresponding message is added to the model’s errors collection. And if the errors collection contains any messages, then the save is abandoned and false is returned. In short, the failed validations prevent the user from being saved to the database, which is exactly what we want!
4. To see which validations are failing, inspect the validation error messages by accessing the errors collection. To dig down into the actual error messages, tack on a call to full_messages to get an array of error messages.

[ANSWER]

>> user.errors.full_messages

You should get the following:

=> ["Password can't be blank", "Name can't be blank", "Email is invalid"]

Notice that has_secure_password added a validation to ensure a password is present when creating a new user, in addition to the name and email validations we declared. Nice!

  1. Go ahead and assign just a name and email for the user.

[ANSWER]

>> user.name = "Larry"
>> user.email = "larry@example.com"

These attributes map directly to the name and email database columns.

  1. Next, set a password for the user by assigning a value to the virtual password attribute.

[ANSWER]

>> user.password = "abracadabra"

Remember, the password attribute is a virtual attribute that was dynamically defined by the has_secure_password method. Unlike a typical attribute, when we assign a value to the password attribute it doesn’t try to store the value in a corresponding password database column. That’s good because we don’t have a password column and we don’t want plain-text passwords stored in our database!
Instead, assigning a value to the password attribute causes the plain-text version of the password to be encrypted and the encrypted version is then stored in the password_digest column. It appears to be a clever sleight of hand, but now you know it’s not actually magic. It works because assigning a value to the password attribute calls the special password= method defined by has_secure_password . And that method turns around and does the encryption for us.

  1. Now for the big reveal: Print the value of the password_digest attribute.
>> user.password_digest

You should get a string of what looks like gibberish, such as:

"$2a$10$wTBwLqnYrXffr.ainX60qOVB6hWeF4T1rU3RMHTL2olZ.erAmJS7O"

That string, typically referred to as an irreversible digest , is the result of running the plain-text password through the one-way hash algorithm in the bcrypt gem.

  1. Now set a password confirmation that doesn’t match the password and try to save the user record again.

[ANSWER]

>> user.password_confirmation = "alakazam"

>> user.save
=> false

Think of a typical sign-up form that prompts for a password and makes you re-enter it to confirm that the passwords match. That’s pretty common, and has_secure_password has you covered. It added a password_confirmation attribute and a validation that requires a password confirmation to be present, as well.

  1. Check the validation error messages and you should get the following:
>> user.errors.full_messages
=> ["Password confirmation doesn't match Password"]

Note that the password_confirmation attribute is also a virtual attribute . It doesn’t map to a database column. Instead, when you assign a value to password_confirmation , the value is simply stored temporarily in an instance variable. Behind the scenes, has_secure_password runs the validations against that instance variable.

  1. Then assign a matching password confirmation so that you can finally (successfully) save the user to the database.

[ANSWER]

>> user.password_confirmation = "abracadabra"

>> user.save
=> true
  1. Next, create a second user by calling the new method and passing it a hash of attribute names and values: name , email , password , and password_confirmation . Save it and make sure it successfully saves without any validation errors.

[ANSWER]

>> user = User.new(name: "Daisy", email: "daisy@example.com", password: "open-sesame", password_confirmation: "open-sesame")
>> user.save
=> true
  1. To demonstrate that the plain-text password is only temporarily stored in memory, find the user you just created by their email address like so:
>> user = User.find_by(email: "daisy@example.com")

Then print the value of the password_digest attribute and you should get the encrypted-version of the password (gibberish):

>> user.password_digest
=> "$2a$12$k9ohzPl5snCOuKvzgqXyMuwxj7YkQtbhopihmQy1OyrcgiGgVFqo."

We assigned a plain-text password when we initially created the user, but is it still there? To check, print the value of the password attribute:

>> user.password
=> nil

Ah, it has a value of nil ! Remember, we fetched the user from the database. Doing that populated all the real (non-virtual) attributes with the values corresponding to each column. But there is no password column, so the password virtual attribute has a value of nil . And that’s exactly what we want: there’s no trace of the original plain-text password!

  1. Finally, since we required that a user’s email be unique, try creating a third user with the same email address as a previously-created user.

[ANSWER]

>> user = User.new(name: "Daisee", email: "daisy@example.com", password: "secret", password_confirmation: "secret")

>> user.save
=> false

It should return false and the database transaction should get rolled back. Notice that the uniqueness validation ran a SQL SELECT query to check if a user with the same email address already existed in the database.

Print the validation error messages.

[ANSWER]

>> user.errors.full_messages
=> ["Email has already been taken"]

Indeed, a user already exists with the same email address so the new (duplicate) user wasn’t created.

  1. Remember how to check that you now have two unique users in the database?

[ANSWER]

>> User.count

Hey, we got a lot for free! By following the tried-and-true convention, has_secure_password takes care of validating that a user has a (confirmed) password and securely stores it for us.

Bonus Round

We’ve seen that has_secure_password automatically gives us validations for the presence and confirmation of a user password. That’s a really good start, but of course you can add more validations as necessary.

For example, suppose you wanted to require passwords to be at least 10 characters in length. To do that, add the following to your User model:

validates :password, length: { minimum: 10, allow_blank: true }

By setting the allow_blank option to true , the length validation won’t run if the password field is blank. That’s important because a password isn’t required when a user updates his name and/or email. So if the password field is left blank when editing the user account, the length validation is skipped.

What About a Gem?

You might be wondering why we just don’t use an off-the-shelf authentication gem such as Devise. It’s not because we have anything against gems. But gems tend to come and go. So rather than investing a lot of time in learning a particular gem, we believe that learning how things work at a fundamental level is a better long-term investment.

Every Rails developer should know at a basic level how an authentication system works. And the best way to gain that understanding is to build one yourself. That way, should you decide down the road to slip a third-party gem such as Devise in your app, you’ll be in a much better position to understand, customize, and troubleshoot if you run into any problems.

Wrap Up

Excellent! Our User model looks to be in good shape, so we’re ready for the next exercise where we’ll tackling the web interface for user accounts. Abracadabra…

User Signup

Exercises

Objective

Now that the User model is in good shape, we need a web interface that supports the following account management functions:

  • List all the user accounts
  • Show a user’s profile page that displays their account information
  • Allow new users to create an account using a sign-up form

Creating the web interface for user accounts is very similar to creating the interface for movies and reviews. So most of this will be review, and good practice!

1. List All Users

To get things rolling, let’s start by displaying a list of users currently in the database. (We’ll link each user to their profile page which we’ll create shortly.) In terms of controller actions and templates, what will you need?

[ANSWER]

an index action and corresponding app/views/users/index.html.erb view template that displays a list of users

If you’re feeling confident, by all means give this exercise a try on your own first before following the steps below.

  1. To get your bearings, check out the defined routes and you’ll notice we already have the following routes for interacting with users:
Helper         HTTP Verb        Path                  Controller#Action
users_path       GET        /users(.:format)           users#index
                 POST       /users(.:format)           users#create
new_user_path    GET        /users/new(.:format)       users#new
edit_user_path   GET        /users/:id/edit(.:format)  users#edit
user_path        GET        /users/:id(.:format)       users#show
                 PUT        /users/:id(.:format)       users#update
                 PATCH      /users/:id(.:format)       users#update
                 DELETE     /users/:id(.:format)       users#destroy

These conventional resource routes exist because the resource generator we ran earlier added the following line to the config/routes.rb file:

resources :users

So we know the URL to use to get a list of users…

  1. Browse to http://localhost:3000/users to list the existing users and you’ll get an all-too-familiar error:
Unknown action

The action 'index' could not be found for UsersController
  1. Following the error, open the empty UsersController that was also generated by the resource generator and define an index action that fetches all the users from the database.
def index
  @users = User.all
end
  1. Then create a corresponding app/views/users/index.html.erb view template. In the template, generate a list of user names with each name linked to the user’s show page. To style the list according to our CSS rules (totally optional), make sure that the ul tag has the CSS class users.

For extra credit, also display how long ago each user account was created and the number of users being listed.

<h1><%= pluralize(@users.size, "User") %></h1>
<ul class="users">
  <% @users.each do |user| %>
    <li>
      <%= link_to user.name, user %>
      created
      <%= time_ago_in_words(user.created_at) %> ago
    </li>
  <% end %>
</ul>
  1. Now reload the index page back in your browser and you should see the two users we created in the console in the previous exercise.

2. Show a User’s Profile Page

When you click on a user’s name in the user listing, we want to show their profile page. For now the profile page will simply display the user’s name and email. We’ll fill in more profile information in upcoming exercises.

In terms of controller actions and templates, what will you need?

[ANSWER]

a show action and corresponding app/views/users/show.html.erb view template that displays the user's profile page

Give it a go on your own first!

  1. Browse to http://localhost:3000/users and try clicking on one of the user names. It should come as no surprise that you get this error:
Unknown action

The action 'show' could not be found for UsersController
  1. Following the error, define a show action that finds the requested user.

[ANSWER]

def show
  @user = User.find(params[:id])
end
  1. Then create the corresponding app/views/users/show.html.erb template that displays the user’s name and email. As a bonus, use the mail_to helper to link the user’s email so that clicking it pops open your favorite email program with a new message addressed to the user.

[ANSWER]

<section class="user">
  <h1><%= @user.name %></h1>
  <h2><%= mail_to(@user.email) %></h2>
</section>
  1. Now navigate from the user listing page to each user’s profile page as a quick visual check. If you want to pick up our CSS styles, you’ll need to use the same HTML markup as found in the solution above.

3. Create New User Accounts

Now that we have a user profile page to land on, we’re ready to let users create new accounts using a sign-up form. To get to that form, they’ll either click a “Sign Up” link in the header or browse to http://localhost:3000/signup. To do that, we’ll need to:

  • Add a route to support the custom URL http://localhost:3000/signup.
  • Generate a convenient “Sign Up” link at the top of every page.
  • Define a new action that renders the new.html.erb view template to display a sign-up form with fields for the user’s name, email, password, and password confirmation.
  • Define a create action that accepts the form data and uses it to create a new user in the database, but only if the user valid.

You’ve got this, so give it an honest try!

  1. According to the existing routes, the URL for displaying the form to create a new user is http://localhost:3000/users/new. That request gets sent to the new action in the UsersController . We need to make that work, but we’d also like to support the more descriptive URL http://localhost:3000/signup. We actually want both URLs to show the sign-up form.In the config/routes.rb file, add a route that maps a GET request for /signup to the new action of the UsersController .

[ANSWER]

get "signup" => "users#new"
  1. Now check out the defined routes and you should see all the conventional resource routes for users and the custom /signup route:
Helper         HTTP Verb        Path                  Controller#Action
users_path       GET        /users(.:format)           users#index
                 POST       /users(.:format)           users#create
new_user_path    GET        /users/new(.:format)       users#new
edit_user_path   GET        /users/:id/edit(.:format)  users#edit
user_path        GET        /users/:id(.:format)       users#show
                 PUT        /users/:id(.:format)       users#update
                 PATCH      /users/:id(.:format)       users#update
                 DELETE     /users/:id(.:format)       users#destroy
signup_path     GET       /signup(.:format)        users#new

Because we know you’re already thinking ahead, identify the route helper method you’ll need to generate a “Sign Up” link.
3. We want the “Sign Up” link to show up in the header at the top of every page, which means the link needs to get generated by the existing app/views/layouts/_header.html.erb partial file.
Start by using the route helper method to generate the “Sign Up” link anywhere inside the nav section of the header, just to get a quick win.

[ANSWER]

<%= link_to "Sign Up", signup_path, class: "button" %>
  1. Then to position the link as we did in the video, first change the existing ul tag to have a class of left, like so:
<ul class="left">
  <li>
    <%= link_to "All Movies", movies_path %>
  </li>
</ul>

Then below that ul tag add another ul tag that has a class of right with a single li, like so:

<ul class="left">
  ...
</ul>
<ul class="right">
  <li>
  </li>
</ul>

Then move the “Sign Up” link into the right li.

<ul class="left">
  ...
</ul>
<ul class="right">
  <li>
    <%= link_to "Sign Up", signup_path, class: "button" %>
  </li>
</ul>
  1. Now reload and click the “Sign Up” link. The URL in your browser’s address field should be http://localhost:3000/signup. Of course, we get an error because we don’t yet have a new action, but we know how to fix that.
  2. Define a new action that instantiates a new User object for the sign-up form to use:

[ANSWER]

def new
  @user = User.new
end
  1. Next, create the corresponding app/views/users/new.html.erb view template that starts with this:
<h1>Sign Up</h1>

hen it needs to generate a sign-up form with the following elements:

  • a text field to enter the user’s name
  • an email field to enter the user’s e-mail address
  • a password field to enter the user’s super-secret password
  • another password field to confirm the user’s password
  • a submit button
  • also use the existing app/views/shared/_errors.html.erb partial to display any validation errors

We’ll want to use this same form when editing a user’s account (in the next exercise) so go ahead and put all the sign-up form code in an app/views/users/_form.html.erb partial file that uses a local variable named user.

[ANSWER]

<%= form_with(model: user, local: true) do |f| %>
  <%= render "shared/errors", object: user %>

  <%= f.label :name %>
  <%= f.text_field :name, autofocus: true %>

  <%= f.label :email %>
  <%= f.email_field :email %>

  <%= f.label :password %>
  <%= f.password_field :password %>

  <%= f.label :password_confirmation, "Confirm Password" %>
  <%= f.password_field :password_confirmation %>

  <%= f.submit %>
<% end %>

It’s worth noting that you could use a standard text field for entering the email address, but using the HTML 5 email field gives a better user experience on some mobile devices. For example, iOS devices display a keyboard with the @ symbol on the primary screen.

  1. Then, back in the new.html.erb template, render the form partial and pass it a local variable named user that has the value of @user. Remember, you don’t use the underscore in the name when rendering the partial.

[ANSWER]

<h1>Sign Up</h1>
<%= render "form", user: @user %>
  1. Reload to make sure the form shows up as you’d expect before moving on.

  2. Next, define a create action that uses the submitted form data to create a new user record in the database. If the user is successfully created, redirect to their profile page and show a cheery flash message to let the user know their account was created. Otherwise, if the user is invalid, redisplay the sign-up form.

[ANSWER]

def create
  @user = User.new(user_params)
  if @user.save
    redirect_to @user, notice: "Thanks for signing up!"
  else
    render :new
  end
end

private

def user_params
  params.require(:user).
    permit(:name, :email, :password, :password_confirmation)
end
  1. Now use the sign-up form to sign up a new user. Try the following combinations for posterity:
  • Leave the password and confirmation fields empty
  • Fill in a password, but leave the confirmation blank
  • Fill in a password and confirmation that do not match
  • Fill in a password and matching confirmation

Make sure you end up creating a new user, landing up on their profile page. Home sweet home!

Bonus Round

Show “Member Since” Date

As a nice touch, on the user’s profile page show the month and year that the user became a member (created an account) on our site. Format the “member since” date as “January 2019”, for example.

[ANSWER]

<h3>Member Since</h3>
<p>
  <%= @user.created_at.strftime("%B %Y") %>
</p>

Add a Profile Image

If you want to give the user profile page a bit more personality, you might consider adding a profile image for each user. A popular way to do that is by integrating with the free Gravatar service. Gravatar lets you upload your preferred profile image (called a global avatar image) to the Gravatar site and associate that image with a particular email address. Then when you create a user account on another site using that same email address, the site can use the Gravatar service to show your preferred profile image. It’s really convenient because you can register your profile image with Gravatar once and the image automatically follows you to any Gravatar-enabled site.

It’s relatively easy to Gravatar-enable an app, and we’ll get you started…

  1. First, to access a user’s profile image, we need to generate an MD5 hash of the user’s email address. To do that, add the following method to your User model:
def gravatar_id
  Digest::MD5::hexdigest(email.downcase)
end

That method simply returns a string that represents the hashed value for the email address. For example, for Mike’s email address we’d get back the string 58add23fa01eae6d736adce86e07ae00 . For every unique email address, the method will return a consistent and unique hashed value. Think of it as your unique Gravatar id, which is why we named the method as such.

  1. Then to request the associated profile image that’s stored on Gravatar’s site, we use a URL with the form http://secure.gravatar.com/avatar/gravatar-id where the gravatar-id part is replaced with a particular user’s gravatar id. For example, Mike’s profile image is at http://secure.gravatar.com/avatar/58add23fa01eae6d736adce86e07ae00. Open that URL in a new browser window and you get Mike’s mugshot. Now, to actually show Mike’s profile image on his profile page, we need to generate an image tag for the image that lives on Gravatar’s site.

To do that, write a custom view helper named profile_image that takes a user object as a parameter. It needs to generate a string that represents the URL for that user’s Gravatar image and then use that URL to generate and return an image tag for the image. Put the helper method in the users_helper.rb file.

[ANSWER]

def profile_image(user)
  url = "https://secure.gravatar.com/avatar/#{user.gravatar_id}"
  image_tag(url, alt: user.name)
end
  1. Next, update the user profile page to call the profile_image helper so that the user’s profile image is shown on the page.

[ANSWER]

<section class="user">
  <%= profile_image(@user) %>
  <h1><%= @user.name %></h1>
  <h2><%= mail_to(@user.email) %></h2>
</section>
  1. Reload a user profile page and you should either see the default Gravatar image (a blue square) or an actual profile image if the user’s email has already been associated with a Gravatar image. Now might be a good time to create your own Gravatar image!

Check the Log File

We’ve been diligent about not storing plain-text passwords in our database, but we also need to be careful not to expose them in other areas of our application.

For example, every time a form is submitted Rails automatically records the submitted form data in the log file. In development mode, everything is logged in the log/development.log file which is also displayed in the command window where your app is running.

Check out the log file and scroll back to the part where you signed up a new user. You should see something like this:

Parameters: {"authenticity_token"=>"xCQafEa4SP/zQt173U13Fy/HTYSlD...", "user"=>{"name"=>"Larry", "email"=>"larry@example.com", "password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]"}, "commit"=>"Create User"}

In particular, notice that the submitted password and password_confirmation form parameters were masked as [FILTERED] so that the actual values aren’t displayed in the log file. This is another example of the Rails defaults trying to help us do the right thing. It works because of the following line in the config/initializers/filter_parameter_logging.rb file:

Rails.application.config.filter_parameters += [:password]

That line simply appends the key :password to the filter_parameters array. Then, before the params hash gets logged, the values for all the keys matching the regular expression /password/ get replaced with the string [FILTERED] . It’s as if someone used a big black marker to hide all the classified information!

Passwords are the most common parameter that need to be filtered from the log files, so Rails takes care of that for you. But you’ll want to consider filtering other sensitive parameters specific to your application.

Wrap Up

Nicely done! Folks are now able to create user accounts complete with super-secret passwords and view a user’s profile page. Next up, we’ll allow users to edit their account information and even delete their account.

Edit User Account

Exercises

Objective

Now that new users can sign up, they probably also want to be able to edit their account information, and (dare we say) perhaps even delete their account.

Since this is mostly review, feel free to just go for it! There’s nothing surprising here. The goal is just to finish building out the UI for user accounts. This will set the stage for more account-related features we’ll implement in upcoming exercises.

1. Edit User Account Information

What controller actions and templates will you need to edit a user’s account information?

[ANSWER]

an edit action that renders an edit form pre-filled with the user's account info (it's the same form used to create a user account) and an update action that accepts the form data and uses it to update a user in the database if the user is valid
  1. First we need to put an “Edit Account” link on the user profile page. Use a route helper method to generate that link on the show page (which we’re calling the user profile page). To apply our CSS styling rules, put it in a div with a class of actions.

[ANSWER]

<section class="user">
  <h1><%= @user.name %></h1>
  <h2><%= mail_to(@user.email) %></h2>

  <div class="actions">
    <%= link_to "Edit Account", edit_user_path(@user),
                  class: "button edit" %>
  </div>
</section>
  1. Then define the edit action which finds the user matching the id parameter in the URL so we can populate the edit form with the user’s existing information.

[ANSWER]

def edit
  @user = User.find(params[:id])
end
  1. Now that we have the user we want to edit, create the corresponding app/views/users/edit.html.erb view template. It needs to display the same form we used to create a new user, which we already have in a partial that expects a user local variable. (Don’t you love it when a plan comes together?) So use that partial to generate the edit form.
<h1>Edit Account</h1>
<%= render "form", user: @user %>
  1. Back in your browser, revel in your work by clicking the “Edit Account” link for a user. You should see a form pre-filled with the user’s name and email, but the password fields are blank… as they should be!

  2. Next, define the update action to use the submitted form data to update the user in the database. If the user is successfully updated, redirect to their profile page with a flash message confirming that their account was successfully updated. Otherwise, if the form data is invalid, redisplay the edit form so the user can give it another try.

[ANSWER]

def update
  @user = User.find(params[:id])
  if @user.update(user_params)
    redirect_to @user, notice: "Account successfully updated!"
  else
    render :edit
  end
end
  1. Then, back in your browser, change the user’s name and/or email, but leave the password fields blank. You should get redirected to the user’s profile page and see the updated information.

  2. Now edit the user account again, and try typing something in the password field. Submit the form and you should get password validation errors. If you type anything into the password fields, then both fields are required and must match. In other words, when updating a user the password-related validations only run if you try to change the password. That’s another nice touch that comes courtesy of using has_secure_password .

  3. Now for a bit of customization. You may have noticed on “Edit Account” form that the default submit button is labeled “Update User”. If you then look at the “Sign Up” form, the submit button is labeled “Create User”. The form builder is smart enough to label the button based on whether the user object represents an existing user already stored in the database (we’re editing it) or a new user not yet stored in the database (we’re creating it). If user is a new record, the button is generated with the label “Create User”; otherwise, the label is “Update User”.

The default button name is convenient, but suppose we want to use account vernacular and have the button say “Create Account” and “Update Account”, respectively. To do that, simply locate the following line in the form partial:

<%= f.submit %>

Then replace that line with the following lines:

<% if user.new_record? %>
  <%= f.submit "Create Account" %>
<% else %>
  <%= f.submit "Update Account" %>
<% end %>

All we do here is call the new_record? method that’s available on all ActiveRecord objects. It returns true if the object hasn’t already been saved to the database and false otherwise. Then depending on the answer, we pass a string label to the submit method. It’s a small thing, but users tend to think in terms of their account and using words that align with their thinking is reassuring.

2. Delete User Accounts

We hope it doesn’t happen often, but users may want to delete their account. What do you need to make that possible?

[ANSWER]

a "Delete Account" link and a destroy action that deletes the user from the database and redirects to the user somewhere, such as the movie listing page

  1. Start by putting a “Delete Account” link on the user profile page in the div with a class of actions.

[ANSWER]

<div class="actions">
  <%= link_to "Edit Account", edit_user_path(@user), class: "button edit" %>
  <%= link_to 'Delete Account', @user, method: :delete,
              data: { confirm: "Permanently delete your account!?" },
              class: "button delete" %>
</div>
  1. Then define the destroy action. Once the user’s record has been destroyed, redirect to the home page and flash an alert message confirming that the account was successfully deleted.

[ANSWER]

def destroy
  @user = User.find(params[:id])
  @user.destroy
  redirect_to movies_url, alert: "Account successfully deleted!"
end
  1. Now go back to the browser and click the “Delete Account” link. You should end up on the application home page. Go to the user listing page and the user you deleted should not be displayed in the listing.

  2. Finally, you might want to use your new account management web interface to re-create the user you just deleted. :slight_smile:

Bonus Round

Add a Username Field

In addition to a full name and email, some sites also allow users to set a unique username. It’s your online nickname or screenname. For example, Mike’s Twitter username is “clarkware”.

Add a username field to the users database table, and allow users to specify a username when creating (and editing) their account. Usernames must be present and only consist of letters and numbers (alphanumeric characters) without spaces. Also, no two users can have the same username in the database. Treat usernames as being case-insensitive.

Try it on your own first (after all, we’re in the bonus round!) and then follow the steps below if you need a hand.

  1. Generate a migration to add a username column to the users table.

[ANSWER]

rails g migration AddUsernameToUsers username:string
  1. Declare appropriate validations in the User model.

[ANSWER]

validates :username, presence: true,
                     format: { with: /\A[A-Z0-9]+\z/i },
                     uniqueness: { case_sensitive: false }
  1. Update the form partial to include a text field for entering the username.
<%= f.label :username %>
<%= f.text_field :username, placeholder: "Alphanumeric characters only!" %>
  1. Don’t forget to add the username field to the list of permitted parameters so that the username can be assigned from form data.

[ANSWER]

def user_params
  params.require(:user).
    permit(:name, :email, :password, :password_confirmation, :username)
end

Wrap Up

And with that, we have a complete user interface for managing user accounts! Working incrementally through it allowed us to review the following Rails fundamentals:

  • Creating a new resource using the generator
  • Applying a migration file
  • Declaring validations in the model
  • Using the Rails console to create records in our database
  • Defining actions to support the conventional resource routes: index , show , new , create , edit , update , and destroy
  • Linking pages together
  • Using partials to reduce view-level code duplication
  • Routing custom URLs and using route helper methods
  • And providing feedback with flash messages

With all this now in place, in the next section we’ll allow users to actually sign in to their account. Open sesame…

Sign In

Exercises

Objective

Now that users can create an account, the next logical step is to let registered users sign in using their email and password. Once signed in, we’ll then need to identify them as they browse from page to page, until they eventually sign out.

If you’ve spent any time on the web, you’ve gone through the typical sign-in process time and again. From the user’s perspective it seems like such a trivial thing, but there’s actually a lot going on behind the scenes. Indeed, getting the entire sign-in process working correctly involves arranging a number of moving parts. As is our development style, we’ll take an incremental approach.

Our first step is simply to get to the point of displaying the sign-in form. To do that, we’ll need to:

  1. Create a SessionsController to manage the session resource
  2. Add specific routes for a singular session resource
  3. Generate a “Sign In” link in the header of every page
  4. Define a new action in the SessionsController that renders a sign-in form with email and password fields
  5. Define a bare-bones create action in the SessionsController that, for now, simply captures the submitted form data
  6. While we’re at it, also define an empty destroy action which we’ll implement in a future exercise

Here’s visually what we want:

1. Generate the SessionsController

Start by creating a SessionsController with empty new , create, and destroy actions.

[ANSWER]

class SessionsController < ApplicationController
  def new
  end

  def create
  end

  def destroy
  end
end

2. Add Session Routes

Then we need to add routes for our new session resource. These routes will be slightly different than the routes for our other resources.

The first difference is we only need routes for the new, create, and destroy actions.

The second difference is the session-related URLs won’t include an :id placeholder when referencing an individual session. For example, to delete a session we don’t need to include the session’s id (its database primary key) in the URL. That’s because, unlike the other resources in our app, the session resource won’t be stored in the database. Instead, the session data will be stored in a cookie that automatically gets sent back to our app with every request. So we don’t need a sessions database table or a Session model. And if sessions aren’t in the database, then it doesn’t make sense to include a session id in the session-related URLs. For any particular request, there’s only one session. Rails calls this a singular resource.

  1. In the config/routes.rb file, define the specific routes for a singular session resource.

[ANSWER]

resource :session, only: [:new, :create, :destroy]
  1. Go ahead and check out the defined routes to see what that gives you. You should see the following three session-related routes:
Helper              HTTP Verb        Path                  Controller#Action
new_session_path       GET        /session/new(.:format)     sessions#new
session_path           DELETE     /session(.:format)         sessions#destroy
                       POST       /session(.:format)         sessions#create

Notice that none of the URLs require an :id parameter as we’re used to seeing with other resource routes. You’ll also notice that the other conventional routes (such as listing sessions) weren’t generated since we used the only option to specify specific routes.

  1. Before moving on, pay careful attention to the naming in the generated routes. The route helper methods and URLs use the singular form ( session ). However, the controller name is the plural form ( sessions ). Rails always keeps you on your toes when it comes to singular and plural naming!

3. Generate a “Sign In” Link

You expect a convenient “Sign In” link at the top of every web page, so the next step is to put one there…

  1. Use a route helper method to generate a “Sign In” link to the left of the existing “Sign Up” link.

[ANSWER]

<ul class="right">
  <li>
    <%= link_to "Sign In", new_session_path, class: "button" %>
  </li>
  <li>
    <%= link_to "Sign Up", signup_path, class: "button" %>
  </li>
</ul>
  1. Over in your browser, reload any page and you should see a “Sign In” link. Click it and the URL should change to http://localhost:3000/session/new.

So the route is recognized and runs the new action of the SessionsController, but of course we don’t yet have a view template for that action.

4. Create the Sign-In Form

Next up we need to create the sign-in form with the following elements:

  • an email label and field to enter the user’s name
  • a password label and field to enter the user’s password
  • a submit button appropriately named “Sign In”
  1. Start by creating the app/views/sessions/new.html.erb view template that simply displays “Sign In” in an h1 tag.

[ANSWER]

<h1>Sign In</h1>
  1. Now create the form with just an email label and field.

Remember, we don’t have a model for sessions so this form won’t be bound to a model object. So when calling the form_with method to generate the form, you’ll need to use the url option rather than the model option we’ve used previously. The value of the url option needs to be the URL where the form will POST the form data. Use a route helper method to generate that URL.

[ANSWER]

<h1>Sign In</h1>

<%= form_with(url: session_path, local: true) do |f| %>
  <%= f.label :email %>
  <%= f.email_field :email, autofocus: true %>
<% end %>
  1. Now add a password label and field.

[ANSWER]

<h1>Sign In</h1>

<%= form_with(url: session_path, local: true) do |f| %>
  <%= f.label :email %>
  <%= f.email_field :email, autofocus: true %>

  <%= f.label :password %>
  <%= f.password_field :password %>
<% end %>
  1. Finally, add a submit button.

[ANSWER]

<h1>Sign In</h1>

<%= form_with(url: session_path, local: true) do |f| %>
  <%= f.label :email %>
  <%= f.email_field :email, autofocus: true %>

  <%= f.label :password %>
  <%= f.password_field :password %>

  <%= f.submit "Sign In" %>
<% end %>

5. Capture Submitted Form Data

In this exercise, we just want the create action to capture the submitted form data. We’ll finish it off in the next exercise.

  1. Just to see the form data that’s submitted, add a fail line to the create action so that the submitted form data is dumped onto an error page:
def create
  fail
end
  1. Then submit the form with data and you should get an error (or a debugging page, depending on how you look at it). Check out the stuff under the “Request” heading and you should see something like this:
 {"authenticity_token"=>"Ur/HVsiN+hwb3An6l...",
  "email"=>"lucy@example.com",
  "password"=>"[FILTERED]",
  "commit"=>"Sign In"}

Notice that the email and password form field values are captured in the corresponding email and password parameters. The password value is automatically filtered when displayed on the error page, but rest assured that the submitted password is actually in the password parameter.

So at this point the params hash contains everything we need to authenticate users by email and password. In the next exercise, we’ll do exactly that! For now, you’re good to go if you see values for the email and password parameters on the error page.

Bonus Round

Define a Custom Sign In Route

Currently, when you click the “Sign In” link the URL in the browser’s address bar is http://localhost:3000/session/new. That’s fairly descriptive if you understand sessions, but you might also want to support the more-friendly URL http://localhost:3000/signin.

  1. Add a route that dispatches the URL http://localhost:3000/signin to the Sign In form.

[ANSWER]

get "signin" => "sessions#new"
  1. Then update the “Sign In” link in the header to use a route helper method to generate the “Sign In” link as /signin rather than /session/new .

[ANSWER]

<%= link_to "Sign In", signin_path, class: "button" %>

Add Another “Sign Up” Link

What happens if a person lands up on the Sign In form but they don’t already have an account? Well, we’d be foolish not to encourage them to create an account. So add a friendly link on the Sign In form that lets them hop right over to the Sign Up form.

[ANSWER]

<h1>Sign In</h1>
<p>
  No account yet? <%= link_to "Sign up!", signup_path %>
</p>

<%= form_with(url: session_path, local: true) do |f| %>
  ...
<% end %>

Require Sign-In Fields

If you want to get fancy with HTML 5, you can use the new required attribute on input fields. Browsers that support HTML 5 won’t allow a form to be submitted until all the fields marked as being required have a value. Those browsers also highlight the fields with missing values.

That’s kinda convenient in the case of a Sign In form since we don’t have a corresponding model with validations. It also saves a trip back to the app if a field is left blank.

So mark the email and passwords fields as being required by adding the required: true option when generating the fields.

[ANSWER]

<%= f.email_field :email, autofocus: true, required: true %>

<%= f.password_field :password, required: true %>

Wrap Up

That’s a great start! Now that we have a familiar sign-in form, next we need to try to authenticate the user when they submit the form…

Authentication

Exercises

Objective

So far we’ve displayed the sign-in form where a user can enter their credentials, but we haven’t actually authenticated the user yet. So here’s where the rubber meets the road! When a user submits their email and password, we need to sign them in.

To finish the sign-in process, we’ll need the create action to do the following:

  1. Find a user in the database with the submitted e-mail address
  2. Verify that the submitted password is correct for that user
  3. Store the authenticated user’s id in the session
  4. Redirect to the user’s profile page
  5. If the submitted email and/or password don’t match a user in the database, we’ll redisplay the sign-in form

Here’s visually what we want:

It’s a fairly straightforward exercise that follows what we did in the video, so let’s jump right into it!

1. Authenticate in the Console

Before we start filling in the create action, let’s practice the first two authentication steps in the console using example email and password values…

  1. First, find a user you created previously by their e-mail address and assign that user to a user variable.

[ANSWER]

>> user = User.find_by(email: "lucy@example.com")
=> #<User id: 2, name: "Lucy", email: "lucy@example.com", ...>
  1. Next, verify a given password is correct for that user. As you saw in the video, the has_secure_password line we added to our User model earlier was kind enough to give us an authenticate method that makes this trivial. Remember, it’s an instance method that you can call on any User object.

First try calling authenticate with an incorrect password.

[ANSWER]

>> user.authenticate("guess")
=> false

It should return false , indicating that the password was incorrect.

  1. Then try authenticating with a correct password.

[ANSWER]

>> user.authenticate("abracadabra")
 => #<User id: 2, name: "Lucy", email: "lucy@example.com", ...>
  1. It should return the User object, indicating that the password was correct.

Hey, that’s pretty easy! Now let’s bring this full circle. When this user created an account, they gave us a plain-text password which got encrypted (hashed) and saved in the password_digest column. Now when that user sign ins, she gives us the same plain-text password which we pass to the authenticate method. It then encrypts the password and compares the result to what’s in the password_digest column. If the passwords don’t match, then authenticate returns false . Otherwise, if the passwords do match, the User object is returned.

So by comparing encrypted passwords instead of plain-text passwords, we’re able to authenticate users without ever storing plain-text passwords. And that’s critically important!

2. Implement the Create Action

Now we’re ready to step back into the create action and finish it off. First, it needs to perform the same authentication steps we just did in the console, only this time using the submitted email and password that came in from the sign-in form. If the user is successfully authenticated, we then need to store their user id in the session and redirect to their profile page. Otherwise, if authentication fails, we need to redisplay the sign-in form.

  1. If you still have that fail line hanging around in create , go ahead and zap it.
  2. With a clean slate, start by finding the user that matches the submitted email address. Assign that user to a variable called user .

[ANSWER]

def create
  user = User.find_by(email: params[:email])
end
  1. Then verify that the submitted password is correct for that user, just like you did in the console.

[ANSWER]

def create
  user = User.find_by(email: params[:email])
  user.authenticate(params[:password])
end
  1. Next, set up an if/else conditional to check whether the user was authenticated or not. Keep in mind:
  • Before calling authenticate , you need to make sure the User object returned by find_by is not nil . Otherwise, calling the authenticate method will cause an error.
  • The authenticate method will return false if the password isn’t correct.

[ANSWER]

def create
  user = User.find_by(email: params[:email])
  if user && user.authenticate(params[:password])
  else
  end
end
  1. If the user is authenticated, record that fact in the session by assigning the user’s id to the :user_id session key. We’ll use the presence of that key/value pair in the session to indicate that the user is signed in. Then redirect to that user’s profile page with a cheery flash notice such as “Welcome back, Lucy!”.

[ANSWER]

def create
   user = User.find_by(email: params[:email])
   if user && user.authenticate(params[:password])
     session[:user_id] = user.id
     redirect_to user, notice: "Welcome back, #{user.name}!"
   else
   end
 end
  1. Finally, handle the case where the user is not authenticated by redisplaying the sign-in form with a flash alert to prompt the user to try again.

[ANSWER]

def create
  user = User.find_by(email: params[:email])
  if user && user.authenticate(params[:password])
    session[:user_id] = user.id
    redirect_to user, notice: "Welcome back, #{user.name}!"
  else
    flash.now[:alert] = "Invalid email/password combination!"
    render :new
  end
end

Finally, hop back into a browser and give this a whirl:

  • Try signing in with an invalid email/password combination, and you should see the red flash message. Bzzt… wrong answer!
  • Then sign in an existing user and you should end up on their profile page with a green flash message confirming that you’re signed in. Welcome back!

Bonus Round

Quick Session Facts

Knowledge is power, so here are a few quick facts about sessions to keep in mind:

  • The session cookie expires when the browser is closed.
  • The session cookie is limited to 4kb in size. So avoid storing large objects in the session, and instead store an object’s id. Then use that id to look up the object in the database, as we did with the User object.
  • The session cookie can’t be forged by a hacker. It’s cryptographically signed to make it tamper-proof, and Rails will reject the cookie if it has been altered in any way. The session cookie is also encrypted, so you can’t even read what’s inside the session cookie. That prevents a malicious person from trying to impersonate a signed-in user by changing the user id in the session, for example.
  • That being said, you should never store sensitive information such as a password in a session!

Use Alternate Credentials

In a previous bonus exercise, you may have added a username field to the users database table. Arrange things to allow a user to sign in using either their e-mail address or their unique username, and their password of course.

  1. First, on the Sign In form you’ll need to change the email_field to text_field so the user can either enter their email or their username. That field is currently named email, so you’ll also want to rename it to something like email_or_username since its value can be either. Finally, change the name of the label so folks know they can use either their email or username.

[ANSWER]

<%= f.label :email_or_username %>
<%= f.text_field :email_or_username, autofocus: true, required: true %>
  1. Then change the create action to attempt to find the user by either their provided email or username. The Ruby || operator comes in really handy here!

[ANSWER]

def create
  user = User.find_by(email: params[:email_or_username]) ||
          User.find_by(username: params[:email_or_username])
  if user && user.authenticate(params[:password])
    ...
  else
    ...
  end
end

Wrap Up

We’re making good progress! We’re able to sign in users and track them in the session. However, it’s not very evident who’s currently signed in. It would feel a lot more welcoming if we displayed the current user’s name in the header. And while we’re at it, we can remove those pesky “Sign Up” and “Sign In” links once a user is signed in. We’ll do exactly that in the next section…