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).
69 lines
2.9 KiB
PL/PgSQL
69 lines
2.9 KiB
PL/PgSQL
-- 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.';
|