Skip to content
Dave Strus edited this page Jul 18, 2015 · 2 revisions

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}.

Restricting access to logged in users

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."