Logging In and Logging Out
Your bookstore has users, but there’s no way for them to login. Let’s change that 😊
-
Let’s start by looking at your application’s routes file. Open
config/routes.rb
in your text editor.You’ve added a couple of resources to the routes file. First, you defined
:books
. This added a series of routes to your application for creating, reading, updating, and deleting your books. Then, you added another resource:reviews
. This added similar routes, but they’re focused on reviews instead of books.Now that your application has users, it might be tempting to add a
:users
resource. This would be useful if we were going to change user data, but we don’t need to change any user data to log someone in.We just need to keep track of a user once they’ve logged in to your bookstore. We’ll represent this as a session - a user creates a new session when they log in.
-
Let’s add a
:session
resource to your routes file. Before the end ofconfig/routes.rb
, add the following line:resource :session
-
Save your changes.
1
2
3
4
5
6
7
8
9
10
Rails.application.routes.draw do
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
root "books#index"
resources :books do
resources :reviews
end
resource :session
end
Let’s see what your application routes look like now.
-
Open Terminal, go the
bookstore
directory, and runrake routes
.Aww, your routes are getting so big 😍
When a user logs in, they’re going to create a new session. What path will they visit to create a new session?
They’ll go to
/session/new
.new_session GET /session/new(.:format) sessions#new
-
Start your application’s web server and visit the new session path at http://localhost:3000/session/new.
Oh boy, another Routing Error
! 😝
-
The error tells you what’s wrong…
uninitialized constant SessionsController
-
Fix this familiar error and come back when you get a different one.
Were you able to get past the Routing Error
?
No worries if you’re stuck, we’ll help you out 😊
-
You’re getting a
Routing Error
because you application doesn’t have aSessionsController
.To fix this, you need to create a
SessionsController
.All controllers follow a similar pattern. They live inside the
app/controllers
directory, and they’re filename matches their name.For example, the
SessionsController
will be a file namedapp/controllers/sessions_controller
.Controllers also share the same roots. Do you remember what your
BooksController
looked like when you first added it?class BooksControllers < ApplicationController end
There wasn’t much to it, remember?
-
If you didn’t create an empty
SessionsController
(app/controllers/sessions_controller
), go ahead and create now.class SessionsController < ApplicationController end
-
Then, revisit http://localhost:3000/session/new to see your next error!
-
Now, you’re getting an
Unknown action
error. Just like the last error, it’s giving you a hint.The action 'new' could not be found for SessionsController
You know what to do, right?!
-
Fix that error. When you’re done, you’ll get one more error.
-
What did you come up with?! Are you seeing a new error??
You had to add a
new
method to theSessionsController
.def new end
-
If you didn’t already add the
new
method, go ahead and add it now.
1
2
3
4
class SessionsController < ApplicationController
def new
end
end
We’re sooooo close to the end of the error train.
Can you think of reason why you’re getting an ActionController::UnknownFormat
error? It looks crazy, but we’ve seen this plenty of times before.
You’re getting an ActionController::UnknownFormat
error because the SessionsController
new
action doesn’t have a template.
-
Fix this error!
Remember, templates for the
SessionsController
will live insideapp/views/sessions
.When you’re done, http://localhost:3000/session/new will render a blank page.
-
Did you figure out how to add the
SessionsController
new
template?You had to add a new directory.
mkdir app/views/sessions
Then, you had to create an empty template file named
new.html.erb
insideapp/views/sessions
.touch app/views/sessions/new.html.erb
-
If you didn’t already, add the empty
SessionsController
new
template file (app/views/sessions/new.html.erb
).If you’re following the steps we just laid out, you’ll have to stop your application’s web server before running the commands. When you’re done, you can restart it.
Ah, has a blank page ever looked so good?
A blank page is great and all, but wouldn’t it be even better if a user could log in?!
In the SessionsController
new
template, we need to render a form where users can enter their credentials to create a new session and log in.
-
Open
app/views/sessions/new.html.erb
and add the following:<%= form_for(:session, url: session_path) do |f| %> <% end %>
The
form_for
might look a little familiar. We’ve used it to build forms for adding and updating books, and adding new reviews. However, there are a few differences.In the past forms you’ve built with
form_for
, you started by passing an instance of the record you wanted to update or create. For example, the new books form has a new instance ofBook
.<%= form_for(@book) do |f| %>
We aren’t doing this for the new session form because we’re not going to save the session in the database. We’re going to save it in the browser (more on that later).
Unlike the other forms, we have to specify a URL for the new session form.
form_for
can figure out where to send the form data when given an instance of a class; otherwise, you have to expcitily set the URL.Other than that, the new session form is just like the other forms you’ve built.
-
Using the new book form in
app/views/books/new.html.erb
as an example, add two text fields to the new sessions form.First, add a text field labeled “Username”.
Then, add a text field labeled “Password”.
Finally, add a form submission button.
1
2
<%= form_for(:session, url: session_path) do |f| %>
<% end %>
-
What does your solution look like? It should look something like this:
<%= form_for(:session, url: session_path) do |f| %> <ul> <li> <%= f.label :username %> <%= f.text_field :username %> </li> <li> <%= f.label :password %> <%= f.text_field :password %> </li> </ul> <%= f.submit(class: "button") %> <% end %>
-
Update your solution to match this solution. Save your changes and take a look at the form by visiting http://localhost:3000/session/new.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<%= form_for(:session, url: session_path) do |f| %>
<ul>
<li>
<%= f.label :username %>
<%= f.text_field :username %>
</li>
<li>
<%= f.label :password %>
<%= f.text_field :password %>
</li>
</ul>
<%= f.submit(class: "button") %>
<% end %>
Yay! The form looks so good!!
But you might’ve noticed a couple of…weird things.
First, did you try typing anything in the “Password” field. It shows up as plain text! That’s not great if we’re trying to protect passwords. Fortunately, we can change this behavior.
Also, the form submission button doesn’t have the friendliest message. “Save Session” is technically right, but most users probably expect to see something like “Login”.
Let’s make these things better!
First, we can change the “Password” field from a text field to a password field.
-
Reopen
app/views/sessions/new.html.erb
and change<%= f.text_field :password %>
to
<%= f.password_field :password %>
-
Now, let’s change the text of the form submission button to “Login”. Change
<%= f.submit(class: "button") %>
to
<%= f.submit("Login", class: "button") %>
-
Save your changes, revisit http://localhost:3000/session/new, and try logging in!
You should have a user with username “CatPower” and a password of “password”.
…
You know how to deal with Unknown action
errors, right?!
-
Fix this error!
When you’re done, submitting the form shouldn’t do anything.
-
How did you get past that
Unknown action
error?You added an empty
create
method to theSessionsController
, right?def create end
-
If you didn’t already, add an empty
create
method to theSessionsController
.
1
2
3
4
5
6
7
class SessionsController < ApplicationController
def new
end
def create
end
end
Nothing happens when you try to login, but how could anything happen. The new session form is being submitted to the SessionsController
create
method, and that method is empty.
Let’s update the create
method so users can login.
-
Just like past forms, the new session form data is available in the
SessionsController
inside theparams
hash. If you take a look at your application’s web server’s output, you can see the params coming in.Parameters: {"utf8"=>"✓", "authenticity_token"=>"C2s/BLrDEpUMmOL+FMx6SqNzLTqKvcLlvpw32+lD2O32Aryi9Kt0tlWVM+gUS3DYhQt1+3q0Tn3kqakWdZylVw==", "session"=>{"username"=>"CatPower", "password"=>"[FILTERED]"}, "commit"=>"Login"}
The form data is tucked in the
session
hash, and it includes a username and password.We can use the form data with the
authenticate
method to log users in. -
First, we’ll need to find the user by their username. Add the following code inside the
create
method:user = User.find_by(username: params[:session][:username])
find_by
is a handy method that let’s us get records from your database with something other than an id. We’re usingfind_by
here to get the user by their username. -
Now that we have the user’s record, we can check if they’ve given us a valid password. Add the following code to the
create
mehtod:if user.authenticate(params[:session][:password]) flash[:notice] = "Welcome back, #{user.username}!" session[:user_id] = user.id redirect_to root_path else flash[:alert] = "Sorry, your username or password is invalid." render :new end
First, we’ll try to
authenticate
the user. If the user is successfully authenticated, we’ll- show them a success message
- save their id in the browser’s session as
user_id
- send them to the
root_path
If the user cannot be authenticated, we’ll
- show them a failure message
- re-render the
new
template so they can try logging in again
-
There’s a lot happening here, so let’s try to break it down to get a better feel for what each of these lines of code is doing.
Before we continue, save your changes.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by(username: params[:session][:username])
if user.authenticate(params[:session][:password])
flash[:notice] = "Welcome back, #{user.username}!"
session[:user_id] = user.id
redirect_to root_path
else
flash[:alert] = "Sorry, your username or password is invalid."
render :new
end
end
end
The Rails Session
Let’s start by looking at this line:
session[:user_id] = user.id
Rails saves a session for every user that visits your application. By default, these sessions are stored in the user’s browser using something called cookies. Cookies are small files that browsers can use to save data on a user’s machine.
To protect the session’s contents, Rails signs and encrypts it so the contents are only accessible by the Rails application.
Rails makes the session available as a session
hash. You can save anything inside the session
hash. For example, we decided to store a user_id
.
session[:user_id] = user.id
Sessions are probably feeling really abstract right now, but they’ll make more sense when you see them in action.
Flash Messages
In the create
method, we also made use of the flash.
flash[:notice] = "Welcome back, #{user.username}!"
flash[:alert] = "Sorry, your username or password is invalid."
The flash is also part of the session, but it doesn’t get saved in the user’s browser. Instead, the flash is cleared after every request.
We can take advantage of the flash’s short life cycle to temporarily show messages. For example, imagine what happens after you a user logs in to your bookstore. It’s nice to see the welcome back message, but you don’t really need continue seeing that message as you make your rounds through the bookstore.
Just like the session, the flash is made available in your application as a flash
hash. You can also store anything in the flash, but it’s commonly used to save things like notice
s and alert
s.
In the create
method, you’re putting the succes message in flash[:notice]
and the failure message is flash[:alert]
. To show these messages, we need to make a change to your application’s layout template.
-
Open
app/views/layouts/application.html.erb
. -
Before the
container
div
, add the following lines:<% if flash[:notice] %> <p class="notice"> <%= flash[:notice] %> </p> <% end %> <% if flash[:alert] %> <p class="alert"> <%= flash[:alert] %> </p> <% end %>
If anything is inside
flash[:notice]
orflash[:alert]
, we’ll render their contents. -
Save your changes.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<!DOCTYPE html>
<html>
<head>
<title>Bookstore</title>
<%= csrf_meta_tags %>
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
</head>
<body>
<div class="navigation">
<div class="center">
<%= link_to "My Super Rad Bookstore", root_path %>
</div>
</div>
<% if flash[:notice] %>
<p class="notice"> <%= flash[:notice] %> </p>
<% end %>
<% if flash[:alert] %>
<p class="alert"> <%= flash[:alert] %> </p>
<% end %>
<div class="container">
<%= yield %>
</div>
</body>
</html>
Now that you have a fully functional create
method, let’s take it out for a spin!
-
Go to http://localhost:3000/session/new and try logging in with an invalid username.
…
This error is a little tricky. Remember when we used find_by
in the SessionsController
create
method to get the user by their username?
user = User.find_by(username: params[:session][:username])
find_by
will return the user only if a user with that username exists. Otherwise, it returns nil
. So when you try to log in with an invalid username, user
becomes nil.
That’s why you’re seeing the NoMethodError
- you can’t call authenticate
on nil
.
To fix this error, we’ll need to make a change in the create
method.
-
Open
app/views/controllers/sessions_controller.rb
. Inside thecreate
method changeif user.authenticate(params[:session][:password])
to
if user && user.authenticate(params[:session][:password])
With this change,
user.authenticate
will only be called if the user exists. -
Save your changes, revisit http://localhost:3000/session/new, and try logging in with an invalid username again.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by(username: params[:session][:username])
if user && user.authenticate(params[:session][:password])
flash[:notice] = "Welcome back, #{user.username}!"
session[:user_id] = user.id
redirect_to root_path
else
flash[:alert] = "Sorry, your username or password is invalid."
render :new
end
end
end
Yay! That’s a lot better, isn’t it?
-
Now try logging in as “CatPower”, but use an invalid password. You should still get the failure message.
-
Finally, try logging in as “CatPower” using “password” as the password.
YAYAY!!!
-
Try clicking around the bookstore.
The success message should go way as soon as you visit a new page.
Now that you’re logged in, let’s make it so you can log out!
Let’s add a “Log out” link to your bookstore’s header.
-
Open
app/views/layouts/application.html.erb
. Change the “navigation”div
from<div class="navigation"> <div class="center"> <%= link_to "My Super Rad Bookstore", root_path %> </div> </div>
to:
<div class="navigation"> <div class="left"> </div> <div class="center"> <%= link_to "My Super Rad Bookstore", root_path %> </div> <div class="right"> <%= link_to "Log Out", session_path, method: :delete %> </div> </div>
Inside the “navigation”
div
, we’re adding twodiv
s: a “left”div
and a “right”div
. The “left”div
is there to just keep things aligned. The “right”div
is really the one we’re interested in.In the “right”
div
, we’re adding a “Log Out” link. When the link is clicked, it will send a DELETE request to thesession_path
(/session
). -
Visit http://localhost:3000 to see the new “Log Out” link.
What do you think will happen if you try logging out? Give it a try 🙃
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<!DOCTYPE html>
<html>
<head>
<title>Bookstore</title>
<%= csrf_meta_tags %>
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
</head>
<body>
<div class="navigation">
<div class="left">
</div>
<div class="center">
<%= link_to "My Super Rad Bookstore", root_path %>
</div>
<div class="right">
<%= link_to "Log Out", session_path, method: :delete %>
</div>
</div>
<% if flash[:notice] %>
<p class="notice"> <%= flash[:notice] %> </p>
<% end %>
<% if flash[:alert] %>
<p class="alert"> <%= flash[:alert] %> </p>
<% end %>
<div class="container">
<%= yield %>
</div>
</body>
</html>
-
Since the “Log Out” link sends a DELETE request, it gets routed to the
SessionsController
destroy
method. Whelp, that explains the error you’re seeing.The action 'destroy' could not be found for SessionsController
-
Let’s create the
destroy
method. Open theSessionsController
(app/controllers/sessions_controller.rb
), and add the following method:def destroy session[:user_id] = nil flash[:notice] = "See ya next time!" redirect_to root_path end
The first line of this method removes the user_id we saved in the session by setting
session[:user_id]
to nil. The second line shows a friendly message confirming the user has been logged out. The final line redirects the user to theroot_path
. -
Save your changes and try logging out! After you’ve logged out, stop your application’s web server.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by(username: params[:session][:username])
if user && user.authenticate(params[:session][:password])
flash[:notice] = "Welcome back, #{user.username}!"
session[:user_id] = user.id
redirect_to root_path
else
flash[:alert] = "Sorry, your username or password is invalid."
render :new
end
end
def destroy
session[:user_id] = nil
flash[:notice] = "See ya next time!"
redirect_to root_path
end
end