haxx populi

Rails logged_in_user idiom

by jja on
Topics:

I recently refactored another developer's code to clean up some massive amounts of database pings to retrieve the current user object. The main problem was in the use of the logged_in_user or current_user idiom in Rails. Several places give good examples but leave out the details that were bugging my project.

user.id

Firstly, don't store the user object in the session! Just store the user object's ID. In LoginController#login, use:

        session[:user_id] = user.id

(full method below).

caching helper_method

Rather than a before filter on ApplicationController, I used a helper method logged_in_user:

class ApplicationController < ActionController::Base
  helper_method :logged_in_user
  private
  def logged_in_user
    return (@logged_in_user=nil) if session[:user_id].nil?
    @logged_in_user ||= User.find_by_id(session[:user_id])
  end
  def reset_logged_in_user
    @logged_in_user=nil
  end
#...
end

The method returns the instance variable of the same name (@logged_in_user). If the variable is not already set, only then does the method use find_by_id to load the object from the database (saving an extra database ping over other examples). An instance variable is scoped to the instance of ApplicationController, i.e. it exists for the duration of the web request. Thus it acts as a cache during the request. We can then call logged_in_user from our views without repeatedly accessing the database:

<% if logged_in_user %>

and we can access any User methods like this:

<% if logged_in_user and logged_in_user.may_view_all? %>

security

For security, be sure your LoginController resets both the user ID in the session and the logged_in_user instance variable:

class LoginController < ApplicationController
#...
  def login
    session[:user_id] = nil
    reset_logged_in_user
    if request.post?
      user = User.authenticate(params[:username], params[:password])
      if user
        session[:user_id] = user.id
        redirect_to(:action => "index")
      else
        flash[:notice] = "Invalid user/password combination"
        params[:password]=nil
      end
    end
  end
  def logout
    session[:user_id] = nil
    reset_logged_in_user
    reset_session
    flash[:notice] = "Logged out"
    redirect_to(:action => "login")
  end
end

alternatives

Here are two other ways of writing logged_in_user that show the logic a little more clearly, if more verbose:

def logged_in_user
    @logged_in_user = User.find_by_id(session[:user_id]) if
      @logged_in_user.nil? and session[:user_id]
    @logged_in_user
end

def logged_in_user
    if session[:user_id].nil?
      @logged_in_user=nil
    elsif @logged_in_user.nil?
      @logged_in_user = User.find_by_id(session[:user_id])
    end
    @logged_in_user
end

ApplicationHelper

Final tip: make sure you don't put a logged_in_user helper method on ApplicationHelper--- that's how our app was pinging the database for the user object tens of times per page view as this method was getting used instead of ApplicationController's!

(comments are closed)