Spécifications KidzCode

Modèles emails Spécifications

Document interne — accès réservé aux administrateurs · Dernière mise à jour : mars 2026

Sommaire

🎯 1. Vision du produit

KidzCode est une application web de sécurité pour enfants. Des QR codes physiques sont distribués aux parents lors de commandes. Si un enfant est perdu, toute personne peut scanner le QR code avec son téléphone pour accéder aux informations de contact du parent — sans avoir besoin d'un compte.

Acteurs

🔄 2. Flux utilisateurs

Parcours parent

Parcours visiteur (scannage)

Parcours visiteur — QR code illisible

Parcours administrateur

🏷️ 3. Format QR code

Format : KID-XXXXX — le préfixe KID- suivi de 5 lettres majuscules aléatoires.

Exemples : KID-ABCDE, KID-ZQRMT, KID-UVWXY

Contrôle : CHECK (id ~ '^KID-[A-Z]{5}$') en base de données.

Capacité théorique : 26⁵ = 11 881 376 codes uniques possibles.

URL encodée dans le QR

https://mondomaine.com/e.html?id=KID-XXXXX

Le paramètre id est lu par e.html via URLSearchParams.

États d'un QR code

claimedactifÉtatDescription
falsetrueDisponibleDans l'inventaire, prêt à être associé
truetrueAssociéLié à un enfant, fonctionnel
falsefalseDésactivéDésactivé par l'admin, non utilisable

🗄️ 4. Schéma base de données

Hébergé sur Supabase (PostgreSQL). Schéma public.

Table profiles

ColonneTypeDescription
idUUID (PK)Référence auth.users(id) ON DELETE CASCADE
nomTEXTNom affiché du parent
emailTEXTEmail (copié depuis auth)
telephoneTEXTNuméro de contact du parent (usage interne — non exposé aux visiteurs)
roleTEXT'utilisateur' ou 'admin'
created_atTIMESTAMPTZDate de création

Table enfants

ColonneTypeDescription
idUUID (PK)Généré automatiquement
user_idUUID (FK)Référence profiles(id) ON DELETE CASCADE
prenomTEXT NOT NULLPrénom de l'enfant
contactTEXT NOT NULLNuméro de contact affiché aux visiteurs lors du scannage (via get_qr_info())
qr_codeTEXT nullableID du QR code associé (dénormalisé, pour affichage rapide)
messageTEXTMessage affiché au visiteur
created_atTIMESTAMPTZDate de création

Table qr_codes

ColonneTypeDescription
idTEXT (PK)Format KID-[A-Z]{5} — contrainte CHECK
claimedBOOLEANtrue = associé à un enfant
actifBOOLEANfalse = désactivé définitivement par admin
enfant_idUUID nullable (FK)Référence enfants(id) ON DELETE SET NULL
claimed_atTIMESTAMPTZDate d'association
created_atTIMESTAMPTZDate d'ajout dans l'inventaire

Table signalements

ColonneTypeDescription
idUUID (PK)Généré automatiquement
contactTEXT NOT NULLNuméro de contact du signalant
localisationTEXT NOT NULLLieu où l'enfant a été trouvé
descriptionTEXTDescription facultative de l'enfant
email_signalantTEXTEmail facultatif du signalant (pour email de remerciement à la clôture)
statutTEXT'ouvert' ou 'ferme'
created_atTIMESTAMPTZDate de création
closed_atTIMESTAMPTZDate de clôture (NULL si encore ouvert)

Table email_templates

ColonneTypeDescription
idTEXT (PK)Identifiant du modèle : 'alerte', 'cloture_tous', 'cloture_signalant'
sujetTEXT NOT NULLSujet de l'email (peut contenir des {{placeholders}})
corpsTEXT NOT NULLCorps de l'email en texte brut (peut contenir des {{placeholders}})
updated_atTIMESTAMPTZDernière modification

Placeholders disponibles : {{contact}}, {{localisation}}, {{description}}, {{date}}.

Table qr_desactives (legacy)

Conservée pour compatibilité ascendante. Non utilisée dans la logique actuelle — la désactivation est gérée par le champ actif dans qr_codes.

Contraintes d'intégrité

🔐 5. Sécurité & Row Level Security

Principes

Policies par table

TablePolicyRègle
profilesselectLecture de son propre profil, ou si admin
profilesupdateMise à jour de son propre profil
profilesupdate (admin)L'admin peut modifier tout profil
enfantsallAccès complet à ses propres enfants (user_id = auth.uid())
enfantsselect (admin)L'admin peut lire tous les enfants
qr_codesall (admin)L'admin a accès complet
qr_codesselectTout utilisateur connecté peut lire (pour vérifier disponibilité)
qr_codesupdateUn parent peut désactiver (actif = false) ses QR codes uniquement
qr_desactivesallAccès complet à ses propres entrées
signalementsinsertN'importe qui (y compris anon) peut créer un signalement
signalementsselect / updateAdmin uniquement (lecture + mise à jour statut / closed_at)
email_templatesselect / updateAdmin uniquement (lecture + modification des modèles)

⚙️ 6. Fonctions SQL

is_admin()

Retourne BOOLEAN. Vérifie si auth.uid() a le rôle 'admin' dans profiles. SECURITY DEFINER pour éviter la récursion des policies RLS.

generate_qr_id()

Génère un identifiant KID-XXXXX aléatoire unique. Boucle jusqu'à trouver un ID non existant. Accessible aux utilisateurs connectés.

claim_qr_code(qr_id TEXT, p_enfant_id UUID)

Associe un QR code à un enfant. Vérifie que l'enfant appartient à auth.uid(). Met à jour claimed = true, enfant_id, claimed_at uniquement si le code existe, est disponible et actif. Retourne BOOLEAN.

release_qr_code(qr_id TEXT)

Libère un QR code (le parent retire l'association). Vérifie ownership via jointure. Remet claimed = false, enfant_id = NULL, claimed_at = NULL. Retourne BOOLEAN.

get_qr_info(qr_id TEXT)

Retourne les données propres à l'enfant : enfant_prenom, enfant_message, enfant_contact. Le nom et le téléphone du profil parent ne sont pas exposés. SECURITY DEFINER. Accessible à anon via GRANT EXECUTE. Filtre : claimed = true AND actif = true. La jointure avec profiles est supprimée.

admin_deactivate_qr_code(qr_id TEXT)

Admin uniquement. Désactive un QR code (actif = false, claimed = false, enfant_id = NULL). Si le code était associé à un enfant, efface aussi enfants.qr_code.

admin_reactivate_qr_code(qr_id TEXT)

Admin uniquement. Remet actif = true sur un QR code désactivé. Le code redevient disponible (non associé).

admin_delete_qr_code(qr_id TEXT)

Admin uniquement. Supprime définitivement un QR code. Retire d'abord enfants.qr_code si associé, puis supprime la ligne.

Trigger on_enfant_deleted

BEFORE UPDATE sur qr_codes. Quand enfant_id passe de non-NULL à NULL (suite à ON DELETE SET NULL), remet automatiquement claimed = false et claimed_at = NULL.

Trigger on_auth_user_created

AFTER INSERT sur auth.users. Crée automatiquement une ligne dans profiles avec les infos de l'inscription (id, nom, email).

🛠️ 7. Interface administrateur (admin.html)

Protection d'accès

La page appelle requireAdmin() au chargement. Si l'utilisateur n'est pas connecté → redirection vers auth.html. Si connecté mais pas admin → redirection vers index.html.

Onglet Utilisateurs

Onglet QR Codes

Actions par QR code

ActionConditionEffet
🖼️ QR CodeToujours disponibleOuvre une modale avec l'image QR + bouton Imprimer ×12
👁 SimulerToujours disponibleOuvre e.html?id=KID-XXXXX dans un nouvel onglet — permet de voir ce qu'un visiteur verrait en scannant le QR code (infos de l'enfant, ou message d'erreur si non associé / désactivé)
DésactiverActif (disponible ou associé)Appelle admin_deactivate_qr_code(). Si associé : avertissement, l'enfant perd son QR
RéactiverDésactivéAppelle admin_reactivate_qr_code(). Le code redevient disponible
SupprimerToujours (avec confirmation)Appelle admin_delete_qr_code(). Suppression définitive

Onglet Signalements

Liste tous les signalements de QR codes illisibles avec : date, contact, localisation, description, email signalant, statut (badge ouvert / fermé).

ActionConditionEffet
📧 AlerterSignalement ouvertAppelle l'Edge Function emails avec action: 'send_alerte'. Envoie un email d'alerte à tous les utilisateurs inscrits.
✓ ClôturerSignalement ouvertAppelle l'Edge Function emails avec action: 'close_signalement'. Marque le signalement ferme, envoie un email de clôture à tous les utilisateurs et un email de remerciement au signalant (si email fourni).

Lien vers admin-emails.html depuis l'onglet Signalements pour modifier les modèles d'emails.

Signalement sur le profil parent

Quand un admin désactive un QR code associé, la carte enfant dans profil.html affiche une bannière d'avertissement ⚠️ avec la classe child-card--warning. Cela permet au parent de savoir que son QR code a été désactivé et de contacter le support.

Détail des enfants d'un utilisateur

Chaque ligne du tableau utilisateurs est cliquable (curseur pointeur, fond bleu au survol). Un clic ouvre une modale affichant les enfants déclarés par cet utilisateur, avec pour chaque enfant : prénom, numéro de contact, numéro de QR code associé, et message personnalisé. La colonne "Enfants" affiche un badge bleu quand le compte a au moins un enfant. La cellule "Actions" stoppe la propagation du clic pour ne pas déclencher la modale par erreur. La requête Supabase utilise select('*, enfants(*)') pour charger toutes les données enfants en une seule requête.

🖨️ 8. Génération & impression de QR codes

Bibliothèque

QRCode.js via CDN (cdnjs.cloudflare.com) — génération côté navigateur, aucun service tiers sollicité.

Chargement : <script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>

Flux de génération (modale admin)

Page d'impression (print-qr.html)

Décision d'architecture

La page d'impression est dans un fichier séparé (print-qr.html) plutôt qu'une fenêtre générée par document.write(). Raison : les balises </script> dans les templates JS terminent le bloc <script type="module"> parent prématurément.

📄 9. Pages de l'application

FichierAccèsDescription
index.htmlPublicPage d'accueil
auth.htmlPublicConnexion / inscription
qui-sommes-nous.htmlPublicÀ propos
e.htmlPublic (QR code)Page visiteur affichant les infos de contact après scan QR
commander.htmlConnectéPage de commande
panier.htmlConnectéPanier
aide.htmlConnectéFAQ / aide
profil.htmlConnectéGestion du profil parent et des enfants
admin.htmlAdminInterface d'administration
print-qr.htmlAdminPage d'impression 12 QR codes (3×4)
admin-emails.htmlAdminÉditeur des 3 modèles d'emails (alerte, clôture tous, clôture signalant)
specs.htmlAdminCe document de spécifications

Fichiers JS partagés

FichierRôle
supabase.jsInit client Supabase. Exporte supabase (client), SUPABASE_URL et SUPABASE_ANON_KEY (utilisés par l'Edge Function via supabase.functions.invoke())
auth.jsHelpers : getSession, getProfile, requireAuth, requireAdmin, logout, initHeader. Le menu et l'icône panier affichés par initHeader varient selon le rôle : visiteur → Accueil + Qui sommes-nous + Connexion, icône panier masquée ; utilisateur connecté → + Commander + Aide + Mon Profil + icône panier ; admin → + Administration
cart.jsGestion du panier (localStorage) : getCart, saveCart, addToCart, updateCartBadge. Inclus sur toutes les pages via <script src="cart.js">

🏗️ 10. Décisions techniques

Pas de FK circulaire

qr_codes.enfant_id référence enfants.id (FK réelle). enfants.qr_code est un TEXT dénormalisé — pas de FK inverse. Cela évite les contraintes circulaires tout en permettant l'affichage rapide côté parent.

Accès visiteur sans RLS

Les visiteurs anonymes n'ont aucune policy de lecture sur les tables. Leur accès passe exclusivement par get_qr_info() (SECURITY DEFINER) — contrôle précis des données exposées, sans risque d'over-fetching.

is_admin() SECURITY DEFINER

Si is_admin() était une fonction normale, son appel dans une policy provoquerait une récursion (la policy tente de lire profiles, ce qui déclenche à nouveau la policy). SECURITY DEFINER exécute la requête en tant que propriétaire de la fonction, contournant RLS.

Trigger de libération

Quand un enfant est supprimé, PostgreSQL met enfant_id à NULL via ON DELETE SET NULL. Le trigger on_enfant_deleted (BEFORE UPDATE) intercepte ce changement pour remettre claimed = false et claimed_at = NULL dans la même transaction.

QR code "brûlé"

Un QR code ne peut être associé qu'à un seul enfant à la fois. Quand un parent libère un QR code ou supprime un enfant, le code redevient disponible pour n'importe quel autre parent. Il n'y a pas de notion d'"ownership" permanent d'un QR code.

Séparation print-qr.html

La génération d'une fenêtre d'impression via document.write() avec des balises <script> intégrées cause des problèmes de parsing HTML. La solution choisie est d'ouvrir une page HTML dédiée (print-qr.html) via window.open() avec les paramètres dans l'URL.

Confirmation contextuelle pour désactivation admin

Le message de confirmation affiché à l'admin varie selon l'état du QR code. Si le code est associé à un enfant, le message précise que cet enfant perdra son QR code. La logique de construction du message est dans le corps de la fonction JS (pas dans l'attribut onclick) pour éviter les problèmes d'apostrophes dans les attributs HTML.

⚡ 11. Edge Function emails

Rôle

Fonction Deno déployée sur Supabase Edge Functions. Gère l'envoi des emails liés aux signalements via l'API Resend. Fichier : supabase/functions/emails/index.ts.

Secrets requis

VariableDescription
RESEND_API_KEYClé API Resend pour l'envoi des emails
FROM_EMAILAdresse expéditeur, ex. KidzCode <noreply@amja.space>
SUPABASE_URLInjecté automatiquement par Supabase
SUPABASE_ANON_KEYInjecté automatiquement par Supabase
SUPABASE_SERVICE_ROLE_KEYInjecté automatiquement par Supabase

Authentification interne

Actions disponibles

ActionDescription
send_alerteCharge le modèle alerte, substitue les placeholders, envoie à tous les emails de la table profiles. Retourne { sent: N }.
close_signalementMarque le signalement ferme + closed_at, envoie le modèle cloture_tous à tous les utilisateurs, et si email_signalant existe, envoie le modèle cloture_signalant au signalant. Retourne { sentUsers: N, sentSignalant: bool }.

Invocation côté client

Utiliser exclusivement supabase.functions.invoke('emails', { body: { action, signalement_id } }) — et non un fetch() manuel. supabase.functions.invoke() injecte automatiquement le token d'authentification dans le header.

Domaine email (Resend)

Le domaine amja.space est configuré dans Resend avec les enregistrements DNS (SPF, DKIM, DMARC) ajoutés chez Porkbun pour autoriser l'envoi depuis noreply@amja.space.

📌 12. Prochaines étapes

TâchePrioritéStatut
Créer e.html (page visiteur après scan QR)Haute✓ Fait
Créer print-qr.html (impression 12 QR codes)Moyenne✓ Fait
Restreindre l'accès visiteur : commander.html, panier.html, aide.html protégés par requireAuth() ; print-qr.html par requireAdmin() ; menu et icône panier adaptés au rôle (masqués pour le visiteur)Haute✓ Fait
Ajouter les QR codes dans l'inventaire adminHauteEn cours
Système de signalement QR illisible (modale, table signalements, onglet admin, emails)Haute✓ Fait
Edge Function emails (Resend, alertes, clôture)Haute✓ Fait
Page admin-emails.html — éditeur des modèles d'emailsMoyenne✓ Fait
Ajouter les vraies photos produits dans commander.htmlMoyenne✓ Fait
Connecter le panier à un vrai système de paiement (Stripe, etc.)MoyenneÀ faire
Mettre en ligne (hébergement du site statique)MoyenneÀ faire
Configurer le vrai domaine dans les URLs QR codeBasseEn attente hébergement