Skip to content

How To: Create a guest user

Garrett George edited this page Mar 17, 2021 · 38 revisions

In some applications, it's useful to have a guest User object to pass around even before the (human) user has registered or logged in. Normally, you want this guest user to persist as long as the browser session persists.

One option is to use the Rails engine for drop in capability: the devise-guests gem, which is based on the article below.

Our approach is to create a guest user object in the database and store its id in session[:guest_user_id]. When (and if) the user registers or logs in, we delete the guest user and clear the session variable. A helper function, current_or_guest_user, returns guest_user if the user is not logged in and current_user if the user is logged in.

# app/controllers/application_controller.rb

class ApplicationController < ActionController::Base

  protect_from_forgery

  # if user is logged in, return current_user, else return guest_user
  def current_or_guest_user
    if current_user
      if session[:guest_user_id] && session[:guest_user_id] != current_user.id
        logging_in
        # reload guest_user to prevent caching problems before destruction
        guest_user(with_retry = false).try(:reload).try(:destroy)
        session[:guest_user_id] = nil
      end
      current_user
    else
      guest_user
    end
  end

  # find guest_user object associated with the current session,
  # creating one as needed
  def guest_user(with_retry = true)
    # Cache the value the first time it's gotten.
    @cached_guest_user ||= User.find(session[:guest_user_id] ||= create_guest_user.id)

  rescue ActiveRecord::RecordNotFound # if session[:guest_user_id] invalid
     session[:guest_user_id] = nil
     guest_user if with_retry
  end

  private

  # called (once) when the user logs in, insert any code your application needs
  # to hand off from guest_user to current_user.
  def logging_in
    # For example:
    # guest_comments = guest_user.comments.all
    # guest_comments.each do |comment|
      # comment.user_id = current_user.id
      # comment.save!
    # end
  end

  def create_guest_user
    u = User.new(name: "guest", email: "guest_#{Time.now.to_i}#{rand(100)}@example.com")
    u.save!(validate: false)
    session[:guest_user_id] = u.id
    u
  end

end

If you use this logging_in function and write the code it needs, you must customize the create action in both the sessions controller and the registrations controller by calling current_or_guest_user after super.

def create
  super
  current_or_guest_user
end

In this way, the handoff code you write in your logging_in function will be called to transfer guest generated content to the new or returning user.

Finally in order to fix the problem with ajax requests you have to turn off protect_from_forgery for the controller action with the ajax request:

skip_before_filter :verify_authenticity_token, only: [:name_of_your_action] 

Another option is to remove protect_from_forgery from application_controller.rb and put in each of your controllers and use :except on the ajax ones:

protect_from_forgery only: :receive_guest

Last but not least, don't forget to add helper_method :current_or_guest_user to the controller to make the method accessible in views.

Authentication (this may interfere with the current_user helper)

If you wish to continue using before_filter :authenticate_user! and have it apply to the guest user, you will need to add a new Warden strategy. For example:

In initializers/some_initializer.rb:

Warden::Strategies.add(:guest_user) do
  def valid?
    session[:guest_user_id].present?
  end

  def authenticate!
    u = User.where(id: session[:guest_user_id]).first
    success!(u) if u.present?
  end
end

And in initializers/devise.rb:

Devise.setup do |config|
  # ...
  config.warden do |manager|
    manager.default_strategies(scope: :user).unshift :guest_user
  end
end

Wait a sec... manager.default_strategies(scope: :user).unshift :guest_user makes the :guest_user strategy the first/zeroth element of the default_strategies array, which means that the :guest_user strategy is the first strategy to be run by Warden. Doesn't this mean that once session[:guest_user_id] is set, the guest_user will be the only thing authenticated, even on successful email/password login? Shouldn't the :guest_user strategy be the last Warden strategy attempted?

Testing

Follow this instructions: How To: Test controllers with Rails 3 and 4 (and RSpec). Add login_guest to ControllerMacros:

module ControllerMacros
  # ...
  def login_guest(guest = false)
    guest ||= FactoryBot.create(:guest_user)
    @request.session[:guest_user_id] = guest.id
  end
end

Add :guest_user to your users.rb factory:

FactoryBot.define do
  factory :user do
    ...
  end

  factory :guest_user, class: 'User' do
    sequence(:email)  { Faker::Internet.email }
    role              :guest
    to_create         { |instance| instance.save(validate: false) }
  end
end

Usage:

describe PagesController, type: :controller do
  context '#index' do
    before do
      login_guest
      get :index
    end

    it { expect(response).to have_http_status(:success) }
    it { expect(response).to render_template('pages/index') }
  end
end
Clone this wiki locally