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 apassword_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.
- Start by generating a resource named
user
(singular) withname
andemail
attributes of typestring
, and apassword
attribute of typedigest
.
[ANSWER]
rails g resource user name:string email:string password:digest
- 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!
- To actually create the
users
database table, you need to run the migration.
rails db:migrate
- 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.
- 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 defaultGemfile
.So you need to uncomment the following line in yourGemfile
:
gem 'bcrypt', '~> 3.1.7'
- 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:
- A name must be present.
[ANSWER]
validates :name, presence: true
- 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+/ }
- 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:
- Fire up a Rails
console
session.
rails c
- 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!
- 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.
- 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.
- 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.
- 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.
- 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.
- 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
- Next, create a second user by calling the
new
method and passing it a hash of attribute names and values:name
,email
,password
, andpassword_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
- 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!
- 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.
- 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.
- 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…
- 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
- Following the error, open the empty
UsersController
that was also generated by the resource generator and define anindex
action that fetches all the users from the database.
def index
@users = User.all
end
- 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’sshow
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>
- 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!
- 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
- Following the error, define a
show
action that finds the requested user.
[ANSWER]
def show
@user = User.find(params[:id])
end
- 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>
- 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 thenew.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!
- 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 theUsersController
. 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 theconfig/routes.rb
file, add a route that maps aGET
request for/signup
to thenew
action of theUsersController
.
[ANSWER]
get "signup" => "users#new"
- 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" %>
- 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>
- 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. - Define a
new
action that instantiates a newUser
object for the sign-up form to use:
[ANSWER]
def new
@user = User.new
end
- 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.
- Then, back in the
new.html.erb
template, render theform
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 %>
-
Reload to make sure the form shows up as you’d expect before moving on.
-
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
- 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…
- 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.
- 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
- 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>
- 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
- 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>
- Then define the
edit
action which finds the user matching theid
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
- 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 %>
-
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!
-
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
-
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.
-
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
. -
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
- 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>
- 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
-
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.
-
Finally, you might want to use your new account management web interface to re-create the user you just deleted.
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.
- Generate a migration to add a
username
column to theusers
table.
[ANSWER]
rails g migration AddUsernameToUsers username:string
- Declare appropriate validations in the
User
model.
[ANSWER]
validates :username, presence: true,
format: { with: /\A[A-Z0-9]+\z/i },
uniqueness: { case_sensitive: false }
- Update the form partial to include a text field for entering the username.
<%= f.label :username %>
<%= f.text_field :username, placeholder: "Alphanumeric characters only!" %>
- 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
, anddestroy
- 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:
- Create a
SessionsController
to manage the session resource - Add specific routes for a singular
session
resource - Generate a “Sign In” link in the header of every page
- Define a
new
action in theSessionsController
that renders a sign-in form with email and password fields - Define a bare-bones
create
action in theSessionsController
that, for now, simply captures the submitted form data - 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.
- In the
config/routes.rb
file, define the specific routes for a singularsession
resource.
[ANSWER]
resource :session, only: [:new, :create, :destroy]
- 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.
- 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…
- 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>
- 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”
- 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>
- 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 %>
- 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 %>
- 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.
- 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
- 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.
- Add a route that dispatches the URL http://localhost:3000/signin to the Sign In form.
[ANSWER]
get "signin" => "sessions#new"
- 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:
- Find a user in the database with the submitted e-mail address
- Verify that the submitted password is correct for that user
- Store the authenticated user’s id in the session
- Redirect to the user’s profile page
- 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…
- 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", ...>
- Next, verify a given password is correct for that user. As you saw in the video, the
has_secure_password
line we added to ourUser
model earlier was kind enough to give us anauthenticate
method that makes this trivial. Remember, it’s an instance method that you can call on anyUser
object.
First try calling authenticate
with an incorrect password.
[ANSWER]
>> user.authenticate("guess")
=> false
It should return false
, indicating that the password was incorrect.
- Then try authenticating with a correct password.
[ANSWER]
>> user.authenticate("abracadabra")
=> #<User id: 2, name: "Lucy", email: "lucy@example.com", ...>
- 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.
- If you still have that
fail
line hanging around increate
, go ahead and zap it. - 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
- 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
- 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 theUser
object returned byfind_by
is notnil
. Otherwise, calling theauthenticate
method will cause an error. - The
authenticate
method will returnfalse
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
- 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
- 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.
- 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 %>
- 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…