What's the best way to handle logging in and out with multiples scopes/roles with Rails/Devise?
I've been searching through the Devise RDocs, Google, on this site for answers to my question, but no luck.
Let's say I have four Devise scopes/roles, and each one has their own attributes, login page, and separate web flows:
- Students
- Professors
- Deans
- Faculties
All of these use the User class and have the following attribues in common.
- Id
- Name
- Password
- Role
Here is an example of the routes I have established to set this up:
devise_for :students, :class_name => 'User'
devise_for :professors, :class_name => 'User'
devise_for :deans, :class_name => 'User'
devise_for :faculties, :class_name => 'User'
devise_for :users
Then I generated the devise scoped views and played around with those.
After that I had to add some code in my application controller to override Devise::RegistrationsController which wanted to route everything to the root path:
def after_sign_in_path_for(resource)
user_role = resource.role
case user_role
when "professor"
professors_url
when "faculty"
faculties_url
when "dean"
deans_url
when "student"
students_url
else
root_path
end
end
def after_sign_out_path_for(resource)
case resource
when :faculty
new_faculty_session_path
when :professor
new_professor_session_path
when :dean
new_dean_session_path
when :student
new_student_session_path
else
root_path
end
end
I have access to excellent helpers such as signed_in? which tells me if any user in any one of the scopes mentioned above is logged in. Great!!! Now I need similar functionality for current_user.
I have access to the following helpers.
- current_student
- current_professor
- current_dean
- current_faculty
They work perfectly, but here is where I have issues. Let's say if I have a view that shares all of these scopes. Now if I try current_student on that view and I'm logged in as a professor it won't work.
For example: I have a partial that I want to include on every page to allow users to logout if they're logged in. This is how I went about it for a student. Works just fine.
<% if student_signed_in? %>
<div style="float: right;">Welcome <%= current_student.name %></div>
<div>
<%= link_to('Logout', destroy_student_session_path, :method => :delete) %>
</div>
<% end %>
What I want to do is something like this which will provide the ability to logout all my scopes/resources regardless if I'm logged in as a student, dean, professor, or faculty:
<% if signed_in? %>
<div style="float: right;">Welcome <%= current_resource.name %></div>
<div>
<%= link_to('Logout', destroy_resource_session_path, :method => :delete) %>
开发者_如何学C</div>
<% end %>
My next step would have been to add my own helper methods to determine the scope like so:
def current_resource
current_professor unless current_professor.nil?
current_student unless current_student.nil?
current_dean unless current_dean.nil?
current_faculty unless current_faculty.nil?
end
def destroy_resource_session_path
destroy_professor_session_path unless current_professor.nil?
destroy_student_session_path unless current_student.nil?
destroy_dean_session_path unless current_dean.nil?
destroy_faculty unless current_faculty.nil?
end
If this approach will work, I'd do it, but it seems highly inefficient and tedious if I decide to add more roles or common functionality in the future...
There has to be a better way? This makes me feel like I'm using Devise incorrectly, or am just missing something somewhere.
After much more research I found a Gem called CanTango that does exactly what I was looking for. It works with Devise and handles multiples User model types. Take a look and see.
https://github.com/kristianmandrup/cantango/wiki/Cantango-with-devise-accounts
Thanks for answering my questions guys. I learned a lot from your answers.
Have you checked out CanCan from Ryan bates? It allows you to use just the Devise user model, and define abilities based on role in your user model. It then gives you access to helper methods can?
and cannot?
to further define your abilities.
EDIT:
I would move the attributes to referenced models, and direct the web flow with CanCan. The examples that you see around the web don't always show the full capabilities that CanCan will give you. You could put links on your landing page to direct them to their appropriate login page which each have the same Devise sign in form. Keep you logic for the after_sign_in_path_for
, and lock down restricted actions with CanCan. This simplifies your routes and the overall application logic and design. Plus it makes troubleshooting Devise errors 75% easier with only one model.
Based on the comments to the above answer, an alternative is to use multiple rails applications (one for each role), and either share the same DB across them (see this question on SO), or make another application solely for authentication and authorization, and manage all sign ins through that.
The above solutions are really only a fallback if you don't want to use one app though. I'd recommend simply to keep doing what you're doing right now, determine the scopes and make methods that can deal with each case.
You can do this a little better with well designed methods. For example, modify destroy_resource_session_path
to take in an argument, current_user
, which is assigned to whichever of current_professor, ... current_faculty
is not nil, and then call destroy_#{current_user}_session_path
. To further elaborate on how to do this:
You don't need exactly need well-defined roles in your application, more of just routing each role to a different set of models/views/controllers , so the best way is to make a separate controller that determines the role, and routes requests to the appropriate controller based on the users role.
EDIT:
In general, I think you're going about it the right way, but you can make it better if you copy the methods that deal with different users (like the 2 you have) into a separate controller with actions like destroy_resource_session_path
, and current_resource
. Similarly, add the after_sign_in_path_for
and after_sign_out_path_for
methods to that new controller
The point is that the new controller should deal with differentiation between types of users, so adding a new type of user is as simple as adding a few lines to that controller only, and modifying routes.rb
.
Just store a set of the roles you have in that controller. Then, you can modify your methods to use the set to figure out which roles exist, so all you have to do is add an entry to the set and an entry to routes.rb
to add a new role. For example, you would modify after_sign_in_path_for(resource)
to:
def after_sign_in_path_for(resource)
user_role = resource.role
if (set_of_roles.include?(user_role))
return #{user_role}_url
else
return root_path
end
end
Note that #{..}
is how you interpolate strings in ruby, so #{user_role}_url
in ruby is equivalent to user_role+"_url"
in Java.
With methods like that, the only thing you have to do to add a new role is to create a new view for that role, define a route called role_name
in routes.rb
, and add the role to set_of_roles
.
I'd recommend researching the single sign-in idea before using it - I'm not familiar with it, but the answers on question I linked to on SO say it overcomplicates things.
精彩评论