mirror of
https://github.com/discourse/discourse.git
synced 2024-12-13 10:03:42 +08:00
fa8cd629f1
This commit adds token_hash and scopes columns to email_tokens table. token_hash is a replacement for the token column to avoid storing email tokens in plaintext as it can pose a security risk. The new scope column ensures that email tokens cannot be used to perform a different action than the one intended. To sum up, this commit: * Adds token_hash and scope to email_tokens * Reuses code that schedules critical_user_email * Refactors EmailToken.confirm and EmailToken.atomic_confirm methods * Periodically cleans old, unconfirmed or expired email tokens
152 lines
5.0 KiB
Ruby
152 lines
5.0 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class EmailUpdater
|
|
include HasErrors
|
|
|
|
attr_reader :user
|
|
attr_reader :change_req
|
|
|
|
def self.human_attribute_name(name, options = {})
|
|
User.human_attribute_name(name, options)
|
|
end
|
|
|
|
def initialize(guardian: nil, user: nil)
|
|
@guardian = guardian
|
|
@user = user
|
|
end
|
|
|
|
def change_to(email, add: false)
|
|
@guardian.ensure_can_edit_email!(@user)
|
|
|
|
email = Email.downcase(email.strip)
|
|
EmailValidator.new(attributes: :email).validate_each(self, :email, email)
|
|
return if errors.present?
|
|
|
|
if existing_user = User.find_by_email(email)
|
|
if SiteSetting.hide_email_address_taken
|
|
Jobs.enqueue(:critical_user_email, type: :account_exists, user_id: existing_user.id)
|
|
else
|
|
error_message = +'change_email.error'
|
|
error_message << '_staged' if existing_user.staged?
|
|
errors.add(:base, I18n.t(error_message))
|
|
end
|
|
end
|
|
|
|
if add
|
|
secondary_emails_count = @user.secondary_emails.count
|
|
if secondary_emails_count >= SiteSetting.max_allowed_secondary_emails
|
|
errors.add(:base, I18n.t("change_email.max_secondary_emails_error"))
|
|
end
|
|
else
|
|
old_email = @user.email
|
|
end
|
|
|
|
return if errors.present? || existing_user.present?
|
|
|
|
if @guardian.is_staff? && @guardian.user != @user
|
|
StaffActionLogger.new(@guardian.user).log_add_email(@user)
|
|
else
|
|
UserHistory.create!(action: UserHistory.actions[:add_email], acting_user_id: @user.id)
|
|
end
|
|
|
|
@change_req = EmailChangeRequest.find_or_initialize_by(user_id: @user.id, new_email: email)
|
|
|
|
if @change_req.new_record?
|
|
@change_req.requested_by = @guardian.user
|
|
@change_req.old_email = old_email
|
|
@change_req.new_email = email
|
|
end
|
|
|
|
if @change_req.change_state.blank? || @change_req.change_state == EmailChangeRequest.states[:complete]
|
|
@change_req.change_state = if @user.staff?
|
|
# Staff users must confirm their old email address first.
|
|
EmailChangeRequest.states[:authorizing_old]
|
|
else
|
|
EmailChangeRequest.states[:authorizing_new]
|
|
end
|
|
end
|
|
|
|
if @change_req.change_state == EmailChangeRequest.states[:authorizing_old]
|
|
@change_req.old_email_token = @user.email_tokens.create!(email: @user.email, scope: EmailToken.scopes[:email_update])
|
|
send_email(add ? :confirm_old_email_add : :confirm_old_email, @change_req.old_email_token)
|
|
elsif @change_req.change_state == EmailChangeRequest.states[:authorizing_new]
|
|
@change_req.new_email_token = @user.email_tokens.create!(email: email, scope: EmailToken.scopes[:email_update])
|
|
send_email(:confirm_new_email, @change_req.new_email_token)
|
|
end
|
|
|
|
@change_req.save!
|
|
@change_req
|
|
end
|
|
|
|
def confirm(token)
|
|
confirm_result = nil
|
|
|
|
User.transaction do
|
|
email_token = EmailToken.confirmable(token, scope: EmailToken.scopes[:email_update])
|
|
if email_token.blank?
|
|
errors.add(:base, I18n.t('change_email.already_done'))
|
|
confirm_result = :error
|
|
next
|
|
end
|
|
|
|
email_token.update!(confirmed: true)
|
|
|
|
@user = email_token.user
|
|
@change_req = @user.email_change_requests
|
|
.where('old_email_token_id = :token_id OR new_email_token_id = :token_id', token_id: email_token.id)
|
|
.first
|
|
|
|
case @change_req.try(:change_state)
|
|
when EmailChangeRequest.states[:authorizing_old]
|
|
@change_req.update!(
|
|
change_state: EmailChangeRequest.states[:authorizing_new],
|
|
new_email_token: @user.email_tokens.create!(email: @change_req.new_email, scope: EmailToken.scopes[:email_update])
|
|
)
|
|
send_email(:confirm_new_email, @change_req.new_email_token)
|
|
confirm_result = :authorizing_new
|
|
when EmailChangeRequest.states[:authorizing_new]
|
|
@change_req.update!(change_state: EmailChangeRequest.states[:complete])
|
|
if !@user.staff?
|
|
# Send an email notification only to users who did not confirm old
|
|
# email.
|
|
send_email_notification(@change_req.old_email, @change_req.new_email)
|
|
end
|
|
update_user_email(@change_req.old_email, @change_req.new_email)
|
|
confirm_result = :complete
|
|
end
|
|
end
|
|
|
|
confirm_result || :error
|
|
end
|
|
|
|
def update_user_email(old_email, new_email)
|
|
if old_email.present?
|
|
@user.user_emails.find_by(email: old_email).update!(email: new_email)
|
|
else
|
|
@user.user_emails.create!(email: new_email)
|
|
end
|
|
@user.reload
|
|
|
|
DiscourseEvent.trigger(:user_updated, @user)
|
|
@user.set_automatic_groups
|
|
end
|
|
|
|
protected
|
|
|
|
def send_email(type, email_token)
|
|
Jobs.enqueue :critical_user_email,
|
|
to_address: email_token.email,
|
|
type: type,
|
|
user_id: @user.id,
|
|
email_token: email_token.token
|
|
end
|
|
|
|
def send_email_notification(old_email, new_email)
|
|
Jobs.enqueue :critical_user_email,
|
|
to_address: @user.email,
|
|
type: old_email ? :notify_old_email : :notify_old_email_add,
|
|
user_id: @user.id,
|
|
new_email: new_email
|
|
end
|
|
end
|