How to escape this redirect loop? - ruby-on-rails

I'm trying to make all a user's illicit attempts to see other users' show page redirect instead to their own show page.
None of my attempts in the else section of this code do the job though.
class UsersController < ApplicationController
def show
if params[:id] == current_user.id.to_s
liked_bookmark_ids = current_user.likes.pluck(:bookmark_id)
liked_bookmarks = Bookmark.where(id:liked_bookmark_ids)
liked_topic_ids = liked_bookmarks.pluck(:topic_id)
#liked_topics = Topic.where(id:liked_topic_ids).order('topics.name')
else
# redirect_to :controller => 'users', :id => current_user.id # gives a screwy url
# redirect_to user_path, :id => 6 # causes a redirect loop
# redirect_to :back # causes a redirect loop
end
end
end
What's the right way to go about it?

redirect_to user_path(current_user)

I think a better approach would be to use a filter to check if it's a request by a current user or not. You can do something like this:
class UsersController < ApplicationController
before_action :check_current_user, only: :show
def show
liked_bookmark_ids = current_user.likes.pluck(:bookmark_id)
liked_bookmarks = Bookmark.where(id:liked_bookmark_ids)
liked_topic_ids = liked_bookmarks.pluck(:topic_id)
#liked_topics = Topic.where(id:liked_topic_ids).order('topics.name')
end
private
def check_current_user
redirect_to current_user, notice: "Not authorized" if params[:id] != current_user.id.to_s
end
end

Related

Issue with before_filter

Please help me try and understand what is happening here:
I need to approve a nested snippet but when I do it says it cannot find book. I think it may be an issue with the routes because the URL in the browser doesn't match the rake routes.
If someone could hold my hand and explain this as you would to a child :)
Couldn't find Book without an ID
Below is the controller with snippets#approve and the before_filter.
class SnippetsController < ApplicationController
before_filter :authenticate_user!
before_filter :find_book
def create
#raise params.inspect
#snippet = #book.snippets.create(params[:snippet])
#snippet.user = current_user
if #snippet.save
redirect_to #book
flash[:success] = "Snippet submitted and awaiting approval."
else
flash[:base] = "Someone else has submitted a snippet, please try again later"
redirect_to #book
end
end
def approve
#raise params.inspect
#snippet = #book.snippets.find(params[:id])
#snippet.update_attribute(:approved, true)
redirect_to admins_path
end
def edit
#snippet = #book.snippets.find(params[:id])
end
def update
#snippet = #book.snippets.find(params[:id])
respond_to do |format|
if #snippet.update_attributes(params[:snippet])
format.html { redirect_to #book, notice: 'Comment was successfully updated.' }
else
format.html { render action: "edit" }
end
end
end
private
def find_book
#raise params.inspect
#book = Book.find(params[:book_id])
end
end
Now I understand that since I'm doing a post my rake routes says this.
/books/:book_id/snippets/:id(.:format)
Here is the routes for the custom route:
active_snippet POST /snippets/:id/activate(.:format)
This is my custom routes for book && snippet :approval
post "books/:id/activate" => "books#approve", :as => "active_book"
post "snippets/:id/activate" => "snippets#approve", :as => "active_snippet"
I've currently got this in my browser ../snippets/2/activate
Erm.... Not sure if I'm thinking correctly.
You're sending a POST request to snippets/:id/activate which calls snippets#approve.
There is a before_filter on the entire SnippetsController that calls find_book which executes #book = Book.find(params[:book_id]). Because your path is snippets/:id/activate, params[:book_id] is nil and hence you are getting that error.
You need to either change your snippets#approve path to include the book_id, or pass the book_id as a POST param so that your before filter has access to it.

Ruby on rails controller code, needs refactor best way to approach for more dry?

I have a welcome wizzard that builds a user profile when first login.Problem is it is quite messy implemented but I have tried to refactor it several times and rewrite it but cannot comeup with something better then below.
In ideal world it would be all inside welcome_controller.rb but this have caused much headache so Now i rewrote the update method for profile_controller instead.
Any thoughts on how to improve this make it more dry and clean? Would love to recieve some good input on this and thoughts perhaps to move all update stuff to welcome controller instead?
WelcomeController:
class WelcomeController < ApplicationController
before_filter :authenticate_user!
before_filter :load_step
layout "welcome"
def sub_layout
"center"
end
def edit
# form updates post to edit since
# profile is non existant yet
params[:step] = "photos" unless params[:step]
#photos = Photo.where(:attachable_id => current_user.id)
#profile = Profile.where(:user_id => current_user.id).first
#photo = Photo.new
if ["photos", "basics", "details", "test"].member?(params[:step])
# force rendering the correct step
case current_user.profile.step
when 1
render :template => "/profiles/edit/edit_photos", :layout => "welcome"
when 2
render :template => "/profiles/edit/edit_basics", :layout => "welcome"
when 3
render :template => "/profiles/edit/edit_details", :layout => "welcome"
when 4
render :template => "/profiles/edit/edit_test", :layout => "welcome"
end
else
render :action => "/profiles/edit/edit_photos"
end
end
def load_step
redirect_to root_path if current_user.profile.complete
case current_user.profile.step
when 1
redirect_to "/welcome" unless params[:controller] == "welcome"
when 2
redirect_to "/welcome/basics" unless params[:controller] == "welcome" && params[:action] == "edit" && params[:step] == "basics"
when 3
redirect_to "/welcome/details" unless params[:controller] == "welcome" && params[:action] == "edit" && params[:step] == "details"
when 4
redirect_to "/welcome/test" unless params[:controller] == "welcome" && params[:action] == "edit" && params[:step] == "test"
end
end
end
ProfileController:
class ProfileController < ApplicationController
...
def update
#profile = Profile.find(params[:id])
#tags = Session.tag_counts_on(:tags)
#profile.form = params[:form]
#match = Match.where(:user_id => current_user.id).first
authorize! :update, #profile
respond_to do |format|
if #profile.update_attributes(params[:profile])
if current_user.profile.complete
format.html { redirect_to "/profiles/#{ current_user.username }/edit/#{ #profile.form }", notice: t('notice.saved') }
else
case current_user.profile.step
when 1
current_user.profile.update_attributes(:step => 2)
format.html { redirect_to "/welcome/basics", notice: t('notice.saved') }
when 2
current_user.profile.update_attributes(:step => 3)
format.html { redirect_to "/welcome/details", notice: t('notice.saved') }
when 3
current_user.profile.update_attributes(:step => 4)
format.html { redirect_to "/welcome/test", notice: t('notice.saved') }
end
end
else
if current_user.profile.complete
format.html { render action: "/edit/edit_" + params[:profile][:form], :what => #profile.form }
else
case current_user.profile.step
when 1
current_user.profile.update_attributes(:step => 2)
format.html { redirect_to "/welcome/basics", notice: t('notice.saved') }
when 2
current_user.profile.update_attributes(:step => 3)
format.html { redirect_to "/welcome/details", notice: t('notice.saved') }
when 3
current_user.profile.update_attributes(:complete => 1)
format.html { redirect_to root_path }
end
end
end
end
end
...
end
Views are in /profiles/edit/*
Wizards are notoriously difficult to get right and I've never seen an implementation that fully satisfied me. I usually go with so called "form objects" and create a restful controller for each step.
There is an excellent (but paid) Railscast on the subject.
The gist is this: You make an object that quacks just like a regular ActiveRecord model, by using ActiveModel.
For instance:
class Welcome::BasicInformation
include ActiveModel::Validations
include ActiveModel::Conversion
extend ActiveModel::Naming
def persisted?
false
end
def initialize(user)
#user = user
end
attr_reader :user
delegate :some_field, :some_other_field, to: :user
validates_presence_of :some_field
def save(params)
user.some_field = params[:some_field]
user.some_other_field = params[:some_other_field]
if valid?
user.step = 2
user.save
end
end
def photo
#photo ||= Photo.new
end
def profile
#profile ||= user.profiles.first
end
end
You'd basically create a model like this for every step.
Then you can create controllers for each step, with a specialized ApplicationController for all the steps:
class Welcome::ApplicationController < ::ApplicationController
layout "welcome"
before_filter :authentice_user!
end
And for each step:
class Welcome::BasicInformationsControlller < Welcome::ApplicationController
def new
#step = Welcome::BasicInformation.new(current_user)
end
def create
#step = Welcome::BasicInformation.new(current_user)
if #step.save(params[:welcome_basic_information])
redirect_to welcome_some_other_step_path, notice: "Yay"
else
render :new
end
end
end
And create a route for each step:
namespace :welcome do
resource :basic_information, only: [:new, :create]
resource :some_other_step, only: [:new, :create]
end
This only leaves some automatic redirects to do, like prohibiting users from going to steps that they're not yet allowed to visit. This might not be as important now that you're using separate URLs for each step.
You can store information about which step to visit in the form objects:
class Welcome::BasicInformation
# ...
def allowed?
user.profile.step == 1
end
end
And then refactor the controllers a bit:
class Welcome::BasicInformationsController < Welcome::ApplicationController
before_filter :allowed?
def new
end
def create
if step.save(params[:welcome_basic_information])
redirect_to welcome_some_other_step_path, notice: "Yay"
else
render :new
end
end
private
def step
#step ||= Welcome::BasicInformation.new(current_user)
end
helper_method :step
def allowed?
redirect_to previous_step_path unless step.allowed?
end
end
This might not be shorter, but I do like how flexible it is, how focussed each step is, how you can do different validations on each step and so on. Each controller/model combination is very easy to follow and will be understandable for others.
There are a couple of things I'd do, but first some thoughts.
Sometimes you have to break restfullness a little to make code more readable. That's the case
It's not a good manner to redirect between controllers as you do in here
So, what I'd do.
Put all the code concerning those steps in a single controller (profile preferably) and adjust url with routing.
Create a single show and single save action
If I understand properly the step that will be shown to user depends ONLY on what User#step is set on current_user. Threfore I think there's really no need to pass any url variables, you can get current/next step from current_user.
Code refactored (may be some errors, didn't test that)
All in ProfileController
def edit
#profile = Profile.find(current_user.id)
#next_step = current_user.step.to_i + 1 # I imply that there's just single permissable next step
render :template => "/profiles/edit/#{#next_step}", :layout => "welcome"
end
def update
#profile = Profile.find(params[:id])
authorize! :update, #profile
if #profile.update_attributes(params[:profile])
# you should pass step number in params so I get's updated by default.
redirect_to "/welcome/basics", notice: t('notice.saved')
else
end
end

return redirect_to in private controller method

Preface: I'm using devise for authentication.
I'm trying to catch unauthorized users from being able to see, edit, or update another user's information. My biggest concern is a user modifying the form in the DOM to another user's ID, filling out the form, and clicking update. I've read specifically on SO that something like below should work, but it doesn't. A post on SO recommended moving the validate_current_user method into the public realm, but that didn't work either.
Is there something obvious I'm doing wrong? Or is there a better approach to what I'm trying to do, either using devise or something else?
My UsersController looks like this:
class UsersController < ApplicationController
before_filter :authenticate_admin!, :only => [:new, :create, :destroy]
before_filter :redirect_guests
def index
redirect_to current_user unless current_user.try(:admin?)
if params[:approved] == "false"
#users = User.find_all_by_approved(false)
else
#users = User.all
end
end
def show
#user = User.find(params[:id])
validate_current_user
#user
end
def new
#user = User.new
end
def edit
#user = User.find(params[:id])
validate_current_user
#user
end
def create
#user = User.new(params[:user])
respond_to do |format|
if #user.save
format.html { redirect_to #user, :notice => 'User was successfully created.' }
else
format.html { render :action => "new" }
end
end
end
def update
#user = User.find(params[:id])
validate_current_user
respond_to do |format|
if #user.update_attributes(params[:user])
format.html { redirect_to #user, :notice => 'User was successfully updated.' }
else
format.html { render :action => "edit" }
end
end
end
private
def redirect_guests
redirect_to new_user_session_path if current_user.nil?
end
def validate_current_user
if current_user && current_user != #user && !current_user.try(:admin?)
return redirect_to(current_user)
end
end
end
The authenticate_admin! method looks like this:
def authenticate_admin!
return redirect_to new_user_session_path if current_user.nil?
unless current_user.try(:admin?)
flash[:error] = "Unauthorized access!"
redirect_to root_path
end
end
EDIT -- What do you mean "it doesn't work?"
To help clarify, I get this error when I try to "hack" another user's account:
Render and/or redirect were called multiple times in this action.
Please note that you may only call render OR redirect, and at most
once per action. Also note that neither redirect nor render terminate
execution of the action, so if you want to exit an action after
redirecting, you need to do something like "redirect_to(...) and
return".
If I put the method code inline in the individual controller actions, they do work. But, I don't want to do that because it isn't DRY.
I should also specify I've tried:
def validate_current_user
if current_user && current_user != #user && !current_user.try(:admin?)
redirect_to(current_user) and return
end
end
If you think about it, return in the private method just exits the method and passes control back to the controller - it doesn't quit the action. If you want to quit the action you have to return again
For example, you could have something like this:
class PostsController < ApplicationController
def show
return if redirect_guest_posts(params[:guest], params[:id])
...
end
private
def redirect_guest_post(author_is_guest, post_id)
redirect_to special_guest_post_path(post_id) if author_is_guest
end
end
If params[:guest] is present and not false, the private method returns something truthy and the #show action quits. If the condition fails then it returns nil, and the action continues.
You are trying and you want to authorize users before every action. I would suggest you to use standard gems like CanCan or declarative_authorization.
Going ahead with this approach you might end up reinventing the wheel.
In case you decide on using cancan, all you have to do is add permissions in the ability.rb file(generated by rails cancan:install)
can [:read,:write,:destroy], :role => "admin"
And in the controller just add load_and_authorize_resource (cancan filter). It will check if the user has permissions for the current action. If the user doesnt have persmissions, then it will throw a 403 forbidden expection, which can be caught in the ApplicationController and handled appropriately.
Try,
before_filter :redirect_guests, :except => [:new, :create, :destroy]
should work.
This is because you are using redirect twice, in authenticate_admin! and redirect_guests for new, create and destroy actions.
"Render and/or redirect were called multiple times in this action. Please note that you may only call render OR redirect, and at most once per action."
That's the reason of the error. In show method, if you are neither the owner of this account nor the admin, you are facing two actions: redirect_to and render
My suggestion is to put all of the redirect logic into before_filter

How can I detect if it's submit from new or edit?

This before_filter validates before update or create record if captcha is correct.
When it's incorrect, it takes me back to previous page but all of the input data will be gone....
How can I remain the input data that was typed in at previous page?
I'd like to use before_filter and apply these 2 actions 'update' and 'create'.
It should detect where the submit is come from and switches where to re-render 'new' or 'edit'
before_filter :simple_captcha_check, :only => [:update, :create]
def simple_captcha_check
if !simple_captcha_valid?
flash[:error] = 'Wrong Captcha!'
redirect_to :back
end
end
Assuming you're creating/updating an User model, your code can look like this:
def simple_captcha_check
if !simple_captcha_valid?
flash[:error] = 'Wrong Captcha!'
if request.put? # We came from an edit request
#user = User.find(params[:id])
#user.attributes = params[:user]
render :action => :edit
elsif request.post? # We came from a new request
#user = User.new params[:user]
render :action => :new
end
end
end

Rails: redirect two pages back

I'm trying to get my head around this one:
Say you have two models where:
:bar has_many :foos
And you have a url like this: http://myapp.com/24-name-of-the-bar-to-param/foos/new
On my site this page shows a lot of information about the bar which users are going to create a foo for. So, even if a user isn't logged in the user will still be able to see the info.
Currently, when the user is logged in, a form to create a new foo is on the left hand side of the web page. When the user isn't logged in it says "Please login or register"
The form explains a lot about how my app works, so I'd like to change it so that even if a user isn't logged in the form will display, and if they click submit it will take them to the login_path and then when they login, back to the path where the submitted the form.
I'm running into this problem: Currently I have a login_required method in my application controller like this:
def store_location
session[:return_to] = request.request_uri
end
def login_required
unless current_user || admin?
store_location
flash[:notice] = "Please log in"
redirect_to login_path and return false
end
end
This login required action is called on the create action of the foo. When I click submit on the form it takes me to http://myapp.com/foos instead of http://myapp.com/24-name-of-the-bar-to-param/foos/new
I assume this is because the login required function is called on the create action and not the new action.
Any ideas?
UPDATE as per request here is the controller code and callbacks:
before_filter :find_bar, :except => [:index, :edit, :update]
before_filter :login_required, :only => [:create]
ssl_required :edit, :update
def new
#foo = Foo.new :amount => "0.00"
#foos = Foo.find(:all, :conditions => ["bar_id = ?", #bar.id], :order => "created_at DESC").paginate :page => params[:page], :per_page => 10
#foos_all = Foo.find(:all, :conditions => ["hatlink_id = ?", #hatlink.id], :order => "created_at DESC")
#current_user = current_user
#topfooers = User.bar_amount(#bar, nil)
#average_foo = #bar.foos.average('amount')
end
def create
#foo = #current_user.foos.build params[:foo]
if (#bar.foos << #foo)
flash[:notice] = "Thank you for fooing!"
redirect_to new_bar_foo_path(#bar)
else
render :action => :new
end
end
private
def find_bar
#bar_id = params[:bar_id]
return(redirect_to(categories_path)) unless #bar_id
#bar = Bar.find(#bar_id)
end
You could store the referring url (if it's present) & redirect to that page if the request was a POST or PUT. Something like:
def store_location
if request.post? || request.put?
session[:return_to] = request.env['HTTP_REFERER']
else
session[:return_to] = request.request_uri
end
end
Silly of me to come up with a solution just five minutes after posting the question. Oh well, here's what I did (and it works).
In the "new" action of foo I added these lines
if !current_user
store_location
end
In the login required method I have added this:
if params[:controller] == "foos" && params[:action] == "create"
#Took out the line for storing the location in this method.
flash[:notice] = "Please log in"
redirect_to login_path and return false

Resources