11

I use the Rails Stack with

  • devise
  • warden
  • confirmable

Now I have a certain requirement related to email confirmation and access provision to unverified users. Let's say there are 3 categories of pages:

  1. case 1 - requires no authentication.
  2. case 2 - requires authentication and also require the user to be confirmed.
  3. case 3 - requires authentication (correct username and password combination) but user need not be confirmed to access them.

With devise and confirmable, implementing case 1 and case 2 is a breeze. Once the user does login/signup, I redirect to "confirm your email page".

My problem is with case 3. Suppose the user does his login/signup. He is yet to confirm his email address but should be allowed to visit certain case 3 routes. When he visits a case 3 routes, I expect:

  • no redirection
  • valid session

Devise with confirmable either allows all the pages to be visited by confirmed users or none. It does not allow access to certain pages with authentication but without confirmation.

I tried overriding the devise confirmed? by implementing this logic:

class ApplicationController < ActionController::Base
   before_filter :verify_user
   def verify_user
       $flag = true if CERTAIN_ROUTES.include?(action_class)
   end
end

class User < ActiveRecord::Base
   def confirmed?
      $flag || !!confirmed_at
   end
end

This barely works for sign in but not for sign up. Also, this is an extremely bad way to achieve it. How should I approach this problem? Other functionalities work fine.

kawadhiya21
  • 2,458
  • 21
  • 34

2 Answers2

9

Instead of overwriting confirmed? you could just overwrite the confirmation_required? model method (docs):

# user.rb
class User < ActiveRecord::Base
  protected

  def confirmation_required?
    false
  end
end

After that you can handle the confirmation logic yourself, by redirecting all unconfirmed users in a before_action when the controllers require confirmation, or you can pull this into your authorization logic (e.g. with pundit).

class ApplicationController < ActionController::Base
   def needs_confirmation
     redirect_to root_path unless current_user.confirmed?
   end
end

class SomeController < ApplicationController
  before_action :needs_confirmation
end
smallbutton
  • 3,377
  • 15
  • 27
  • Thanks a lot for this answer. But here is the issue. I may have 50 routes, among which I need 4 routes to be visited with authentication but without confirmation. I would have to write `before_action :needs_confirmation` in every controller and make sure any other dev who writes a new controller adds this filter. Can you suggest any other neat solution? – kawadhiya21 Aug 09 '18 at 12:48
  • 2
    then just put the ´before_action´ in the `ApplicationController` and use `skip_before_action :needs_confirmation` in the controller where you want to have unconfirmed access. https://apidock.com/rails/v4.2.7/AbstractController/Callbacks/ClassMethods/skip_before_action – smallbutton Aug 09 '18 at 13:00
  • This looks like a better solution. Let me try it out and come back to you on this. Will need 24 hours. – kawadhiya21 Aug 09 '18 at 13:13
  • Thanks a ton. This solution works out for me quite well. – kawadhiya21 Aug 16 '18 at 08:45
3

You should take a look at the gem 'pundit' - it works well with devise.

https://github.com/varvet/pundit

Rather than writing controller before_actions etc, you write policies which will cover each of your authorization requirements, and then use those policies inside your controllers.

For example, in a controller:

class ExampleController < ApplicationController
    before_action { authorize :example }

    def case_one
       # action
    end

    def case_two
       # action
    end

    def case_three
       # action
    end
end

Then your policy would be kept under app/policies/example_policy.rb

class ExamplePolicy < ApplicationPolicy
  attr_reader :user

  def initialize(user, _)
    @user = user
  end

  def case_one?
    true
  end

  def case_two?
    user.present? && user.confirmed_at.present?
  end

  def case_three?
    user.present?
  end
end

It works really well, especially in other cases where you are determining authorization against a type of resource.

Tim Krins
  • 3,134
  • 1
  • 23
  • 25
  • How would I redirect the user to a confirmation page (where they can request a new confirmation email) if confirmed_at is not present? –  Sep 04 '19 at 22:06
  • You can rescue the `Pundit::NotAuthorizedError` and then redirect the user to your confirmation page. See https://github.com/varvet/pundit#rescuing-a-denied-authorization-in-rails for details – Tim Krins Sep 11 '19 at 16:39
  • Ok, but what if I have 50 controllers and I want to give access to only 3 of them to users who do not meet my authorization requirements? I would have to place authorizations in each of the 47 controllers and in each newly created one (assuming that the number of controllers available to all doesn't changed). Am I right or I missed something? – mr_muscle Apr 15 '21 at 01:34
  • mr_muscle - no, you wouldn't do that. You would either change your `ApplicationController` to implement the authorization `before_action` and then skip that before action in controllers where you don't want it ran. This is just one way of doing it, depending on your app you may want to create an `AuthenticatedApplicationController` to inherit from etc. – Tim Krins Apr 16 '21 at 13:32