Best approach to rails app and membership access options - ruby-on-rails

I wanted to see what the community here would suggest for best approach. I've got my app right now where Users have plan_id, so plan_id = "1" would be a trial or free and plan_id = "2" would be subscription plan. Now my controllers check for two things, 1.) the plan_id if its 1 or 2... 2.) if on the free plan, check the number of posts they have saved in the DB and 3.) if on free plan checks the # of categories they have.
I want to limit my users to the # of posts and categories, if they hit that limit they have to subscribe to unlimited plan = plan 2.
Now I was also looking at Cancan or Easy_Roles but in my case I'm not sure that would work.
Should I stay with my solution that works or change it and implement Cancan or Easy_roles? I'm looking for suggestions.
Thank you,

You should be able to use CanCan for this if you'd like. The way to set it up is to define the abilities in a following way (untested code, but you get the idea):
class Ability
include CanCan::Ability
def initialize(user)
can :manage, :all
cannot :create, Post do |p|
user.plan_id == 1 and user.posts.count > X
end
cannot :create, Category do |c|
user.plan_id == 1 and user.categories.count > Y
end
end
end
The disadvantage of this is that when the user is not authorized a generic exception is thrown and you don't know why it was thrown. So when you handle it you have to either check again:
rescue_from CanCan::AccessDenied do |exception|
if user.plan_id == 1 and user.posts.count > X
render 'too_many_posts'
elsif user.plan_id == 1 and user.categories.count > Y
render 'too_many_categories'
else
...
end
end
or pass it in the message when checking in the controller:
authorize! :create, Post, :message => "Unable to create so many posts"

Related

Rails 4 - Couldn't find User without an ID

I'm new to rails, so any explanation & advise would much appreciated.
i have a webpage in which i would like any user to view that page not just the current_user, but i am unsure how to correctly define the instance variable #user in my controller
in my static_pages_controller.rb i have the below action recruiterpg
static_pages_controller.rb
def recruiterpg
#user = User.find(params[:user_id])
#adverts = #user.adverts
#applications = #user.forms
end
in my controller, i have defined user as #user = User.find(params[:user_id]) but this breaks my code in the views; views/static_pages/recruiterpg.html.erb
when i define user as #user = current_user my code in the views works perfectly fine
what am trying to do is: for my views, the recruiterpg.html.erb, i would like
any user to be able to view the page not only the current_user to
view the page. Could one kindly advise me and explain to me how to
define #user correctly in my status_pages_controller.rb. i also
tried #user = User.find(params[:id]) but my code still breaks in the
views - i get the error message
Couldn't find User without an ID
You need to make sure you are passing a user_id to the recruiterpg action. For example, try this url in your browser (set user_id to a known id in the users table):
http://localhost:3000/dashboard?user_id=1
A suggested modification to your action:
def recruiterpg
#user = User.find params.require(:user_id)
#adverts = #user.adverts
#applications = #user.forms
end
If params[:user_id] isn't defined, you want to find a way to make visible what is being defined.
If you throw the following statements into your controller...
def recruiterpg
...
puts params
...
end
...you should see something like the following get spit out in your console when you load the page...
{"controller"=>"static_pages", "action"=>"recruiterpg", "id"=>"49"}
Take a look at the Rails guide for parameters. They can get defined in one of three ways.
One: As a query string similar to Sean's answer above.
Two: Routing parameters. See section 4.3 in the Rails guide. In your case, that would mean you should have something like the following in routes.rb:
get '/dashboard/:user_id' => 'staticpages#recruiterpg'
Note that there's nothing magic about :user_id in that string.
Three: From a form which it doesn't seem like applies here, since a user isn't submitting data.
Since you're new, here is some information for you:
User Story
Firstly, the best way to resolve errors is to identify your user story.
A "user story" is a software principle in which you put the "user's perspective" first -- explaining how the software should work in conditions defined from how the user engages with it.
One of the main issues you have with your question is your user story is very weak; it's hard to decifer what you're trying to achieve.
Next time you ask a question, you should try your hardest to describe how the user should see your app, before providing code snippets :)
Controller
Your main issue is an antipattern.
An antipattern is basically a "hack" which will likely break another part of your app in future. Kind of like duct tape for software):
#app/controllers/static_pages_controller.rb
class StaticPagesController < ApplicationController
def recruiterpg
#user = User.find(params[:user_id])
#adverts = #user.adverts
#applications = #user.forms
end
end
So you're showing a static page but yet you want to populate it with data?
Hmm...
What you should be doing is something like the following:
#config/routes.rb
resources :users do
resources :recruiters, only: :index #-> url.com/users/:user_id/recruiters
end
#app/controllers/recruiters_controller.rb
class RecruitersController < ApplicationController
def index
#user = User.find params[:user_id]
#adverts = #user.adverts
#applications = #user.forms
end
end
This will allow you to populate the following view:
#app/views/recruiters/index.html.erb
<%= #adverts %>
--
It's important to note the structure of the controller / routes here.
The issue you have is that you're calling a "static page" and expecting to have params available to find a User. This can only happen if you have params available...
Params
Rails is not magic, and as such if you want to look up a user, you have to provide the parameters to do so.
This is why you're able to look up current_user -- the params are already set for this user.
As such, you'll need to use something called nested routes in order to attain a user ID other than that of current_user:
#config/routes.rb
resources :users do
resources :recruiters #-> url.com/users/:user_id/recruiters
end

Cancan Ability Issue with Inheritance

I'm having problems restricting the data shown to a specific user group using cancan..
My Users have many Products. And Products have many Vouchers.
In my routes.rb I have this:
resources :products do
resources :vouchers
end
In ability.rb:
can [:create, :update, :read ], Voucher, :product => { :user_id => user.id }
And in my Voucher controller:
def index
...
if params[:product_id]
#voucher = Voucher.find_all_by_product_id(params[:product_id])
end
...
end
Finally, in my view, I'm trying to display a list of vouchers in a Product group associated with current user.
For example:
http://localhost:3000/products/eef4e33116a7db/voucher
This lists the vouchers in the product group however, ALL users can see every voucher / product..
I'll assume my abilities are wrong. Help please :)
Have a look at the can can wiki for fetching records: https://github.com/ryanb/cancan/wiki/Fetching-Records
If you're calling load_and_authorize_resource, you'll either be able to do something similar to one of these two things:
def index
...
if params[:product_id]
#vouchers = #vouchers.where(product_id: params[:product_id])
end
...
end
load_and_authorize_resource should automatically assign the #vouchers instance variable based on the accessible_by parameters for that controller action. If this isn't working, just define it more explicitly:
if params[:product_id]
#vouchers = Voucher.includes(:product).where(product_id: params[:product_id]).where(product: { user_id: current_user.id })
end
For anyone else having this issue, I needed to do the following in my vouchers controller:
#product = Product.accessible_by(current_ability).find(params[:product_id])
#voucher = #product.vouchers
However, although this did actually block other users from viewing the results, loading the product first with accessible_by led to an exception which required a separate rescue block.

How do I use cancan to authorize an array of resources?

I have a non-restful controller that I am trying to use the cancan authorize! method to apply permissions to.
I have a delete_multiple action that starts like so
def delete_multiple
#invoices = apparent_user.invoices.find(params[:invoice_ids])
I want to check that the user has permission to delete all of these invoices before proceeding. If I use
authorize! :delete_multiple, #invoices
permission is refused. My ability.rb includes the following
if user.admin?
can :manage, :all
elsif user.approved_user?
can [:read, :update, :destroy, :delete_multiple], Invoice, :user_id => user.id
end
Is it a matter of looping through my array and calling authorize individually or is there a smarter way of doing things? I'm starting to feel like doing authorizations would be easier manually than by using cancan for a complicated non-restful controller (although I have plenty of other restful controllers in my app where it works great).
A little late in here but you can write this in your ability class
can :delete_multiple, Array do |arr|
arr.inject(true){|r, el| r && can?(:delete, el)}
end
EDIT
This can be written also as:
can :delete_multiple, Array do |arr|
arr.all? { |el| can?(:delete, el) }
end
It seems that authorize! only works on a single instance, not an array. Here's how I got around that with Rails 3.2.3 and CanCan 1.6.7.
The basic idea is to count the total records that the user is trying to delete, count the records that are accessible_by (current_ability, :destroy), then compare the counts.
If you just wanted an array of records that the user is authorized to destroy, you could use the array returned by accessible_by (current_ability, :destroy). However I'm using destroy_all, which works directly on the model, so I wound up with this count-and-compare solution.
It's worthwhile to check the development log to see how the two SELECT COUNT statements look: the second one should add WHERE phrases for the authorization restrictions imposed by CanCan.
My example deals with deleting multiple messages.
ability.rb
if user.role_atleast? :standard_user
# Delete messages that user owns
can [:destroy, :multidestroy], Message, :owner_id => user.id
end
messages_controller.rb
# Suppress load_and_authorize_resource for actions that need special handling:
load_and_authorize_resource :except => :multidestroy
# Bypass CanCan's ApplicationController#check_authorization requirement:
skip_authorization_check :only => :multidestroy
...
def multidestroy
# Destroy multiple records (selected via check boxes) with one action.
#messages = Message.scoped_by_id(params[:message_ids]) # if check box checked
to_destroy_count = #messages.size
#messages = #messages.accessible_by(current_ability, :destroy) # can? destroy
authorized_count = #messages.size
if to_destroy_count != authorized_count
raise CanCan::AccessDenied.new # rescue should redirect and display message
else # user is authorized to destroy all selected records
if to_destroy_count > 0
Message.destroy_all :id => params[:message_ids]
flash[:success] = "Permanently deleted messages"
end
redirect_to :back
end
end

Would like to have users to be separated into groups in my Rails app

In my application I want users to be able to create a group and invite other users to that group for collaboration. The important thing is that these groups are separated so their posts are not mixed. I have looked for awhile and I am not really sure how to get started on this problem. Any help will be appreciated!
TIA
I found this link but not sure how to apply it.
http://www.icoretech.org/2010/03/rails-users-groups-memberships-enter-workflow/
That link has a very sophisticated implementation of user groups and memberships. It even shows how to use the awesome Workflow gem to implement a state machine to track the process of joining a group. Honestly, I doubt you'll get a much better answer. I suggest you just take the code in the blog post as a starting point and make modifications to suit your needs.
The only thing missing is invitations. I would keep it simple and just add an invitation_token column to Group. When an invitation is sent, the token is used to generate a SHA-1 hash which can be part of the link sent to the invited user. When the link is clicked, the controller can check if the invitation code is valid and add the user to the group.
Here's a little sample code to give an idea of the implementation. I'm sure there is plenty of room for improvement, but hope it gives you some direction:
# in your Group model
def redeem_token(some_code, invitee_name)
invitation_token == decode_invitation_code(some_code, invitee_name)
end
def decode_invitation_code(encrypted, salt)
# use EzCrypto or something similar : http://ezcrypto.rubyforge.org/
# use the invitation_token as the password
# and the invitee name as the salt
EzCrypto::Key.decrypt_with_password invitation_token, salt, encrypted
end
def generate_invitation_for(user)
# use invitee name as salt
# and invitation_token as both password and content
EzCrypto::Key.encrypt_with_password invitation_token,
user.name,
invitation_token
end
# in your routes.rb do something like
resources :groups do
member do
get 'invitation/:invitation_token', :action => :invitation
end
# ...
end
# in your groups_controller.rb
def invitation
#group = Group.find(:id)
if #group.redeem_token(params[:invitation_token], current_user.name)
#group.add_member(current_user)
redirect_to root_path, :alert => "You were added to the group!"
else
redirect_to root_path, :alert => Invitation code not valid!"
end
end
Hope you find this helpful.

Error handling in rails

Oddly I'm having a hard time finding good docs about basic error handling in rails. I'd appreciate any good links as well as thoughts on a handling errors in a really basic method like this:
def self.get_record(id)
People.first( :conditions => ["id = ?", id] )
end
1) I could verify that id != nil, and that it's numeric.
2) I could also then verify that a record is found.
Is there anything else?
Are both #1 and #2 recommended practice? In both cases would you simply create a flash message with the error and display it, or is that giving away too much information?
As I'm sure you know, this is just like People.find(id), except that find raises an error.
However, People.find_by_id(id) returns nil if no record is found, which I suspect takes care of all you need. You don't need to check that what you put into ActiveRecord is the correct data type and the like; it handles SQL injection risks, so to check ahead of time would not affect actual behavior.
If we're just looking at the show action, though, there's an even more elegant way: rather than using find_by_id and checking for nil, use find, let an error bubble up, and let the controller catch it with rescue_from. (By default, in production, ActiveRecord::RecordNotFound will be caught and rescued by showing a generic 404, but you can customize this behavior if necessary.)
class UsersController < ApplicationController
rescue_from ActiveRecord::RecordNotFound, :with => :not_found
def show
#user = User.find params[:id]
end
protected
def not_found
flash[:error] = "User not found"
redirect_to users_path
end
end
Code untested, for illustrative purposes only ;)
Don't do flash[:notice]'s insted just say that "Record not found"
The two things required by you can be done as follows:
1) I could verify that id != nil, and that it's numeric.
def self.get_record(id)
People.first( :conditions => ["id = ?", id] ) if id.integer? unless id.blank?
end
2) I could also then verify that a record is found.
def self.get_record(id)
#people = People.first( :conditions => ["id = ?", id] ) if id.integer? unless id.blank?
flash[:notice] = #people.blank? # this will print true/false depending on value in #people
end
Hope it works for you. :D

Resources