I have the following code that allows a user to request a password reset in an AJAX form:
<%= form_for(resource, :as => resource_name, :url => password_path(resource_name), :html => { :method => :post },:remote =>'true') do |f| %>
<%= devise_error_messages! %>
<div><%= f.label :email %><br />
<%= f.email_field :email %></div>
<div><%= f.submit "Send me reset password instructions" %></div>
<% end %>
This is allowing the behavior whereby if the user clicks the button repeatedly, or presses "enter" repeatedly, before the server can provide a response, a corresponding # of password reset emails are being sent.
The following is within devise/password_controller.rb
def create
self.resource = resource_class.send_reset_password_instructions(resource_params)
if successfully_sent?(resource)
flash[:notice] = "You will receive an email with instructions about how to reset your password in a few minutes."
respond_to do |format|
format.html #responds with default html file
format.js
end
else
respond_to do |format|
format.html #responds with default html file
format.js{ render :js => "$(\".deviseErrors\").html(\"<span class='login-error'>Could not send reset instructions to that address.</span>\");" } #this will be the javascript file we respond with
end
end
end
Is there a way to only respond to the first submission?
Thanks
I think this idea is pretty useful if you're dealing with customers, who instead of waiting for the email will re-request 3 or 4 times, at which point the first one might turn up, but will by now have an invalid link. Hysteresis or just re-sending the same link are nice to have, but as I mentioned above it's no longer (?) in the devise code, which just handles expiring old reset requests, not limiting the sending of new ones.
I've gone with a simplified version of trh's idea, which selectively forwards to the original devise code. In case there's been a request sent within the last hour it just pretends it's sent it again, and assumes that Mailgun or whoever you are using will get the message where it needs to go.
class Members::PasswordsController < Devise::PasswordsController
def create
self.resource = resource_class.find_by_email(resource_params[:email])
if resource && (!resource.reset_password_sent_at.nil? || Time.now > resource.reset_password_sent_at + 1.hour)
super
else
flash[:notice] = I18n.t('devise.passwords.send_instructions')
respond_with({}, location: after_sending_reset_password_instructions_path_for(resource_name))
end
end
end
Behaves like this:
specify "asking twice sends the email once only, until 1 hour later" do
member = make_activated_member
ActionMailer::Base.deliveries.clear
2.times do
ensure_on member_dashboard_path
click_on "Forgotten your password?"
fill_in "Email", :with => member.email
click_on "Send me password reset instructions"
end
# see for mail helpers https://github.com/bmabey/email-spec/blob/master/lib/email_spec/helpers.rb
expect(mailbox_for(member.email).length).to eq(1)
expect(page).to have_content(I18n.t('devise.passwords.send_instructions'))
Timecop.travel(Time.now + 2.hours) do
expect {
ensure_on member_dashboard_path
click_on "Forgotten your password?"
fill_in "Email", :with => member.email
click_on "Send me password reset instructions"
}.to change{mailbox_for(member.email).length}.by(+1)
end
end
Bonus points for updating it to re-send the original email with the same link, as in this test:
specify "asking twice sends the same link both times" do
member = make_activated_member
ActionMailer::Base.deliveries.clear
2.times do
visit member_dashboard_path
click_on "Forgotten your password?"
fill_in "Email", :with => member.email
click_on "Send me password reset instructions"
end
# see for mail helpers https://github.com/bmabey/email-spec/blob/master/lib/email_spec/helpers.rb
mails = mailbox_for(member.email)
expect(mails.length).to eq(2)
first_mail = mails.first
second_mail = mails.last
expect(links_in_email(first_mail)).to eq(links_in_email(second_mail))
end
I would recommend to use JavaScript to prevent multiple submissions.
$('form#reset_password').on('submit', function() {
$(this).find('input[type="submit"]').attr('disabled', 'disabled')
})
This will set the submit button as "disabled" status and user can't submit again.
Reference about form's disabled attribute: http://www.w3schools.com/tags/att_input_disabled.asp*
Add: Response to thr's answer
I browsed Devise source and found there should be a solution at model level. To set the max interval allowed between each resetting request, add such in resource model
class User < ActiveRecord::Base
def self.reset_password_with
1.day
# Determine the interval. Any time objects will do, say 1.hour
end
end
Then Devise::Models::Recoverable will check this value to decide if a token should be sent. I have not verified this but it should work.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With