« Back to Altalogy blog

Rails 6 (and 5): User Accounts with 3 types of Roles – Devise, Rails Admin, CanCanCan

User accounts of several types are the common functionality of web applications. Ruby on Rails ecosystem provides several helpful gems: Devise for user authentication, CanCanCan for authorization, and RailsAdmin for admin panels.

The following article has been moved here from codepany.com blog. The content was updated to the newest Ruby and Rails versions, but it’s still compatible with Rails 5 and Ruby 2.3.0.

The article presents how to set up user accounts for two admin roles (superadmin, supervisor) and one end-user role. The admin roles grant access to the admin panel built with Rails Admin. Devise gem handles authentication, and CanCanCan gem does authorization.

Rails: 5.2.4 or 6.0.1
Ruby: 2.6.3
Database: MySQL or PostgreSQL

Let’s assume that you work on the existing Rails project with an already initialized MySQL database.

Plan

  1. Create User model using Devise gem.
  2. Set up authentication by Devise gem.
  3. Generate views, links and controllers for Devise’s Users.
  4. Create rails_admin dashboard.
  5. Set up authorization using Cancancan.
  6. Give an access to Rails Admin only for admins (superadmin, supervisor) users using Cancancan.
  7. Setup mailer for Devise.

User accounts – Devise

Devise installation

Open Gemfile and add:

gem 'devise'

Run bundler and Devise generator:

$ bundle install
$ rails generate devise:install

The last command prompts a few instructions which have to be done manually. First, add to config/environments/development.rb:

config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

You can insert your machine’s IP instead of localhost. You can also change the port.

Next, we should set a root_url in config/routes.rb. Probably you have done it already in your project. But if you have not already, do it now and set root to some existing route. For example:

root to: "home#index"

The third instruction is about flash messages, which notify about successful and failed login attempts. We will use toastr gem. The installation of Toastr gem is different for Rails 5 and Rails 6. Rails 6 uses Webpacker, so there we will have to use npm or yarn.


Toastr for Rails 5

Skip this chapter if you are using Rails 6.

Add to Gemfile:

gem 'toastr-rails'

To application.css and application.js add the following:

/* app/asssets/javascripts/application.css */

*= require toastr

// app/asssets/javascripts/application.js

//= require toastr

JQuery

Toastr requires jQuery. If you are already using jQuery in your project, you can skip this part. Otherwise, install it now. Add to Gemfile:

gem 'jquery-rails'

Run bundle install, and then import jQuery before toastr in app/assets/javascripts/application.js :

//= require jquery
//= require jquery_ujs
//= require toastr

Toastr for Rails 6

Use npm or yarn to install toastr:

$ yarn add toastr

Import toastr in app/javascripts/packs/application.js:

global.toastr = require("toastr")

To import styles, we need to create subfolder stylesheets under app/javascript. Next, create application.scss under app/javascript/stylesheets, and import:

// app/javascript/stylesheets/application.scss
@import 'toastr'

Last thing is to import app/javascript/stylesheets/application.scss in app/javascript/packs/application.js:

import "../stylesheets/application"

After you installed toastr in your project, let’s use it to display Devise notifications and alerts. Add following code to app/views/layouts/application.rb:

<% unless flash.empty? %>
   <script type="text/javascript">
      <% flash.each do |f| %>
    <% type = f[0].to_s.gsub('alert', 'error').gsub('notice', 'info') %>
   	 toastr['<%= type %>']('<%= f[1] %>');
   <% end %>
   </script>
<% end %>

Devise generator

In next step, we generate the User model:

$ rails generate devise user

Let’s add an initial user in migration before running db:migrate. It can be done also with seeds, but this way, you always get an initial account after setting up a project from scratch.

Open db/migrate/xxx_devise_create_user.rb and add:

class DeviseCreateUsers < ActiveRecord::Migration[5.0]
  def change
    create_table :users do |t|
      ...
    end

    add_index :users, :email,                unique: true
    add_index :users, :reset_password_token, unique: true

    # Initialize first account:
    User.create! do |u|
        u.email     = 'test@test.com'
        u.password    = 'password'
    end

  end
end

Run:

$ rake db:migrate

The last thing is to add devise_for :user to the config/routes.rb:

Rails.application.routes.draw do
  devise_for :user
  (...)
end

Authenticate user

Let’s assume that our application has:

  • landing page – available for anyone – in our example app it can be home#index (index action of home controller),
  • dashboard – this is available only for logged in users – user has to sign in to see this area of the app.

Let’s add following to app/controllers/application_controller.rb:

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception

  before_action :authenticate_user!

end

The authenticate_user! forces an authentication before every action in all controllers. You can check this now by running your project and opening any page in the web browser. Instead of the desired page, you should see the login form. Mostly, some views are public, so authenticate_user! shouldn’t be applied for them. You can use except or only selectors along with before_action. You can move before_action :authenticate_user! to specific controllers. And you can use skip_before_action for selected actions. We’ll use the last option.

In config/routes.rb, you should have set a root_url. In this example, we used home#index (home controller + index action). Open the controller for the associated route and add following code:

skip_before_action :authenticate_user!, :only => [:index]

Now you should see your page without any authentication.

Do you remember that we’ve created a test user account in migration with the following params?

  • email: test@test.com
  • password: password

You can now try to open another page and enter this to the login form to get access to the restricted area of your app.

Links and redirections

Let’s add a few links to navigate in the application, including login and log out buttons. In a public page, like home#index, add:

<% if current_user.nil? %>
	<%= link_to new_user_session_path, class: 'login-button' do %>Sign in<% end %>
<% else %>
	<%= link_to app_dashboard_index_path, class: 'login-button' do %>Go to App<% end %>
	<%= link_to destroy_user_session_path, method: :delete do %>Log out<% end %>
<% end %>

You can add the above code to the layout to use it in many views. But in our case, we have only one page with public access. It is a landing page under home#index route. The rest of the application is a dashboard available only to logged-in users.

Not signed in user will see the Sign in button redirecting to the login form. Signed users will see Go to App button redirecting to the dashboard.

For now, after logging in, the user is redirected to the root path, so back to the login page. We can change this by overriding Devise’s after_sign_in_path_for. Add the code to app/controllers/application_controller.rb:

def after_sign_in_path_for(resource)
  app_dashboard_index_path
end

Change app_dashboard_index_path to any private view in your application.

Below are other links that you can use in your project.

<% if user_signed_in? %>

  <%= link_to('Edit registration', edit_user_registration_path) %>
  <%= link_to('Logout', destroy_user_session_path, :method => :delete) %>        

<% else %>

  <%= link_to('Register', new_user_registration_path)  %>
  <%= link_to('Login', new_user_session_path)  %>  

<% end %>

The user_signed_in? method determines if a user is already signed in.

Devise Views and Controllers

The default Devise Views and Controllers are good enough for the article purposes so that we won’t customize them. However, if you want to customize any of them, use the following commands:

# To customize views:
$ rails generate devise:views users

# To customize controllers:
$ rails generate devise:controllers users

If you generate controllers, you may need to update routes.rb by setting paths to customized actions:

devise_for :users, controllers: {
    sessions: 'users/sessions',
    passwords: 'users/passwords',
    registrations: 'users/registrations'
}

Admin Users – Rails Admin gem

Add to Gemfile:

gem 'rails_admin', '~> 2.0'

Run:

$ bundle install
$ rails g rails_admin:install

Now you should get access to Rails Admin panel under /admin in the web browser.

Configuration

You can customize Rails Admin gem in config/initializers/rails_admin.rb. To use devise for authentication purpose, add:

## == Devise ==
  config.authenticate_with do
    warden.authenticate! scope: :user
  end
  config.current_user_method(&:current_user)

Rails 6 Troubleshooting:

On 9 December 2019, Rails Admin works on some minor issues for Rails 6, but contributors actively work on them. You may need to install Rails Admin pointing Github repository in Gemfile:

gem 'rails_admin', git: 'https://github.com/sferik/rails_admin.git'

For more details, you can visit Github page and issues.


Authorization – CanCanCan

We use CanCanCan gem to restrict access to some parts of the app. Whatsmore, we grant different permissions for specific roles. Add to Gemfile:

gem 'cancancan'

Run:

$ bundle install

Next, use this command to generate ability class:

$ rails g cancan:ability

You can define abilities in app/models/ability.rb. But first, we have to set different roles. It’s up to you how you want to define roles. We will use three roles: superadmin, supervisor, and user. We add three boolean columns for each role in the user model. Let’s start by generating migration:

$ rails generate migration add_roles_to_users superadmin_role:boolean supervisor_role:boolean user_role:boolean

Before we run the migration, let’s edit it. Open db/migrate/xxx_add_roles_to_users.rb and adjust the code:

class AddRolesToUsers < ActiveRecord::Migration[5.2]
  def change
    add_column :users, :superadmin_role, :boolean, default: false
    add_column :users, :supervisor_role, :boolean, default: false
    add_column :users, :user_role, :boolean, default: true
  end
end

We’ve defined default values for superadmin and supervisor as a false, and true default value for user_role. Now we can run:

$ rake db:migrate

To verify current user’s role, you can use:

current_user.superadmin_role?
current_user.supervisor_role?
current_user.user_role?

Views

To display some elements only for users with a specific role, you can use:

<% if current_user.superadmin_role? || current_user.supervisor_role? %>
	<p>Visible only for superadmins and supervisors! </p>
<% end %>

Or you can use CanCanCan abilities:

<% if can? :manage, User %>
	<p>Visible only for superadmins and supervisors! </p>
<% end %>

And in app/model/ability.rb:

class Ability
  include CanCan::Ability

  def initialize(user)
    # Define abilities for the passed in user here. For example:

    user ||= User.new # guest user (not logged in)
    if user.superadmin_role?
      can :manage, :all
    end
    if user.supervisor_role?
      can :manage, User
    end

  end
end

Controllers

To authorize user access to controller’s actions, let’s add the following to a specific controller:

load_and_authorize_resource

It will use a before action to load the resource into an instance variable and authorize it for every action.

Rails Admin and CanCanCan

In this part, we set access to our /admin dashboard only for superadmin users. First, we have to configure config/initializers/rails_admin.rb:

config.authorize_with :cancan

Next, let’s modify app/model/ability.rb:

user ||= User.new # guest user (not logged in)
if user.superadmin_role?
      can :manage, :all
      can :access, :rails_admin       # only allow admin users to access Rails Admin
      can :manage, :dashboard         # allow access to dashboard
end
if user.supervisor_role?
      can :manage, User
end

Manage User accounts

The initial user test@test.com created in migration doesn’t have access to Rails Admin and to manage user accounts. It is because of abilities set in ability.rb. You can temporary give anyone access to Rails Admin and user accounts so that you can create new users with superadmin and supervisor roles. To do this, add can :manage, :all for any user in app/models/ability.rb:

user ||= User.new # guest user (not logged in)
can :manage, :all  # <---------- TO GIVE TEMPORARY ACCESS TO EVERYTHING FOR EVERYONE
if user.superadmin_role?
      can :manage, :all
      can :access, :rails_admin       # only allow admin users to access Rails Admin
      can :manage, :dashboard         # allow access to dashboard
end
if user.supervisor_role?
      can :manage, User
end

Run server with rails s, login as test@test.com (password: password), and open /admin in the web browser. Next, navigate to the Users view. Create new accounts or update existing users with different roles. After all, you can remove can :manage, :all added few moments ago.

An alternative is to create a migration do database with adding a new superadmin or supervisor role:

$ rails generate migration add_superadmin

Edit migration:

class AddSuperadmin < ActiveRecord::Migration[5.2]
  def change
    User.create! do |u|
        u.email     = 'test_admin@test.com'
        u.password  = 'password'
        u.superadmin_role = true
    end
  end
end

Run migration:

$ rails db:migrate

Run server and login as test_admin@test.com. Now you should be able to open Rails Admin under /admin URL.

Emails

Devise sends emails to user whenever they want to reset password or confirm registration. We’ll set also welcome email after registration.

The Rails application needs a SMTP server to send emails. You can connect your own email account, like Gmail. To set up SMTP settings, open config/environments/development.rb and add / modify following code:

config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

config.action_mailer.perform_caching = false

config.action_mailer.perform_deliveries = true

config.action_mailer.raise_delivery_errors = true

config.action_mailer.delivery_method = :smtp

config.action_mailer.smtp_settings = {
  user_name:      Rails.application.secrets.mail_username,
  password:       Rails.application.secrets.mail_password,
  domain:         'gmail.com',
  address:       'smtp.gmail.com',
  port:          '587',
  authentication: :plain,
  enable_starttls_auto: true
}

Next, we add mail_username and mail_password to config/secrets.yml:

development:
  secret_key_base: 1d7...fc1
  mail_username: test@example.com
  mail_password: Password123

Replace test@example.com and Password123 with your data.

In config/initializers/devise.rb you can modify default mailer sender:

config.mailer_sender = 'test@example.com'

Now you can try to reset a password. Open in web browser: /users/password/new . And try to send reset password instructions.


TROUBLESHOOTING: Google accounts

You may get an error about an incorrect username and password when Rails application tries to log in to your Gmail account to send an email. It’s because that Google blocks suspicious connections. Check your Gmail inbox where you should receive an alert about logging in from an unsecure application after your Rails application tried to send an email. The first workaround is to allow less secure aps to access your Gmail account. If you are going to enable access for unsecure applications, remember that is not a recommended way, and you should disable this option after all.

Alternative is changing config.action_mailer.smtp_settings.port to 465 may help. But then, you may get another error: “end of file reached”, because Google closes the connection before your server is ready for that.


Last is a welcome email, which should be sent after creating a new user account. Add to the User model app/models/user.rb:

after_create :send_admin_mail
def send_admin_mail
  UserMailer.send_welcome_email(self).deliver_later
end

Generate UserMailer:

$ rails generate mailer UserMailer

Add to app/mailers/application_mailer.rb:

class UserMailer < ApplicationMailer
  default from: 'test@example.com'

  def send_welcome_email(user)
    @user = user
    mail(:to => @user.email, :subject => "Welcome!")
  end

end

Now we have to create mailer views in html and text formats. Create two files in app/views/user_mailer/ directory: sendwelcomeemail.html.erb:

<!DOCTYPE html>
<html>
  <head>
    <meta content='text/html; charset=UTF-8' http-equiv='Content-Type' />
  </head>
  <body>
    <h1>Welcome, <%= @user.email %></h1>
    <p>
      Access to app under following URL: <%= @url %>.
    </p>
    <p>Thanks for joining and have a great day!</p>
  </body>
</html>

… and sendwelcomeemail.text.erb:

Welcome, <%= @user.email %>
===============================================

Access to app under following URL: <%= @url %>.

Thanks for joining and have a great day!

LinkedIn

Join fellow developers getting practical tutorials and coding tips every week