Routing for Multiple User Roles - ruby-on-rails

I have a user model that differentiates into 6 roles, and I am defining different variables and directing them to different view files (similar format, but different tables of information), but within the same controller action, because they all have similar pages "overview", "profile", "messages", etc.
Now the controller is really messy, and has multiple if/else statements. I believe I should be changing the routes so that each user has its own controller, eliminating the use of the if/else monstrosity that currently invades the controller.
def index
if current_user.admin?
....
end
if current_user.moderator?
....
end
end
Question: How do I perform the routing such that url will be
www.website.com/1/schedule, where 1 = current_user.id, while having different view files rendered from the different controllers?
I am thinking of doing a AdminController and a ModeratorController to handle this, but am not sure how to do the routing, or if indeed this is the best way to do it. Thanks for advice in advance!

I think you are doing it the wrong way , If you have similar pages for different role then I think you are accessing same model for handling different roles.
If this is the case then you should use Gem like Cancan (authorization library for Ruby on Rails).

To address your second concern, I don't think this would be the best approach. Try this - move your logic away from multiple controllers, and keep the logic in the ApplicationController. I do not think that you should separate the different roles to controllers. Instead, look to your User model and put in a method that checks the privilege level for the different users. You could create methods in your User model that you could call in your controllers to see if the user is allowed access to the action. before_action would be recommended here.
If you decide to keep multiple controllers, I recommend a gem like Authority. Makes it much easier to keep track of different privileges, even across different controllers - I think routing is addressed as well: https://github.com/nathanl/authority

Related

Rails 5 Multiple Devise Models using same controllers - architecture

I currently have a rails 5 app that has 3 Devise Models and hence 3 separate logins. This is working well.
The challenge I am having is that all three models share the majority of the same resources, however with different authorisation levels. For example, one Devise model may be allowed new and create actions but not destroy etc etc.
Currently, I have controllers namespaced within each Devise model in controllers. So, for example, controllers > admins > same_resource_controller.rb and controllers > seats > same_resource_controller.rb. These files share a lot of the same code and it's creating unnecessary maintenance that I don't feel should be there.
So, If I was to use the controllers > same_resource_controller.rb at a top level I can foresee some fundamental issues:
Authentication, can I put a authenticate_user! in the controller that reads from application_controller:
def authenticate_user
authenticate_admin! || authenticate_seat! || authenticate_client!
end
current_user, is it ok to have, and use, a method like this in the application_controller.rb?
def current_user
current_admin || current_seat || current_client
end
The resulting potential issue is associating records, for example when querying, current_user.another_model.yet_another_model is not the same for and Admin as it is for a Seat.
I really like the idea of cutting down my controllers and tests and associated files by a third but I'm really unsure as to the correct architecture.
If anyone has been through the same or has any good links (I have struggled to find articles on dealing with controllers when multiple Devise models) I would really appreciate some help! Thank you!
Do they all care the same underlying table? At first sight, my question would be why not use a single resource, perhaps with type attribute if you need to flag the client, seat or admin. Are you using CanCan, Pundit, or another community-supported authorization framework?

Using 1 Controller in 2 layout

I have a logic question and I cannot figure out how to do it. First of all, I am working on an social networking site, and I completed the site in pure PHP, but now I am re-writing the backend in rails.
My questions is, I generated UsersController, it has new, create, show, edit, update, delete, and destroy.
I thought I could use "new" to display sign up page, "create" to process sign up, "show" to display profile page, "edit" to display account settings and "update" to process edit.
I might have a logic problem here, maybe I should put "new" and "create" in a signup controller. This where I get confused. The problem with the first logic I said, I have 2 layouts, one of them is for before login, and the other one is for after login. (You can imagine Facebook's header before login, and after login).
So, when I have 2 different layout, I cannot use 1 controller in 2 layout. Because sign up page has "before login header design" and account settings and profile has "after login header design". And as you can guess I define layout in controller.
I don't know if I explained well. Thank you.
By default, Rails will look-up a layout with the same name as the controller, or else application.html.erb. But you can also specify one controller-wide (which won't help you, but bear with me)
class SomethingController
layout "some_name"
...
That's still layout-wide, so not what you need.
But you can also specify a specific layout on each call to render in an action:
def edit
#some logic
render "some_template", :layout => "some_layout"
end
Or, to take the default template lookup, but still specify a layout:
def edit
# some logic
render :layout => "some_layout"
end
There's another way you can specify layouts too, which might be especially appropriate for the use case of "one layout if not logged in, another if logged in":
class SomeController
layout :method_name_to_determine_layout
# ... actions ...
protected
def method_name_to_determine_layout
if current_user
"logged_in_layout_name"
else
"not_logged_in_layout_name"
end
end
You can learn more about the different ways to specify layouts in the Layouts and Rendering Rails Guide
Hope this helps.
Rails has basic CRUD default actions. Additionally each action can have different processing depending on the HTTP verb. You can also add custom actions & routes.
It is best to follow standard Rails practices for each default action. For example, "new" action should route to the form to create a new user when accessed via GET. An HTTP POST to the form should route to the "create" action.
If you need to add an additional controller action, do so with a custom method. Again, I stress, simple CRUD actions should follow normal Rails conventions.
Read more about routing
Read this guide many times to understand simple CRUD actions in Rails
Instead of using 1 controller in 2 layouts, I decided to use separate controllers. So, I have profile_controller, which has "edit" and "update" for account settings and "show" to display profile. And I also users_controller, which has followings: login, login_attempt, signup, signup_attempt, etc..
So, I am not putting signup and edit together in 1 controller, instead using 2 different controllers is much better and clean, I guess.
Sounds like you're trying to roll your own authentication.
I'd recommend using Devise... great tutorial here:
The reason for this is two-fold.
Firstly, Devise gives you the ability to split your app between authenticated and non-authenticated users. Namely, it provides the user_signed_in?, devise_controller? and current_user helpers to aid with this.
This might not appear like a big deal, but it will actually help you with your layouts (I'll describe more in a second).
Secondly, Devise is pre-rolled. Your questions about how to handle signups and registrations have already been solved. Of course, there's nothing preventing you from making your own authentication (Devise is just built on Warden after all), but it should give you some ideas on how this has been done already.
In regards your original question (about layouts), the other answer is very good (in terms of setting layouts per method etc).
To add to it, I would say that you have to remember that Rails is a series of classes. As such, setting the layout option in the controller is the best way to ensure you're getting the correct one.
Here's Rails explanation on it:
#app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
layout :your_layout
private
def your_layout
devise_controller? || !user_signed_in? ? "new_user" : "application"
end
end
I've found it better to keep your logic as terse as possible. IE only have one set of logic for the layout. We tend to keep it in the ApplicationController, overriding where necessary.
--
Finally, your questions also highlighted a base misunderstanding of Rails. I'm not being disrespectful; I've found the clearer your perception of the framework, the better you can work with it:
You have to remember several key points with Rails:
It's an MVC (Model View Controller) framework
It's built on Ruby; hence is object orientated
maybe I should put "new" and "create" in a signup controller. This where I get confused.
If you take Devise as a model, you'll see that you could treat your controllers as layers of abstraction for objects. That is, as Devise shows us, you can have sessions and registrations controllers.
I'm not advocating you do this exactly, I am trying to show that if you put the focus onto the objects you're working with, it becomes clearer where your controller actions should be placed.
You'll also be best understanding the CRUD (Create Read Update Destroy) nature of Rails controllers & objects. The standard Rails controller is set up as such:
Whilst this is not strict, it does give you another indication as to the structure your controllers should adhere to.
You may know this stuff already!

Model-level authorization in Rails

I want to implement authorization in my Rails application on a model level (not controller), in a similar way that validation on models is done. What is the best way to do this?
If it is implemented in the models itself, the main problem is that the models don't have access to the current user. I've seen solutions like: Thread.current[:user_id] = session[:user_id], but that doesn't seem like a good idea.
I've seen a different approach where variants of the methods like create, find and new are created, accepting an additional parameter for the current user.
Another approach would be to implement all the methods in the User/role class, so for example user.posts.create or user.readable_posts.find would be used instead of Post.create or Post.find.
Which of these approaches would be suggested? Are there any better ways to implement the authorization? Are there any plugins that makes this easier? I need an approach that scales well for multiple roles and models.
I would recommend you to look at declarative authorization. It works with both models and controllers.
The way it do what you are asking is having a before_filter in the applicationController that sets Authorization.current_user = current_user where Authorization is a module.
I think that approach is the best one, it keeps the models clean and you don't have to remember to include the user everywhere, but can filter it in the models callback functions instead.
Why would you do this? Isn't controller level identification enough?
if #user.is_able_to_do_this?
do_this
else
blah!
end
... and in your model
def is_able_to_do_this
if self.rights > Constant::Admin_level_whatever
return true
end
return false
end

Using multiple controllers in one view in Rails

I'm working on something like a social networking mesh; I am using different API's from various websites, e.g. Last.FM, Delicious, Twitter, ...
I've created one controller per each website (currently, there are 7).
Sample views:
localhost:3000/lastfm <- All datas i gathered from user's Last.fm account
localhost:3000/twitter <- All datas i gathered from user's Twitter account
...
Now I want to show these datas in one view (localhost:3000/index.hmtl) by using these different controllers.
Components are deprecated, creating one controller and bury all the API's in that seems ugly, too..
So I don't know how to do this. Any idea?
First off, you should put all of the data-storing and data-gathering methods into Resources and Models so they are accessible from all controllers. You can keep the internal data-mutating operations in your individual controllers though. Once you have it organized like this, you could do what Hobo does: create a controller just for the front page, "front_controller" if you will. Here you can display data gathered from all of your models and resources, as well as links to your other controller actions.
These are a few interesting thoughts on better organizing your models and controllers (fat models, skinny controllers is a rule of thumb.
Since you said you are using other API's (like lastfm and twitter), you might want to take a look at this railscasts about creating non ActiveRecord models (models that are not tied to a database)
here is some pseudo code, keep in mind its really only targeted at your question.
# pseudo code
class TwitterController < ApplicationController
def index
#services = {
:twitter => TwitterModel.find(:all, ...),
}
end
def update_twitter
TwitterUpdaterClass.update { |twit|
_m = TwitterModel.new
_m.message = twit.msg
_m.from = twit.from
# ..
_m.save
}
end
end
class MyIndexController < ApplicationController
def index
#services = {
:twitter => TwitterModel.find(:all, ...),
:lastfm => LastFmModel.find(:all, ...)
}
end
end
it might be much better to have background-workers update your rest-services instead a controller which you need to invoke every time you want to fetch recent tweets.
here is nice article showing - 6 ways to run background jobs in rubyonrails
# more pseudo code
class TwitterWorker < BackgrounDRb::MetaWorker
set_worker_name :twitter_worker
def create(args = nil) # instead of TwitterController.update_twitter
TwitterUpdaterClass.update { |twit|
_m = TwitterModel.new
_m.message = twit.msg
_m.from = twit.from
# ..
_m.save
}
end
end
First off, you should put all of the data-storing and data-gathering methods into Resources and Models so they are accessible from all controllers. You can keep the internal data-mutating operations in your individual controllers though. Once you have it organized like this, you could do what Hobo does: create a controller just for the front page, "front_controller" if you will. Here you can display data gathered from all of your models and resources, as well as links to your other controller actions.
I think you should read a bit about rails' MVC architecture.
It seems to me that you are neglecting the M(odel) part of it. The models should hold the data, thus being the most important part of your application.These are a few interesting thoughts on better organizing your models and controllers (fat models, skinny controllers is a rule of thumb. Since you said you are using other API's (like lastfm and twitter), you might want to take a look at this railscast about creating non ActiveRecord models (models that are not tied to a database)
If you also provide an API for your users, I suggest using a RESTful approach, as it can really be easy to develop and maintain once you get the hang of it. You should read more about resources, as your localhost/lastfm and localhost/twitter are resources and not views.
Hope this helps. Good luck

Why do I need to work harder to make my Rails application fit into a RESTful architecture?

I started a Rails project recently and decided to use RESTful controllers. I created controllers for my key entities (such as Country) and added index, new, edit, create, show, update and delete. I added my map.resources :country to my routes file and life was good.
After development progressed a little, I started to encounter problems. I sometimes needed extra actions in my controller. First there was the search action that returned the options for my fancy autocompleting search box. Then came the need to display the countries in two different ways in different places in the application (the data displayed was different too, so it wasn't just two views) - I added the index_full action. Then I wanted to show a country by name in the URL, not by id so I added the show_by_name action.
What do you do when you need actions beyond the standard index, new, edit, create, show, update, delete in a RESTful controller in Rails? Do I need to add (and maintain) manual routes in the routes.rb file (which is a pain), do they go in a different controller, do I become unRESTful or am I missing something fundamental?
I guess I am asking, do I need to work harder and add actions into my routes.rb file for the privilege of being RESTful? If I wasn't using map.resources to add the REST goodies, the standard :controller/:action, :controller/:action/:id routes would handle pretty much everything automatically.
I would treat search as a special case of index. Both actions return a collection of resources. The request parameters should specify things like page, limit, sort order, and search query.
For example:
/resources/index # normal index
/resources/index?query=foo # search for 'foo'
And in resources_controller:
before_filter :do_some_preprocessing_on_parameters
def index
#resources = Resource.find_by_param(#preprocessed_params)
end
As for index_full and search_by_name, you might look at splitting your current controller into two. There's a smell about what you've described.
Having said that, you're absolutely right that there's no point in forcing your app to user restful routes when it doesn't deliver anything over /:controller/:action/:id. To make the decision, look how frequently you're using the restful resource route helpers in forms and links. If you're not using them, I wouldn't bother with it.
If I go beyond the standard CRUD actions with my models, I normally just add the methods as required. Searching is something I add to many controllers, but not every one, so I add it and maintain the routes normally:
map.resources :events, :collection => { :search => :get }
Moving these actions to an entirely separate controller might keep some of your controllers RESTful, but I find that keeping them in context is far more useful.
REST does not specify that you can't have additional views. No real world application is going to be able use only the supplied actions; this is why you can add your own actions.
REST is about being able to make stateless calls to the server. Your search action is stateless each time as the data so far is supplied back, correct? Your alternate display action is also stateless, just a different view.
As to if they should be manual routes or a new controller, that depends on how distinct the activity is. Your alternate view, if it provides a full set of CRUD (create, read, update, delete) operations would do well to be in a new controller. If you only have an alternate view to the data, I would just add an alternate view action.
In other words, it doesn't sound like your application is failing to be RESTful, it is more an issue of realizing that the automatically generated feature set is a starting point, not a conclusion.
In my opinion they may have gone a bit off the rails here. What happened to DRY?
I'm just getting back into Rails not having done much development with it since beta and I'm still waiting for the light-bulb to come on here. I'm still giving it a chance but if it hasn't happened for me by the end of my current project I'll probably just drop-back to the old standard routes and define the methods as I actually need them for the next one.
I won't go on to explain more about REST since I think that has been answered in this question, however I will talk a little bit about the default route.
My main problem with the default route is that if you have multiple sites using the same Rails app it can look horrible.
For example there may be controllers that you don't want people to be able to see on one app:
http://example1.somesite.com/example_2/foo/bar/1
compare this to
/:controller/:action/:id
This would go to the controller example_2/foo, action bar and id 1
I consider this to be the main flaw of Rails' default route and this is something that RESTful routes (with subdomain extensions) or only named routes (map.connect 'foo' ... ) can fix.
To remain RESTful in your design, you need to rethink what you call a resource.
In your example a show action for a search controller, (search resource) is the direction to remain restful.
In mine, I have a dashboard controller (show) and controllers for single fields of in-place ecditors (show and update)

Resources