Rails Cancan - How to Restrict Users From Accessing Custom URL Route - ruby-on-rails

I have a custom route setup that has a location_id in the url (see below)
resources :menu_items, :path => "/location_menu/:location_id"
So when I hit /location_menu/1 it will show me location_1's menu, /location_menu/2 will show location_2's menu, etc.
Each user is associated to multiple locations (has_many :locations)
I am trying to use cancan to restrict users from viewing certain menu_item URLS.
For example: User 1 is associated with location 1 and 2. So they can only view the page /location_menu/1 and /location_menu/2. But they would not be able to view /location_menu/3.
I created a custom method as a before_filter in my controller:
before_filter :location_check
...
def location_check
#location = Location.find(params[:location_id])
authorize! :see_location, #location
end
In my ability.rb
can :see_location, MenuItem do |location| location && user.location_ids.include?(location.id) end
For some reason, this does not work for me. What could I be doing wrong? If you guys could help me, I would really appreciate it!
Thanks.

Check once within ability.rb,
user.location_ids.each do |l|
can :view, MenuItem, location_id: l
end

First, I'm not seeing what benefit you get from defining a custom route rather than either a nested resource, but it looks likes its inflating your domain language and resulting in a situation where any particular resource could be referred to by several different names - this will probably become confusing enough that it's worth it to simplify now.
In your location_check method, you are authorizing see_location on a Location instance, but the portion of the Ability class you've shown us concerns a MenuItem class. Try defining your ability like this:
can :see_location, Location, id: user.location_ids
Edit
If you need a cancan action to authorize MenuItems directly, try this (assuming a belongs_to :location relationship on MenuItem):
can :view, MenuItem, location_id: user.location_ids
As for your routes, think even simpler. All you need is this:
resources :location_menus
...and then everything related to this goes in a LocationMenusController. Don't worry that there's no model by the same name - you still can still lookup your locations with Location.find(params[:id]). From what I understand, everything about the location menu pages hinges around the current user's access to a particular location, so you can treat LocationMenu as a kind of virtual resource wrapped around Location.

Related

Map multiple paths to a controller

I have a controller called CardController. Currently I have routes like card_path that map to /cards/:id. I would like to make it so that I can use /trips/:id and /events/:id that map to the same /cards/:id. I know I'll have to override card_path eventually but is it possible to set up my routes file for this? Do I need to set up a Trip and Event controller that just redirect to the card actions?
Edit:
Trips should completely map to cards, meaning 'trips/1/edit' should end up at 'cards/1/edit', 'trips/1/images/12' should end up at 'cards/1/images/12'
I ended up adding some controller to the routes file.
routes.rb
def card_routes
member do
get 'test'
end
end
class TripsController < CardsController; end
resources :trips { card_routes }
resources :cards { card_routes }
Now /trips/1/test and /cards/1/test go to the same place.
You can easily do something like:
get 'trips/:id' => 'cards#show'
Try accessing different trips in your browser, trips/1 or trips/2 (if cards with those ids exist), and they should redirect to the appropriate card.
If you haven't already, I recommend taking a few minutes and reading the Routing Guide, as it's really comprehensive and shows different ways of accomplishing things:
http://guides.rubyonrails.org/routing.html

Creating semi-private urls for show action in rails app

I am building a small app, that allows users to create lists and within these lists they can add gifts that they want. So far its very similar to a ToDo list app.
I have three models:
User - Can have many Lists
List - Can have many gifts and belongs to User
Gift - Belongs to List
In my List model as well as storing the name of the list, Im also creating a unique string of letters and numbers and storing it as shared_key in the record. The code looks like this:
def create_unique_url
begin
self.shared_key = SecureRandom.urlsafe_base64(10)
end while self.class.exists?(shared_key: shared_key)
end
and ideally I want the url to look something like this app.com/public/long_string_shared_key_goes here
My main Question is, how should do I go about setting up a route to access the record at this public address.
Should I create another controller called public and have a show method there? Or should I create a public action in my llist controller and somehow manually create a route to it?
Since it's just a matter of single action I'd not suggest to redefine the #to_param, since it might affect all of your existing functionality. Still a matter of taste, mostly
routes:
resources :lists, except: [ :show ]
get '/public/:shared_key' => 'lists#show'
controller:
def show
#list = List.find_by(shared_key: params[:shared_key])
end
view:
link_to list.name, list_path(shared_key: list.shared_key)
In you case i would just alter a little the showing of the list. First in the list model redefine the to_param method
def to_param
long_string_shared_key_goes #by default this was returning the id - example...to access show a list you had to navigate to /lists/:id
end
Now you need to change the routes.rb
match 'public/:long_string_shared_key_goes', to :'lists#show', via: [:get]
Now you just have to change some find methods (if you are accessing the lists with example list.find(params[:id] you now have to take in consideration that you dont have the id in the params, but you long_string_shared_key).
Hope this answered your question.

Add new view to a controller in Rails

I have a controller, clients_controller, with corresponding index, show, edit, delete, new & form views. Is there a way to create a new view like clients/prospects.html.erb that acts the same way as clients/index.html.erb, except is routed at clients/prospects/?
I've tried this:
match '/clients/prospects' => 'clients#prospects'
And some other things in routes.rb, but of course get the error "Couldn't find Client with id=prospects".
The goal here is basically to have a prospects view and a clients view, and by simply switching the hidden field to a 1, it (in the user's mind) turns a prospect into a client (it's a CRM-like app).
There's a couple of things you need to do. First you need to put the your custom route before any generic route. Otherwise Rails assumes the word "prospects" is an id for the show action. Example:
get '/clients/prospects' => 'clients#prospects' # or match for older Rails versions
resources :clients
Also you need to copy / paste the index method in your ClientsController and name it prospects. Example:
class ClientsController < ApplicationController
def index
#clients = Client.where(prospect: false)
end
def prospects
#prospects = Client.where(prospect: true)
end
end
Lastly, you need to copy the index.html.erb view and name the copy prospects.html.erb. In the example above you would have to work with the #prospects instance variable.
Create a new action in clients controller named prospects. And then define a collection route in routes.rb for it as either resource full way. Or u directly use match as you were doing.
What you're doing is not wrong (although I'd change match to get, otherwise POST and DELETE requests to that url will also render your prospects view). Presumably you have
resources :clients
in your routes file? If so, what you have will probably work if you just move the line you quoted above the resources declaration -- the problem is that /clients/prospects matches the show route for the clients resource, so if it's defined first then that's the route that gets matched.
However, there's a more idiomatic way to define this route
resources :clients do
collection do
get :prospects
end
end
See Rails Routing documentation for more
Also see migu's answer for what else needs to be done once the url is being routed correctly (though there are other things you can do -- if you the two views are similar enough, you can reuse the view template, for example).

Obtaining ID of containing resource via params[:id] for custom actions

I have the following routes in my config/routes.rb file:
resources :employees do
get 'dashboard'
get 'orientation'
end
employees refers to a regular resource handling the standard RESTful actions. dashboard and orientation are what I currently refer to "custom actions" which act on Employee instances. I apologize if I have my terminology mixed up and dashboard and orientation are really something else. These custom actions respond to URLs as follows:
http://myhost/employees/1/dashboard
i.e. They're "member" actions much like show, edit etc.
Anyway, this all works well enough. Regular actions such as show on EmployeesController obtain the ID of the associated Employee through params[:id]. However, with this current structure, dashboard and orientation have to use params[:employee_id] instead. This is not too difficult to deal with, but does lead to some additional code complexity as my regular before_filters which expect params[:id] don't work for these two actions.
How do I have the routing system populate params[:id] with the ID for these custom actions in the same way as show etc.? I've tried various approaches with member instead of get for these actions but haven't got anything to work the way I would like yet. This app is built using Ruby on Rails 3.2.
This might help you:
resources :employees do
member do
get 'dashboard'
get 'orientation'
end
end
and the above will generate routes like below, and then you will be able to use params[:id] in your EmployeesController.
dashboard_employee GET /employees/:id/dashboard(.:format) employees#dashboard
orientation_employee GET /employees/:id/orientation(.:format) employees#orientation
I haven't tested this example, but you can set the resourceful paths explicitly.
Something like this might work:
resources :employees, path: '/employees/:id' do
get 'dashboard', path: '/dashboard'
get 'orientation', path: '/orientation'
end

Rails Active Record Polymorphic Nested Resource Navigation

I've run into a bit of an issue and not sure how to get round it.
We have a number of polymorphic nested resources in our datamodel, eg:
Destination > Accommodation > Address
Destination > Attraction > Address
So it is possible to arrive at the Address controller from multiple parents. I need to be able to associate these correctly and also navigate back up the tree of parents.
Address is the same model in these cases, so my first solution for
this was to created nested resources in the routes file.
We then also started to use this nesting to provide a breadcrumb
navigation thing, so when our URLS get like this:
localhost:3000/destinations/1/accommodations/3/address/new
We can split it up and use it to navigate back down the path to any level.
I also, to make the controller generic, I use the nested resources to
work out what the parent resource for map is, so the controller looks
like this:
def new
#parent = find_parent_model
if !#parent.nil?
#destination = #parent.destinations.new
[...]
def find_parent
params.each do |name, value|
if name =~ /(.+)_id$/
return $1.classify.constantize.find(value)
end
end
nil
end
This works. But the problem is that we have 1800 lines of nested resources in the routes.rb file and now it takes the rails app about 5 minutes to start, and it sits
there using 500MB of ram. :S
Does anyone know of a less crazy way of doing this?
You might want to give up on using the nested resources syntax for the routing.
A single route like
get 'destinations/:destination_id/:parent_type/:parent_id/address/new' => 'address#new'
would match all of resources, and in AddressController#new you could have
#parent = params[:parent_type].constantize.find(params[:parent_id])
You might also want to check that #parent is of one of the expected types afterwards.

Resources