-- Migration : fonctions SECURITY DEFINER dans `public` -- -- Seul point d'accès (lecture ET écriture) à photobooth.events / -- photobooth.photos, puisque le schéma `photobooth` n'est jamais exposé via -- PostgREST (cf. 20260916090000_create_photobooth_schema.sql). Chaque -- fonction ci-dessous ne renvoie/n'accepte que le strict nécessaire pour son -- appelant (anon vs service_role), avec des grants minimaux. -- -- search_path = public, photobooth, pg_temp sur toutes les fonctions de ce -- fichier : nécessaire pour qu'elles résolvent `events`/`photos` sans avoir -- à préfixer chaque référence par `photobooth.` (fait quand même ci-dessous -- par lisibilité/explicité, le search_path reste une sécurité supplémentaire -- contre une résolution ambiguë si un jour une table `events`/`photos` -- apparaissait aussi dans public). -- -- ⚠️ Ne pas appliquer depuis cet agent (Kévin applique lui-même). -- ============================================================================= -- 1. event_accepts_uploads — anon -- ============================================================================= -- Utilisée par la policy RLS storage.objects (cf. -- 20260916090500_storage_event_photos_policies.sql) et par -- public.insert_public_photo() ci-dessous : vérifie qu'un événement est actif -- et non expiré, sans exposer la ligne photobooth.events. create or replace function public.event_accepts_uploads(p_event_id uuid) returns boolean language sql stable security definer set search_path = public, photobooth, pg_temp as $$ select exists ( select 1 from photobooth.events e where e.id = p_event_id and e.statut = 'actif' and e.expire_at is not null and now() < e.expire_at ); $$; comment on function public.event_accepts_uploads(uuid) is 'Vrai si l''événement (photobooth.events) est actif et non expiré. Utilisée par public.insert_public_photo() et par les policies storage.objects du bucket event-photos.'; revoke all on function public.event_accepts_uploads(uuid) from public; grant execute on function public.event_accepts_uploads(uuid) to anon; -- ============================================================================= -- 2. get_public_event — anon -- ============================================================================= -- Résout un slug public en informations minimales d'événement pour la page -- d'upload, sans jamais exposer admin_token, client_nom ou facture_montant. -- -- Comportement volontaire (confirmé en relecture sécurité le 2026-09-15, -- inchangé ici) : renvoie l'événement même si statut != 'actif', pour que le -- front affiche un message adapté ("événement terminé/expiré") plutôt qu'une -- absence de résultat indifférenciée. Jugé acceptable : le slug n'est pas un -- secret (diffusé via QR code), et les colonnes renvoyées ne sont pas -- sensibles. Point de comportement produit à valider explicitement par -- Kévin et consigner dans docs/decisions.md (pas encore fait à ce jour). create or replace function public.get_public_event(p_slug text) returns table ( id uuid, nom text, statut text, date_evenement date, expire_at timestamptz ) 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 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, jamais admin_token/client_nom/facture_montant), pour la page d''upload. Nom aligné sur le contrat attendu par src/e/upload.js.'; revoke all on function public.get_public_event(text) from public; grant execute on function public.get_public_event(text) to anon; -- ============================================================================= -- 3. insert_public_photo — anon (NOUVEAU, remplace l'ancien POST direct sur -- la table, qui n'est plus possible puisque photobooth n'est plus exposé) -- ============================================================================= -- Chemin d'écriture unique pour les invités. Revalide elle-même (ne fait PAS -- confiance à un event_id/url_storage "déjà bons" côté appelant) : -- 1. l'événement est actif et non expiré (event_accepts_uploads) ; -- 2. url_storage commence bien par "/" (même règle que le -- CHECK sur la table, revérifiée ici en amont pour renvoyer une erreur -- métier explicite plutôt que de compter uniquement sur la contrainte -- table, qui renverrait une erreur Postgres générique moins lisible -- côté front). -- ⚠️ CONTRAT FRONT : src/e/upload.js fait aujourd'hui un POST REST direct -- sur (l'ex-)`pq_photos`. Ce chemin n'existe plus dans cette architecture -- (photobooth.photos n'est pas exposée). Le front doit être mis à jour pour -- appeler `POST {SUPABASE_URL}/rest/v1/rpc/insert_public_photo` avec un -- corps JSON `{ p_event_id, p_url_storage, p_nom_invite, p_message }` — hors -- périmètre de cette migration (pas de modification de src/ par cet agent), -- signalé explicitement dans le rapport de tâche. 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 ) returns uuid language plpgsql security definer set search_path = public, photobooth, pg_temp as $$ declare v_photo_id uuid; 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; -- Les bornes de longueur sur nom_invite/message sont déjà appliquées par -- les CHECK de la table (cf. 20260916090200_create_photos_table.sql) ; pas -- besoin de les dupliquer ici, l'INSERT échouera proprement sinon. insert into photobooth.photos (event_id, url_storage, nom_invite, message) values (p_event_id, p_url_storage, p_nom_invite, p_message) returning id into v_photo_id; return v_photo_id; end; $$; comment on function public.insert_public_photo(uuid, text, text, text) is 'Chemin d''écriture unique pour les invités (remplace le POST REST direct sur photobooth.photos, non exposée). Revalide event_accepts_uploads() et la cohérence url_storage/event_id avant insertion. Retourne l''id de la ligne créée. Lève une exception (errcode P0001) si l''événement n''accepte pas d''upload ou si url_storage est incohérent.'; revoke all on function public.insert_public_photo(uuid, text, text, text) from public; grant execute on function public.insert_public_photo(uuid, text, text, text) to anon; -- ============================================================================= -- 4. storage_event_photos_upload_allowed — anon (logique inchangée) -- ============================================================================= -- Utilisée par la policy INSERT sur storage.objects (bucket event-photos). -- Elle vivait déjà dans public hier (les policies storage.objects sont -- indépendantes de l'exposition de schéma Postgres, cf. consigne de tâche), -- donc pas de changement de schéma ici — seule la référence à -- event_accepts_uploads() est conservée telle quelle (son corps interne -- pointe maintenant vers photobooth.events, mais la signature/l'usage ne -- changent pas pour cette fonction-ci). create or replace function public.storage_event_photos_upload_allowed(p_name text) returns boolean language plpgsql stable security definer set search_path = public, photobooth, storage, pg_temp as $$ declare v_event_id uuid; begin -- Premier segment du chemin ({event_id}/{uuid}.{ext}) = storage.foldername(name)[1]. -- Cast en uuid dans un bloc protégé : un path malformé doit être refusé -- proprement, pas planter la requête. begin v_event_id := (storage.foldername(p_name))[1]::uuid; exception when others then return false; end; if v_event_id is null then return false; end if; return public.event_accepts_uploads(v_event_id); end; $$; comment on function public.storage_event_photos_upload_allowed(text) is 'Vrai si le premier segment du chemin storage.objects.name (convention {event_id}/{uuid}.{ext}) correspond à un event_id existant, actif et non expiré (public.event_accepts_uploads). Jamais d''exception levée (path invalide -> false). Utilisée par la policy INSERT anonyme sur storage.objects du bucket event-photos.'; revoke all on function public.storage_event_photos_upload_allowed(text) from public; grant execute on function public.storage_event_photos_upload_allowed(text) to anon; -- ============================================================================= -- 5. get_event_for_admin — service_role UNIQUEMENT (NOUVEAU) -- ============================================================================= -- Renvoie admin_token EN CLAIR : ne doit jamais être accordée à anon ni -- authenticated. Conçue pour n'être appelée QUE par l'Edge Function -- `admin-gallery` (service_role), qui fait elle-même la comparaison en -- temps constant entre le token reçu en query param et cette valeur — cette -- fonction ne fait aucune vérification de token elle-même, c'est -- volontaire : la logique de comparaison reste côté Edge Function (Deno), -- pas dupliquée en SQL. create or replace function public.get_event_for_admin(p_slug text) returns table ( id uuid, nom text, statut text, date_evenement date, expire_at timestamptz, admin_token text ) 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.admin_token from photobooth.events e where e.slug = p_slug; $$; comment on function public.get_event_for_admin(text) is 'Renvoie un événement (photobooth.events) AVEC admin_token en clair. Réservée à service_role (Edge Function admin-gallery uniquement) : ne JAMAIS accorder EXECUTE à anon/authenticated.'; revoke all on function public.get_event_for_admin(text) from public, anon, authenticated; grant execute on function public.get_event_for_admin(text) to service_role; -- ============================================================================= -- 6. get_event_photos — service_role UNIQUEMENT (NOUVEAU) -- ============================================================================= -- Renvoie les photos d'un événement (galerie admin / export ZIP). Ordre -- chronologique croissant (uploaded_at asc) — l'Edge Function admin-gallery -- inverse l'ordre côté JS pour l'affichage galerie (plus récent en premier) -- si besoin, cf. supabase/functions/admin-gallery/index.ts. create or replace function public.get_event_photos(p_event_id uuid) returns table ( id uuid, url_storage text, nom_invite text, message text, uploaded_at timestamptz ) language sql stable security definer set search_path = public, photobooth, pg_temp as $$ select p.id, p.url_storage, p.nom_invite, p.message, p.uploaded_at from photobooth.photos p where p.event_id = p_event_id order by p.uploaded_at asc; $$; comment on function public.get_event_photos(uuid) is 'Renvoie les photos (photobooth.photos) d''un événement, triées par uploaded_at croissant. Réservée à service_role (Edge Function admin-gallery) : ne JAMAIS accorder EXECUTE à anon/authenticated (aucune vérification d''admin_token ici, c''est fait en amont côté Edge Function).'; revoke all on function public.get_event_photos(uuid) from public, anon, authenticated; grant execute on function public.get_event_photos(uuid) to service_role; -- ============================================================================= -- 7. get_expired_event_ids — service_role UNIQUEMENT (NOUVEAU) -- ============================================================================= -- Utilisée par l'Edge Function `expire-events` pour savoir quels dossiers -- Storage `{event_id}/` purger. Ne renvoie que des uuid (pas de slug/statut -- détaillé) : c'est tout ce dont cette fonction a besoin. create or replace function public.get_expired_event_ids() returns setof uuid language sql stable security definer set search_path = public, photobooth, pg_temp as $$ select id from photobooth.events where statut = 'expire'; $$; comment on function public.get_expired_event_ids() is 'Renvoie les id des événements (photobooth.events) au statut ''expire''. Réservée à service_role (Edge Function expire-events) : ne JAMAIS accorder EXECUTE à anon/authenticated.'; revoke all on function public.get_expired_event_ids() from public, anon, authenticated; grant execute on function public.get_expired_event_ids() to service_role;