In a recent project, one of the requirements was to provide different output based on user role. For example, managers (a user role) would be presented with a slightly different UI than operators (another user role). This included things like additional menu items in the layout, additional action links in the view, etc. For the most part this was fairly trivial, I had access to the user object and could use it to make decisions on presentation directly within the view
In some cases, the views would be drastically different depending on user role. To avoid littering my views with logic, I decided to create different role-based templates and then wrote a small method that would determine which view template to render
I could then use this within any controller that required role based view rendering
The above would render manager_show.html.erb
if the user was a manager or operator_show.html.erb
if the user was an operator. So far so good.
Same route, entirely different content
This is where things got tricky. When logging into the system, users needed to be redirected to their personal dashboards via a common /dashboard
path and, depending on their role, would need to see entirely different content. In other words, while the URL to access the dashboard would be the same for all users, the data being loaded and presented would be entirely different and dependant on their user role.
A possible approach would have been to use a single controller with a conditional that determined what data to load and which view template to render
All requests for /dashboard
could then be routed to this single controller
While this would work, the controller would end up fairly fat, especially if I needed to support more roles in the future. What I really wanted was to use multiple controllers and a way to route the /dashboard
path to a controller determined by the user role. Enter Routing Constraints.
Routing constraints are just that - they allow you to define a constraint that the Rails router should use when matching a route. Constraints can be written using Regex or based on any method on the Request object that returns a string. For example
The above constraint would map the /photos
path to the PhotosController
’s index action if the requested url contained the admin subdomain (e.g. http://admin.example.com/photos).
For more complex constraints, you can create a ruby class that responds to matches?
and then pass an initialization of this class as an argument to the :constraints
option when defining your route
This is exactly what I was looking for. I created a new folder under /app
called constraints
and placed the following into a file called manager_route_constraint.rb
The class is very simple, it contains the matches?
method which looks up the current user (from a session cookie) and checks to see whether that user is a manager. If the user IS a manager, the constraint matches and the route is mapped. If the user IS NOT a manager, the constraint doesn’t match and the route is instead mapped to an alternate controller
This worked well for my particular case but I wanted to make it flexible enough to be re-used with other roles in the future. I renamed the file to role_route_constraint.rb
and modified it slightly
The modified class works in a similar fashion as before but instead of just checking for a particular role, it yields to the block passed into the initialize
method. This allows it to be used as a constraint for any role
With that in place, if a manager requested the /dashboard
path they would be routed to the show action of the ManagerDashboardsController
while operators requesting the same path would be routed to the show action of the OperatorDashboardsController
. The specific controller would then load whatever data was required and present it accordingly.
Overall, I’m very happy with this approach. Constraining a route based on user role allows me to map different controllers to a single path and move the deciding logic to the routing layer. Also, because the data being loaded and presented is so different depending on the user requesting it, having it handled by multiple controllers is much cleaner.