-
Notifications
You must be signed in to change notification settings - Fork 0
10 User Sessions
We need to keep track of when users are and aren't authenticated. Let's make a sessions controller with the following actions:
-
new
for the login page -
create
for actually logging in -
destroy
for logging out.
There's no point in showing, listing, or updating sessions, so we'll leave them out of the routes. We'll use a RESTful route for create
only, and define named routes for destroy
and new
.
config/routes.rb
resources :sessions, only: [:create]
delete 'logout' => 'sessions#destroy'
get 'login' => 'sessions#new'
We'll require the DELETE
HTTP verb for logouts, just as Devise does.
Let's create the SessionsController
. Once again, we'll use the welcome
layout, rather than the default application
layout.
app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
layout 'landing'
def new
@user = User.new
end
end
Let's make a login form to go with it.
app/views/sessions/new.html.erb
<h3>Sign In</h3>
<%= form_for @user, url: sessions_path do |f| %>
<p>
<%= f.label :username %><br>
<%= f.text_field :username %>
</p>
<p>
<%= f.label :password %><br>
<%= f.password_field :password %>
</p>
<%= f.submit 'Sign In', class: 'btn btn-default' %>
<% end %>
HAML:
%h3 Sign In
= form_for @user, url: sessions_path do |f|
%p
= f.label :username
%br/
= f.text_field :username
%p
= f.label :password
%br/
= f.password_field :password
= f.submit 'Sign In', class: 'btn btn-default'
You should now see a lovely form at /login. If you actually try to sign in, however, it will blow up upon trying to render create
.
To implement sessions#create
, we need to check if the user exists and whether the password entered encrypts to match the password_digest
value in our database. We won't make that comparison manually. has_secure_password
adds an instance method to the User
model called authenticate
that handles it for us. Pretty cool, huh?
If the user successfully authenticates, we'll assign the user_id
to a session variable. This is stored in an encrypted cookie, and can be read in on page load to set current_user
. Upon setting the session variable, we'll redirect to the root_url
.
If authentication fails, we'll render the login form, making available a @user
instance variable with just the username
set.
Don't forget to permit the username
and password
fields for mass assignment.
app/controllers/sessions_controller.rb
def create
user = User.find_by username: user_params[:username]
if user.present? && user.authenticate(user_params[:password])
session[:user_id] = user.id
redirect_to root_url, notice: t('session.flash.create.success')
else
@user = User.new username: user_params[:username]
flash.now.alert = t('session.flash.create.failure')
render :new
end
end
private
def user_params
params.require(:user).permit(:username, :password)
end
Add the English text for our session-related flash messages to the locale file.
config/locales/en.yml
en:
hello: "Hello world"
user:
flash:
create:
success: "Thanks for signing up!"
failure: "There was a problem with your registration."
session:
flash:
create:
success: "Welcome!"
failure: "There was a problem logging in with those credentials."
Visit /login and try signing in with the user you added earlier.
It works, but aside from the flash message, there's no indicated that we're logged in. Let's create a current_user
helper method available to all controllers. Define the new method in ApplicationController
.
app/controllers/application_controller.rb
#...
helper_method :current_user
private
def current_user
@current_user ||= User.find(session[:user_id]) if session[:user_id]
end
Now we need a way to sign out. Our sessions#destroy
method just needs to clear the session variable and redirect to root_url
.
app/controllers/sessions_controller.rb
def destroy
session[:user_id] = nil
redirect_to root_url, notice: t('session.flash.destroy.success')
end
We'll also want to add a flash message to en.yml
.
destroy:
success: "Come back soon!"
failure: "We couldn't log you out. Maybe you weren't logged in!"
When a user is signed in, let's show their name if name
is set. Otherwise, we'll show their username. We'll add a display_name
method to User
to achieve that.
app/models/user.rb
def display_name
name.presence || username
end
Now let's add a logout link to the application
layout. It will need to use the DELETE
HTTP method.
<header>
<div class="well">
Nevernote
<div class="user-links">
<% if current_user.present? -%>
Signed in as <%= current_user.display_name %>.
<%= link_to 'Logout', logout_path, method: :delete %>
<% else -%>
<%= link_to 'Sign Up', sign_up_path %> or
<%= link_to 'Login', login_path %>
<% end -%>
</div>
</div>
</header>
HAML:
%header
.well
Nevernote
.user-links
- if current_user.present?
Signed in as #{current_user.display_name}.
= link_to 'Logout', logout_path, method: :delete
- else
= link_to 'Sign Up', sign_up_path
or
= link_to 'Login', login_path
Let's go back to our sign up page and add a link to the login page.
app/views/users/new.html.erb
<h3>Sign Up for Nevernote</h3>
<%= form_for @user do |f| %>
<p>
<%= f.label :name %><br>
<%= f.text_field :name %>
</p>
<p>
<%= f.label :username %><br>
<%= f.text_field :username %>
</p>
<p>
<%= f.label :password %><br>
<%= f.password_field :password %>
</p>
<%= f.submit 'Sign Up', class: 'btn btn-default' %>
<span class="login">Already have an account? <%= link_to 'Sign in', login_path %>.</span>
<% end %>
HAML:
%h3 Sign Up for Nevernote
= form_for @user do |f|
%p
= f.label :name
%br/
= f.text_field :name
%p
= f.label :username
%br/
= f.text_field :username
%p
= f.label :password
%br/
= f.password_field :password
= f.submit 'Sign Up', class: 'btn btn-default'
%span.login
Already have an account? #{link_to 'Sign in', login_path}.
Notice that last line of HAML. We can use ruby string interpolation inline with plain text in HAML.
Let's make a link in the other direction too. Edit the login page (sessions/new
).
app/views/sessions/new.html.erb
<h3>Sign In</h3>
<%= form_for @user, url: sessions_path do |f| %>
<p>
<%= f.label :username %><br>
<%= f.text_field :username %>
</p>
<p>
<%= f.label :password %><br>
<%= f.password_field :password %>
</p>
<%= f.submit 'Sign In', class: 'btn btn-default' %>
<span class="login">Don't have an account? <%= link_to 'Sign up!', sign_up_path %>.</span>
<% end %>
HAML:
%h3 Sign In
= form_for @user, url: sessions_path do |f|
%p
= f.label :username
%br/
= f.text_field :username
%p
= f.label :password
%br/
= f.password_field :password
= f.submit 'Sign In', class: 'btn btn-default'
%span.login
Don't have an account? #{link_to 'Sign up!', sign_up_path}.
Now that we have current_user
, let's make an application-wide method to redirect unauthenticated users to the login page.
app/controllers/application_controller.rb
def authorize_user
if current_user.nil?
redirect_to login_path, :'alert-danger' => t('session.flash.unauthenticated')
end
end
Later we'll use this as a before_action
in our other controllers in order to restrict access.
Everything look good? Commit!
$ git add .
$ git commit -m "Implement authentication."