DRY Controllers and Helpers using Forwardable

I don't know about you, but I've found myself wanting the same methods available in both a controller and a view. In my case, I had an 'authorized?' method for determining if a user is logged in.

Initially, I had two implementations, one in ApplicationController, and a different one in ApplicationHelper. I was young, and naive.

After starting some BDD with test/spec and mocha, I ran into problems because they were different. It was past time to be more DRY.

So let's think about this. As it turns out, 'controller' is a method available to your views, so, if you have 'something' in your controller, you should be able to hit it in your view by going 'controller.something'. This would give you something like:

<% if controller.authorized? %>
  Aieeeeeeeeee
<% end %>

I don't really like how that reads though. If only I could get it to read like I originally had when there were two implementations

<% if authorized? %>
  Aieeeeeeeeee
<% end %>

I then remembered reading long ago that you Ruby has built in constructs for doing delegation. But alas, that was ages ago and my memory was blank. After racking my mind, jumping up and down, and praying the seach engine deities, I found it: Forwardable

My intent is to delegate authorized? to the controller. Here's what the ApplicationHelper ends up looking like:

module ApplicationHelper
  extend Forwardable
  def_delegator :controller, :authorized?

  ...
end

Simple and awesome. I can dig that.

Authentication and authorization: Remembering were you came from

Most webapps need authorization and authentication of some sort, right?

If you happen to have a copy of Agile Web Development with Rails, then you are fortunate, as it covers adding support for that very thing.

It is a bit simplistic though. Books have to have a finite length, and so some not everything can be covered in super depth.

I followed their recipe for authentication on my blog, and my annoyance was that if you request an authorized action, you get prompted to login, but after logging in, you get sent to a single page you set in code, NOT the resource you were initially requesting.

Fortunately, this is pretty simple to address.

So if you were following things in the book, in ApplicationController, you have something like:

def authorize
  unless User.find_by_id(session[:user_id])
    flash[:notice] = "Please log in"
    redirect_to(:controller => "login" , :action => "login" )
  end
end

So the trick here is to record what was being requested in the session. The URI being requested is available from request's request_uri method. The updated authorize looks like:

def authorize
  unless User.find_by_id(session[:user_id])
    flash[:notice] = "Please log in"
    session[:request_uri] = request.request_uri
    redirect_to(:controller => "login" , :action => "login" )
  end
end

We remember what the URI requested was, and get sent to the login page in UserController. The trick is to do something useful with it in the login action. Initially, login will look like:

def login
  session[:user_id] = nil
  if request.post?
    user = User.authenticate(params[:name], params[:password])
    if user
      session[:user_id] = user.id
      redirect_to(:action => "index" )
    else
      flash[:notice] = "Invalid user/password combination"
    end
  end
end

If the request URI is stashed on the session, we want to redirect it and reset the value, otherwise, just redirect to the default page we had before. Here's the resulting code:

def login
  session[:user_id] = nil
  if request.post?
    user = User.authenticate(params[:name], params[:password])
    if user
      session[:user_id] = user.id
      if session[:request_uri]
        redirect_to(session[:request_uri])
        session[:request_uri] = nil
      else
        redirect_to(:action => "index" )
      end
    else
      flash[:notice] = "Invalid user/password combination"
    end
  end
end

Pretty straightforward, right?