Search code examples
ruby-on-railsdeviseruby-on-rails-5pundit

Rails, Devise, Pundit - authorise Profile created from Devise registration controller


Feel free to say if you think something is wrong. I extended Devise Registration controller to create a Profile object to every new user:

class Users::RegistrationsController < Devise::RegistrationsController

  def new
    resource = build_resource({})
    resource.profile = Profile.new
    resource.profile.user_id = @user.id
    respond_with resource 
  end

They both are has_one - has_one related and in database:

create_table :profiles do |t|
  t.belongs_to :user, index: { unique: true }, foreign_key: true
end

So to get the right profile of current user, I must:

private 
  def set_profile
    @profile = Profile.where(user_id: current_user.id).first 
  end

And this kinda solves the problem - seems other users cant go around this query and access other profiles (or CAN THEY?), but for other resources I use Pundit to control authorisation, so now it feels a bit messy.

So thats one concern. Other - I still don't know how to act when there is no user logged, because if visiting any restricted resource, this:

private
 def set_some_resource
 end
end

Throws - "undefined method `id' for nil:NilClass) - how is best to avoid this?

Thanks for any advices.


Solution

  • You may want to start by reading the Rails guides on assocations.

    To create a one to one association you use belongs_to on the side with the foreign key column and has_one on the other.

    class User
      has_one :profile
    end
    
    class Profile
      belongs_to :user
    end
    

    ActiveRecord then automatically links the records together. In general you should avoid setting ids (or getting associated records by ids) explicitly and instead use the assocations:

    class Users::RegistrationsController < Devise::RegistrationsController
      # ...
      def new
        # calls Devise::RegistrationsController#new 
        super do |user|
          user.profile.new
        end
      end
    end
    

    Devise is pretty nifty and lets you pass a block to tap into the flow instead of copypasting the whole action.

    Simularily you would fetch the current users profile with:

    private 
      def set_profile
        @profile = current_user.profile
      end
    

    You can set if the callback should be called by using the if: option.

    before_action :set_profile, if: :user_signed_in?
    

    But if the action requires authentication you should make sure that it is after :authenticate_user! anyways which will halt the filter chain.

    And this kinda solves the problem - seems other users cant go around this query and access other profiles (or CAN THEY?), but for other resources I use Pundit to control authorisation, so now it feels a bit messy.

    You don't need to use Pundit to authorize creating a profile or fetching the current users profile. Since the profile is fetched via the user the is no way for another user to access it (well without hacking).

    what you might want to authorize is the show, index, edit etc actions if you create a ProfilesController.