Pundit : Custom redirection within one user action - ruby-on-rails

I'm trying to centralize authentification in pundit policies instead of having it in my controllers. It works well but I lost some flexibility in customizing redirection and flash message.
How could I transfer the information about which authentification didn't pass to the Pundit::NotAuthorizedError rescuing function ? One action can have 2 steps of authentification: 1. user.paid? 2. user.is_allowed_to_update? and I want custom message and redirection for each case.
The exception.query solution is not working cause it only allow to customize flash and redirection for each action and not within one action.
Below is a more detailed explanation of the situation
WITHOUT PUNDIT
Comment_Controller
def update
if user.didnt_pay?
flash[:message] = nice_message
redirect_to payment_page_path
elsif user.is_not_allowed_to_perform_action
flash[:message] = less_nice_message
redirect_to dashboard_path
end
end
And now
WITH PUNDIT
Comment_Controller
def update
authorize #comment
end
Comment_policy
def update?
user.paid? && user_is_allowed_to_perform_action
end
ApplicationController
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
def user_not_authorized
flash[:message] = one_message_for_all_error # THIS IS WHAT I WANT TO CUSTOMIZE
redirect_to one_path_for_all_error # THIS IS WHAT I WANT TO CUSTOMIZE
end

A possibility for customizing this error messages it to set the expected message in the Policy and later getting it from the controller. How to do so?
The exception object you get as an argument in the controller
class CommentsController < ApplicationController
def user_not_authorized(exception)
end
end
comes with a policy attribute which links you to the offending policy. So, lets say that in your policy you want to set a particular message when some clause is not fulfilled:
class AnimalPolicy < ApplicationPolicy
attr_accessor :error_message
def new?
if !authorization_clause
#error_message = "Something terrible happened"
false
else
true
end
end
end
Hence, in your controller, you would have to set this error_message into your flash or wherever you want it to be:
class CommentsController < ApplicationController
def user_not_authorized(exception)
flash[:error] = exception.policy.try(:error_message) || "Default error message"
redirect_to root_path
end
end
It's a bit clumsy solution, but it worked for me

In my solution I propose two methods, one when the user has a good answer the other when the answer is not favorable ... pundit has a method (user_not_authorized) which allows to manage that one could duplicate and Adapt to your suggestions
def update
if user.didnt_pay?
authorize #comment
user_congratulation
elsif user.is_not_allowed_to_perform_action
user_not_authorized
end
end
in ApplicationController
past this rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
and after You'll create two private methods in your controller called
user_not_authorized and user_congratulation
private
def user_not_authorized
flash[:alert] = "less_nice_message"
redirect_to dashboard_path
end
def user_congratulation
flash[:alert] = "nice_message"
redirect_to payment_page_path
end
end
for more information visit this link https://github.com/elabs/pundit#rescuing-a-denied-authorization-in-rails
Although this post is old, I thought fit to answer because I was also in need of a good answer, which was not the case! I hope to have helped

Related

How should I write code if a params[:id] doesn't exist in the db

I'm writing a show action in a controller and want to branch if no id received from params[:id] exists in my database.
My app is still being created. I've tried conditional branching and exception handling. However, they didn't work as expected.
class ArticlesController < ApplicationController
#...
def show
begin
#article = Article.find(params[:id])
rescue
render root
end
end
#...
end
I expect to be redirect to the root if any error occurs.
The above code returns the record if it is found successfully. However, an ActiveRecord::RecordNotFound in ArticlesController#show occurs if no record is found.
How should I write code for this error?
How should I write code for this error?
The short answer is "you shouldn't".
Exceptions should, IMO, be exceptional. It sounds like you might expect this circumstance, so I suggest you write code that handles the scenario without raising an exception.
If you really think you'll have circumstances where the Article record does not exist for the given params[:id] (seems a little odd, but I guess it's possible), then I would suggest that you use .find_by instead of .find:
class ArticlesController < ApplicationController
#...
def show
if #article = Article.find_by(id: params[:id])
# do something with the article
else
render root
end
end
#...
end
It seems like to you might want to do a redirect instead of a render:
class ArticlesController < ApplicationController
#...
def show
if #article = Article.find_by(id: params[:id])
# do something with the article
else
redirect_to root_path
end
end
#...
end
But, maybe not...
Also, rescuing everything like this:
def show
begin
#article = Article.find(params[:id])
rescue
render root
end
end
...is generally viewed as code smell. There are a lot of articles on the interwebs about why.
You can handle the exception from ArticlesController but I advise put the code at
ApplicationController like this:
rescue_from ActiveRecord::RecordNotFound do
redirect_back fallback_location: root_path, alert: t("resource_not_found")
end
Maybe You can code like this:
def show
#article = Article.find_by(id: params[:id])
unless #article.present?
flash[:error] = "Not Found."
redirect_to root_path
end
end
why write so much code, while you can achieve with two lines only.
class ArticlesController < ApplicationController
def show
#article = Article.find_by(id: params[:id])
redirect_to root_path unless #article.present?
end
end
Your code should be written like this:
def show
begin
#article = Article.find(params[:id])
rescue
redirect_to root_path
end
end

Login/signup function not working, #current_user not working, sessions not working

I'm new to Rails, and am working on a practice app that involves a simple login function. I've been following a tutorial from CodeAcademy by the books, however the code is not working in quite a few ways. First of all, the sessions do not set, even though Rails is executing the rest of the code inside the "if" block shared with the session declaration (btw, no errors are returned).
The session controller:
class SessionsController < ApplicationController
def new
end
def create
#user = User.find_by_username(params[:session][:name])
if #user && #user.authenticate(params[:session][:password])
session[:user_id] = #user.id
redirect_to '/posts'
else
session[:user_id] = nil
flash[:warning] = "Failed login- try again"
redirect_to '/login'
end
end
def destroy
session[:session_id] = nil
redirect_to login_path
end
end
Extrapolating from that issue, my "current_user" function is not working, which is most likely because the session is not being set.
The application controller:
class ApplicationController < ActionController::Base
def current_user
return unless session[:user_id]
#current_user ||= User.find(session[:user_id])
end
def require_user
redirect_to '/login' unless current_user
end
end
Any help is much appreciated. Let me know if you need to see anything else.
NOTE: I know I should use Devise, and I am planning to in my future, more serious projects. However, like I said, this is a practice/test app to help develop my coding skills, and before using a "magical" gem like Devise I want to get hands-on experience with making a login system myself.
I think the error is that session_controller is not able to find the current_user.
Write the following code in application_controller:
class ApplicationController < ActionController::Base
helper_method :current_user
def current_user
return unless session[:user_id]
#current_user ||= User.find(session[:user_id])
end
def require_user
redirect_to '/login' unless current_user
end
end
Letme know if it works
There are a few possible problems.
First, #current_user is not set until the current_user method is called. And as #Neha pointed out, you'll need to add a helper method to your ApplicationController so that all your views will have access to the current_user method. Add this line to your ApplicationController:
helper_method :current_user
Now, to diagnose the problem, let's set something up that lets you get some visibility into your session and current_user.
First, in views/layouts/application.html.erb, just after the line that says <= yield %>, add this:
<%= render 'layouts/footer' %>
Then add a new file views/layouts/_footer.html.erb:
<hr/>
Session:<br/>
<% session.keys.each do |key| %>
<%= "#{key}: #{session[key]}" %><br/>
<% end %>
<br/>
User:<br/>
<%= current_user&.username || '[None]' %>
Now at the bottom of every view you can see the details of your session.
In your sessions#create action, you have a potential problem with finding your User. You are using params[:session][:name] where you probably should be using params[:session][:username].
Also, tangentially, the proper way to destroy a session is not by setting session[:id] to nil, but instead to use reset_session. So your SessionsController should look like this:
class SessionsController < ApplicationController
def new
end
def create
#user = User.find_by_username(params[:session][:username])
if #user && #user.authenticate(params[:session][:password])
session[:user_id] = #user.id
redirect_to '/posts'
else
session[:user_id] = nil
flash[:warning] = "Failed login- try again"
redirect_to '/login'
end
end
def destroy
reset_session
redirect_to login_path
end
end

how to make clean code in controller rails

how to make this code clean in rails?
profiles_controller.rb :
class ProfilesController < ApplicationController
before_action :find_profile, only: [:edit, :update]
def index
#profiles = Profile.all
end
def new
#profile = Profile.new
end
def create
profile, message = Profile.create_object(params["profile"], current_user)
flash[:notice] = message
redirect_to profile_url
end
def edit
end
def update
profile, message = #profile.update_object(params["profile"])
flash[:notice] = message
redirect_to profile_url
end
private
def find_profile
#profile = Profile.friendly.find(params["id"])
end
end
i look flash[:notice] and redirct_to profile_url is duplicate in my code, how to make the code to clean and dry?
How about moving the repetitive code to a separate method and call that method inside the actions.
def flash_redirect # you can come up with a better name
flash[:notice] = message
redirect_to profile_url
end
then in update action:
def update
profile, message = #profile.update_object(params["profile"])
flash_redirect
end
do the same thing for create action
UPDATE:
in case you are wondering about usingafter_action, you can't use it to redirect as the call-back is appended after the action runs out its course. see this answer
Take a look at Inherited Resources. It's based on the fact that many CRUD controllers in Rails have the exact same general structure. It does most of the work for you and is fully customisable in case things are done a little different in your controllers.
Using this gem, your code would look like this:
class ProfilesController < InheritedResources::Base
def create
redirect_to_profile(*Profile.create_object(params[:profile], current_user))
end
def update
redirect_to_profile(*#profile.update_object(params[:profile]))
end
private
def redirect_to_profile(profile, message)
redirect_to(profile_url, notice: message)
end
def resource
#profile ||= Profile.friendly.find(params[:id])
end
end
The create and update methods return multiple values, so I used the splat operator to DRY this up.
create_object and update_object don't follow the Rails default, so we need to implement those actions for Inherited Resources instead. Currently they don't seem to be handling validation errors. If you can, refactor them to use ActiveRecord's save and update, it would make everything even easier and DRYer.

General rescue throughout controller when id not found - RoR

I have stumbled upon a situation where my application looks for an id that does not exist in the database. An exception is thrown. Of course, this is a pretty standard situation for any web developer.
Thanks to this answer I know that using rescue deals with the situation pretty neatly, like so:
def show
#customer = Customer.find(params[:id])
rescue ActiveRecord::RecordNotFound #customer with that id cannot be found
redirect_to action: :index #redirect to index page takes place instead of crashing
end
In case the customer cannot be found, the user gets redirected to the index page. This works absolutely fine.
Now, this is all nice, but I need to do the same rescue attempts in actions like show, edit, destroy, etc, i.e. every controller method that needs a specific id.
Having said that, here's my question:
Isn't there any way to generally tell my controller that if it can't find the id in any of its methods, it shall redirect to the index page (or, generally, perform a specific task)?
You must use rescue_from for this task. See example in the Action Controller Overview Guide
class ApplicationController < ActionController::Base
rescue_from ActiveRecord::RecordNotFound, :with => :record_not_found
private
def record_not_found
redirect_to action: :index
end
end
Rails has a built-in rescue_from class method:
class CustomersController < ApplicationController
rescue_from ActiveRecord::RecordNotFound, with: :index
...
end
If you're talking about doing this within a single controller (as opposed to doing this globally in every controller) then here are a couple options:
You can use a before_filter to setup your resource:
class CustomerController < ApplicationController
before_filter :get_customer, :only => [ :show, :update, :delete ]
def show
end
private
def get_customer
#customer = ActiveRecord.find(params[:id])
rescue ActiveRecord::RecordNotFound
redirect_to :action => :index
end
end
Or you might use a method instead. I've been moving in this direction rather than using instance variables inside views, and it would also help you solve your problem:
class CustomerController < ApplicationController
def show
# Uses customer instead of #customer
end
private
def customer
#customer ||= Customer.find(params[:id])
rescue ActiveRecord::RecordNotFound
redirect_to :action => :index
end
helper_method :customer
end
In certain cases, I would recommend that you use Model.find_by_id(id) as opposed to Model.find(id). Instead of throwing an exception, .find_by_id returns nil. if the record could not be found.
Just make sure to check for nils to avoid NoMethodError!
P.S. For what it's worth, Model.find_by_id(id) is functionally equivalent to Model.where(id: id), which would allow you to build out some additional relations if you want.

Rails redirect based on user type

I'm learning Rails by building a shop application and I'm having a bit of trouble with redirects. I have 3 roles in the application:
Buyer
Seller
Administrator
Depending on which type they are logged in as then I would like to redirect to a different page/action but still show the same URL for each (http://.../my-account).
I don't like having to render partials in the same view, it just seems messy, is there another way to achieve this?
The only way I can think of is to have multiple actions (e.g. buyer, seller, administrator) in the accounts controller but that means the paths will look like http://.../my-account/buyer or http://.../my-account/seller etc.
Many thanks,
Roger
I've put my code below:
models/user.rb
class User < ActiveRecord::Base
def buyer?
return type == 'buyer'
end
def seller?
return type == 'seller'
end
def administrator?
return type == 'administrator'
end
...
end
controllers/accounts_controller.rb
class AccountsController < ApplicationController
def show
end
end
controllers/user_sessions_controller.rb
class UserSessionsController < ApplicationController
def new
#user_session = UserSession.new
end
def create
#user_session = UserSession.new(params[:user_session])
if #user_session.save
if session[:return_to].nil?
# I'm not sure how to handle this part if I want the URL to be the same for each.
redirect_to(account_path)
else
redirect_to(session[:return_to])
end
else
#user_session.errors.clear # Give as little feedback as possible to improve security.
flash[:notice] = 'We didn\'t recognise the email address or password you entered, please try again.'
render(:action => :new)
end
end
def destroy
current_user_session.destroy
current_basket.destroy
redirect_to(root_url, :notice => 'Sign out successful!')
end
end
config/routes.rb
match 'my-account' => 'accounts#show'
Many thanks,
Roger
In UserSessionsController#create (i.e.: the login method) you could continue to redirect to the account path (assuming that goes to AccountsController#show) and then render different views according to the role. I.e.: something like this:
class AccountsController < ApplicationController
def show
if current_user.buyer?
render 'accounts/buyer'
elsif current_user.seller?
render 'accounts/seller'
elsif current_user.administrator?
render 'accounts/administrator
end
end
end
Better yet, you could do this by convention...
class AccountsController < ApplicationController
def show
render "accounts/#{current_user.type}"
end
end
If I understand you question correctly, then the solution is simple.
You can just call the method you want inside your controller. I do this in my project:
def create
create_or_update
end
def update
create_or_update
end
def create_or_update
...
end
In your case it should be:
def action
if administrator? then
admin_action
elsif buyer? then
buyer_action
elseif seller? then
seller_action
else
some_error_action
end
end
You should probably explicitly call "render" with an action name in each of those actions, though.

Resources