From e09b3ffcf84681b1022cb26e443a57ebfde8bd3b Mon Sep 17 00:00:00 2001 From: Emiliano Frascaro Date: Thu, 20 Aug 2026 09:51:33 +0200 Subject: [PATCH] Corregge validazione upload copertina e generazione slate al go-live. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rifiuta file troppo grandi o non-immagine via Marcel prima di attach, e genera la slate in sync anche quando cover_image esiste ma cover_slate non è ancora pronta. Co-authored-by: Cursor --- .../app/controllers/api/v1/base_controller.rb | 5 +++++ .../concerns/branding_attachments.rb | 22 +++++++++++++++++++ .../controllers/public/clubs_controller.rb | 2 +- .../controllers/public/teams_controller.rb | 2 +- backend/app/models/concerns/coverable.rb | 4 ++-- .../app/services/streams/cover_resolver.rb | 11 ++++++++++ .../services/streams/cover_slate_ensurer.rb | 12 ++++++---- 7 files changed, 50 insertions(+), 8 deletions(-) diff --git a/backend/app/controllers/api/v1/base_controller.rb b/backend/app/controllers/api/v1/base_controller.rb index 37cd38f..440d9fb 100644 --- a/backend/app/controllers/api/v1/base_controller.rb +++ b/backend/app/controllers/api/v1/base_controller.rb @@ -2,6 +2,7 @@ module Api module V1 class BaseController < ApplicationController rescue_from Teams::EntitlementError, with: :render_entitlement_error + rescue_from BrandingAttachments::CoverUploadError, with: :render_cover_upload_error rescue_from Youtube::BroadcastService::Error, with: :render_youtube_error private @@ -20,6 +21,10 @@ module Api billing_url: error.billing_url }, status: :forbidden end + + def render_cover_upload_error(error) + render json: { error: error.message, error_code: "cover_upload_invalid" }, status: :unprocessable_entity + end end end end diff --git a/backend/app/controllers/concerns/branding_attachments.rb b/backend/app/controllers/concerns/branding_attachments.rb index 21d12e8..ca59143 100644 --- a/backend/app/controllers/concerns/branding_attachments.rb +++ b/backend/app/controllers/concerns/branding_attachments.rb @@ -20,10 +20,32 @@ module BrandingAttachments return if file.blank? entitlements.assert_can_upload_cover! + validate_cover_file!(file) record.cover_slate.purge if record.cover_slate.attached? record.cover_image.attach(file) end + def validate_cover_file!(file) + allowed_types = Coverable::COVER_IMAGE_TYPES + + # Rileva MIME dal contenuto (magic bytes) ignorando il declared_type del client. + io = file.respond_to?(:tempfile) ? file.tempfile : file + io.rewind if io.respond_to?(:rewind) + detected = Marcel::MimeType.for(io, name: file.respond_to?(:original_filename) ? file.original_filename : nil) + io.rewind if io.respond_to?(:rewind) + + unless detected.in?(allowed_types) + raise CoverUploadError, I18n.t("coverable.errors.invalid_type") + end + + byte_size = file.respond_to?(:size) ? file.size : File.size(file.path) + if byte_size > Coverable::MAX_COVER_BYTES + raise CoverUploadError, I18n.t("coverable.errors.too_large") + end + end + + class CoverUploadError < StandardError; end + def enqueue_cover_slate_generation(record) GenerateCoverSlateJob.enqueue_for(record) if record.cover_image.attached? end diff --git a/backend/app/controllers/public/clubs_controller.rb b/backend/app/controllers/public/clubs_controller.rb index fe99952..2249a55 100644 --- a/backend/app/controllers/public/clubs_controller.rb +++ b/backend/app/controllers/public/clubs_controller.rb @@ -80,7 +80,7 @@ module Public @entitlements = @club.teams.first&.entitlements flash.now[:alert] = e.record.errors.full_messages.join(", ") render :edit, status: :unprocessable_entity - rescue Teams::EntitlementError => e + rescue Teams::EntitlementError, BrandingAttachments::CoverUploadError => e @entitlements = @club.teams.first&.entitlements flash.now[:alert] = e.message render :edit, status: :unprocessable_entity diff --git a/backend/app/controllers/public/teams_controller.rb b/backend/app/controllers/public/teams_controller.rb index 1f41ac8..2abde0d 100644 --- a/backend/app/controllers/public/teams_controller.rb +++ b/backend/app/controllers/public/teams_controller.rb @@ -59,7 +59,7 @@ module Public @entitlements = @team.entitlements flash.now[:alert] = e.record.errors.full_messages.join(", ") render :edit, status: :unprocessable_entity - rescue Teams::EntitlementError => e + rescue Teams::EntitlementError, BrandingAttachments::CoverUploadError => e @club = @team.club @entitlements = @team.entitlements flash.now[:alert] = e.message diff --git a/backend/app/models/concerns/coverable.rb b/backend/app/models/concerns/coverable.rb index 3d1f300..8f08f11 100644 --- a/backend/app/models/concerns/coverable.rb +++ b/backend/app/models/concerns/coverable.rb @@ -8,8 +8,8 @@ module Coverable has_one_attached :cover_image has_one_attached :cover_slate - validate :cover_image_file_type, if: -> { cover_image.attached? && cover_image.blob&.new_record? } - validate :cover_image_file_size, if: -> { cover_image.attached? && cover_image.blob&.new_record? } + validate :cover_image_file_type, if: -> { cover_image.attached? } + validate :cover_image_file_size, if: -> { cover_image.attached? } end def cover_image_attached? diff --git a/backend/app/services/streams/cover_resolver.rb b/backend/app/services/streams/cover_resolver.rb index 58b6c8a..8edfcba 100644 --- a/backend/app/services/streams/cover_resolver.rb +++ b/backend/app/services/streams/cover_resolver.rb @@ -41,6 +41,17 @@ module Streams nil end + # Restituisce il record con cover_image nella catena anche se la slate non è ancora su disco. + # Usato da CoverSlateEnsurer per sapere cosa generare al go-live. + def pending_cover_record + return nil unless @entitlements.can_use_custom_cover? + + candidates.each do |record, _source| + return record if record.cover_image.attached? + end + nil + end + def default_cover_url DEFAULT_COVER_URL end diff --git a/backend/app/services/streams/cover_slate_ensurer.rb b/backend/app/services/streams/cover_slate_ensurer.rb index 00db1c9..a73e5cf 100644 --- a/backend/app/services/streams/cover_slate_ensurer.rb +++ b/backend/app/services/streams/cover_slate_ensurer.rb @@ -11,10 +11,8 @@ module Streams end def ensure! - result = @resolver.resolve - return unless result - - record = result.record + record = find_pending_cover_record + return unless record return if slate_ready?(record) GenerateCoverSlate.call(record) @@ -25,6 +23,12 @@ module Streams private + # Cerca il record con cover_image nella catena di ereditarietà, + # anche se la slate non è ancora pronta (a differenza di resolve che richiede la slate su disco). + def find_pending_cover_record + @resolver.pending_cover_record + end + def slate_ready?(record) return false unless record.cover_slate.attached?