As a project adds more features, the application_controller.rb file tends to gather more and more code and soon it becomes very big and very messy.
Take a look at this application_controller.rb before the refactoring was done.
As we can see many disjoint entities exist within that file. In the first look itself we can see that it doesn't adhere to the DRY principle.
One method has nothing to do with another method. All the after_action declarations are physically too far from the implementation of the method. For example method set_honeybadger_context is 67 lines apart from the implementation of that method.
Using concerns to keep sanity
Concerns in Rails are like Ruby modules that extend the ActiveSupport::Concern module. Rails controllers come with concerns directory. All modules that reside inside concerns directory are automatically loaded by Rails. It's created by Rails team so that we can put related stuff together as a concern in that directory. So let's try to use it.
Using concerns is another form of keeping code DRY.
Moving functionality into a concern
Let's extract the helper methods for sending back a response for API requests from ApplicationController class and move it inside the api_responders.rb concern.
Let's try the same for authorization related code:
Run the following command to create the api_responders.rb concern:
1touch app/controllers/concerns/api_responders.rb
Now add the following lines of code to the api_responders.rb file:
1module ApiResponders 2 extend ActiveSupport::Concern 3 4 private 5 6 def respond_with_error(message, status = :unprocessable_entity, context = {}) 7 is_exception = message.kind_of?(StandardError) 8 error_message = is_exception ? message.record&.errors_to_sentence : message 9 render status: status, json: { error: error_message }.merge(context) 10 end 11 12 def respond_with_success(message, status = :ok, context = {}) 13 render status: status, json: { notice: message }.merge(context) 14 end 15 16 def respond_with_json(json = {}, status = :ok) 17 render status: status, json: json 18 end 19end
Similarly let us extract the exception rescuing and its corresponding handler methods out of the ApplicationController to the api_rescuable.rb concern.
Run the following command to create the api_rescuable.rb concern:
1touch app/controllers/concerns/api_rescuable.rb
Now add the following lines of code to the api_rescuable.rb file:
1module ApiRescuable 2 extend ActiveSupport::Concern 3 4 included do 5 rescue_from ActiveRecord::RecordNotFound, with: :handle_record_not_found 6 rescue_from ActiveRecord::RecordInvalid, with: :handle_validation_error 7 rescue_from ActiveRecord::RecordNotUnique, with: :handle_record_not_unique 8 rescue_from ActionController::ParameterMissing, with: :handle_api_error 9 rescue_from Pundit::NotAuthorizedError, with: :handle_authorization_error 10 end 11 12 private 13 14 def handle_validation_error(exception) 15 respond_with_error(exception) 16 end 17 18 def handle_record_not_found(exception) 19 respond_with_error(exception.message, :not_found) 20 end 21 22 def handle_record_not_unique(exception) 23 respond_with_error(exception) 24 end 25 26 def handle_api_error(exception) 27 respond_with_error(exception, :internal_server_error) 28 end 29 30 def handle_authorization_error 31 respond_with_error(t("authorization.denied"), :forbidden) 32 end 33end
There are a couple of key points to note here:
-
Helper methods declared inside the ApiResponders module are used inside the ApiRescuable module. We can say that the ApiRescuable module is dependent on ApiResponders module but we do not need to include ApiResponders inside ApiRescuable.
We can do so because, since ActiveSupport::Concern dependencies are gracefully resolved.
-
We have used an included block inside the ApiRescuable module. The included block is available because we are extending the ActiveSupport::Concern module.
When a class includes a module, the code present inside the included block will be executed within the scope of the including class.
In the above code, when a class includes the ApiRescuable class, the rescue_from callbacks will be executed within the scope of that class. Whenever an exception occurs inside that class or it's child class, the rescue_from callback will be called.
The included block usually contains code like callbacks and macros.
Let us now move the authentication functionality out of the ApplicationController to the authenticable.rb concern.
Run the following command to create the authenticable.rb concern:
1touch app/controllers/concerns/authenticable.rb
Open app/controllers/concerns/authenticable.rb and add following code:
1module Authenticable 2 extend ActiveSupport::Concern 3 4 included do 5 before_action :authenticate_user_using_x_auth_token 6 end 7 8 private 9 10 def authenticate_user_using_x_auth_token 11 user_email = request.headers["X-Auth-Email"].presence 12 auth_token = request.headers["X-Auth-Token"].to_s 13 user = user_email && User.find_by!(email: user_email) 14 is_valid_token = auth_token && ActiveSupport::SecurityUtils.secure_compare(user.authentication_token, auth_token) 15 if is_valid_token 16 @current_user = user 17 else 18 respond_with_error(t("session.could_not_auth"), :unauthorized) 19 end 20 end
Let's also modify our TasksController to invoke verify_authorized and verify_policy_scoped methods after certain specific actions:
1class TasksController < ApplicationController 2 after_action :verify_authorized, except: :index 3 after_action :verify_policy_scoped, only: :index 4 #------previous code ------- 5 end
verify_authorized raises an error if pundit authorization has not been performed in specified actions. That is why we invoke it as an after_action hook. It is used to prevent the programmer from forgetting to call authorize from specified action methods.
Like verify_authorized, Pundit also adds verify_policy_scoped to our controller. It tracks and makes sure that policy_scope is used in the specified actions. This is mostly useful for controller actions like index which find collections with a scope and don't authorize individual instances.
Sanitized version
In order to make the application_controller even thinner and neater, we just need to include the necessary concerns, rather than defining the functionality with the controller.
For example, in a fully fledged application, once all the code is moved to concerns, then the application_controller.rb would look something like this (no need to add the following changes):
1class ApplicationController < ActionController::Base 2 include Authenticable 3 include Authorizable 4 include ApiException 5 include SetHoneybadgerContext 6 include RedirectHttpToHttps 7 include EnsureTermsOfServiceIsAccepted 8 include EnsureUserOnboarded 9 include DataLoader 10end
Now let's modify our current application_controller and include the concerns we created in the previous section. Fully replace the contents of application_controller.rb file like so:
1class ApplicationController < ActionController::Base 2 include ApiResponders 3 include ApiRescuable 4 include Authenticable 5 include Pundit 6 7 private 8 9 def current_user 10 @current_user 11 end 12end
The @current_user is assigned when the authenticate_user_using_x_auth_token inside the Authenticable concern is invoked when the server receives an API request.
By convention, we prefer to name concerns with able suffix. It suggests the addition of ability.
An important point to keep in mind is that we need to use concerns only when the logic has to be shared within multiple controllers or related files.
If the logic is only specific to a controller, then we can either write it in the private section or move it inside a helper.
Now let's commit these changes, where we moved pundit related functionalities into Authorizable concern:
1git add -A 2git commit -m "Moved functionality from application controller to concerns"