Files
photobooth-qr/supabase/migrations/20260918100000_create_event_rpc.sql
T
KevinLecou 8568b48461 Ajoute la création d'événement via n8n et refond la DA en cartes papier
- Nouvelle Edge Function create-event + RPC public.create_event : génère
  slug/admin_token/expire_at et insère l'event, réservée à service_role,
  appelée par le workflow n8n "Créer un événement" (Form Trigger, publié).
- Passe DA "polaroid poussé à fond" : les cartes d'état (chargement/erreur/
  succès), la modale de bienvenue et le formulaire passent en papier clair
  avec ruban washi tape et léger tilt, plutôt qu'en carte sombre sur fond
  sombre (ombre invisible dans l'ancienne version). Admin resté sobre/sombre
  mais cartes plus lisibles (liseré coloré, contraste renforcé).
- Générateur QR et carton imprimable : lisent maintenant ?slug= et
  ?baseUrl= pour se pré-remplir depuis le lien renvoyé par create-event.
- Correctifs : icône manquante sur "Télécharger" (admin), bug [hidden]
  anticipé sur l'état vide de la galerie invité.
2026-09-18 12:16:25 +02:00

155 lines
6.8 KiB
PL/PgSQL

-- Migration : fonction `create_event` (public, service_role uniquement)
-- Réf. : docs/decisions.md ("2026-09-18 (bis)", création d'événement via un
-- formulaire n8n + Edge Function `create-event`), docs/modele-donnees.md
-- (table `events`), 20260916090400_create_public_functions.sql (pattern
-- des fonctions SECURITY DEFINER réservées à service_role).
--
-- Avant cette migration, la création d'un event se faisait uniquement en SQL
-- manuel (cf. docs/cahier-des-charges.md, livrable non coché "Documentation
-- courte : comment créer un nouvel événement manuellement"). Cette fonction
-- devient le point d'entrée unique, appelée par la nouvelle Edge Function
-- `supabase/functions/create-event/index.ts` (elle-même appelée par un
-- workflow n8n serveur-à-serveur, jamais depuis le front).
--
-- ⚠️ Ne pas appliquer depuis cet agent (Kévin applique lui-même).
-- unaccent est nécessaire pour dériver un slug lisible à partir de `nom`
-- (ex: "Mariage Julie & Marc" -> "mariage-julie-marc"), même logique que
-- `sanitizeFilenamePart` côté admin-gallery/index.ts mais en SQL (extension
-- contrib standard, pas de dépendance tierce).
create extension if not exists unaccent with schema extensions;
-- =============================================================================
-- photobooth.generate_unique_slug — interne (pas un point de contrat public)
-- =============================================================================
-- Dérive un slug lisible à partir d'un texte libre, et garantit son unicité
-- contre photobooth.events.slug en ajoutant un suffixe numérique en cas de
-- collision ("mariage-julie-marc", puis "mariage-julie-marc-2", etc.).
-- Fonction interne au schéma photobooth : ce n'est pas un point de contrat
-- public (comme photobooth.events_set_expire_at()), pas besoin d'être dans
-- public ni d'avoir des grants dédiés (appelée uniquement par
-- public.create_event, SECURITY DEFINER, propriétaire postgres).
create or replace function photobooth.generate_unique_slug(p_source text)
returns text
language plpgsql
as $$
declare
v_base_slug text;
v_candidate text;
v_suffix integer := 1;
-- Garde-fou défensif : en pratique la boucle se termine toujours au
-- premier ou deuxième tour (collision rarissime), mais on évite une
-- boucle infinie en cas de bug/donnée pathologique.
v_max_attempts constant integer := 1000;
begin
v_base_slug := lower(trim(both '-' from
regexp_replace(extensions.unaccent(coalesce(p_source, '')), '[^a-zA-Z0-9]+', '-', 'g')
));
if v_base_slug = '' then
v_base_slug := 'evenement';
end if;
v_candidate := v_base_slug;
while exists (select 1 from photobooth.events where slug = v_candidate) loop
v_suffix := v_suffix + 1;
v_candidate := v_base_slug || '-' || v_suffix;
if v_suffix > v_max_attempts then
raise exception 'slug_generation_failed'
using errcode = 'P0001',
detail = format('Impossible de générer un slug unique pour "%s" après %s tentatives.', p_source, v_max_attempts);
end if;
end loop;
return v_candidate;
end;
$$;
comment on function photobooth.generate_unique_slug(text) is
'Dérive un slug lisible et unique (photobooth.events.slug) à partir d''un texte libre (typiquement events.nom) : normalise via unaccent, remplace tout ce qui n''est pas [a-zA-Z0-9] par un tiret, ajoute un suffixe numérique en cas de collision. Fonction interne, appelée uniquement par public.create_event.';
-- =============================================================================
-- create_event — service_role UNIQUEMENT (NOUVEAU)
-- =============================================================================
-- Point d'entrée unique pour créer un événement. Réservée à service_role :
-- appelée par l'Edge Function `create-event`, elle-même appelée uniquement
-- par le workflow n8n de création d'événement (jamais depuis le front, pas
-- de formulaire de création dans l'appli photobooth-qr, cf. décision
-- "2026-09-18 (bis)").
--
-- Ne calcule NI admin_token NI expire_at explicitement : ces deux valeurs
-- sont déjà garanties par la table elle-même (default
-- `encode(extensions.gen_random_bytes(32), 'hex')` pour admin_token, trigger
-- `trg_events_set_expire_at` pour expire_at, cf.
-- 20260916090100_create_events_table.sql) — dupliquer cette logique ici
-- créerait deux sources de vérité pour le même comportement.
create or replace function public.create_event(
p_nom text,
p_date_evenement date,
p_client_nom text default null,
p_max_photos_per_guest integer default null
)
returns table (
id uuid,
slug text,
nom text,
date_evenement date,
expire_at timestamptz,
admin_token text,
client_nom text,
max_photos_per_guest integer
)
language plpgsql
security definer
set search_path = public, photobooth, extensions, pg_temp
as $$
declare
v_slug text;
begin
-- Revalidation défensive côté SQL (l'Edge Function valide déjà ces mêmes
-- règles avant l'appel RPC, cf. supabase/functions/create-event/index.ts) :
-- même principe que public.insert_public_photo(), qui ne fait pas confiance
-- à un appelant "déjà propre".
if p_nom is null or trim(p_nom) = '' then
raise exception 'nom_required'
using errcode = 'P0001',
detail = 'Le nom de l''événement est requis.';
end if;
if p_date_evenement is null then
raise exception 'date_evenement_required'
using errcode = 'P0001',
detail = 'La date de l''événement est requise.';
end if;
if p_max_photos_per_guest is not null and p_max_photos_per_guest <= 0 then
raise exception 'max_photos_per_guest_invalid'
using errcode = 'P0001',
detail = 'max_photos_per_guest doit être un entier strictement positif si fourni.';
end if;
v_slug := photobooth.generate_unique_slug(p_nom);
return query
insert into photobooth.events (slug, nom, date_evenement, client_nom, max_photos_per_guest)
values (v_slug, p_nom, p_date_evenement, p_client_nom, p_max_photos_per_guest)
returning
events.id,
events.slug,
events.nom,
events.date_evenement,
events.expire_at,
events.admin_token,
events.client_nom,
events.max_photos_per_guest;
end;
$$;
comment on function public.create_event(text, date, text, integer) is
'Crée un événement (photobooth.events) : génère un slug unique à partir de `nom`, laisse la table calculer admin_token (default) et expire_at (trigger). Réservée à service_role (Edge Function create-event) : ne JAMAIS accorder EXECUTE à anon/authenticated.';
revoke all on function public.create_event(text, date, text, integer) from public, anon, authenticated;
grant execute on function public.create_event(text, date, text, integer) to service_role;