设计在更新时不确认电子邮件

Posted

技术标签:

【中文标题】设计在更新时不确认电子邮件【英文标题】:Devise not confirming email on update 【发布时间】:2022-01-03 14:26:21 【问题描述】:

我正在使用设备进行身份验证。我正在覆盖设计令牌生成器,以便我可以使用 6 位代码并覆盖它,以便我可以支持手机号码确认。

如果用户使用电子邮件注册并且 OTP 是通过电子邮件发送的。注册似乎工作正常。用户使用电子邮件注册。发送 OTP 并在确认后确认用户。

但是当用户尝试更新电子邮件时。我正在使用相同的方法来发送确认码(就像在注册时一样),用户被保存在 unconfirmed_email 中。邮件通过电子邮件发送,但确认后不会将用户电子邮件从 unconfirmed_email 字段复制到电子邮件字段。

这可能是什么问题。

app/services/users/confirmation_code_sender.rb

# frozen_string_literal: true

module Users
  class ConfirmationCodeSender
    attr_reader :user

    def initialize(id:)
      @user = User.find(id)
    end

    # rubocop :disable Metrics/AbcSize
    def call
      generate_confirmation_token!

      if user.email?
        DeviseMailer.confirmation_instructions(
          user,
          user.confirmation_token,
           to: user.unconfirmed_email || user.email 
        ).deliver_now
      else
        Telco::Web::Sms.send_text(recipient: user.unconfirmed_mobile || user.mobile_number, message: sms_text)
      end
    end
    # rubocop :enable Metrics/AbcSize

    private

    def generate_confirmation_token!
      user.confirmation_token = TokenGenerator.token(6)
      user.confirmation_sent_at = DateTime.current
      user.save!(validate: false)
    end

    def sms_text
      I18n.t('sms.confirmation_token', token: user.confirmation_token)
    end
  end
end

app/services/users/phone_or_email_updater.rb

# frozen_string_literal: true

module Users
  class PhoneOrEmailUpdater < BaseService
    def call
      authorize!(current_user, to: :user?)

      current_user.tap do |user|
        user.update!(unconfirmed_mobile: params[:unconfirmed_mobile], unconfirmed_email: params[:unconfirmed_email])
        ConfirmationCodeSender.new(id: user.id).call
      end
    end
  end
end

config/nitializers/confirmable.rb

# frozen_string_literal: true

# Overriding this model to support the confirmation for mobile number as well

module Devise
  module Models
    module Confirmable
      def confirm(args = )
        pending_any_confirmation do
          return expired_error if confirmation_period_expired?

          self.confirmed_at = Time.now.utc
          saved = saved(args)
          after_confirmation if saved
          saved
        end
      end

      def saved(args)
        @saved ||= if pending_reconfirmation?
                     skip_reconfirmation!
                     save!(validate: true)
                   else
                     save!(validate: args[:ensure_valid] == true)
                   end
      end

      def pending_reconfirmation?
        if unconfirmed_email.present?
          self.email = unconfirmed_email
          self.unconfirmed_email = nil
          true
        elsif unconfirmed_mobile.present?
          self.mobile_number = unconfirmed_mobile
          self.unconfirmed_mobile = nil
          true
        else
          false
        end
      end

      private

      def expired_error
        errors.add(
          :email,
          :confirmation_period_expired,
          period: Devise::TimeInflector.time_ago_in_words(self.class.confirm_within.ago)
        )
        false
      end
    end
  end
end

移动更新似乎工作正常,但电子邮件没有更新。我正在使用 graphql 更新电子邮件

在控制台中我尝试使用.confirm,但它似乎无法正常工作,用户电子邮件未得到确认

【问题讨论】:

【参考方案1】:

在您的pending_reconfirmation? 中,self.unconfirmed_email 被分配为nil。好像pending_reconfirmation? 只在saved 中被调用,然而它也被pending_any_confirmation 调用。

https://github.com/heartcombo/devise/blob/8593801130f2df94a50863b5db535c272b00efe1/lib/devise/models/confirmable.rb#L238

# Checks whether the record requires any confirmation.
def pending_any_confirmation
  if (!confirmed? || pending_reconfirmation?)
    yield
  else
    self.errors.add(:email, :already_confirmed)
    false
  end
end

那么当pending_reconfirmation? 第二次在saved 中被调用时,pending_reconfirmation?将返回 false,因为 unconfirmed_email 为 nil。

您最好不要在以? 结尾的方法内进行实际分配,这将是一个隐含的副作用。也许创建一个以! 结尾的新方法来更改属性的值。

例如:

module Devise
  module Models
    module Confirmable
      def confirm(args = )
        pending_any_confirmation do
          return expired_error if confirmation_period_expired?

          self.confirmed_at = Time.now.utc
          saved = saved(args)
          after_confirmation if saved
          saved
        end
      end

      def saved(args)
        @saved ||= if pending_reconfirmation?
          reconfirm_email! if unconfirmed_email.present?
          reconfirm_mobile! if unconfirmed_mobile.present?
          skip_reconfirmation!
          save!(validate: true)
        else
          save!(validate: args[:ensure_valid] == true)
        end
      end

      def pending_reconfirmation?
        unconfirmed_email.present? || nconfirmed_mobile.present?
      end

      def reconfirm_email!
        self.email = unconfirmed_email
        self.unconfirmed_email = nil
      end

      def reconfirm_mobile!
        self.mobile_number = unconfirmed_mobile
        self.unconfirmed_mobile = nil
      end

      private

      def expired_error
        errors.add(
          :email,
          :confirmation_period_expired,
          period: Devise::TimeInflector.time_ago_in_words(self.class.confirm_within.ago)
        )
        false
      end
    end
  end
end

【讨论】:

以上是关于设计在更新时不确认电子邮件的主要内容,如果未能解决你的问题,请参考以下文章

使用设计跳过开发中的确认电子邮件

从本地主机设计电子邮件确认

更新确认电子邮件迁移

发送确认链接并单击原始电子邮件后,用新电子邮件更新用户的个人资料

设计新用户确认(通过电子邮件验证)

.Net Core Entity Framework 电子邮件确认“单击此处”链接不更新“EmailConfirmed”数据库属性