Eric / Brooklyn

Visual

Blog Index

Random 5

Stripe and Tap

Multi-tab Websockets

Ruby vs. Ruby

Cookies

Web Accounts

Emoji Bits

Honeypot Bots

Bad Content

JS Data Chutzpah

Sendgrid Ban

Clean URLs

Git

Concerns

Rails UJS

Extending Devise

ENV Variables

See All

Extending Devise Controllers

March 2020

Account management on a Rails app

If you’re building a Rails app, chances are you’re using Devise for authentication and account management. With all this convenience, we lose a bit of control.

What have we lost?

Our Controllers — specifically our UsersController (assuming we’re using Devise to handle our User Model.)

We’ve outsourced the UsersController to Devise, so we don’t handle any of the usual Rails RESTful routing. So for example, if we wanted to add something to the create action of the UsersController, we can’t.

Note that Devise doesn’t have a UsersController, instead they use Devise::RegistrationsController and Devise::SessionsController.

What would we want to add to Devise’s Controllers?

Maybe we want to send a Welcome email after a user creates an account. Yes, this could be completed using an after_create in the User Model (which we have direct access to), but we’d rather not, for what seems like a slightly nitpicky detail.

This detail did get me banned from SendGrid though, for better or worse, so maybe it’s worth paying attention to. The full story discussed elsewhere on the blog, but the short version is that we don’t want to send Welcome emails when doing admin things like seeding the database (for dev/staging), or creating test users in the Console.

And the after_create will respond to a User.create when seeding, but an after_action for the create action in the model won’t respond to the seeding. The controller only listens when a person goes to a route and creates an account.

Why else might we want to extend Devise’s Controllers?

Maybe we want to remember some activity from a non-user when she becomes a user.

An example would be if a logged-out/non-user user tries to follow another user — we want to remember this and do something about it when the user logs in/creates an account.

Could we do this somewhere in the Model? Not easily. We handle this UX behavior using cookies and the Model doesn’t like them, because they’re unrelated to CRUD’ing a resource. The Controller likes cookies, because it handles communicating with browsers (where the cookies live).

Extending Controllers

As mentioned before, we don’t have access to the devise controllers, for good reason, because we’d probably break something.

But we can extend them.

class XYZ < class ABC

This is how a class in Ruby can inherit stuff from another class.

More specifically, our new RegistrationsController looks like:

class Users::RegistrationsController < Devise::RegistrationsController
  after_action :do_something, only: :create

  private
  
  def do_something
	  # do something after the create action of the
	  # Devise::RegistrationsController is run
  end

end

Note that the class name is Users::RegistrationsController, because we have placed the registrations_controller.rb file inside controllers/users . We do this for clarity, as we also have a sessions_controller.rb in there as well.

Routes

We’ll also have to tell Devise about our new controllers. In our routes.rb:

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

Problem arises

Because we’re using an after_action on a controller action, we run do_something regardless of what happens in the create action.

Since we’re looking at the create action of a RegistrationsController, we only want to do something if the user has successfully registered.

Currently, if registration fails, do_something still runs.

Note that this is different than what would happen for an after_create callback on a model object (where the callback only fires if the create is successful).

Guard Cases

To be safe, let’s introduce a guard case so we don’t respond to failed registrations!

class Users::RegistrationsController < Devise::RegistrationsController
  after_action :do_something, only: :create

  private
  
  def do_something
	  return unless user_signed_in?
		
		# user_signed_in? is a Devise helper	  
	  # we only do something if the user successfully registers
  end

end

Conclusion

That’s all.