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
+39
View File
@@ -0,0 +1,39 @@
// Comparaison de secrets en temps constant, pour éviter les attaques par
// mesure de timing sur des comparaisons naïves (`a === b`) qui court-
// circuitent dès le premier caractère différent.
//
// On hache d'abord les deux valeurs en SHA-256 (Web Crypto native, pas de
// dépendance tierce) : ça a deux effets utiles ici -
// 1. la comparaison finale porte sur deux buffers de longueur FIXE
// (32 octets), donc la longueur du secret fourni par l'appelant ne
// fuite pas non plus (pas de court-circuit "longueur différente" sur
// l'entrée brute) ;
// 2. on peut comparer octet par octet sans early-return (accumulation par
// OR bit à bit) pour un temps d'exécution indépendant du contenu.
async function sha256(input: string): Promise<Uint8Array> {
const bytes = new TextEncoder().encode(input);
const digest = await crypto.subtle.digest("SHA-256", bytes);
return new Uint8Array(digest);
}
function constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean {
// a et b sont toujours des digests SHA-256 ici (32 octets), donc cette
// comparaison de longueur ne dépend jamais des entrées utilisateur.
if (a.length !== b.length) return false;
let diff = 0;
for (let i = 0; i < a.length; i++) {
diff |= a[i] ^ b[i];
}
return diff === 0;
}
/** Vrai si `provided` correspond à `expected`, en temps constant. */
export async function timingSafeEqual(
provided: string | null | undefined,
expected: string | null | undefined
): Promise<boolean> {
if (!provided || !expected) return false;
const [a, b] = await Promise.all([sha256(provided), sha256(expected)]);
return constantTimeEqual(a, b);
}