10 User Sessions - getfretless/elevennote GitHub Wiki

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', as: :logout
get    'login' => 'sessions#new',      as: :login

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 %>

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: "You are now logged out."

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

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 ElevenNote</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 %>

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 %>

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
  redirect_to login_path, alert: 'Please sign in to view that page.' if current_user.nil?
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."
⚠️ **GitHub.com Fallback** ⚠️