e51f0bfee7
Account incineration could leave orphaned records due to missing cascade declarations and async jobs failing when the account was already gone. Cascade fixes: - Add Account::Searchable concern to clean up Search::Query (delete_all) and Search::Record (destroy_all, respects SQLite FTS dependent: :destroy) - Add before_destroy in Account::Storage to delete storage entries - Suppress storage entry recording during incineration so attachment purge callbacks don't create entries or enqueue materialize jobs - Guard Access#clean_inaccessible_data_later with unless user.destroyed? to avoid enqueuing pointless jobs during user cascade Job tenancy: - Extract AccountTenanted concern from the global ActiveJob initializer into ApplicationJob (include) with targeted prepends for ActionMailer::MailDeliveryJob and Turbo broadcast jobs - Defer account resolution from deserialize to perform so that missing accounts raise DeserializationError inside the execution path where discard_on can handle it Tests: - Comprehensive incineration test covering 35 model types with before/after assertions and full enqueued job processing - Mailer deliver_later test verifying account context survives job serialization for multi-account users - Turbo broadcast test verifying account-scoped URLs in rendered partials
62 lines
1.8 KiB
Ruby
62 lines
1.8 KiB
Ruby
require "zlib"
|
|
|
|
module User::Avatar
|
|
extend ActiveSupport::Concern
|
|
|
|
ALLOWED_AVATAR_CONTENT_TYPES = %w[ image/jpeg image/png image/gif image/webp ].freeze
|
|
MAX_AVATAR_DIMENSIONS = { width: 4096, height: 4096 }.freeze
|
|
AVATAR_COLORS = %w[
|
|
#AF2E1B #CC6324 #3B4B59 #BFA07A #ED8008 #ED3F1C #BF1B1B #736B1E #D07B53
|
|
#736356 #AD1D1D #BF7C2A #C09C6F #698F9C #7C956B #5D618F #3B3633 #67695E
|
|
].freeze
|
|
|
|
included do
|
|
has_one_attached :avatar, dependent: :purge_later do |attachable|
|
|
attachable.variant :thumb, resize_to_fill: [ 256, 256 ], process: :immediately
|
|
end
|
|
|
|
scope :with_avatars, -> { preload(:account, :avatar_attachment) }
|
|
|
|
validate :avatar_content_type_allowed, :avatar_dimensions_allowed, if: :avatar_attached?
|
|
end
|
|
|
|
def avatar_attached?
|
|
avatar.attached?
|
|
end
|
|
|
|
def avatar_thumbnail
|
|
avatar.variable? ? avatar.variant(:thumb) : avatar
|
|
end
|
|
|
|
def avatar_background_color
|
|
AVATAR_COLORS[Zlib.crc32(to_param) % AVATAR_COLORS.size]
|
|
end
|
|
|
|
# Avatars are always publicly accessible
|
|
def publicly_accessible?
|
|
true
|
|
end
|
|
|
|
private
|
|
def avatar_content_type_allowed
|
|
if !ALLOWED_AVATAR_CONTENT_TYPES.include?(avatar.content_type)
|
|
errors.add(:avatar, "must be a JPEG, PNG, GIF, or WebP image")
|
|
end
|
|
end
|
|
|
|
def avatar_dimensions_allowed
|
|
return unless avatar.blob.analyzed? || avatar.blob.analyze
|
|
|
|
width = avatar.blob.metadata[:width]
|
|
height = avatar.blob.metadata[:height]
|
|
|
|
if width && width > MAX_AVATAR_DIMENSIONS[:width]
|
|
errors.add(:avatar, "width must be less than #{MAX_AVATAR_DIMENSIONS[:width]}px")
|
|
end
|
|
|
|
if height && height > MAX_AVATAR_DIMENSIONS[:height]
|
|
errors.add(:avatar, "height must be less than #{MAX_AVATAR_DIMENSIONS[:height]}px")
|
|
end
|
|
end
|
|
end
|