20

I have three roles: Instuctor, Student, Admin and each have controllers with a "home" view.

so this works fine,

get "instructor/home", :to => "instructor#home"
get "student/home", :to => "student#home"
get "admin/home", :to => "admin#home"

I want to write a vanity url like below which will route based on the role of the user_id to the correct home page.

get "/:user_id/home", :to => "instructor#home" or "student#home" or "admin#home"

How do I accomplish this?

Arslan Ali
  • 17,418
  • 8
  • 58
  • 76
sidj
  • 525
  • 1
  • 5
  • 12
  • 2
    It may be simpler to have a 'catch-all' controller that redirects a user to the proper controller/action pair depending on what type of user it is. – John Jun 27 '12 at 15:45

2 Answers2

73

I'm providing an alternate approach as this SO question comes up near the top when searching for role based routing in Rails.

I recently needed to implement something similar but wanted to avoid having a large number of conditionals in the controller - this was compounded by the fact that each of my user roles required completely different data to be loaded and presented. I opted to move the deciding logic to the routing layer by using a Routing Constraint.

# app/constraints/role_route_constraint.rb

class RoleRouteConstraint
  def initialize(&block)
    @block = block || lambda { |user| true }
  end

  def matches?(request)
    user = current_user(request)
    user.present? && @block.call(user)
  end

  def current_user(request)
    User.find_by_id(request.session[:user_id])
  end
end

The most important part of the above code is the matches? method which will determine whether or not the route will match. The method is passed the request object which contains various information about the request being made. In my case, I'm looking up the :user_id stored in the session cookie and using that to find the user making the request.

You can then use this constraint when defining your routes.

# config/routes.rb
Rails.application.routes.draw do
  get 'home', to: 'administrators#home', constraints: RoleRouteConstraint.new { |user| user.admin? }
  get 'home', to: 'instructors#home', constraints: RoleRouteConstraint.new { |user| user.instructor? }
  get 'home', to: 'students#home', constraints: RoleRouteConstraint.new { |user| user.student? }  
end

With the above in place, an administrator making a request to /home would be routed the home action of the AdministratorsController, an instructor making a request to /home would be routed to the home action of the InstructorsController, and a student making a request to /home would be routed to the home action of the StudentsController.

More Information

If you're looking for more information, I recently wrote about this approach on my blog.

Bart Jedrocha
  • 11,450
  • 5
  • 43
  • 53
  • 4
    Definitely a better answer than the accepted one. It helped me with a slightly less complex problem, but one that _could not_ have been accomplished via logic in a controller. Thanks! – siannopollo Nov 10 '16 at 21:30
  • Thank you! Having conditional redirects in the routes.rb is much better. – aaandre Jan 04 '18 at 21:29
  • Wonderful answer, I read and learned a lot about constraints! – Francois Feb 19 '18 at 16:06
  • I knew something like this is possible, I hate doing redirects when I have all the data I need. With the request I have the user id, so I should be able to decide without making another request. So thank you this saves me. – Nafaa Boutefer Mar 11 '18 at 21:00
16

You can't do this with routes because the routing system does not have the information required to make this decision. All Rails knows at this point of the request is what the parameters are and does not have access to anything in the database.

What you need is a controller method that can load whatever data is required, presumably the user record, and redirects accordingly using redirect_to.

This is a fairly standard thing to do.

Update:

To perform all of this within a single controller action you will need to split up your logic according to role. An example is:

class HomeController < ApplicationController
  def home
    case
    when @user.student?
      student_home
    when @user.admin?
      admin_home
    when @user.instructor
      instructor_home
    else
      # Unknown user type? Render error or use a default.
    end
  end

protected
  def instructor_home
    # ...
    render(:template => 'instructor_home')
  end

  def student_home
    # ...
    render(:template => 'student_home')
  end

  def admin_home
    # ...
    render(:template => 'admin_home')
  end
end
Art Knipe
  • 99
  • 1
  • 6
tadman
  • 208,517
  • 23
  • 234
  • 262
  • I was thinking of implementing a controller method called "user_redirect" and implement this using redirect. My goal is to display a vanity url like http://example.com/johndoe/home right after they login. Can I accomplish that? – sidj Jun 27 '12 at 16:25
  • Do you want the URL in the browser to be that? If so you need to do something internally to hide it. This usually involves a controller that can render one of many different templates, or a standard template with either conditional sections or conditionally loaded partials. – tadman Jun 27 '12 at 18:32
  • Yes, I want the URL in the browser to be that. Can you please expand on that? Thanks! – sidj Jun 27 '12 at 19:58
  • Using the same URL means the same controller action has to execute, which in turn means that action needs to be able to handle all the different use cases. Remember you can render different templates with `render(:template => '...')` within your controller. See the amended answer. – tadman Jun 28 '12 at 14:15
  • You can't do this =/= I don't know how. – Adam Grant Sep 01 '17 at 21:18