discourse/app/models/user_email.rb
Kelv 04ba5baec0
DEV: ensure rebaking works even when some users have inconsistent data (#30261)
* DEV: add db consistency check for UserEmail

* DEV: add db consistency check for UserAvatar

* DEV: ignore inconsistent data related to user avatars when deciding whether to rebake old posts


Co-authored-by: Alan Guo Xiang Tan <gxtan1990@gmail.com>

---------

Co-authored-by: Alan Guo Xiang Tan <gxtan1990@gmail.com>
2024-12-16 19:48:25 +08:00

126 lines
3.4 KiB
Ruby

# frozen_string_literal: true
class UserEmail < ActiveRecord::Base
belongs_to :user
attr_accessor :skip_validate_email
attr_accessor :skip_validate_unique_email
attr_accessor :skip_normalize_email
before_validation :strip_downcase_email
before_validation :normalize_email
validates :email, presence: true
validates :email, email: true, if: :validate_email?
validates :primary, uniqueness: { scope: [:user_id] }, if: %i[user_id primary]
validate :user_id_not_changed, if: :primary
validate :unique_email, if: :validate_unique_email?
scope :secondary, -> { where(primary: false) }
before_save -> { destroy_email_tokens(self.email_was) }, if: :will_save_change_to_email?
after_destroy { destroy_email_tokens(self.email) }
def self.ensure_consistency!
user_ids_without_primary_email = DB.query_single <<~SQL
SELECT u.id
FROM users u
LEFT JOIN user_emails ue ON u.id = ue.user_id AND ue.primary = true
WHERE ue.id IS NULL;
SQL
user_ids_without_primary_email.each do |user_id|
UserEmail.create!(
user_id: user_id,
# 64 max character length of local-part for the email address https://datatracker.ietf.org/doc/html/rfc5321#section-4.5.3.1.1
email: "#{SecureRandom.alphanumeric(64)}@missing-primary-email.invalid",
primary: true,
)
end
end
def normalize_email
self.normalized_email =
if self.email.present?
username, domain = self.email.split("@", 2)
username = username.gsub(".", "").gsub(/\+.*/, "")
"#{username}@#{domain}"
end
end
private
def strip_downcase_email
if self.email
self.email = self.email.strip
self.email = self.email.downcase
end
end
def validate_email?
return false if self.skip_validate_email
email_changed?
end
def validate_unique_email?
return false if self.skip_validate_unique_email
will_save_change_to_email?
end
def normalize_emails?
return false if self.skip_normalize_email
SiteSetting.normalize_emails?
end
def unique_email
email_exists =
if self.normalize_emails?
self
.class
.where("lower(email) = ? OR lower(normalized_email) = ?", email, normalized_email)
.exists?
else
self.class.where("lower(email) = ?", email).exists?
end
self.errors.add(:email, :taken) if email_exists
end
def user_id_not_changed
if self.will_save_change_to_user_id? && self.persisted?
self.errors.add(
:user_id,
I18n.t(
"activerecord.errors.models.user_email.attributes.user_id.reassigning_primary_email",
),
)
end
end
def destroy_email_tokens(email)
EmailToken.where(email: email).destroy_all
end
end
# == Schema Information
#
# Table name: user_emails
#
# id :integer not null, primary key
# user_id :integer not null
# email :string(513) not null
# primary :boolean default(FALSE), not null
# created_at :datetime not null
# updated_at :datetime not null
# normalized_email :string
#
# Indexes
#
# index_user_emails_on_email (lower((email)::text)) UNIQUE
# index_user_emails_on_normalized_email (lower((normalized_email)::text))
# index_user_emails_on_user_id (user_id)
# index_user_emails_on_user_id_and_primary (user_id,primary) UNIQUE WHERE "primary"
#