Extending Devise – Registrations Controller

Introduction

In this post, I want to share my experiences in using Devise as an authentication solution for my company’s Web app.

While building Motonow, the team had to make a couple of though decisions and choosing the right authentication solution was one of them. To be honest, most of the projects I’ve joined had already overcome this phase so I’ve never participated in such conversations until Motonow.

After doing some prototypes, we’ve decided to use Devise because:

  1. It has most of the features we want to implement already in place;
  2. It’s really straightforward to start with;
  3. It’s well documented;
  4. It’s widely adopted in the Rails community;

Everything was going well until the domain model started to grow in the sense that Devise’s basic configuration was not enough for us. At that point, we needed to extend Devise, more specifically the RegistrationsController.

Domain Model

At the beginning we only had a User model so integrating with devise was pretty easy. After a while, a new Company model was necessary and integrating with Devise was not as straightforward as we would like to. The code below shows how both classes are defined.

User.rb

class User < ActiveRecord::Base

  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable, :confirmable,
    :recoverable, :rememberable, :trackable, :validatable

  belongs_to :company

  validates :name, presence: true
  validates :cpf, presence: true
  validates :cpf, uniqueness: true

end

Company.rb

class Company < ActiveRecord::Base

  has_many :users

  validates :cnpj, presence: true
  validates :cnpj, uniqueness: true

end

Our challenge is to create a signup form with user and company information so both can be validated and saved (take a look at the signup form so you can have an idea). To make it work as expected, we have to pass through the following steps:

Rails fields_form tag

The first step is setting up the view. We need to provide to the final user inputs related to both User (email, password) and Company (in this case CNPJ which is its unique identifier). So, the registration view looks something like this:

app/views/devise/registrations/new.html.erb

<%= form_for(resource, :as => resource_name, :url => registration_path(resource_name)) do |f| %>
  <div class="controls">
    <%= f.fields_for :company do |company_form| %>
      <%= company_form.text_field :cnpj  %>
      <%= render partial: "devise/shared/field_error", locals: { model: resource.company, field: :cnpj } %>
    <% end %>
  </div>
  <div class="controls">
    <%= f.text_field :cpf %>
    <%= render partial: "devise/shared/field_error", locals: { model: resource, field: :cpf } %>
  </div>
  <div class="controls">
    <%= f.text_field :email %>
    <%= render partial: "devise/shared/field_error", locals: { model: resource, field: :email } %>
  </div>
  <div class="controls">
    <%= f.password_field :password %>
    <%= render partial: "devise/shared/field_error", locals: { model: resource, field: :password } %>
  </div>
  <div class="controls">
    <%= f.password_field :password_confirmation %>
    <%= render partial: "devise/shared/field_error", locals: { model: resource, field: :password_confirmation } %>
  </div>
  <%= f.submit "Submit" %>
<% end %>

No surprises here. We are using existing Rails form_for and fields_for FormHelper to build the form for User model and its nested Company model.

Unfortunately, when trying to load the page we get a NoMethodError: undefined method `cnpj’ for nil:NilClass. Basically, we have to initialize User’s company attribute. The default approach would be doing that in the controller, in this case the RegistrationController#new method. And that’s exactly what we did.

Extending Devise RegistrationsController.rb

The first thing we did was creating a RegistrationsController class that extends Devise’s controller to solve the NoMethodError problem.

RegistrationsController.rb

class RegistrationsController < Devise::RegistrationsController

  def new
    build_resource({})
    self.resource.company = Company.new
    respond_with self.resource
  end

end

As you can see, we just initialize the company attribute inside the index action. Next, we need to setup Devise to use our new controller as opposed to the default one. This work is done in the routes file.

routes.rb

Motonow::Application.routes.draw do

  devise_for :users, controllers: { registrations: "registrations" }

  # other routes

end

Cool. Now, the page seems to load fine.

However, when posting the form not all the parameters will be allowed for mass-assignment. As explained in Devise’s documentation, due to Rails 4 strong parameters plugin we need to permit additional parameters explicitly.

The team decided to do that in the RegistrationsController itself instead of the ApplicationController. By using this approach, we believe that all Devise related changes are encapsulated in one place instead of spread through different files. Here’s the new registrations controller:

RegistrationsController.rb

class RegistrationsController < Devise::RegistrationsController

  def new
    build_resource({})
    self.resource.company = Company.new
    respond_with self.resource
  end

  def create
    super
  end

  private

  def sign_up_params
    allow = [:email, :password, :password_confirmation, :cpf, [company_attributes: [:cnpj]]
    params.require(resource_name).permit(allow)
  end

end

Rails accepts_nested_attributes_for method

Finally, we need to change the User model to use accepts_nested_attributes_for so the Company is saved through its parent in the moment of the registration. That’s easy.

User.rb

class User < ActiveRecord::Base

  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable, :confirmable,
    :recoverable, :rememberable, :trackable, :validatable

  belongs_to :company

  validates :name, presence: true
  validates :cpf, presence: true
  validates :cpf, uniqueness: true

  accepts_nested_attributes_for :company

end

Now, we are good. Everything seems to work fine, including validating and saving both User and Company.

Tests

All the development was done driven by automated tests. I am posting the relevant parts of our tests so you can use it as a guide.

require 'spec_helper'

describe RegistrationsController do
  render_views

  before(:each) do
    @request.env["devise.mapping"] = Devise.mappings[:user]
  end

  context "create" do
    before(:each) do
      @user_params = {
        cpf: "047.696.094-11",
        email: "usermail@gmail.com",
        password: "AbCdEfGh9876",
        password_confirmation: "AbCdEfGh9876",
        company_attributes: {
          cnpj: "10.508.998/0001-79",
        }
      }
    end

    it "should create new user" do
      assert_difference "User.count" do
        post :create, user: @user_params
      end

      user = User.find_by_cpf(@user_params[:cpf])
      expect(user).not_to be_nil
    end

    it "should create new company" do
      assert_difference "Company.count" do
        post :create, user: @user_params
      end

      user = User.find_by_cpf(@user_params[:cpf])
      expect(user.company).not_to be_nil
      expect(user.company.cnpj).to eq(@user_params[:company_attributes][:cnpj])
    end

    context "invalid user attributes" do
      before(:each) do
        @user_params[:email] = ""
      end

      it "should not create user nor company" do
        assert_no_difference "User.count" do
          assert_no_difference "Company.count" do
            post :create, user: @user_params
          end
        end
      end

      it "should display error messages in the view" do
        post :create, user: @user_params
        user = assigns(:user)
        expect(user.errors[:email]).to_not be_empty
        assert_select "div.field_error", html: user.errors[:email].first
      end
    end

    context "invalid company attributes" do
      before(:each) do
        @user_params[:company_attributes][:cnpj] = ""
      end

      it "should not create user nor company" do
        assert_no_difference "User.count" do
          assert_no_difference "Company.count" do
            post :create, user: @user_params
          end
        end
      end

      it "should display error messages in the view" do
        post :create, user: @user_params
        user = assigns(:user)
        expect(user.errors["company.cnpj"]).to_not be_empty
        assert_select "div.field_error", html: user.errors["company.cnpj"].first
      end
    end
  end
end

Conclusion

In this post, I show the approach we took while implementing the authentication solution for Motonow. As we decided to use Devise, we had some challenges to solve. Because we spent a decent amount of time to implement the validation/creation of our models when registering a user, I decided to share the findings with the community. Also, I am pretty sure that there are better ways to solve the same problem, so I am curious the hear your feedback.

Of course, our code is a bit bigger and more complex than I describe in the examples. But I hope they can help you to overcome the challenges you and your company have when using Devise (which is pretty awesome).

Cheers,

2 thoughts on “Extending Devise – Registrations Controller

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s