-- Migration : limite de photos par invité, réglable par événement -- Réf. : demande explicite de Kévin le 2026-09-16. -- -- max_photos_per_guest (photobooth.events) : NULL = illimité (comportement -- par défaut, inchangé). Réglable uniquement en SQL à la création de l'event -- pour l'instant (pas d'UI admin dédiée en V1, cohérent avec le reste du -- produit). -- -- guest_id (photobooth.photos) : identifiant anonyme généré et persisté -- côté client (localStorage), envoyé à chaque insert_public_photo. Sert -- uniquement à faire respecter max_photos_per_guest — contournable si -- l'invité change d'appareil ou vide son storage, ce n'est pas une -- identité vérifiée. -- -- Déjà appliqué et testé en conditions réelles sur kevin-lecou-hub le -- 2026-09-16 (2 photos acceptées, 3e refusée avec max_photos_per_guest=2 ; -- ancienne signature sans guest_id supprimée pour éviter tout contournement). alter table photobooth.events add column if not exists max_photos_per_guest integer check (max_photos_per_guest is null or max_photos_per_guest > 0); comment on column photobooth.events.max_photos_per_guest is 'Nombre max de photos qu''un même invité peut envoyer pour cet événement. NULL = illimité. Réglé à la création de l''event (pas d''UI admin dédiée en V1).'; alter table photobooth.photos add column if not exists guest_id uuid; comment on column photobooth.photos.guest_id is 'Identifiant anonyme côté client (localStorage), pour appliquer max_photos_per_guest. Pas une identité vérifiée.'; create index if not exists idx_photos_event_guest on photobooth.photos (event_id, guest_id); -- get_public_event : ajoute max_photos_per_guest pour que le front puisse -- bloquer l'UI proactivement (avant même une tentative d'envoi refusée). drop function if exists public.get_public_event(text); create function public.get_public_event(p_slug text) returns table ( id uuid, nom text, statut text, date_evenement date, expire_at timestamptz, max_photos_per_guest integer ) language sql stable security definer set search_path = public, photobooth, pg_temp as $$ select e.id, e.nom, e.statut, e.date_evenement, e.expire_at, e.max_photos_per_guest from photobooth.events e where e.slug = p_slug; $$; comment on function public.get_public_event(text) is 'Résout un slug public en informations minimales d''événement (photobooth.events, y compris max_photos_per_guest), pour la page d''upload.'; revoke all on function public.get_public_event(text) from public; grant execute on function public.get_public_event(text) to anon; -- insert_public_photo : ancienne signature (4 arguments, sans guest_id) -- supprimée -- elle créait une ambiguïté de résolution de surcharge côté -- PostgREST (erreur PGRST203) et permettrait sinon de contourner -- max_photos_per_guest en omettant guest_id. drop function if exists public.insert_public_photo(uuid, text, text, text); create or replace function public.insert_public_photo( p_event_id uuid, p_url_storage text, p_nom_invite text default null, p_message text default null, p_guest_id uuid default null ) returns uuid language plpgsql security definer set search_path = public, photobooth, storage, pg_temp as $$ declare v_photo_id uuid; v_max_per_guest integer; v_current_count integer; begin if not public.event_accepts_uploads(p_event_id) then raise exception 'event_not_accepting_uploads' using errcode = 'P0001', detail = 'L''événement est inconnu, inactif ou expiré.'; end if; if not starts_with(p_url_storage, p_event_id::text || '/') then raise exception 'url_storage_event_mismatch' using errcode = 'P0001', detail = 'url_storage doit commencer par "/".'; end if; if not exists ( select 1 from storage.objects where bucket_id = 'event-photos' and name = p_url_storage ) then raise exception 'storage_object_not_found' using errcode = 'P0001', detail = 'Aucun fichier trouvé dans Storage pour ce chemin. Uploadez le fichier avant d''enregistrer la ligne.'; end if; select max_photos_per_guest into v_max_per_guest from photobooth.events where id = p_event_id; if v_max_per_guest is not null then if p_guest_id is null then raise exception 'guest_id_required' using errcode = 'P0001', detail = 'Cet événement limite le nombre de photos par invité : un identifiant invité est requis.'; end if; select count(*) into v_current_count from photobooth.photos where event_id = p_event_id and guest_id = p_guest_id; if v_current_count >= v_max_per_guest then raise exception 'guest_photo_limit_reached' using errcode = 'P0001', detail = format('Limite de %s photo(s) par invité atteinte pour cet événement.', v_max_per_guest); end if; end if; insert into photobooth.photos (event_id, url_storage, nom_invite, message, guest_id) values (p_event_id, p_url_storage, p_nom_invite, p_message, p_guest_id) returning id into v_photo_id; return v_photo_id; end; $$; comment on function public.insert_public_photo(uuid, text, text, text, uuid) is 'Chemin d''écriture unique pour les invités. Revalide event_accepts_uploads(), la cohérence url_storage/event_id, l''existence du fichier Storage, ET la limite max_photos_per_guest (par guest_id anonyme côté client) si définie sur l''event.'; revoke all on function public.insert_public_photo(uuid, text, text, text, uuid) from public; grant execute on function public.insert_public_photo(uuid, text, text, text, uuid) to anon;