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).
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.
First, we must add the Devise gem in the Core module gemspec file.
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:
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.
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).
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:
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
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
.
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.
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:
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>
<!-- ... -->
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"
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:
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
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.
When we ran the Devise generator, it added a new line to the routes.rb
file, as shown below:
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:
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.
admin
columnTo 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
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:
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):
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
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) ====================
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
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:
core/app/controllers/blast/application_controller.rb
module Blast
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
before_action :authenticate_user!
end
end
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.
Let’s tweak the view a bit to make it look better. See Listing 13:
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.
Now let’s update the registration page. Simply click on Sign Up, and you’ll see what is shown in Figure 3.
Let’s prettify it by replacing the view code with Listing 14:
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.
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.
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.
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:
And we get Figure 7
Awesome! We’re making some visual progress!
To update our account, click on ‘My Account’. The result is Figure 8.
Hmm, not great. Here’s the updated code for this view:
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.
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:
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:
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:
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.
It’s now time to push our changes to GitHub. The flow is the same as in the previous chapter.
git status
git add .
git commit -m "Authentication"
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.
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.
In the next chapter, we will be working on setting up a testing environment for BlastCRM.