Ajoute une limite de photos par invité, réglable par événement
Nouvelle colonne max_photos_per_guest (NULL = illimité). Appliquée via un guest_id anonyme persisté en localStorage — pas une identité vérifiée, contournable en changeant d'appareil, mais fait respecter côté serveur (source de vérité) et bloque l'UI proactivement côté client. Ancienne signature de insert_public_photo (sans guest_id) supprimée pour empêcher tout contournement. Testé en conditions réelles sur kevin-lecou-hub (2 photos acceptées, 3e refusée avec max_photos_per_guest=2).
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
-- 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 "<event_id>/".';
|
||||
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;
|
||||
Reference in New Issue
Block a user