Browser URL changes after http post fail - ruby-on-rails

I have a login page on which the authentication can be successful or not. Here is the page new.html.erb:
<%=form_with scope: :session, url: sessions_path, local: true, html: {class: "login-form"} do |f| %>
<%= f.label :email, t("session.new.email") %>
<%= f.email_field :email %>
<%= f.label :password, t("session.new.password") %>
<%= f.password_field :password %>
<%= f.submit t('session.new.login'), class: "submit" %>
<% end %>
It is associated to a sessions_controller.rb, which is the following:
class SessionsController < ApplicationController
def create
# Find the user with the matching email
user = User.find_by(email: params[:session][:email].downcase)
# Check the user exists in DB and that the provided password matches
if user && user.authenticate(params[:session][:password])
# Log the user through the session helper
log_in user
# Redirect to the hive
redirect_to ideas_path
else
# The authentication failed. Display an error message
flash.now[:error] = I18n.t('session.new.invalid_credentials')
# The render is done to reinitiate the page
render :new
end
end
end
In my routes.rb, I just have for this purpose:
resources :sessions
When executing rails routes, I have the following declared routes:
Now my problem is on the login fail. In my controller, in this case, I add a message in the flash messages and then re-render the same page new.html.erb. But in the browser, the login request POST has been sent on the url /sessions. The problem is the current URL on my browser becomes /sessions instead of staying on /sessions/new. This is as if the POST request changed the URL in my browser. But this is in fact just an AJAX request, isn't it?
I have found this blog post that wonders the same about this phenomenon (I'm not the author)
I have found a workaround, but I'd prefer avoid using it and understand the bevahior. If i replace my routes by the following, this works:
get '/login', to: 'sessions#new'
post '/login', to: 'sessions#create'
I can understand why this works: the get and post url are the same, so the browser doesn't change its URL.
Have you any idea?
EDIT:
I finally found a solution. I'm not sure this is the "rails way", but this works as expected. I have just changed the controller to do a redirection to the same page, with a flash request to transmit the login fail information:
def create
# Find the user with the matching email
user = User.find_by(email: params[:session][:email].downcase)
# Check the user exists in DB and that the provided password matches
if user && user.authenticate(params[:session][:password])
# Log the user through the session helper
log_in user
# Redirect to the hive
redirect_to ideas_path
else
# The authentication failed. Display an error message through a flash
# message after redirect to the same page
redirect_to new_session_path, alert: I18n.t('session.new.invalid_credentials')
end
end

When the form gets submitted the browser performs a regular HTTP POST requsest to the /sessions endpoint. No AJAX there.
The way your routes are configured this POST request will be handled by your sessions#create action.
Notice the code there. You'll see that the happy path (successful login) calls redirect_to. In case of login errors though, the controller calls render.
The difference is that in the first case the response is a 302 Redirect which the browser follows. That's why you see the URL changing in the browser. In the second case the response is just 200 OK with a bunch of HTML for the browser to render. The URL won't change because the browser wasn't instructed to navigate elsewhere.
Here's an extensive explanation of how redirects work in the browser in case you're interested.

Related

No route matches [GET] when trying to [PATCH] with link_to

I want to send clients that did not complete a checkout an email with a magic link that will log them in before hitting an update action in a controller.
I'm sending the following link in the email body:
<%= link_to(
"Continue to checkout",
"#{checkout_url(host: #account.complete_url, id: #user.current_subscription_cart)}?msgver=#{#user.create_message_verifier}",
method: :patch,
subscription_cart: { item_id: #item_id },
) %>
My checkouts_controller has an update action:
def update
# update cart with item_id param and continue
end
And my routes look like this:
resources :checkouts, only: [:create, :update]
which gives the following update route:
checkout_path PATCH /checkouts/:id(.:format) checkouts#update
The link_to in the email body produces a link with a data-method="patch" property
<a data-method="patch" href="https://demo.test.io/checkouts/67?msgver=TOKEN">Continue to checkout</a>
=> https://demo.test.io/checkouts/67?msgver=TOKEN
but when I click on it I get the following error:
No route matches [GET] "/checkouts/67"
Why is it attempting a GET request when I'm specifying method: :patch ?
As pointed out by #AbM you need to send out a link to a route that responds to GET requests. Emails clients are unlikely to let you run JS or include forms in the email body - so you shouldn't assume that you'll be able to send anything but GET.
If you want an example of how this can be done you don't have to look further then the Devise::Confirmable module that solves pretty much the exact same problem:
Prefix Verb Method URI Pattern Description
new_user_confirmation GET /users/confirmation/new Form for resending the confirmation email
user_confirmation GET /users/confirmation The link thats mailed out with a token added - actually confirms the user
POST /users/confirmation Resends the confirmation email
The beauty of this design is that users confirmation are modeled as a RESTful resource even if no separate model exists.
In your case the implementation could look something like:
resources :checkouts do
resource :confirmation, only: [:new, :create, :show]
end
# Handles email confirmations of checkouts
class ConfirmationsController < ApplicationController
before_action :set_checkout
# GET /checkouts/1/confirmation/new
# Form for resending the confirmation email
def new
end
# POST /checkouts/1/confirmation
# Resends the confirmation email - because shit happens.
def create
#todo generate new token and resend the confirmation email
end
# GET /checkouts/1/confirmation&token=jdW0rSaYXWI7Ck_rOeSL-A
# Confirms the checkout by verifying that a valid token is passed
def show
if #checkout.valid_token?(params[:token])
#checkout.confirm!
redirect_to '/whatever_the_next_step_is'
else
flash.now('Invalid token')
render :new, status: :unauthorized
end
end
private
def set_checkout
#checkout = Checkout.find(params[:checkout_id])
end
end
Here we are taking a slight liberty with the rule that that GET requests should always be idempotent as clicking the link actually updates the checkout. Its a fair tradeoff though as it requires one less click from the user.
If you want to maintain that idempotency you could send a link to the edit action which contains a form to update the confirmation. Devise::Invitable does this.

How to redirect to login page/ or making user login successful after confirm email in devise token auth

I am using devise token auth with React frontend and now I am making user confirmable through email. Email is being sent but somehow it generates a wrong URL.
The URL that is generated is.
http://localhost:3001/auth/confirmation.4?confirmation_token=AoWH2yYxuHHnBzJRF746
My routes.
new_user_confirmation GET /auth/confirmation/new(.:format) users/confirmations#new
user_confirmation GET /auth/confirmation(.:format) users/confirmations#show
POST /auth/confirmation(.:format) users/confirmations#create
My app/views/devise/mailer/confirmation_instructions.html.erb
<p>Welcome <%= #email %>!</p>
<p>You can confirm your account email through the link below:</p>
<p><%= link_to 'Confirm my account', user_confirmation_url(confirmation_token: #token) %></p>
After clicking confirm my account, it takes me to my application successfully, but with the wrong URL. I will be happy if I achieve one of the following.
User is automatically logged in after confirming.
It goes to login page after confirm account and after that he gives thee credentials and login.
For 1, I have overridden the confirmations_controller.rb like.
# frozen_string_literal: true
class Users::ConfirmationsController < Devise::ConfirmationsController
# The path used after confirmation.
def after_confirmation_path_for(resource_name, resource)
sign_in(resource) # In case you want to sign in the user
root_path
end
end
In routes.rb
mount_devise_token_auth_for 'User', at: 'auth', controllers: { confirmations: 'users/confirmations' }
First, the url helper that you use in the email doesn't need to receive the #resource - this is why it generates the url with the .4. It should rather be
<p><%= link_to 'Confirm my account', user_confirmation_url(confirmation_token: #token) %></p
Second, according to this tutorial, you can redirect the user after confirmation, like you tried already.
https://github.com/heartcombo/devise/wiki/How-To:-Add-:confirmable-to-Users#redirecting-user
Just give a path in your app after you signed in the user
def after_confirmation_path_for(resource_name, resource)
sign_in(resource) # In case you want to sign in the user
your_new_after_confirmation_path
end
So no need to go to session_new again after signing in.

How to stop form from being re-submitted from particular url again

I have a path http://www.example.com/confirm_post/12, which submits a post request to posts#confirm_post. When a user successfully submits a form from that path, is there a way to stop the user from visiting the same path again (it could be from the browser back button or the user hitting the url manually)?
When a user successfully submits a form from that path, is there a way
to stop the user from visiting the same path again (it could be from
the browser back button or the user hitting the url manually)?
The browser does not treat POST and GET requests equally. POST requests are non-idempotent so the browser will in most cases actually warn you if you try to back up.
However you can't actually prevent the browser from resending any request but this usually handled by just adding a simple conditional in the controller or a model validation such as a uniqueness validation. Your controller must be able to handle repeated or unauthorized requests and return a response without unintended side effects.
In this case I would handle it like so:
resources :posts do
patch :confirm
end
Prefix Verb URI Pattern Controller#Action
post_confirm PATCH /posts/:post_id/confirm(.:format) posts#confirm
...
# on the edit or show view
<% unless #post.confirmed? %>
<%= button_to("Confirm", post_confirm_path(#post), method: :patch) %>
<% end %>
class PostsController < ApplicationController
# ...
# PATCH /posts/:post_id
def confirm
#post = Post.find(params[:post_id])
if #post.confirmed?
# we return since we want to bail early
redirect_to #post, error: "Post already confirmed." and return
end
if #post.update(confirmed: true)
redirect_to #post, success: "Post confirmed."
else
redirect_to #post, error: "Post could not be confirmed"
end
end
# ...
end
Note that this uses the PATCH HTTP verb instead of POST since we are updating a resource - not creating a new resource.

No action respond to id in rails routing problem

I am adding a functionality of resetting password in my rails application
In my users page , i have all the users listing
If i am logging in as a admin below the users a link to reset password to users are available which will set a default password to them
<%= link_to "Reset Password",reset_password_user_path %>
In my controller users
def reset_password
#user = params[:user]
puts "which user #{#user}"
#user.password = "12345"
if #user.save
flash[:notice] = "Password successfully reseted"
redirect_to user_path
else
flash[:error]= "Password not reste!"
redirect_to user_path
end
end
In my routes
map.resources :users, :member => {:reset_password => :post}
When i run my users page , i am getting the error as below
reset_password_user_url failed to generate from {:action=>"reset_password", :controller=>"users"} - you may have ambiguous routes, or you may need to supply additional parameters for this route. content_url has the following required parameters: ["users", :id, "reset_password"] - are they all satisfied?
WHen i gave the link as
<%= link_to "Reset Password",reset_password_user_path(user) %>
then i am getting the error as No action respond to 2
Processing UsersController#2 (for 127.0.0.1 at 2011-03-14 11:38:33) [GET]
Parameters: {"action"=>"2", "id"=>"reset_password", "controller"=>"users"}
How to resolve this..
Alternatively <%= link_to "Reset Password", reset_password_user_path(user), :method => "post" %> assuming you have Prototype or jQuery.
This way you are still posting to the action as it makes changes to the user.
Try with get method instead of post in routes.

Preserving form submission through log in / sign up in Rails

Say I have a site like this (generic Q&A site) in Rails and I wanted this "ask" page w/ a text box to be the first page a user sees, even if he's not logged in. He enters a question, and on the 'new' method I check that he's not logged in, and bounced him to /session/new, where he can either log in or create a new account. Question is, how do I (and what is the best way to) preserve that question that he initially asked all through this process?
I'm understanding the flow of action described in the question to be
user is presented with a form
user is redirected to log in page on submit
user is redirected back to form on successful log in
repopulate form on load (Question asks how to do this step)
user finally submits their form.
With steps 2-4 omitted if the user is logged in.
I'm sorry, but I see your question more as a symptom of an underlying UI issue than a rails question.
If only logged in users can post questions, then why display the text box?
If a user is going to have log in any way, why not get that out of the way first. An even better solution is to integrate the log in and form.
Something like this in the view:
<% form_for :question do |form| %>
<% unless logged_in? %>
<% fields_for :session do |session_form|%>
<%= session_form.label :login %>
<%= session_form.text_field :login %>
<%= session_form.label :password %>
<%= session_form.password_field :password %>
<%end%>
<%end%>
<%= form.text_area :question %>
<%end%>
And in the controller
def new
...
unless params[:session].nil?
self.current_user = User.authenticate(params[:session][:login], params[:session][:password])
end
if logged_in?
flash[:notice] = "Logged in successfully"
else
flash[:error] = "Incorrect username and or password."
end
if logged_in? && #question.save
.... process successful entry
else
... process unsuccessful entry
end
end
Edit: Mohamad's raises the question of reusing this pattern across multiple controllers and forms. So the answer was updated to address reuse of this pattern.
To simplify this for reuse, you could put this block in a helper function that is referenced in the before_filter for actions that require it.
def login
unless params[:session].nil?
self.current_user = User.authenticate(params[:session][:login], params[:session][:password])
if logged_in?
flash[:notice] = "Logged in successfully"
else
flash[:error] = "Incorrect username and or password."
end
end
end
as in:
before_filter :login => :only [:new , :edit, :update, :delete]
On the view side, it shouldn't be too hard to construct a new variant of form_for that embeds the session parameters. Maybe form_for_with_session?
As for handling an unsuccessful response, I would suggest helper function that takes a block of code. Sorry I don't have time to write out or test one for you.
You keep it in the session. So after logging in, when the user goes back to asking his question, you see there's already something in session.
And you can directly display it.
def create
if current_user # Implement this method in your auth framework
#question = Question.new(params[:question] || session.delete[:question])
# (the usual stuff you'd do to save)
else
session[:question] = params[:question]
redirect_to :controller => :sessions, :action => "new"
end
end
Then, after your user creation and authentication stuff is all done in your login action, just make sure you POST back to this create action if session[:question] is defined.

Resources