Rapport Technique

INFRASTRUCTURE
SYSTÈME

Analyse détaillée de la stack technique, de l'architecture backend, du flux de paiement et de la sécurité biométrique.

Vue d'ensemble

Une architecture moderne, sans serveur dédié

SmartLocker est conçue comme une Progressive Web App serverless dans son esprit : un backend Flask léger orchestrant des services managés (Firebase, Stripe), un frontend vanilla optimisé mobile, et l'API WebAuthn native pour la biométrie. Cette approche permet une scalabilité immédiate sans gestion d'infrastructure complexe.

Stack Technique

Chaque brique a été choisie pour sa fiabilité et sa documentation

Backend

Python 3.10 / Flask, hébergé sur PythonAnywhere (free tier). Routes REST, sessions Flask, webhook Stripe idempotent.

Base de données

Firebase Realtime Database (projet smartlocker-94e11). Synchronisation temps réel des états casiers, sessions et paired_lockers.

Authentification

Firebase Auth (email/mot de passe + Google OAuth). Stockage de idToken en session pour les appels API authentifiés.

Paiement

Stripe Checkout. Webhook checkout.session.completed avec idempotency key pour éviter les doubles paiements.

Biométrie

API WebAuthn native. Compatible iOS Safari (Passkey / iCloud Keychain) et Android Chrome (empreinte capteur).

Frontend / PWA

HTML5, CSS3, JS ES6 vanilla. Chart.js pour les statistiques. Service Worker v5 + Web App Manifest pour l'installation.

Architecture des fichiers

Organisation modulaire du projet Flask

flask_app.py + blueprints

Backend découpé en modules selon les conventions Flask : flask_app.py (point d'entrée) enregistre des blueprints par domaine (routes_auth, routes_sport, routes_school, routes_admin, routes_api…), avec extensions.py (Firebase/Stripe) et helpers.py (utilitaires + @require_admin).

config.py

Toutes les clés API isolées (Firebase, Stripe). Ignoré par git via .gitignore. Jamais versionné.

templates/

7 templates : index.html, settings.html, locker_school.html, locker_sport.html, history.html, admin.html, payment_success.html.

static/

Assets, Service Worker v5, Manifest PWA, icônes, CSS. Cache stratégique pour le mode hors-ligne.

.gitignore

Protège config.py, *.env, __pycache__ et tout fichier sensible.

README.md

Documentation complète : installation, configuration Firebase, configuration Stripe, déploiement PythonAnywhere.

Flux de fonctionnement

Du paiement au déverrouillage

1. Réservation

L'utilisateur sélectionne un créneau dans le calendrier. Flask crée une Stripe Checkout Session et redirige vers la page de paiement.

2. Webhook Stripe

Au succès du paiement, Stripe POST sur /stripe/webhook. Flask vérifie la signature, crée la session dans Firebase, passe le casier en reserved.

3. Transition d'état

À l'arrivée du créneau, le casier passe automatiquement de reserved à unlocked. L'utilisateur peut alors déverrouiller via WebAuthn.

4. Notifications

Le frontend déclenche des notifications navigateur 5 min et 1 min avant l'expiration via l'API Notification.

5. Fin de session

À l'expiration, le casier repasse en locked automatiquement. La session est archivée dans /sessions/{uid}.

6. Historique

L'utilisateur consulte ses sessions passées et exporte le tout en CSV via /history/export.

Modèle de données Firebase

Structure hiérarchique JSON

/lockers/{id}

État live de chaque casier : state (locked / reserved / unlocked), current_user, expires_at, updated_at.

/users/{uid}

Profil utilisateur : email, photo, is_premium, paired_lockers (dict de casiers appairés), date d'inscription.

/sessions/{uid}/{session_id}

Historique des sessions : locker_id, started_at, ended_at, amount_paid, stripe_session_id.

/webauthn/{uid}

Credential IDs WebAuthn enregistrés par utilisateur. Permet la vérification serveur de la signature biométrique.

/stripe_events/{event_id}

Idempotency key des événements Stripe. Empêche le double-traitement d'un même webhook en cas de retry.

/admins

Liste des emails admin (case-insensitive). Contrôle l'accès au tableau de bord via le décorateur @require_admin.

Tableau de bord Admin

Détection par email avec décorateur @require_admin

État live

État des deux casiers en temps réel, auto-refresh toutes les 60 secondes via JavaScript setInterval.

KPI

Sessions totales, revenus Stripe, nombre d'utilisateurs, taux d'occupation hebdomadaire.

Gestion Premium

Activation/désactivation du statut Premium des utilisateurs via /admin/toggle_premium.

Table utilisateurs

Liste complète avec statut Premium, nombre de sessions, dernière activité. Tri et filtre côté client.

Reset à distance

Possibilité de forcer le state=locked d'un casier en cas de problème, sans intervention physique.

Logs d'erreur

Visualisation des erreurs Stripe (paiements échoués) et WebAuthn (tentatives biométriques refusées).

Sécurité

Sept couches de protection

Clés isolées

Toutes les clés API (Firebase, Stripe) dans config.py, ignoré par git via .gitignore.

Webhook signé

Vérification de la signature Stripe sur chaque webhook pour empêcher les requêtes forgées.

WebAuthn

Authentification sans mot de passe, biométrie locale uniquement (la clé privée ne quitte jamais l'appareil).

idToken Firebase

Chaque appel à la base de données est authentifié par un idToken court-vivant (1h), pas par l'uid.

HTTPS forcé

PythonAnywhere force HTTPS sur tous les endpoints. WebAuthn refuse de fonctionner en HTTP.

Idempotency

Les webhooks Stripe sont déduplicaqués via leur event_id stocké en base. Pas de double paiement possible.

Défis techniques rencontrés

Les bugs qui ont fait progresser le projet

WebAuthn sur iOS

Safari traite isUserVerifyingPlatformAuthenticatorAvailable() comme consommant le geste utilisateur, bloquant l'appel WebAuthn suivant. Résolu en supprimant la pré-vérification.

WebAuthn sur Android

L'option residentKey: 'preferred' provoquait un NotReadableError sur Chrome Android. Résolu en la supprimant pour utiliser le capteur en mode non-discoverable.

Login Google

Le bug original stockait l'uid au lieu de l'idToken, cassant tous les appels Firebase suivants. Résolu en récupérant credential.idToken du flow OAuth.

Double paiement Stripe

Sans idempotency key, un retry réseau créait deux paiements. Résolu en stockant les event_id Stripe dans Firebase et en filtrant les doublons.

Boucle de reload

Le timer de locker_school.html déclenchait un reload infini à T=0. Résolu en ajoutant une condition d'arrêt et un cleanup de l'setInterval.

Email admin

La détection admin était case-sensitive : Admin@inraci.be ne matchait pas. Résolu avec .lower() sur les deux côtés de la comparaison.

Pourquoi ces choix techniques ?

Justification des technologies retenues

Pourquoi Flask ?

Léger, lisible, idéal pour un projet pédagogique. Documentation excellente, intégration native avec Jinja2, déploiement trivial sur PythonAnywhere.

Pourquoi Firebase ?

Auth + DB + Storage en un seul service. Synchronisation temps réel native, SDK web maintenu par Google, free tier généreux.

Pourquoi Stripe ?

Standard de l'industrie pour les paiements en ligne. Documentation impeccable, mode test illimité, webhook fiable, conformité PCI gérée.

Pourquoi WebAuthn ?

Standard W3C, supporté nativement par Safari et Chrome. Pas de SDK propriétaire, pas de coût, sécurité cryptographique éprouvée.

Pourquoi PWA et non app native ?

Une seule base de code pour iOS et Android. Pas de validation par les stores, déploiement instantané, mises à jour automatiques.

Pourquoi PythonAnywhere ?

Gratuit, Python natif, configuration WSGI ultra-simple, console SSH incluse, HTTPS gratuit. Parfait pour un TFE.