Chapter 5

The Core Module: Authentication

Our modular application is still pretty basic. It’s time to allow people to create an account and login. We’re not going to reinvent the wheel, so we’ll use Devise, the great authentication gem (which is actually built as a Rails engine).

Box 5.1. Create our chapter branch

As we mentioned in the previous chapter in Box aside:branches_as_chapters, we will be adding our code for each chapter as a separate branch. Let’s do this now:

git checkout -b Chapter-5

We will be doing this at the start of each chapter (with the relevant chapter number, of course), so that it is easier for you to follow our code.

5.1. Users & Authentication

5.1.1. Adding Devise as a dependency

First, we must add the Devise gem in the Core module gemspec file.

Listing 0.1: Added Devise dependencies blast_crm/engines/core/bast_core.gemspec
  .
  .
  .
  spec.add_dependency "rails", "~> 5.2.3"

  spec.add_dependency 'bootstrap', '~> 4.3.1'
  spec.add_dependency 'jquery-rails', '~> 4.3.3'
  spec.add_dependency 'sass-rails', '~> 5.0'

  spec.add_dependency 'devise', '~> 4.6.2'

  spec.add_development_dependency "sqlite3", "~> 1.4.1"
end

Next, we need to require it inside the core engine. Notice that we are requiring devise before our engine; we need to do it this way in order to be able to override the views from Devise correctly, otherwise the devise views will be loaded after our overrides:

Listing 0.2: Requiring Devise dependencies blast_crm/engines/core/lib/blast/core.rb
require 'devise'
require_relative 'core/engine'
require 'sass-rails'
require 'bootstrap'
require 'jquery-rails'

module Blast
  module Core
  end
end

Now run bundle install from the parent application. Don’t forget to restart your Rails server if it was running.

5.1.2. Run the Devise generator

Devise comes with a handy generator that we’re going to use to create the required files. Run the following command from inside the Core engine:

rails generate devise:install

which will give us the following output:

  create  config/initializers/devise.rb
  create  config/locales/devise.en.yml
===============================================================================
 ...

One of the two files this command generated is a file named devise.rb in blast_crm/engines/core/config/initializers/, which contains the configuration for Devise. We have to tweak a few things since we are running Devise from inside an engine (Devise being an engine itself).

5.1.3. Configuring Devise

Listing 3 shows the updated configuration for Devise, with the changed values for router_name, parent_controller and mailer_sender. The generated file contains a lot of commented explanation for each option, so don’t hesitate to go through them to learn more about Devise (we have left out these comments from the listing, to keep the code cleaner).

The values we’re changing are scoping Devise to our current engine:

Listing 0.3: Devise configuration core/config/initializers/devise.rb
# Use this hook to configure devise mailer, warden hooks and so forth.
# Many of these configuration options can be set straight in your model.
Devise.setup do |config|
  # ...
  # *** We uncommented and updated this line
  config.parent_controller = 'Blast::ApplicationController'
  # ...
  # *** We updated this line
  config.mailer_sender = 'crm@blast.com'

  # ...
  require 'devise/orm/active_record'

  # ...
  # *** We uncommented this line
  config.authentication_keys = [:email]
  # ...
  config.case_insensitive_keys = [:email]
  # ...
  config.strip_whitespace_keys = [:email]
  # ...
  config.skip_session_storage = [:http_auth]
  # ...
  config.stretches = Rails.env.test? ? 1 : 11
  # ...
  config.reconfirmable = true
  # ...
  config.expire_all_remember_me_on_sign_out = true
  # ...
  config.password_length = 6..128
  # ...
  config.email_regexp = /\A[^@\s]+@[^@\s]+\z/
  # ...
  config.reset_password_within = 6.hours
  # ...
  config.sign_out_via = :delete
  # ...
  # *** We uncommented and updated this line
  config.router_name = :blast
  # ...
end

5.1.4. Adding Flash messages

We’re going to add flash messages so that Devise can tell a user when something goes wrong. To do that, and keep our code clean, we’re going to use a helper method.

But first, we need to reorganize the helpers folder in the Core engine. Run the below command from the Core engine:

mv app/helpers/core app/helpers/blast

Our new Structure now is:

core/app/helpers/
  blast/
    application_helper.rb

We also have to add the Blast namespace to the application_helper.rb.

Listing 0.4: Updated applicationhelper.rb core/app/helpers/blast/application_helper.rb
module Blast
  module ApplicationHelper
  end
end

Since this is a sample app, we’re going to put all the helper methods inside the ApplicationHelper. In a real application, you should split your helpers into different files, depending on the kind of ‘help’ they provide. Take a look at Listing 5.

Listing 0.5: applicationhelper.rb with Flash methods core/app/helpers/blast/application_helper.rb
module Blast
  module ApplicationHelper
    FLASH_CLASSES = {
      notice: 'alert alert-info',
      success: 'alert alert-success',
      alert: 'alert alert-danger',
      error: 'alert alert-danger'
    }.freeze

    def flash_class(level)
      FLASH_CLASSES[level]
    end
  end
end

Add the following code at the top of the container in the layout file inside the Core engine, right before the jumbotron:

Listing 0.6: Updated Layout file core/app/views/layouts/blast/application.html.erb
<!-- ... -->

<div class='container' role='main'>
  <% flash.each do |key, value| %>
    <div class="<%= flash_class(key.to_sym) %>"><%= value %></div>
  <% end %>

  <%= yield %>
</div>

<!-- ... -->

5.1.5. Generate the User model

Now it’s time to generate a User model with Devise. Devise has a neat generator to do that. We’ll have to edit a few things since we are working inside engines.

Run the following command from inside the Core engine:

rails generate devise User

You should see the following output:

invoke  active_record
create    db/migrate/TIMESTAMP_devise_create_blast_users.rb
create    app/models/blast/user.rb
insert    app/models/blast/user.rb
 route  devise_for :users, class_name: "Blast::User"

5.1.6. Add migrations to parent app paths

Wait, our migration is inside the Core engine. How are we going to migrate it from the parent app? We don’t want to run the migrations manually from each engine one after the other… that would be annoying. Well, we just have to tell the parent application to look for migrations inside the engines.

To do that, we need to add an initializer to the Core engine. Open the engine.rb file located at core/lib/blast/core/engine.rb and update it to reflect the contents of Listing 7:

Listing 0.7: Initializer for Core engine core/lib/blast/core/engine.rb
module Blast
  module Core
    class Engine < ::Rails::Engine
      isolate_namespace Blast

      initializer :append_migrations do |app|
        unless app.root.to_s.match?(root.to_s)
          config.paths['db/migrate'].expanded.each do |p|
            app.config.paths['db/migrate'] << p
          end
        end
      end
    end
  end
end
Box 5.2. Why not blast:install:migrations

If any of you have read the Rails Guides, you might have noticed that in the section titled “Engine Setup”, it instructs you to run

rails blast:install:migrations

from the parent application.

What this will do is import all the migrations to the parent application. Running db:migrate or db:rollback will then use the migrations in the parent application, and not the engine. The downside to this (other than being more clunky) is that if you rollback and update your migration, you need to remove the migration from the parent application, re-import the migrations and run db:migrate again.

We’re sure that you can see that by using the method we have shown you, you will never forget to re-import your migrations (something that we have done countless times, and has been the cause of many debugging headaches).

Excellent! We still have one more thing we need to do before migrating.

5.1.7. Fix the Devise Routes

When we ran the Devise generator, it added a new line to the routes.rb file, as shown below:

Listing 0.8: routes.rb file after Devise generator core/config/routes.rb
Blast::Core::Engine.routes.draw do
  devise_for :users, class_name: 'Blast::User'
  root to: 'dashboard#index'
end

Unfortunately, that’s not good enough. We also need to tell Devise that we’re working inside an engine. We do this by making the change shown in Listing 9 below:

Listing 0.9: routes.rb file after Devise generator core/config/routes.rb
Blast::Core::Engine.routes.draw do
  devise_for :users, class_name: 'Blast::User', module: :devise

  root to: 'dashboard#index'
end

The class_name option specifies the name of our User model. module is here to tell Devise that we’re not running it inside a regular Rails application.

5.1.8. Add the admin column

To differentiate regular users from administrators, we will add a new column on the users table. To do so, let’s generate a new migration (again, from within the Core engine):

rails generate migration add_admin_to_blast_users admin:boolean
Box 5.3. Why are we adding to blast_users and not users?

You might’ve noticed that instead of typing add_admin_to_users, we typed add_admin_to_blast_users. Earlier on, when we generated the devise user, did you notice that the output contained the following line?

  create db/migrate/TIMESTAMP_devise_create_blast_users.rb

Note the word “blast” in there, even though we never typed it. This is because, when we generate (using rails) a model within a namespaced engine (which we have already spent time setting up), all tables are created with the namespace prepended to the table name. In our case, this is blast_users. This is done so that there is no conflict between tables for Models of the same name in two different engines. Clever right?

The only downside to this is that when we create migrations manually, we need to remember this so that we can prepend the namespace ourselves. So, when we ran our migration command, we had to add “blast_users” to the command, so that Rails can know to add the column to the right table, as you can see it Listing 10 (i.e. to the blast_users table that was created, and not the users table that does not exist). Had we not done this, we would have had to go into the migration file and correct the table name manually.

This will give us the following output:

invoke  active_record
create    db/migrate/TIMESTAMP_add_admin_to_blast_users.rb

and created the file core/db/migrate/TIMESTAMP_add_admin_to_blast_users.rb with the content below:

Listing 0.10: Contents of add_admin_to_blast_users.rb migration core/db/migrate/TIMESTAMP_add_admin_to_blast_users.rb
class AddAdminToBlastUsers < ActiveRecord::Migration[5.2]
  def change
    add_column :blast_users, :admin, :boolean
  end
end

Add default: false to the add_column line in order to set the default value as false (new users are not administrators when created):

Listing 0.11: Add default to add_admin_to_blast_users.rb migration core/db/migrate/TIMESTAMP_add_admin_to_blast_users.rb
class AddAdminToBlastUsers < ActiveRecord::Migration[5.2]
  def change
    add_column :blast_users, :admin, :boolean, default: false
  end
end

5.1.9. Migrate

Let’s run our first modular migration. Go back up to the parent application folder and run:

rails db:migrate

The table for the User model should be created without any problem, as shown in the output below:

== TIMESTAMP DeviseCreateBlastUsers: migrating ===========================
-- create_table(:blast_users)
   -> 0.0005s
-- add_index(:blast_users, :email, {:unique=>true})
   -> 0.0004s
-- add_index(:blast_users, :reset_password_token, {:unique=>true})
   -> 0.0005s
== TIMESTAMP DeviseCreateBlastUsers: migrated (0.0016s) ==================

== TIMESTAMP AddAdminToBlastUsers: migrating =============================
-- add_column(:blast_users, :admin, :boolean, {:default=>false})
   -> 0.0004s
== TIMESTAMP AddAdminToBlastUsers: migrated (0.0005s) ====================

1.10 Copy views from Devise

Devise’s default views are nice, but we want to customize them to match our graphical identity… Bootstrap style. So we’re going to generate the view files and edit them.

Run the following command from inside the Core engine to generate editable Devise views:

rails g devise:views

The following output shows us that we were successful:

invoke  Devise::Generators::SharedViewsGenerator
create    app/views/devise/shared
create    app/views/devise/shared/_error_messages.html.erb
create    app/views/devise/shared/_links.html.erb
invoke  form_for
create    app/views/devise/confirmations
create    app/views/devise/confirmations/new.html.erb
create    app/views/devise/passwords
create    app/views/devise/passwords/edit.html.erb
create    app/views/devise/passwords/new.html.erb
create    app/views/devise/registrations
create    app/views/devise/registrations/edit.html.erb
create    app/views/devise/registrations/new.html.erb
create    app/views/devise/sessions
create    app/views/devise/sessions/new.html.erb
create    app/views/devise/unlocks
create    app/views/devise/unlocks/new.html.erb
invoke  erb
create    app/views/devise/mailer
create    app/views/devise/mailer/confirmation_instructions.html.erb
create    app/views/devise/mailer/email_changed.html.erb
create    app/views/devise/mailer/password_change.html.erb
create    app/views/devise/mailer/reset_password_instructions.html.erb
create    app/views/devise/mailer/unlock_instructions.html.erb

1.11 Add authenticate_user!

Before we let users access the dashboard, we need to check if they are authenticated. If they’re not, we want them to be redirected to the login page. To restrict access in this way, we need to add a before_action to the ApplicationController located inside the Core engine. We can see how we do this in Listing 12 below:

Listing 0.12: Adding before_action for authentication core/app/controllers/blast/application_controller.rb
module Blast
  class ApplicationController < ActionController::Base
    protect_from_forgery with: :exception
    before_action :authenticate_user!
  end
end

1.12 Update Devise new session view

Now start the application (run rails s from the parent application) and try to access the app at http://localhost:3000 - you should be redirected to the sign-in view. But it does not look good at all, as we can see from Figure 1.

https://s3.amazonaws.com/devblast-modr-book/images/figures/02_05/devise_new_session_default
Figure 1

Let’s tweak the view a bit to make it look better. See Listing 13:

Listing 0.13: Tweaked New Session View core/app/views/devise/sessions/new.html.erb
<h2>Log in</h2><hr>
<%= form_for(resource, as: resource_name, url: session_path(resource_name),
                       html: { class: 'form-horizontal' }) do |f| %>
  <div class="form-group">
    <%= f.label :email, class: "col-sm-2 control-label" %>
    <div class="col-sm-6">
      <%= f.email_field :email, autofocus: true,
                                class: "form-control" %>
    </div>
  </div>

  <div class="form-group">
    <%= f.label :password, class: "col-sm-2 control-label" %>
    <div class="col-sm-6">
      <%= f.password_field :password, autocomplete: "off",
                                      class: "form-control" %>
    </div>
  </div>

  <% if devise_mapping.rememberable? -%>
    <div class="form-group">
      <div class="col-sm-6 col-sm-offset-2">
        <%= f.check_box :remember_me %> <%= f.label :remember_me %>
      </div>
    </div>
  <% end %>

  <div class="form-group">
    <div class="col-sm-6 col-sm-offset-2">
      <%= f.submit "Sign in", class: 'btn btn-primary' %>
    </div>
  </div>

  <div class="form-group">
    <div class="col-sm-6 col-sm-offset-2">
      <%= render "devise/shared/links" %>
    </div>
  </div>
<% end %>

Nothing really interesting in this file; we’re simply redesigning the page. Let’s take a look at our results in Figure 2.

https://s3.amazonaws.com/devblast-modr-book/images/figures/02_05/devise_new_session_updated
Figure 2

1.13 Update Devise new registration view

Now let’s update the registration page. Simply click on Sign Up, and you’ll see what is shown in Figure 3.

https://s3.amazonaws.com/devblast-modr-book/images/figures/02_05/devise_new_registration_default
Figure 3

Let’s prettify it by replacing the view code with Listing 14:

Listing 0.14: Tweaked New Registration View file core/app/views/devise/registrations/new.html.erb
<h2>Sign up</h2>
<hr>
<%= form_for(resource, as: resource_name, url: registration_path(resource_name),
                       html: {class: 'form-horizontal'}) do |f| %>
  <%= devise_error_messages! %>

    <div class="form-group">
      <%= f.label :email, class: "col-sm-4 control-label" %>
      <div class="col-sm-6">
        <%= f.email_field :email, autofocus: true, autocomplete: "email",
                                  class: "form-control" %>
      </div>
    </div>

    <div class="form-group">
      <%= f.label :password, class: "col-sm-4 control-label" %>
      <div class="col-sm-6">
          <%= f.password_field :password, autocomplete: "off",
                                          class: "form-control" %>
      </div>
    </div>

   <div class="form-group">
      <%= f.label :password_confirmation, class: "col-sm-4 control-label" %>
      <div class="col-sm-6">
        <%= f.password_field :password_confirmation, autocomplete: "off",
                                                      class: "form-control" %>
      </div>
   </div>

   <div class="form-group">
      <div class="col-sm-offset-2 col-sm-6">
        <%= f.submit "Sign up", class: "btn btn-primary" %>
      </div>
   </div>

   <div class="form-group">
      <div class="col-sm-offset-2 col-sm-6">
        <%= render "devise/shared/links" %>
      </div>
   </div>
<% end %>

And we can see a prettier version in Figure 4.

https://s3.amazonaws.com/devblast-modr-book/images/figures/02_05/devise_new_registration_updated
Figure 4

We’re going to skip the forgot password and a few other Devise views because, well, it’s not really interesting. We’re sure you’ve got the idea of what we’re doing. Let’s work on the authentication instead.

1.14 Register & Sign In

Moment of truth… Let’s try to create a user and sign in. If everything goes well, you should end up on the dashboard we created earlier.

https://s3.amazonaws.com/devblast-modr-book/images/figures/02_05/devise_registration
Figure 5
https://s3.amazonaws.com/devblast-modr-book/images/figures/02_05/devise_registered_and_signed_in
Figure 6

1.15 Update the navbar

Next, we need to give the ability to our users to edit their details. Devise gives us the current_user method and we’re going to use it to show the nav bar to authenticated users only. Replace the <nav> section in the application.html.erb file with the contents of Listing 15:

Listing 0.15: Nav Bar updates in application.html.erb core/app/views/layouts/blast/application.html.erb
<!-- ... -->

<nav class="navbar navbar-expand-lg navbar-light bg-light
            navbar-inverse navbar-fixed-top mb-4">
  <%= link_to 'BlastCRM', blast.root_path, class: 'navbar-brand' %>
  <button class="navbar-toggler" type="button" data-toggle="collapse"
          data-target="#navbarSupportedContent"
          aria-controls="navbarSupportedContent"
          aria-expanded="false"
          aria-label="Toggle navigation">
    <span class="navbar-toggler-icon"></span>
  </button>

  <div class="collapse navbar-collapse" id="navbarSupportedContent">
    <ul class="navbar-nav mr-auto">
      <li class="nav-item">
        <%= link_to 'Dashboard', blast.root_path, class: 'nav-link' %>
      </li>
    </ul>
    <div class="pull-right">
      <ul class="navbar-nav mr-auto">
        <% if current_user %>
          <li class="nav-item">
            <%= link_to 'My Account', blast.edit_user_registration_path,
                        class: "nav-link" %>
          </li>
          <li class="nav-item">
            <%= link_to 'Logout', blast.destroy_user_session_path,
                        class: "nav-link", method: :delete %>
          </li>
        <% end %>
      </ul>
    </div>
  </div>
</nav>

<!-- ... -->

And we get Figure 7

https://s3.amazonaws.com/devblast-modr-book/images/figures/02_05/my_account
Figure 7

Awesome! We’re making some visual progress!

1.16 Update the “My Account” view

To update our account, click on ‘My Account’. The result is Figure 8.

https://s3.amazonaws.com/devblast-modr-book/images/figures/02_05/devise_edit_registration_default
Figure 8: Ew.

Hmm, not great. Here’s the updated code for this view:

Listing 0.16: Tweaked Edit Registration View file core/app/views/devise/registrations/edit.html.erb
<h2>Edit <%= resource_name.to_s.humanize %></h2>
<hr>
<%= form_for(resource, as: resource_name, url: registration_path(resource_name),
                       html: { method: :put, class: "form-horizontal" }) do |f| %>
  <%= devise_error_messages! %>

  <div class="form-group">
    <%= f.label :email, class: "col-sm control-label"  %>
    <div class="col-sm-6">
        <%= f.email_field :email, class: "form-control"  %>
    </div>
  </div>

  <div class="form-group">
    <%= f.label :password, class: "col-sm control-label"  %>
    <div class="col-sm-6">
      <%= f.password_field :password, autocomplete: "off",
                          class: "form-control"  %>
    </div>
  </div>

  <div class="form-group">
    <%= f.label :password_confirmation, class: "col-sm control-label" %>
    <div class="col-sm-6">
      <%= f.password_field :password_confirmation, autocomplete: "off",
                            class: "form-control"  %>
    </div>
  </div>

  <div class="form-group">
    <%= f.label :current_password, class: "col-sm control-label" %>

    <div class="col-sm-6">
      <%= f.password_field :current_password, autocomplete: "off",
                            class: "form-control" %>
    </div>
  </div>
  <div class="form-group">
    <div class="col-sm-offset-2 col-sm-6">
      <%= f.submit "Update", class: "btn btn-primary" %>
    </div>
  </div>
<% end %>

<h2>Cancel my account</h2>
<hr>
<p>Unhappy?
<%= button_to "Cancel my account", registration_path(resource_name),
                                   data: { confirm: "Are you sure?" },
                                   method: :delete,
                                   class: "btn btn-danger" %></p>
<hr>
<%= link_to "Back", :back, class: "btn btn-default" %>

See the fruits of our labour in Figure 9.

https://s3.amazonaws.com/devblast-modr-book/images/figures/02_05/devise_edit_registration_updated
Figure 9: Much better, isn’t it?

1.17 Add active link to the navbar

In order to let users know on which page they currently are, let’s add a highlight to the navbar menu. We’ll be using a helper method named active that checks if the current page matches the given path. Take a look at Listing 17:

Listing 0.17: Added active method to application_helper core/app/helpers/blast/application_helper.rb
module Blast
  module ApplicationHelper
    FLASH_CLASSES = {
      notice: 'alert alert-info',
      success: 'alert alert-success',
      alert: 'alert alert-danger',
      error: 'alert alert-danger'
    }.freeze

    def flash_class(level)
      FLASH_CLASSES[level]
    end

    def active(path)
      current_page?(path) ? 'active' : ''
    end
  end
end

If the given path matches the current page, we’ll return the string "active" and use it as the link class. You can use it in the nav bar this way:

Listing 0.18: Updating navbar for "active" core/app/views/layouts/blast/application.html.erb
<!-- ... -->

<div class="collapse navbar-collapse" id="navbarSupportedContent">
  <ul class="navbar-nav mr-auto">
    <li class="nav-item <%= active(blast.root_path) %>">
      <%= link_to 'Dashboard', blast.root_path, class: 'nav-link' %>
    </li>
  </ul>
  <div class="pull-right">
    <ul class="navbar-nav mr-auto">
      <% if current_user %>
        <li class="nav-item <%= active(blast.edit_user_registration_path) %>">
          <%= link_to 'My Account', blast.edit_user_registration_path,
                      class: "nav-link" %>
        </li>
        <li class="nav-item">
          <%= link_to 'Logout', blast.destroy_user_session_path,
                      class: "nav-link", method: :delete %>
        </li>
      <% end %>
    </ul>
  </div>
</div>

<!-- ... -->

Pretty simple, right? Give it a try now. The menu you’re currently on should be highlighted, as shown in Figure 10:

https://s3.amazonaws.com/devblast-modr-book/images/figures/02_05/navbar_highlighted
Figure 10

1.18 Test it

We will write some automated tests very soon, but for now, just ensure that everything is working. Login, logout, navigation, etc. If everything looks fine, let’s continue and start working on the admin panel.

5.2. Pushing Our Changes

It’s now time to push our changes to GitHub. The flow is the same as in the previous chapter.

  1. Check the changes:
    git status
    
  2. Stage them:
    git add .
    
  3. Commit them:
    git commit -m "Authentication"
    
  4. Push to your GitHub repo if you’ve configured it:
    git push origin Chapter-5
    

    Remember that we’re working on a different branch for each chapter. git push will also push the current branch and git push –all will push all branches.

5.3. Wrap Up

We’ve reached the end of another chapter. In this one, we have created a User model and made it possible for users to authenticate themselves.

5.3.1. What did we learn?

  • How to work with Devise and create a user.
  • How to run migrations for all engines from the parent application.
  • How to configure Devise to play nicely when integrated inside another engine.

5.3.2. Next Step

In the next chapter, we will be working on setting up a testing environment for BlastCRM.