No route matches [GET] when trying to [PATCH] with link_to - ruby-on-rails

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.

Related

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.

How do I make a follow-up form private in Rails?

I want to send an email to customers that allows them to edit some of the information for their purchase, so my basic set-up is sending them a link to /follow_up_form/purchase.id.
This clearly won't work because we don't want anyone just typing in that URL and changing the user's information, but our site doesn't have any login necessary to make purchases.
Is there a way to autogenerate a secret URL, or pass through some sort of authenticity token? This feels like it should be simple but I don't have any good ideas.
Thanks!
You may use rails 5 has_secure_token and embed it into an URL as a parameter like /follow_up_form/purchase.id?tok='some_toke' And check this parameter when returning the form for edition. This will indeed make stuff more secure.
I would add a token column to the purchase, and set it after creation:
require 'secure_random'
class Purchase < ActiveRecord::Base
before_create :regenerate_token
def regenerate_token
self.token = SecureRandom.urlsafe_base64(24)
end
end
You can then setup follow ups just like you would any other nested resource:
resources :purchases do
resources :follow_ups, only: [:new, :create]
end
To create a link to the form where the user gives feedback use:
<%= link_to("Tell us what you think", new_purchase_follow_up_url(#purchase, token: #purchase.token)) %>
This adds an query string parameter to the URL containing the access token.
To authenticate in the controller we check if the access token matches what we have stored for the purchase:
class FollowUpsController < ApplicationConrtroller
before_action :set_purchase!
before_action :check_token!
# ...
private
def set_purchase!
#purchase = Purchase.find(params[:purchase_id])
end
def check_token!
unless params[:access_token] == #purchase.token
redirect_to root_path, error: 'You are not authorized' and return false
end
end
end
To pass the token between the new and create action add it to the form as a hidden input:
<%= form_for(#follow_up) do |f| %>
<%= hidden_field_tag :access_token, #purchase.token %>
<% end %>

Updating A Single Attribute in Rails from email

i'm having some issues trying to update an attribute outisde my web app (No route matches [GET] "/admin/justifications/19/approve").
The user should approve or reject a permission from their emails...
admin/justifications_controller.rb
class JustificationsController < BaseController
before_action :find_justification
# PATCH/PUT /admin/justifications/1/approve
def approve
#justification.approve
#justification.create_activity :approve, owner: current_user, recipient: #justification.user
redirect_to request.referer
end
# PATCH/PUT /admin/justifications/1/reject
def reject
#justification.reject
#justification.create_activity :reject, owner: current_user, recipient: #justification.user
redirect_to request.referer
end
routes
scope :admin, module: :admin do
resources :justifications, except: :all do
member do
patch :approve
patch :reject
end
end
...
end
Tho this works well in my web page, but it breaks when users try to open the generated links sent to their emails.
Is it something im missing here??
Any help would be great. Thnks!!
Your approve action is only available via PATCH or PUT and the link you press in your email sends the request via GET
There are lots of questions in SO asking how to send a different method than GET from the link you send in the email and the answer for that is: It is not possible. You have to open an GET action to be accessed from your email links.
There is no way to natively add links which send a PATCH request - Rails uses a data-method attribute together with some clever javascript to fake PATCH, PUT and DELETE requests.
However this only works if jquery-ujs is loaded in the client, which is problematic since many email clients and even webmail clients block emails from running javascript for security reasons.
What you need to do is add a GET route.

Rails singular route issue

I have an account/settings page people can visit to update their account. It's a singular resource, so they can (or should) only be able to update their own account settings. I'm running into a weird URL format when there are form errors displayed.
If they are on /account/settings/edit and try to submit the form with errors (not a valid email address, for example) they are redirected to /account/settings.1 where it shows them what went wrong (in our example, not a valid email address).
Everything "works" but I was wondering why there is a .1 being appended to the URL. I figured they would be sent back to account/settings or account/settings/edit where they can correct the error. Am I doing something wrong?
routes.rb
namespace :account do
resource :settings, :only => [:show, :edit, :update]
end
settings_controller.rb
def edit
#account = Account.find(session[:account][:id])
end
def update
#account = Account.find(session[:account][:id])
if #account.update_attributes(params[:account])
redirect_to account_settings_path
else
render 'edit'
end
end
rake routes
edit_account_settings GET /account/settings/edit(.:format) account/settings#edit
account_settings GET /account/settings(.:format) account/settings#show
account_settings PUT /account/settings(.:format) account/settings#update
Make sure you generate your paths using edit_account_settings_path, NOT edit_account_settings_path(#user). For singular resources you shouldn't pass in a resource, because, as you say, there is only one of them.

Rails redirect_to post method?

redirect_to :controller=>'groups',:action=>'invite'
but I got error because redirect_to send GET method I want to change this method to 'POST' there is no :method option in redirect_to what will I do ? Can I do this without redirect_to.
Edit:
I have this in groups/invite.html.erb
<%= link_to "Send invite", group_members_path(:group_member=>{:user_id=>friendship.friend.id, :group_id=>#group.id,:sender_id=>current_user.id,:status=>"requested"}), :method => :post %>
This link call create action in group_members controller,and after create action performed I want to show groups/invite.html.erb with group_id(I mean after click 'send invite' group_members will be created and then the current page will be shown) like this:
redirect_to :controller=>'groups',:action=>'invite',:group_id=>#group_member.group_id
After redirect_to request this with GET method, it calls show action in group and take invite as id and give this error
Couldn't find Group with ID=invite
My invite action in group
def invite
#friendships = current_user.friendships.find(:all,:conditions=>"status='accepted'")
#requested_friendships=current_user.requested_friendships.find(:all,:conditions=>"status='accepted'")
#group=Group.find(params[:group_id])
end
The solution is I have to redirect this with POST method but I couldn't find a way.
Ugly solution: I solved this problem which I don't prefer. I still wait if you have solution in fair way.
My solution is add route for invite to get rid of 'Couldn't find Group with ID=invite' error.
in routes.rb
map.connect "/invite",:controller=>'groups',:action=>'invite'
in create action
redirect_to "/invite?group_id=#{#group_member.group_id}"
I call this solution in may language 'amele yontemi' in english 'manual worker method' (I think).
The answer is that you cannot do a POST using a redirect_to.
This is because what redirect_to does is just send an HTTP 30x redirect header to the browser which in turn GETs the destination URL, and browsers do only GETs on redirects
It sounds like you are getting tripped up by how Rails routing works. This code:
redirect_to :controller=>'groups',:action=>'invite',:group_id=>#group_member.group_id
creates a URL that looks something like /groups/invite?group_id=1.
Without the mapping in your routes.rb, the Rails router maps this to the show action, not invite. The invite part of the URL is mapped to params[:id] and when it tries to find that record in the database, it fails and you get the message you found.
If you are using RESTful routes, you already have a map.resources line that looks like this:
map.resources :groups
You need to add a custom action for invite:
map.resources :groups, :member => { :invite => :get }
Then change your reference to params[:group_id] in the #invite method to use just params[:id].
I found a semi-workaround that I needed to make this happen in Rails 3. I made a route that would call the method in that controller that requires a post call. A line in "route.rb", such as:
match '/create', :to => "content#create"
It's probably ugly but desperate times call for desperate measures. Just thought I'd share.
The idea is to make a 'redirect' while under the hood you generate a form with method :post.
I was facing the same problem and extracted the solution into the gem repost, so it is doing all that work for you, so no need to create a separate view with the form, just use the provided by gem function redirect_post() on your controller.
class MyController < ActionController::Base
...
def some_action
redirect_post('url', params: {}, options: {})
end
...
end
Should be available on rubygems.

Resources