Devise / ActionMailer发送重复的电子邮件以进行注册确认

时间:2023-01-16 10:10:01

My rails application uses devise to handle registration, authentication, etc. I'm using the confirmable module. The bug is this– when a user registers with email, Devise is sending two confirmation emails with different confirmation links. One link works, the other directs the user to an error page.

我的rails应用程序使用devise来处理注册,身份验证等。我正在使用可确认模块。错误是这样的 - 当用户注册电子邮件时,Devise正在发送两封带有不同确认链接的确认电子邮件。一个链接有效,另一个链接将用户指向错误页面。

Devise spits out a message associated with the error: "Confirmation token is invalid" and takes the user to the Resend Confirmation Email page.

Devise吐出与错误相关的消息:“确认令牌无效”并将用户带到重新发送确认电子邮件页面。

I'm hosting with heroku and using sendgrid to send the emails. update: The bug also occurs on localhost.

我正在托管heroku并使用sendgrid发送电子邮件。更新:该bug也发生在localhost上。

I have no idea where the root of this bug is, and this might be more code than what you need to see:


models/user.rb

我不知道这个bug的根源在哪里,这可能比你需要看到的更多代码:models / user.rb

...

devise :database_authenticatable, :registerable, :omniauthable,
     :recoverable, :rememberable, :trackable, :validatable, 
     :confirmable, :authentication_keys => [:login]

...

## callbacks
after_create :account_created

# called after the account is first created
def account_created

  # check if this activiy has already been created
  if !self.activities.where(:kind => "created_account").blank?
    puts "WARNING: user ##{self.id} already has a created account activity!"
    return
  end

  # update points
  self.points += 50
  self.save

  # create activity
  act = self.activities.new
  act.kind = "created_account"
  act.created_at = self.created_at
  act.save

end

...

def confirmation_required?
  super && (self.standard_account? || self.email_changed)
end

...



controllers/registrations_controller.rb

class RegistrationsController < Devise::RegistrationsController
  def update
    unless @user.last_sign_in_at.nil?

      puts "--------------double checking whether password confirmation is required--"
      ## if the user has not signed in yet, we don't want to do this.

      @user = User.find(current_user.id)
      # uncomment if you want to require password for email change
      email_changed = @user.email != params[:user][:email]
      password_changed = !params[:user][:password].empty?

      # uncomment if you want to require password for email change
      # successfully_updated = if email_changed or password_changed

      successfully_updated = if password_changed
        params[:user].delete(:current_password) if params[:user][:current_password].blank?
        @user.update_with_password(params[:user])
      else
        params[:user].delete(:current_password)
        @user.update_without_password(params[:user])
      end

      if successfully_updated
        # Sign in the user bypassing validation in case his password changed
        sign_in @user, :bypass => true
        if email_changed
          flash[:blue] = "Your account has been updated! Check your email to confirm your new address. Until then, your email will remain unchanged."
        else
          flash[:blue] = "Account info has been updated!"
        end
        redirect_to edit_user_registration_path
      else
        render "edit"
      end
    end
  end
end



controllers/omniauth_callbacks_controller

class OmniauthCallbacksController < Devise::OmniauthCallbacksController

  skip_before_filter :verify_authenticity_token

    def facebook
        user = User.from_omniauth(request.env["omniauth.auth"])
    if user.persisted?
      flash.notice = "Signed in!"

      # if the oauth_token is expired or nil, update it...
      if (DateTime.now > (user.oauth_expires_at || 99.years.ago) )
        user.update_oauth_token(request.env["omniauth.auth"])
      end

      sign_in_and_redirect user
    else
      session["devise.user_attributes"] = user.attributes
      redirect_to new_user_registration_url
    end
    end
end



config/routes.rb

...

devise_for :users, controllers: {omniauth_callbacks: "omniauth_callbacks", 
                                :registrations => "registrations"}

...

I'm happy to provide more information if needed. I'm also open to customizing/overriding the devise mailer behavior, but I don't know how to go about that.

如果需要,我很乐意提供更多信息。我也愿意定制/覆盖设计邮件程序的行为,但我不知道如何去做。

Much thanks!

3 个解决方案

#1


13  

Solved!

I was able to override Devise::Mailer and force a stack trace to find out exactly what was causing duplicate emails. Devise::Mailer#confirmation_instructions was being called twice, and I found out that the problem was with my :after_create callback, shown below:

我能够覆盖Devise :: Mailer并强制执行堆栈跟踪以找出导致重复电子邮件的确切内容。 Devise :: Mailer#confirmation_instructions被调用两次,我发现问题出在my:after_create回调中,如下所示:


in models/user.rb...

after_create :account_created

# called after the account is first created
def account_created

...

  # update points
  self.points += 50
  self.save

...

end

Calling self.save somehow caused the mailer to be triggered again. I solved the problem by changing when the points are added. I got rid of the after_create call and overrode the confirm! method in devise to look like this:

以某种方式调用self.save会导致邮件再次被触发。我通过改变添加点的时间来解决问题。我摆脱了after_create调用并覆盖了确认!设计中的方法看起来像这样:

def confirm!
  super
  account_created
end

So now the user record doesn't get modified (adding points) until after confirmation. No more duplicate emails!

所以现在用户记录在确认之后才会被修改(添加点)。没有更多重复的电子邮件!

#2


8  

I originally went with Thomas Klemm's answer but I went back to look at this when I had some spare time to try and figure out what was happening as it didn't feel right.

我最初选择了Thomas Klemm的答案,但是当我有空闲时间尝试找出发生的事情时,我回过头来看看这个,因为它感觉不对。

I tracked the 'problem' down and noticed that it only happens when :confirmable is set in your devise (User) model and reconfirmable is enabled in the devise initializer - which in hindsight makes a lot of sense because essentially in the after_create we ARE changing the User model, although we aren't changing the email address - I suspect Devise may do this because the account isn't confirmed yet, but in any case it is easy to stop the second email just by calling self.skip_reconfirmation! in the after_create method.

我追踪了'问题'并注意到只有在以下情况下才会发生:在您的设计(用户)模型中设置确认并在设计初始化程序中启用再确认 - 事后看来很有意义,因为基本上在after_create中我们正在改变用户模型,虽然我们没有更改电子邮件地址 - 我怀疑Devise可能会这样做,因为帐户尚未确认,但无论如何只需通过调用self.skip_reconfirmation就可以轻松停止第二封电子邮件!在after_create方法中。

I created a sample rails project with a couple of tests just to ensure the correct behaviour. Below are the key excerpts. If you have far too much time on your hands, you can see the project here: https://github.com/richhollis/devise-reconfirmable-test

我创建了一个带有几个测试的示例rails项目,以确保正确的行为。以下是关键摘录。如果您手上有太多时间,可以在此处查看项目:https://github.com/richhollis/devise-reconfirmable-test

app/models/User.rb

class User < ActiveRecord::Base
  # Include default devise modules. Others available are:
  # :token_authenticatable, :confirmable,
  # :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable, :confirmable

  # Setup accessible (or protected) attributes for your model
  attr_accessible :email, :password, :password_confirmation, :remember_me

  after_create :add_attribute

  private

  def add_attribute
    self.skip_reconfirmation!
    self.update_attributes({ :status => 200 }, :without_protection => true)
  end
end

initializers/devise.rb

# Use this hook to configure devise mailer, warden hooks and so forth.
# Many of these configuration options can be set straight in your model.
Devise.setup do |config|

  ..
  ..

  # If true, requires any email changes to be confirmed (exactly the same way as
  # initial account confirmation) to be applied. Requires additional unconfirmed_email
  # db field (see migrations). Until confirmed new email is stored in
  # unconfirmed email column, and copied to email column on successful confirmation.
  config.reconfirmable = true

  ..
  ..

end

spec/models/user_spec.rb

require 'spec_helper'

describe User do

  subject(:user) { User.create(:email => 'nobody@nobody.com', :password => 'abcdefghijk') }

  it "should only send one email during creation" do
    expect {
      user
    }.to change(ActionMailer::Base.deliveries, :count).by(1)
  end

  it "should set attribute in after_create as expected" do
    user.status.should eq(200)
  end

end

Running the rspec tests to ensure only one email is sent confirms the behaviour:

运行rspec测试以确保只发送一封电子邮件确认行为:

..

Finished in 0.87571 seconds 2 examples, 0 failures

完成0.87571秒2例,0次失败

#3


2  

Thanks for your great solution, Stephen! I've tried it and it works perfectly to hook into the confirm! method. However, in this case, the function is being called (as the name says) when the user clicks the confirmation link in the email he receives.

谢谢你的出色解决方案,斯蒂芬!我已经尝试过了,它完美地融入了确认!方法。但是,在这种情况下,当用户单击他收到的电子邮件中的确认链接时,将调用该函数(如名称所示)。

An alternative is to hook into the generate_confirmation_token method, so your method is called directly when the confirmation token is created and the email is sent.

另一种方法是挂钩generate_confirmation_token方法,因此在创建确认令牌并发送电子邮件时会直接调用您的方法。

# app/models/user.rb
def generate_confirmation_token
  make_owner_an_account_member
  super # includes a call to save(validate: false), 
        # so be sure to call whatever you like beforehand
end

def make_owner_an_account_member
  self.account = owned_account if owned_account?
end

Relevant source of the confirmation module.

确认模块的相关来源。

#1


13  

Solved!

I was able to override Devise::Mailer and force a stack trace to find out exactly what was causing duplicate emails. Devise::Mailer#confirmation_instructions was being called twice, and I found out that the problem was with my :after_create callback, shown below:

我能够覆盖Devise :: Mailer并强制执行堆栈跟踪以找出导致重复电子邮件的确切内容。 Devise :: Mailer#confirmation_instructions被调用两次,我发现问题出在my:after_create回调中,如下所示:


in models/user.rb...

after_create :account_created

# called after the account is first created
def account_created

...

  # update points
  self.points += 50
  self.save

...

end

Calling self.save somehow caused the mailer to be triggered again. I solved the problem by changing when the points are added. I got rid of the after_create call and overrode the confirm! method in devise to look like this:

以某种方式调用self.save会导致邮件再次被触发。我通过改变添加点的时间来解决问题。我摆脱了after_create调用并覆盖了确认!设计中的方法看起来像这样:

def confirm!
  super
  account_created
end

So now the user record doesn't get modified (adding points) until after confirmation. No more duplicate emails!

所以现在用户记录在确认之后才会被修改(添加点)。没有更多重复的电子邮件!

#2


8  

I originally went with Thomas Klemm's answer but I went back to look at this when I had some spare time to try and figure out what was happening as it didn't feel right.

我最初选择了Thomas Klemm的答案,但是当我有空闲时间尝试找出发生的事情时,我回过头来看看这个,因为它感觉不对。

I tracked the 'problem' down and noticed that it only happens when :confirmable is set in your devise (User) model and reconfirmable is enabled in the devise initializer - which in hindsight makes a lot of sense because essentially in the after_create we ARE changing the User model, although we aren't changing the email address - I suspect Devise may do this because the account isn't confirmed yet, but in any case it is easy to stop the second email just by calling self.skip_reconfirmation! in the after_create method.

我追踪了'问题'并注意到只有在以下情况下才会发生:在您的设计(用户)模型中设置确认并在设计初始化程序中启用再确认 - 事后看来很有意义,因为基本上在after_create中我们正在改变用户模型,虽然我们没有更改电子邮件地址 - 我怀疑Devise可能会这样做,因为帐户尚未确认,但无论如何只需通过调用self.skip_reconfirmation就可以轻松停止第二封电子邮件!在after_create方法中。

I created a sample rails project with a couple of tests just to ensure the correct behaviour. Below are the key excerpts. If you have far too much time on your hands, you can see the project here: https://github.com/richhollis/devise-reconfirmable-test

我创建了一个带有几个测试的示例rails项目,以确保正确的行为。以下是关键摘录。如果您手上有太多时间,可以在此处查看项目:https://github.com/richhollis/devise-reconfirmable-test

app/models/User.rb

class User < ActiveRecord::Base
  # Include default devise modules. Others available are:
  # :token_authenticatable, :confirmable,
  # :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable, :confirmable

  # Setup accessible (or protected) attributes for your model
  attr_accessible :email, :password, :password_confirmation, :remember_me

  after_create :add_attribute

  private

  def add_attribute
    self.skip_reconfirmation!
    self.update_attributes({ :status => 200 }, :without_protection => true)
  end
end

initializers/devise.rb

# Use this hook to configure devise mailer, warden hooks and so forth.
# Many of these configuration options can be set straight in your model.
Devise.setup do |config|

  ..
  ..

  # If true, requires any email changes to be confirmed (exactly the same way as
  # initial account confirmation) to be applied. Requires additional unconfirmed_email
  # db field (see migrations). Until confirmed new email is stored in
  # unconfirmed email column, and copied to email column on successful confirmation.
  config.reconfirmable = true

  ..
  ..

end

spec/models/user_spec.rb

require 'spec_helper'

describe User do

  subject(:user) { User.create(:email => 'nobody@nobody.com', :password => 'abcdefghijk') }

  it "should only send one email during creation" do
    expect {
      user
    }.to change(ActionMailer::Base.deliveries, :count).by(1)
  end

  it "should set attribute in after_create as expected" do
    user.status.should eq(200)
  end

end

Running the rspec tests to ensure only one email is sent confirms the behaviour:

运行rspec测试以确保只发送一封电子邮件确认行为:

..

Finished in 0.87571 seconds 2 examples, 0 failures

完成0.87571秒2例,0次失败

#3


2  

Thanks for your great solution, Stephen! I've tried it and it works perfectly to hook into the confirm! method. However, in this case, the function is being called (as the name says) when the user clicks the confirmation link in the email he receives.

谢谢你的出色解决方案,斯蒂芬!我已经尝试过了,它完美地融入了确认!方法。但是,在这种情况下,当用户单击他收到的电子邮件中的确认链接时,将调用该函数(如名称所示)。

An alternative is to hook into the generate_confirmation_token method, so your method is called directly when the confirmation token is created and the email is sent.

另一种方法是挂钩generate_confirmation_token方法,因此在创建确认令牌并发送电子邮件时会直接调用您的方法。

# app/models/user.rb
def generate_confirmation_token
  make_owner_an_account_member
  super # includes a call to save(validate: false), 
        # so be sure to call whatever you like beforehand
end

def make_owner_an_account_member
  self.account = owned_account if owned_account?
end

Relevant source of the confirmation module.

确认模块的相关来源。