Mar 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.