Reputation: 1168
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
Upvotes: 4
Views: 3423
Reputation: 21
You can do something like this in Devise:
class User < ActiveRecord::Base
def send_reset_password_instructions
super unless reset_password_sent_at.present? && reset_password_sent_at > DateTime.now - 1.day
end
end
Where 1.day
is the interval between allowed password resets.
Upvotes: 2
Reputation: 24815
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.
Upvotes: 3
Reputation: 1068
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
Upvotes: 4
Reputation: 7339
If you're really just trying to keep people from double clicking submit, then restricting by javascript is the way to go as billy-chan suggested in his answer.
If you want to limit the amount of time between sending requests to a given use, then you can set the resource and wrap that functionality in an if statement checking the time stamp when the last password request was sent. Something like this
def create
self.resource = resource_class.find_by_email(resource_params[:email])
if resource.reset_password_sent_at.nil? || Time.now > resource.reset_password_sent_at + 5.minutes
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
else
flash[:error] = "Passwords can only be reset every 5 minutes."
respond_to do |format|
format.html #responds with default html file
format.js
end
end
end
Upvotes: 1