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
- Visiteur (anonyme) — accède uniquement à
index.htmletqui-sommes-nous.html; peut accéder àe.htmlen scannant un QR code - Parent (utilisateur connecté) — crée son profil, ajoute ses enfants, associe un QR code à un enfant, configure le message affiché
- Administrateur — gère l'inventaire de QR codes, accède à la liste des utilisateurs et enfants
🔄 2. Flux utilisateurs
Parcours parent
- Crée un compte sur
auth.html(email + mot de passe + nom) - Accède à son profil sur
profil.html: ajoute ses enfants (prénom, contact) - Associe un QR code à un enfant en saisissant son identifiant
KID-XXXXX - Peut rédiger un message personnalisé affiché à la personne qui scanne
- Peut désassocier un QR code (le code redevient disponible dans l'inventaire)
- Peut supprimer un enfant (le QR code associé est automatiquement libéré)
Parcours visiteur (scannage)
- Scanne le QR code → redirigé vers
e.html?id=KID-XXXXX - La page appelle la fonction SQL
get_qr_info()(SECURITY DEFINER) - Affiche uniquement les données propres à l'enfant : prénom, numéro de contact (
enfants.contact), message personnalisé - Le nom et le téléphone du profil parent ne sont pas exposés
- Si le QR est désactivé ou non associé → message d'erreur approprié
Parcours visiteur — QR code illisible
- Sur
index.html, clique sur le bouton "J'ai trouvé un enfant avec un QR code illisible" - Une modale s'ouvre : champs contact (obligatoire), localisation (obligatoire), description (facultatif), email (facultatif)
- À la soumission, une ligne est insérée dans la table
signalements(accessible aux anonymes via RLS) - Le visiteur reçoit un message de confirmation dans la modale
Parcours administrateur
- Accède à
admin.html(protection par rôle) - Onglet Utilisateurs : liste tous les comptes, peut promouvoir/rétrograder admin
- Onglet QR Codes : gère l'inventaire complet
- Onglet Signalements : consulte les signalements de QR codes illisibles, envoie des alertes email, clôture les dossiers
🏷️ 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
| claimed | actif | État | Description |
|---|---|---|---|
| false | true | Disponible | Dans l'inventaire, prêt à être associé |
| true | true | Associé | Lié à un enfant, fonctionnel |
| false | false | Dé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
| Colonne | Type | Description |
|---|---|---|
id | UUID (PK) | Référence auth.users(id) ON DELETE CASCADE |
nom | TEXT | Nom affiché du parent |
email | TEXT | Email (copié depuis auth) |
telephone | TEXT | Numéro de contact du parent (usage interne — non exposé aux visiteurs) |
role | TEXT | 'utilisateur' ou 'admin' |
created_at | TIMESTAMPTZ | Date de création |
Table enfants
| Colonne | Type | Description |
|---|---|---|
id | UUID (PK) | Généré automatiquement |
user_id | UUID (FK) | Référence profiles(id) ON DELETE CASCADE |
prenom | TEXT NOT NULL | Prénom de l'enfant |
contact | TEXT NOT NULL | Numéro de contact affiché aux visiteurs lors du scannage (via get_qr_info()) |
qr_code | TEXT nullable | ID du QR code associé (dénormalisé, pour affichage rapide) |
message | TEXT | Message affiché au visiteur |
created_at | TIMESTAMPTZ | Date de création |
Table qr_codes
| Colonne | Type | Description |
|---|---|---|
id | TEXT (PK) | Format KID-[A-Z]{5} — contrainte CHECK |
claimed | BOOLEAN | true = associé à un enfant |
actif | BOOLEAN | false = désactivé définitivement par admin |
enfant_id | UUID nullable (FK) | Référence enfants(id) ON DELETE SET NULL |
claimed_at | TIMESTAMPTZ | Date d'association |
created_at | TIMESTAMPTZ | Date d'ajout dans l'inventaire |
Table signalements
| Colonne | Type | Description |
|---|---|---|
id | UUID (PK) | Généré automatiquement |
contact | TEXT NOT NULL | Numéro de contact du signalant |
localisation | TEXT NOT NULL | Lieu où l'enfant a été trouvé |
description | TEXT | Description facultative de l'enfant |
email_signalant | TEXT | Email facultatif du signalant (pour email de remerciement à la clôture) |
statut | TEXT | 'ouvert' ou 'ferme' |
created_at | TIMESTAMPTZ | Date de création |
closed_at | TIMESTAMPTZ | Date de clôture (NULL si encore ouvert) |
Table email_templates
| Colonne | Type | Description |
|---|---|---|
id | TEXT (PK) | Identifiant du modèle : 'alerte', 'cloture_tous', 'cloture_signalant' |
sujet | TEXT NOT NULL | Sujet de l'email (peut contenir des {{placeholders}}) |
corps | TEXT NOT NULL | Corps de l'email en texte brut (peut contenir des {{placeholders}}) |
updated_at | TIMESTAMPTZ | Derniè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é
- Suppression d'un utilisateur → supprime ses enfants (CASCADE) → libère ses QR codes (SET NULL + trigger)
- Suppression d'un enfant → libère son QR code (SET NULL + trigger
on_enfant_deleted) - Pas de FK circulaire :
qr_codes.enfant_idpointe versenfants, maisenfants.qr_codeest un TEXT dénormalisé (non une FK)
🔐 5. Sécurité & Row Level Security
Principes
- RLS activé sur toutes les tables :
profiles,enfants,qr_codes,qr_desactives,signalements,email_templates - Les visiteurs anonymes n'ont aucun accès direct aux tables — ils passent par des fonctions SECURITY DEFINER
- La fonction
is_admin()est SECURITY DEFINER pour éviter la récursion des policies
Policies par table
| Table | Policy | Règle |
|---|---|---|
profiles | select | Lecture de son propre profil, ou si admin |
profiles | update | Mise à jour de son propre profil |
profiles | update (admin) | L'admin peut modifier tout profil |
enfants | all | Accès complet à ses propres enfants (user_id = auth.uid()) |
enfants | select (admin) | L'admin peut lire tous les enfants |
qr_codes | all (admin) | L'admin a accès complet |
qr_codes | select | Tout utilisateur connecté peut lire (pour vérifier disponibilité) |
qr_codes | update | Un parent peut désactiver (actif = false) ses QR codes uniquement |
qr_desactives | all | Accès complet à ses propres entrées |
signalements | insert | N'importe qui (y compris anon) peut créer un signalement |
signalements | select / update | Admin uniquement (lecture + mise à jour statut / closed_at) |
email_templates | select / update | Admin 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
- Liste tous les profils avec email, nom, téléphone, rôle, date d'inscription
- Bouton pour promouvoir un utilisateur en admin ou le rétrograder
Onglet QR Codes
- Statistiques : total / disponibles / associés / désactivés
- Formulaire d'ajout : saisie manuelle ou génération automatique via
generate_qr_id() - Tableau avec colonnes : ID, statut (badge coloré), enfant associé, date de réclamation, actions
Actions par QR code
| Action | Condition | Effet |
|---|---|---|
| 🖼️ QR Code | Toujours disponible | Ouvre une modale avec l'image QR + bouton Imprimer ×12 |
| 👁 Simuler | Toujours disponible | Ouvre 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ésactiver | Actif (disponible ou associé) | Appelle admin_deactivate_qr_code(). Si associé : avertissement, l'enfant perd son QR |
| Réactiver | Désactivé | Appelle admin_reactivate_qr_code(). Le code redevient disponible |
| Supprimer | Toujours (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é).
| Action | Condition | Effet |
|---|---|---|
| 📧 Alerter | Signalement ouvert | Appelle l'Edge Function emails avec action: 'send_alerte'. Envoie un email d'alerte à tous les utilisateurs inscrits. |
| ✓ Clôturer | Signalement ouvert | Appelle 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)
- Clic sur 🖼️ → la modale s'ouvre
new QRCode(container, { text: url, width: 200, height: 200 })génère l'image- L'URL encodée est
e.html?id=KID-XXXXX(chemin relatif) - Bouton "Imprimer ×12" → ouvre
print-qr.html?id=KID-XXXXX&base=https://...dans un nouvel onglet
Page d'impression (print-qr.html)
- Reçoit
idetbaseviaURLSearchParams - Génère une grille 3 colonnes × 4 lignes (12 QR codes)
- Chaque cellule : image QR + identifiant
KID-XXXXXen dessous - Appel automatique à
window.print()après génération - CSS
@media print: pas de marges, fond blanc, grille pleine page
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
| Fichier | Accès | Description |
|---|---|---|
index.html | Public | Page d'accueil |
auth.html | Public | Connexion / inscription |
qui-sommes-nous.html | Public | À propos |
e.html | Public (QR code) | Page visiteur affichant les infos de contact après scan QR |
commander.html | Connecté | Page de commande |
panier.html | Connecté | Panier |
aide.html | Connecté | FAQ / aide |
profil.html | Connecté | Gestion du profil parent et des enfants |
admin.html | Admin | Interface d'administration |
print-qr.html | Admin | Page d'impression 12 QR codes (3×4) |
admin-emails.html | Admin | Éditeur des 3 modèles d'emails (alerte, clôture tous, clôture signalant) |
specs.html | Admin | Ce document de spécifications |
Fichiers JS partagés
| Fichier | Rôle |
|---|---|
supabase.js | Init client Supabase. Exporte supabase (client), SUPABASE_URL et SUPABASE_ANON_KEY (utilisés par l'Edge Function via supabase.functions.invoke()) |
auth.js | Helpers : 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.js | Gestion 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
| Variable | Description |
|---|---|
RESEND_API_KEY | Clé API Resend pour l'envoi des emails |
FROM_EMAIL | Adresse expéditeur, ex. KidzCode <noreply@amja.space> |
SUPABASE_URL | Injecté automatiquement par Supabase |
SUPABASE_ANON_KEY | Injecté automatiquement par Supabase |
SUPABASE_SERVICE_ROLE_KEY | Injecté automatiquement par Supabase |
Authentification interne
- L'option "Verify JWT" doit être désactivée dans les paramètres de l'Edge Function (Supabase Dashboard → Edge Functions → emails → Settings). La vérification est gérée manuellement dans le code.
- La fonction crée un userClient avec la clé anon + le header
Authorizationde la requête pour vérifier le JWT viaauth.getUser(). - Elle crée ensuite un adminClient avec la clé service role pour les opérations admin (lecture
profiles,signalements,email_templates). - Seul un utilisateur avec
role = 'admin'dansprofilespeut déclencher les actions.
Actions disponibles
| Action | Description |
|---|---|
send_alerte | Charge le modèle alerte, substitue les placeholders, envoie à tous les emails de la table profiles. Retourne { sent: N }. |
close_signalement | Marque 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âche | Priorité | 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 admin | Haute | En 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'emails | Moyenne | ✓ Fait |
Ajouter les vraies photos produits dans commander.html | Moyenne | ✓ 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 code | Basse | En attente hébergement |