Ajoute la V1 fonctionnelle : upload invité, galerie admin, QR code, backend Supabase

Schéma dédié `photobooth` (jamais exposé via PostgREST, accès exclusivement
via fonctions SECURITY DEFINER dans public) avec RLS, policies Storage sur
bucket privé event-photos, Edge Functions admin-gallery et expire-events,
et job pg_cron d'expiration J+7. Le tout déployé et testé en conditions
réelles sur le projet Supabase kevin-lecou-hub avant relecture sécurité
(faille d'abus sur insert_public_photo corrigée en cours de route).

Côté front : page upload invité (compression + HEIC + RGPD), galerie admin
avec export ZIP, générateur de QR code autonome. Workflow n8n de purge
Storage J+7 fourni (à activer manuellement côté n8n).
This commit is contained in:
2026-09-16 11:32:31 +02:00
parent 2007eab006
commit 9b1461978b
37 changed files with 6255 additions and 4 deletions
@@ -0,0 +1,68 @@
-- Migration : durcissement de public.insert_public_photo()
-- Réf. : relecture sécurité finale du 2026-09-16 (reviewer-securite).
--
-- Problème trouvé : insert_public_photo() ne vérifiait pas que le fichier
-- référencé par p_url_storage existe réellement dans Storage. N'importe qui
-- connaissant un event_id actif (trivialement récupérable via
-- get_public_event(slug), le slug étant public par conception, diffusé via
-- QR code) pouvait appeler cette RPC directement, en boucle, sans jamais
-- uploader de fichier réel — polluant la galerie admin de lignes fantômes
-- (images "cassées") ou de contenu arbitraire dans nom_invite/message.
--
-- Correctif : vérifie l'existence de l'objet dans storage.objects avant
-- d'insérer la ligne. Ne supprime pas entièrement le risque d'abus (pas de
-- rate limiting/CAPTCHA), mais un attaquant doit désormais réellement
-- uploader un fichier (soumis à file_size_limit/allowed_mime_types) pour
-- chaque ligne créée, ce qui relève nettement le coût de l'abus. Risque
-- résiduel accepté pour la V1 (usage perso) ; à revisiter avant la V2/V3
-- commerciale si le volume d'événements/invités augmente.
--
-- Appliqué et testé en conditions réelles sur kevin-lecou-hub le 2026-09-16
-- (insert sans upload préalable -> rejeté ; insert après upload réel ->
-- fonctionne toujours).
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, storage, 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 "<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;
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. Revalide event_accepts_uploads(), la cohérence url_storage/event_id, ET l''existence réelle du fichier dans storage.objects avant insertion. Retourne l''id de la ligne créée.';