how to prevent a DELETE HTTP request from succeeding in this situation? - ruby-on-rails

Rails beginner here..
I have a users resource where I implemented a callback that's supposed to prevent an admin user from deleting herself.
before_filter :admin_no_delete, only: :destroy
def admin_no_delete
admin_id = current_user.id if current_user.admin?
redirect_to root_path if params[:id] == admin_id
end
If this looks familiar to some, it's from Michael Hartl's rails tutorial, exercise #10 here but I tried to do it differently, not as he suggested.
My (lame) test for this fails
describe "deleting herself should not be permitted" do
before do
delete user_path(admin)
end
it { should_not redirect_to(users_path) }
end
But exposing a delete link for the admin user just to test and clicking on that link, it seems like the callback actually succeeds in executing (redirecting to root_path).
I was able to invoke the destroy action using jQuery to delete the record being protected by the callback (using Web Inspector's javascript console):
$.ajax({url: 'http://localhost:3000/users/104', type: 'DELETE', success: function(result){alert(result)} })
Looking for ideas on how to prevent a DELETE HTTP request from succeeding in this situation.. also any ideas on how to properly test for this kind of situation?
Thanks.

Simple: params[:id] is a string, while admin_id is a Fixnum. You can just change it as follows and it should work:
redirect_to root_path if params[:id].to_i == admin_id
The logic you're using seems a little odd to me, though. Why use a before filter if it's just for one action, and why change the redirect? I think the logic should be directly in the destroy action and look something like this:
def destroy
unless current_user.admin? && current_user.id == params[:id].to_i
User.find(params[:id]).destroy
flash[:success] = "User destroyed."
end
redirect_to users_path
end

You're comparing admin_id, an integer with params[:id]. Values in params are always strings (or arrays/hashes containing more strings) so the comparison will always fail.

Related

Ruby, before_action confusion

In ruby, I'm trying to prevent users from adding someone as a friend if either one of them has the other on their ignore list. In the friendships controller, I'm trying to define
class FriendshipsController < ApplicationController
before_action :ignored_user, only: :create
And
def ignored_user
if current_user.foes.include?(#user)
flash[:notice] = "Unable to send friend request."
redirect_to :back
end
end
In this context, current_user is the user who is logged in, #user is the user whose profile they're viewing, and foes means "either someone they're choosing to ignore, or someone who's ignoring them." All of that seems to work except the "if current_user blah blah" line. If I replace it with "if current_user.admin?" then it works exactly as you'd expect. If that user is an admin, they can't send anyone friend requests. If I replace it with "if current_user.id == 2", it also works as you'd expect. I've tried replacing the faulty line with many, many variations of the above code, including things like:
if Disagreement.where(foe_id: [#user, params[:id]], user_id: [current_user, params[:id]]).first
which works elsewhere, just not in the friendships controller.
As for error messages: there are none. My attempts always either end with the friend being added when they shouldn't be, or all friend requests being blocked, including friend requests to users who aren't ignored. Any help with this would be greatly appreciated.
A much smarter person than myself gave me the solution. I completely removed the "before_action :ignored_user, only: :create" part, as well as the "def ignored_user" section. All of that has been replaced with this:
class FriendshipsController < ApplicationController
def create
#user = User.find(params[:friend_id])
if current_user.foes.include?(#user)
flash[:notice] = "Unable to send friend request. You are ignoring this user or vice versa."
redirect_to :back
puts "#user is: #{#user}" #This line only existed for the sake of figuring out the problem
puts "#user.id is: #{#user.id}" #Same
puts "current_user.foes are: #{current_user.foes}" #Same
else
#friendship = current_user.friendships.build(friend_id: params[:friend_id])
if #friendship.save
flash[:notice] = "Friend request sent!"
redirect_to :back
end
end
end
The problem was that the friendships controller couldn't recognize #user in any of the ways it's normally defined, due to the params[:id] part. Because that's rewritten for use in the friendships controller, the controller assumes that by "id," you mean the id of some friendship, not a user's id. The person who gave me the solution realized that you could just replace ":id" with ":friend_id" since, in the context of this action, those two IDs will always be synonymous, since you'll always be adding people from their own profile page. So I never figured out how to directly reference a user's ID from within a different controller, but I've learned the next best thing.

Destroy user - with custom authentication (rails)

Quick question: I was following this tutorial where they built user authentication system instead of using devise.
My issue is the tutorial misses the destroy action in which devise has ready and does so well.
My create action is
User_controller.rb
def create
#user = User.create(user_params)
session[:user_id] = #user.id
if #user.valid?
flash[:notice] = "You've successfully Created Your Account! Welcome!"
redirect_to root_path
else
flash[:notice] = "Opps Something went bad, :/ Try again please"
render action: 'new'
end
end
I really hope this is not a total nuub question event though I am one. But can somebody offer some tips for a destroy action ? and also how would that action appear in routes and through a link_to method. I want to create a deactivate page that gives a send off and the user is able to cancel their account. Any cool tips toward the deactivate page on the side will be much appreciated.
The Hartl rails tutorial covers this quite well IMO. Once you have the destroy action defined in your controller, you could create a link to deactivate their account calling the destroy action and redirect to the home page, or a goodbye page. As long as users is listed as a resource in your routes, you shouldn't need to modify your routes as DELETE is a standard CRUD command.
https://www.railstutorial.org/book/updating_and_deleting_users
for example:
user_controller
def destroy
User.find(params[:id]).destroy
flash[:success] = "User deleted"
redirect_to users_url
end
view
<%= link_to "delete", user, method: :delete,
data: { confirm: "You sure?" } %>
For the deactivate page, maybe you can add a boolean column in your users table, say is_active,
and another controller action for deactivation, say deactivate, which will just set the is_active column as false for that user.
see sample routes.rb for the route.
#ncarroll 's sample is correct, for the routes, if you have in your routes.rb:
Rails.application.routes.draw do
resources :users do
put :deactivate
end
end
This will automatically create routes for the RESTful actions, which includes destroy.

Rails / Devise: Gracefully ask user to sign in on signed-out create

I'm continuing to tweak the Rails Getting Started project to get the authentication behavior I want.
I want to control what is allowed and not allowed at the level of specific actions rather than for a whole controller. For example, you don't need to be signed in to view posts (index / show), but you must be signed in to access the form to submit a new post (new) and to get a submitted post processed (create).
Since I would like people to be redirected to sign-in if they're not signed-in, and I'll be using that snippet over and over again in a million places, I put this in the application controller:
def authcheck
unless user_signed_in?
redirect_to new_user_session_path
end
end
For new posts, this seems to work:
def new
authcheck #see application controller
#post = Post.new
end
But for the case where I have two tabs open and I have the first one on the new post form, but I log out on the second one, then try to submit the post on the first form, I get an error about the user being null even though it seems to me like it should have been redirected:
def create
authcheck
#when not signed in, causes error "undefined method 'posts' for nil:NilClass"
#post = current_user.posts.new(post_params)
if #post.save
redirect_to #post
else
render 'new'
end
end
Actually, I was getting an exception page originally but I changed exception to null_session in the application controller's protect_from_forgery with: :exception line.
Basically: Why isn't it redirecting to the sign-in page like it was when it was just about showing the form for a new post? And, from there, what might you suggest I should do about it?
I think I was labouring under a false impression. I thought that a redirect_to was the end of execution. But it doesn't seem to work that way - after a redirect line I could, for instance, tie up the server in an attempt to square the circle, even if it's not tied to spitting out a webpage.
I modified my "authcheck" to return true or false, then put everything in if blocks. The first example didn't fail the old way because it was just instantiating a new post but it didn't matter if it was actually saved or not.
Change to application controller routine:
def authcheck
unless user_signed_in?
redirect_to new_user_session_path
return false
end
return true
end
Changes to Posts controller routines:
def new
if authcheck #see application controller
#post = Post.new
end
end
def create
if authcheck
#post = current_user.posts.new(post_params)
if #post.save
redirect_to #post
else
render 'new'
end
end
end
I suspect I'm not really doing things the accepted way, so I'll hold out a few days before accepting my own answer. :-)

How to route RESTfully with multiple entry points?

I have a model called Project, which is a collection of information stored by a Company. This company can create projects two ways.
The first is the standard RESTful way - the company goes to the Project index, then clicks 'New Project', and upon creation is returned to the index with a flash message.
The second is a 'quick create' that can be accessed when a company is looking at a client's profile. From here, the company can enter some basic information and send this off to create a project with that client (the client is specified automatically here).
The second of these two scenarios has a project being accessed from clients/show. Sending this data to projects/create would ordinarily route the company to projects/index, but I don't want that. In this case, the create action is meaningfully different in that certain fields are treated differently, and the redirect is also different. What would you suggest I do?
Build an alternative 'create_from_client' action in projects.
Build a 'create_project' action in clients.
Send a parameter to projects/create and set client_id and redirect to client/show if that parameter exists.
Something else I'm not aware of.
Thanks!
You can leverage the referrer directly from the Request object and fork based on that, similar to how redirect_to :back works.
From the Rails API docs for the redirect_to options hash:
:back - Back to the page that issued the request.
Useful for forms that are triggered from multiple places.
Short-hand for redirect_to(request.env["HTTP_REFERER"])
So you can simply do something like this:
def create
#project = Project.new( params[:project] )
#project.save
respond_with #project, location: get_location!
end
private
def get_location!
case request.env["HTTP_REFERER"]
# Your routing logic here.
end
This is nice and easy to unit test, too, if you're into that. :)
context "if the user came from the regular form" do
before { controller.request.env["HTTP_REFERER"] = "regular_form_url" }
it "redirects to the index path" do
post :create
response.should redirect_to :index
end
end
context "if the user came from the quick-create form" do
before { controller.request.env["HTTP_REFERER"] = "quick_create_url" }
it "redirects to some other path" do
post :create
response.should redirect_to some_other_path
end
end
I would just add another action to the controller, 'quick_create' or whatever. You can dry out the form with partials and parameters to the partial to tell how to render things...This just seems like the easiest way.
I've got this semi-rational (or is that semi-irrational) hang up against leveraging the referrer...
I ussualy add hidden referer field with current URL then redirect to it
For example
def create
#project = Project.new params[:project]
#project.save
respond_with #project, :location => params[:referer] || [:index, :projects]
end

How to redirect to previous page in Ruby On Rails?

I have a page that lists all of the projects that has sortable headers and pagination.
path:
/projects?order=asc&page=3&sort=code
I choose to edit one of the projects
path:
projects/436/edit
When I click save on that page, it calls the projects controller / update method. After I update the code I want to redirect to the path that I was on before I clicked edit a specific project. In other words, I want to be on the same page with the same sorting.
I saw link_to(:back) and thought that :back may work in redirect_to(:back), but that's a no go.
puts YAML::dump(:back)
yields the following:
:back
How can I get this to work?
In your edit action, store the requesting url in the session hash, which is available across multiple requests:
session[:return_to] ||= request.referer
Then redirect to it in your update action, after a successful save:
redirect_to session.delete(:return_to)
Why does redirect_to(:back) not work for you, why is it a no go?
redirect_to(:back) works like a charm for me. It's just a short cut for
redirect_to(request.env['HTTP_REFERER'])
http://apidock.com/rails/ActionController/Base/redirect_to (pre Rails 3) or http://apidock.com/rails/ActionController/Redirecting/redirect_to (Rails 3)
Please note that redirect_to(:back) is being deprecated in Rails 5. You can use
redirect_back(fallback_location: 'something') instead (see http://blog.bigbinary.com/2016/02/29/rails-5-improves-redirect_to_back-with-redirect-back.html)
I like Jaime's method with one exception, it worked better for me to re-store the referer every time:
def edit
session[:return_to] = request.referer
...
The reason is that if you edit multiple objects, you will always be redirected back to the first URL you stored in the session with Jaime's method. For example, let's say I have objects Apple and Orange. I edit Apple and session[:return_to] gets set to the referer of that action. When I go to edit Oranges using the same code, session[:return_to] will not get set because it is already defined. So when I update the Orange, I will get sent to the referer of the previous Apple#edit action.
This is how we do it in our application
def store_location
session[:return_to] = request.fullpath if request.get? and controller_name != "user_sessions" and controller_name != "sessions"
end
def redirect_back_or_default(default)
redirect_to(session[:return_to] || default)
end
This way you only store last GET request in :return_to session param, so all forms, even when multiple time POSTed would work with :return_to.
In rails 5, as per the instructions in Rails Guides, you can use:
redirect_back(fallback_location: root_path)
The 'back' location is pulled from the HTTP_REFERER header which is not guaranteed to be set by the browser. Thats why you should provide a 'fallback_location'.
request.referer is set by Rack and is set as follows:
def referer
#env['HTTP_REFERER'] || '/'
end
Just do a redirect_to request.referer and it will always redirect to the true referring page, or the root_path ('/'). This is essential when passing tests that fail in cases of direct-nav to a particular page in which the controller throws a redirect_to :back
For those who are interested, here is my implementation extending MBO's original answer (written against rails 4.2.4, ruby 2.1.5).
class ApplicationController < ActionController::Base
after_filter :set_return_to_location
REDIRECT_CONTROLLER_BLACKLIST = %w(
sessions
user_sessions
...
etc.
)
...
def set_return_to_location
return unless request.get?
return unless request.format.html?
return unless %w(show index edit).include?(params[:action])
return if REDIRECT_CONTROLLER_BLACKLIST.include?(controller_name)
session[:return_to] = request.fullpath
end
def redirect_back_or_default(default_path = root_path)
redirect_to(
session[:return_to].present? && session[:return_to] != request.fullpath ?
session[:return_to] : default_path
)
end
end
link_to 'get me back', :back
The symbol :back is your swiss army knife.
I wonder if this will work
def edit
if request.referer != request.original_url
#return_here = request.referer
end
end
and use #return_here as a hidden value in the submit form.
of course reloading will kill this so just go back to a default fall back as needed.

Resources