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,44 @@
-- Migration : création de la table `photobooth.photos`
-- Réf. : docs/modele-donnees.md (section "Table `photos`")
--
-- Table déplacée de `public.pq_photos` (2026-09-15) vers `photobooth.photos`
-- (2026-09-16), cf. 20260916090100_create_events_table.sql pour le contexte
-- du changement de schéma.
--
-- ⚠️ Ne pas appliquer depuis cet agent (Kévin applique lui-même).
create table if not exists photobooth.photos (
id uuid primary key default gen_random_uuid(),
event_id uuid not null references photobooth.events (id) on delete cascade,
-- Contrainte d'isolation multi-tenant (héritée de la relecture sécurité du
-- 2026-09-15, conservée à l'identique) : url_storage DOIT commencer par
-- "<event_id>/", pour empêcher qu'une ligne photobooth.photos de l'event A
-- pointe vers un objet Storage de l'event B. S'applique à TOUT rôle qui
-- insère, y compris le rôle propriétaire des fonctions SECURITY DEFINER
-- (postgres) — défense en profondeur même si, avec ce nouveau schéma, le
-- seul chemin d'écriture "normal" est déjà revérifié explicitement dans
-- public.insert_public_photo() (cf. 20260916090400_create_public_functions.sql).
url_storage text not null
check (starts_with(url_storage, event_id::text || '/')),
-- Bornes alignées sur les `maxlength` du formulaire (src/e/index.html :
-- 60 pour le prénom, 300 pour le message).
nom_invite text check (char_length(nom_invite) <= 60),
message text check (char_length(message) <= 300),
uploaded_at timestamptz not null default now()
);
comment on table photobooth.photos is
'Photos/vidéos déposées par les invités pour un événement donné. Schéma photobooth jamais exposé via PostgREST : seul chemin d''écriture pour anon = public.insert_public_photo().';
comment on column photobooth.photos.event_id is
'Événement associé (photobooth.events). Suppression en cascade si l''événement est supprimé.';
comment on column photobooth.photos.url_storage is
'Chemin du fichier dans Supabase Storage (bucket `event-photos`, chemin `{event_id}/{uuid}.{ext}`). CHECK : doit commencer par "<event_id>/" (isolation multi-tenant).';
comment on column photobooth.photos.nom_invite is
'Nom saisi par l''invité (optionnel, aucune donnée d''identification obligatoire, cf. docs/rgpd.md). Limité à 60 caractères (aligné sur src/e/index.html).';
comment on column photobooth.photos.message is
'Message laissé par l''invité (optionnel). Limité à 300 caractères (aligné sur src/e/index.html).';
-- Index pour les accès en lecture par événement (galerie admin, export ZIP,
-- job d'expiration/suppression).
create index if not exists idx_photos_event_id
on photobooth.photos (event_id);