Halt! Who goes there!

If we publish a blogging platform to the internet with no security, bad people will post spam and mess it up for everyone. Let’s add a sign-in form to keep the bad guys out.

I like the Sorcery gem because it just quietly does its thing without imposing its opinions on registration flow like Devise does.

From the Sorcery readme:

// Gemfile
gem 'sorcery'
bundle install
rails generate sorcery:install
rails db:migrate

That will have created a User model for us.

class User < ApplicationRecord
  authenticates_with_sorcery!
end

We’ll need a SessionsController for the sign in method.

rails g controller Sessions

I think it’s interesting to show the tests for this. It can be a mini-tutorial for folks who have never used Sorcery before and need help writing integration tests with it.

require "test_helper"

class SessionsControllerTest < ActionDispatch::IntegrationTest
  setup do
    @sally = User.create! email: 'sally@example.com',
                          password: 'letmein'
  end

  test 'sign in renders a form' do
    get sign_in_url
    assert_response :success

    assert_select 'form' do
      assert_select '#credentials_email'
      assert_select '#credentials_password'
      assert_select 'input.submit'
    end
  end

  test 'redirect to home page after sign in' do
    post sessions_url, params: {
        credentials: {
            email: 'sally@example.com',
            password: 'letmein',
        }
    }
    assert_redirected_to root_url
  end

  test 'rerender the form if sign in fails' do
    post sessions_url, params: {
        credentials: {
            email: 'sally@wrong.com',
            password: 'wrong',
        }
    }
    assert_response :success

    assert_select '.form' do
      assert_select '.error', 'Invalid Login'
      assert_select '#credentials_email' do |elements|
        assert_equal 'sally@wrong.com', elements[0][:value]
      end
    end
  end
end
// routes.rb

  resources :sessions
  get :sign_in,  to: 'sessions#new', as: :sign_in
class SessionsController < ApplicationController
  def new
  end

  def create
    credentials = credential_params

    login(credentials[:email], credentials[:password]) do |user, failure|
      if failure
        @email = credentials[:email]
        @error = failure
        render action: 'new'
      else
        redirect_back_or_to(:root)
      end
    end
  end

  private
  def credential_params
    params.
        require(:credentials).
        permit(:email, :password)
  end
end

The view is pretty simple but it took me a while to remember how to write ERB. I usually use HAML as it’s so much easier to read and write.

-# views/sessions/new.html.erb
<div class='session-page'>
  <div class='session-panel'>
    <h1>Sign in</h1>

    <%= form_for :credentials, url: {action: :create}, html: {class: 'form'} do |form| %>
      <div class='error'>
        <%= t @error %>
      </div>
      <div class='input email'>
        <%= form.label :email %>
        <%= form.email_field :email,
                             value: @email,
                             required: true,
                             autofocus: true,
                             placeholder: 'Enter your email address'
        %>
      </div>
      <div class='input password'>
        <%= form.label :password %>
        <%= form.password_field :password, 
                                required: true, 
                                placeholder: 'Enter your password' %>
      </div>
      <div class='actions'>
        <%= form.submit 'Sign in', class: 'button submit primary' %>
      </div>
    <% end %>
  </div>
</div>

That test passes and a little bit of CSS magic gives me this

I’d like the sign in to be in React someday but it’ll be a distraction for now and I’ll come back to it later. No one said that all my UI must be in React anyway.

If I create a user in the Rails console, I’ll be able to sign in.

User.create! email: 'sally@example.com', password: 'letmein'

Now we can protect the important controller actions. I find it safest to require login on everything and to then make exceptions for the public-facing actions.

class ApplicationController < ActionController::Base
  before_action :require_login

class PostsController < ApplicationController
  skip_before_action :require_login, only: [:index, :show]

class SessionsController < ApplicationController
  skip_before_action :require_login

If I run all the Rails tests now, I see a failure.

PostsControllerTest#test_create_a_blog_post:
Expected response to be a <2XX: success>, 
but was a <302: Found> redirect to <http://www.example.com/>
Response body: <html><body>You are being <a href="http://www.example.com/">redirected</a>.</body></html>

I can update the test to sign in first.

  test 'create a blog post requires a sign in' do
    post posts_url(format: :json), params: {
        post: {
            title: 'A new day',
            body: 'First post!'
        }
    }
    assert_redirected_to root_url
  end

  test 'create a blog post' do
    sign_in @sally
    post posts_url(format: :json), params: {
        post: {
            title: 'A new day',
            body: 'First post!'
        }
    }
    assert_response :success
    # ...
  end 

The sign_in method exists in a test helper:

module TestHelpers
  module AuthenticationTestHelper
    def sign_in user, options={}
      password = options[:password] || 'letmein'
      post sessions_url,
           params: {
               credentials: {
                   email:    user.email,
                   password: password
               },
           }
    end
  end
end

It’s a bit of faffing to set up a test helper that is accessible to all the tests, but it make them more readable if you do.

# test/test_helper.rb
ENV['RAILS_ENV'] ||= 'test'
require_relative "../config/environment"
require "rails/test_help"
Dir['test/test_helpers/*.rb'].each {|file| require file.sub(/^test\//, '') }

class ActiveSupport::TestCase
  parallelize(workers: :number_of_processors)
  fixtures :all
end

class ActionDispatch::IntegrationTest
  include TestHelpers::JsonTestHelper
  include TestHelpers::AuthenticationTestHelper
end

While I am here, I’ll create a test helper for JSON testing too. That’s been bothering me for a while. I just updated the test above to use it.

module TestHelpers
  module JsonTestHelper
    def json_response options={}
      assert_response options[:expected_response] || :success
      JSON.parse response.body, symbolize_names: true
    end
  end
end

The last thing left to do is to hide the post Editor if the user is not signed in. There are a bunch of ways we can send the current user to the React client. The easiest, for now, is to add a data property to the div that contains the React application. We can do something more sophisticated later.

<div id='react' data-user-id='<%= current_user&.id%>' />

We can read this property from Javascript and store it somewhere where it is accessible from any component.

I’m sure this will get more complex later but let’s keep it simple for now.

// users/current_user.js
const current_user = {
  id: null,
}

export function signIn(id=true) {
  current_user.id = id
}

export function signOut() {
  current_user.id = null
}

export function signedIn() {
  return current_user.id
}

We can read the ID and store it when the app loads.

// application/index.jsx
document.addEventListener('DOMContentLoaded', () => {
  const react = document.querySelector('#react')
  signIn(react.getAttribute('data-user-id'))
  // ...

Now we can call the signedIn() method from the editor and render nothing if the user is not signed in.

    if(!signedIn()) return null

We can also call signIn() and signOut() in our tests.