# ============================================================================
# [LIVRE] SESSIONS SOUS LE CAPOT - IMPLÉMENTATION CUSTOM
# ============================================================================
#
# [OBJECTIF] OBJECTIFS :
# - Comprendre sessions en Python pur
# - Implémenter sessions custom from scratch
# - Sessions avec autres frameworks (Django, FastAPI, etc.)
# - Sessions côté serveur sans framework
# - Comprendre ce qui se passe réellement
#
# [TEMPS] TEMPS : ~4-5 heures
# [DOCS] NIVEAU : Intermédiaire à Avancé
# ============================================================================


# ============================================================================
# [GUIDE] CHAPITRE 1 : ANATOMIE D'UNE SESSION - LES BASES
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Ce qu'est RÉELLEMENT une session
[OK] Comment fonctionne le mécanisme cookie-session
[OK] Comment signer et vérifier des cookies
[OK] Comment sérialiser des données
"""


# ----------------------------------------------------------------------------
# [ANALYSE] QU'EST-CE QU'UNE SESSION VRAIMENT ?
# ----------------------------------------------------------------------------

"""
DÉFINITION TECHNIQUE

Une session = 3 composants :

1. SESSION ID : Identifiant unique (random string)
   Exemple : "a1b2c3d4e5f6789012345678901234567890"

2. SESSION DATA : Données associées à l'ID
   Exemple : {"user_id": 123, "cart": [1, 2, 3]}

3. MÉCANISME DE STOCKAGE : Où sont les données ?
   - Client-side : Données dans cookie (signées)
   - Server-side : Cookie = ID, données sur serveur


PROCESSUS COMPLET :

┌─────────────┐                              ┌─────────────┐
│  NAVIGATEUR │                              │   SERVEUR   │
└──────┬──────┘                              └──────┬──────┘
       │                                            │
       │  1. GET /login (sans cookie)               │
       │──────────────────────────────────────────[BLACK_RIGHT-POINTING_TRIANGLE] │
       │                                            │
       │                                            │ 2. Créer session
       │                                            │    ID = random()
       │                                            │    Data = {}
       │                                            │
       │  3. Response + Set-Cookie: session=abc123  │
       │ [BLACK_LEFT-POINTING_TRIANGLE]──────────────────────────────────────────│
       │                                            │
   4. Stocke                                        │
   cookie localement                                │
       │                                            │
       │  5. GET /dashboard                         │
       │     Cookie: session=abc123                 │
       │──────────────────────────────────────────[BLACK_RIGHT-POINTING_TRIANGLE] │
       │                                            │
       │                                            │ 6. Lire cookie
       │                                            │    Récupérer data
       │                                            │
       │  7. Response (personnalisée)               │
       │ [BLACK_LEFT-POINTING_TRIANGLE]──────────────────────────────────────────│
       │                                            │
"""


# ----------------------------------------------------------------------------
# [SECURISE] SIGNER UN COOKIE (HMAC)
# ----------------------------------------------------------------------------

"""
PROBLÈME : COOKIE NON SIGNÉ = MODIFIABLE


Sans signature :
"""

# Serveur envoie cookie
# Set-Cookie: user_id=123

# [X] Attaquant modifie dans navigateur
# Cookie: user_id=999

# Serveur lit cookie -> pense que user_id=999
# -> Accès au compte de quelqu'un d'autre !


"""
SOLUTION : HMAC (Hash-based Message Authentication Code)


Principe :
1. Serveur crée signature = HMAC(data, secret_key)
2. Cookie = data + signature
3. Client ne peut pas modifier sans casser signature
"""

import hmac
import hashlib
import base64
import json

class CookieSigner:
    """
    [IDEE] SIGNER ET VÉRIFIER COOKIES
    
    Implémentation from scratch du mécanisme de Flask
    """
    
    def __init__(self, secret_key):
        """
        Args:
            secret_key: Clé secrète (bytes ou str)
        """
        if isinstance(secret_key, str):
            secret_key = secret_key.encode('utf-8')
        
        self.secret_key = secret_key
    
    def sign(self, value):
        """
        Signer une valeur
        
        Args:
            value: Valeur à signer (str)
        
        Returns:
            str: valeur.signature (base64)
        
        Example:
            >>> signer = CookieSigner('my-secret')
            >>> signer.sign('user_id=123')
            'user_id=123.InVzZXJfaWQ9MTIzIg.abc123...'
        """
        # 1. Encoder valeur en bytes
        value_bytes = value.encode('utf-8')
        
        # 2. Créer signature HMAC-SHA256
        signature = hmac.new(
            self.secret_key,
            value_bytes,
            hashlib.sha256
        ).digest()
        
        # 3. Encoder signature en base64 (pour HTTP)
        signature_b64 = base64.urlsafe_b64encode(signature).decode('utf-8')
        
        # 4. Retourner valeur.signature
        return f"{value}.{signature_b64}"
    
    def unsign(self, signed_value):
        """
        Vérifier signature et extraire valeur
        
        Args:
            signed_value: Valeur signée (str)
        
        Returns:
            str: Valeur originale si signature valide
        
        Raises:
            ValueError: Si signature invalide
        
        Example:
            >>> signer = CookieSigner('my-secret')
            >>> signed = signer.sign('user_id=123')
            >>> signer.unsign(signed)
            'user_id=123'
        """
        # 1. Séparer valeur et signature
        try:
            value, signature_b64 = signed_value.rsplit('.', 1)
        except ValueError:
            raise ValueError("Invalid signed value format")
        
        # 2. Recréer signature depuis valeur
        expected_signature = hmac.new(
            self.secret_key,
            value.encode('utf-8'),
            hashlib.sha256
        ).digest()
        
        # 3. Décoder signature fournie
        try:
            provided_signature = base64.urlsafe_b64decode(signature_b64)
        except Exception:
            raise ValueError("Invalid signature encoding")
        
        # 4. Comparer signatures (timing-safe)
        if not hmac.compare_digest(expected_signature, provided_signature):
            raise ValueError("Signature verification failed - cookie tampered!")
        
        # 5. Retourner valeur si OK
        return value


"""
DÉMONSTRATION
"""

# Créer signer
signer = CookieSigner('my-secret-key-12345')

# Signer données
data = "user_id=123"
signed = signer.sign(data)
print(f"Signed: {signed}")
# -> user_id=123.XzE4YjQ5ZTE2YjE5ZTQ5ZjNhNjE4MjQ5ZTE2YjE5...

# Vérifier signature
try:
    original = signer.unsign(signed)
    print(f"Original: {original}")  # -> user_id=123
except ValueError as e:
    print(f"Error: {e}")

# [X] Essayer de modifier
tampered = signed.replace("123", "999")
try:
    signer.unsign(tampered)
except ValueError as e:
    print(f"Tampering detected: {e}")
    # -> Signature verification failed - cookie tampered!


"""
[IDEE] CE QUE FAIT FLASK

Flask fait exactement ça avec itsdangerous :

from itsdangerous import URLSafeTimedSerializer

serializer = URLSafeTimedSerializer(app.config['SECRET_KEY'])

# Signer
signed = serializer.dumps({'user_id': 123})

# Vérifier
data = serializer.loads(signed)  # max_age pour expiration
"""


# ----------------------------------------------------------------------------
# [PACKAGE] SÉRIALISER DES DONNÉES
# ----------------------------------------------------------------------------

"""
PROBLÈME : COOKIE = STRING UNIQUEMENT

On veut stocker :
- Dicts : {"user_id": 123, "cart": [1, 2, 3]}
- Lists : [1, 2, 3]
- Numbers : 123

Mais cookie = seulement string !


SOLUTION : SÉRIALISATION
"""

class SessionSerializer:
    """
    [IDEE] SÉRIALISER/DÉSÉRIALISER DONNÉES SESSION
    
    JSON + Base64 pour stocker structures complexes
    """
    
    @staticmethod
    def serialize(data):
        """
        Sérialiser données Python -> string
        
        Args:
            data: Dict, list, etc.
        
        Returns:
            str: Données encodées en base64
        
        Example:
            >>> SessionSerializer.serialize({'user_id': 123})
            'eyJ1c2VyX2lkIjogMTIzfQ=='
        """
        # 1. Convertir en JSON
        json_str = json.dumps(data, separators=(',', ':'))
        
        # 2. Encoder en bytes
        json_bytes = json_str.encode('utf-8')
        
        # 3. Encoder en base64 (safe pour URL/cookies)
        b64_bytes = base64.urlsafe_b64encode(json_bytes)
        
        # 4. Retourner string
        return b64_bytes.decode('utf-8')
    
    @staticmethod
    def deserialize(data_str):
        """
        Désérialiser string -> données Python
        
        Args:
            data_str: String encodée
        
        Returns:
            Original data
        
        Example:
            >>> SessionSerializer.deserialize('eyJ1c2VyX2lkIjogMTIzfQ==')
            {'user_id': 123}
        """
        try:
            # 1. Décoder base64
            b64_bytes = data_str.encode('utf-8')
            json_bytes = base64.urlsafe_b64decode(b64_bytes)
            
            # 2. Décoder bytes -> string
            json_str = json_bytes.decode('utf-8')
            
            # 3. Parser JSON
            data = json.loads(json_str)
            
            return data
            
        except Exception as e:
            raise ValueError(f"Deserialization failed: {e}")


"""
DÉMONSTRATION
"""

# Données complexes
session_data = {
    'user_id': 123,
    'username': 'alice',
    'cart': [1, 2, 3],
    'preferences': {
        'theme': 'dark',
        'language': 'fr'
    }
}

# Sérialiser
serialized = SessionSerializer.serialize(session_data)
print(f"Serialized: {serialized}")
# -> eyJ1c2VyX2lkIjoxMjMsInVzZXJuYW1lIjoiYWxpY2UiLCJjYXJ0IjpbMSwyLDNdLC...

# Désérialiser
deserialized = SessionSerializer.deserialize(serialized)
print(f"Deserialized: {deserialized}")
# -> {'user_id': 123, 'username': 'alice', ...}


# ============================================================================
# [GUIDE] CHAPITRE 2 : SESSION CLIENT-SIDE FROM SCRATCH
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Implémenter sessions client-side complètes
[OK] Créer gestionnaire de sessions
[OK] Intégrer avec serveur HTTP basique
[OK] Comprendre EXACTEMENT comment Flask fonctionne
"""


# ----------------------------------------------------------------------------
# [CONSTRUCTION] SESSION MANAGER CLIENT-SIDE
# ----------------------------------------------------------------------------

import uuid
from datetime import datetime, timedelta

class ClientSideSessionManager:
    """
    [IDEE] GESTIONNAIRE DE SESSIONS CLIENT-SIDE
    
    Implémentation complète from scratch
    Données stockées dans cookie signé (comme Flask par défaut)
    """
    
    def __init__(self, secret_key, cookie_name='session', max_age=None):
        """
        Args:
            secret_key: Clé secrète pour signature
            cookie_name: Nom du cookie
            max_age: Durée de vie en secondes (None = session browser)
        """
        self.signer = CookieSigner(secret_key)
        self.serializer = SessionSerializer()
        self.cookie_name = cookie_name
        self.max_age = max_age
    
    def create_session(self, data=None):
        """
        Créer nouvelle session
        
        Args:
            data: Données initiales (dict)
        
        Returns:
            str: Cookie value (signed + serialized)
        
        Example:
            >>> manager = ClientSideSessionManager('secret')
            >>> cookie_value = manager.create_session({'user_id': 123})
        """
        if data is None:
            data = {}
        
        # Ajouter métadonnées
        session_data = {
            '_id': str(uuid.uuid4()),  # Session ID unique
            '_created': datetime.utcnow().isoformat(),
            **data
        }
        
        # Sérialiser
        serialized = self.serializer.serialize(session_data)
        
        # Signer
        signed = self.signer.sign(serialized)
        
        return signed
    
    def read_session(self, cookie_value):
        """
        Lire session depuis cookie
        
        Args:
            cookie_value: Valeur du cookie
        
        Returns:
            dict: Données session ou {} si invalide
        
        Example:
            >>> data = manager.read_session(cookie_value)
            >>> print(data['user_id'])  # 123
        """
        if not cookie_value:
            return {}
        
        try:
            # 1. Vérifier signature
            serialized = self.signer.unsign(cookie_value)
            
            # 2. Désérialiser
            data = self.serializer.deserialize(serialized)
            
            # 3. Vérifier expiration (si max_age défini)
            if self.max_age:
                created = datetime.fromisoformat(data.get('_created'))
                age = (datetime.utcnow() - created).total_seconds()
                
                if age > self.max_age:
                    # Session expirée
                    return {}
            
            return data
            
        except (ValueError, KeyError) as e:
            print(f"Session read error: {e}")
            return {}
    
    def update_session(self, cookie_value, updates):
        """
        Mettre à jour session existante
        
        Args:
            cookie_value: Cookie actuel
            updates: Nouvelles données (dict)
        
        Returns:
            str: Nouveau cookie value
        
        Example:
            >>> new_cookie = manager.update_session(
            ...     old_cookie,
            ...     {'cart': [1, 2, 3]}
            ... )
        """
        # Lire session actuelle
        current_data = self.read_session(cookie_value)
        
        # Fusionner avec updates
        current_data.update(updates)
        
        # Recréer cookie
        # On garde _id et _created
        session_id = current_data.get('_id')
        created = current_data.get('_created')
        
        # Retirer métadonnées temporairement
        user_data = {k: v for k, v in current_data.items() 
                     if not k.startswith('_')}
        
        # Recréer avec métadonnées
        session_data = {
            '_id': session_id or str(uuid.uuid4()),
            '_created': created or datetime.utcnow().isoformat(),
            **user_data
        }
        
        # Sérialiser et signer
        serialized = self.serializer.serialize(session_data)
        signed = self.signer.sign(serialized)
        
        return signed
    
    def destroy_session(self):
        """
        Détruire session (retourne cookie vide)
        
        Returns:
            str: Cookie vide pour suppression
        """
        return ""
    
    def format_set_cookie_header(self, cookie_value, 
                                  path='/', 
                                  httponly=True, 
                                  secure=False,
                                  samesite='Lax'):
        """
        Formater header Set-Cookie HTTP
        
        Args:
            cookie_value: Valeur du cookie
            path: Path du cookie
            httponly: HttpOnly flag
            secure: Secure flag (HTTPS only)
            samesite: SameSite attribute
        
        Returns:
            str: Header Set-Cookie complet
        
        Example:
            >>> header = manager.format_set_cookie_header(cookie_value)
            >>> # 'session=abc123...; Path=/; HttpOnly; SameSite=Lax'
        """
        parts = [f"{self.cookie_name}={cookie_value}"]
        
        if path:
            parts.append(f"Path={path}")
        
        if self.max_age:
            parts.append(f"Max-Age={self.max_age}")
            
            # Calculer expires (pour vieux navigateurs)
            expires = datetime.utcnow() + timedelta(seconds=self.max_age)
            expires_str = expires.strftime('%a, %d %b %Y %H:%M:%S GMT')
            parts.append(f"Expires={expires_str}")
        
        if httponly:
            parts.append("HttpOnly")
        
        if secure:
            parts.append("Secure")
        
        if samesite:
            parts.append(f"SameSite={samesite}")
        
        return '; '.join(parts)


"""
DÉMONSTRATION COMPLÈTE
"""

# Créer manager
manager = ClientSideSessionManager(
    secret_key='my-super-secret-key-12345678',
    cookie_name='my_session',
    max_age=3600  # 1 heure
)

# 1. CRÉER SESSION
print("\n1. CRÉER SESSION")
print("=" * 60)

cookie_value = manager.create_session({'user_id': 123, 'username': 'alice'})
print(f"Cookie value: {cookie_value[:50]}...")

# Header Set-Cookie
header = manager.format_set_cookie_header(cookie_value)
print(f"Set-Cookie: {header}")


# 2. LIRE SESSION
print("\n2. LIRE SESSION")
print("=" * 60)

data = manager.read_session(cookie_value)
print(f"Session data: {data}")
print(f"User ID: {data.get('user_id')}")
print(f"Username: {data.get('username')}")


# 3. METTRE À JOUR SESSION
print("\n3. METTRE À JOUR SESSION")
print("=" * 60)

new_cookie = manager.update_session(cookie_value, {
    'cart': [1, 2, 3],
    'last_page': '/products'
})
print(f"New cookie: {new_cookie[:50]}...")

updated_data = manager.read_session(new_cookie)
print(f"Updated data: {updated_data}")


# 4. ESSAYER TAMPERING
print("\n4. TAMPERING (modification malveillante)")
print("=" * 60)

# Modifier cookie
tampered = cookie_value.replace('alice', 'bob')
tampered_data = manager.read_session(tampered)
print(f"Tampered data: {tampered_data}")  # {} - signature invalide !


# 5. SESSION EXPIRÉE
print("\n5. SESSION EXPIRÉE")
print("=" * 60)

# Manager avec max_age court
short_manager = ClientSideSessionManager('secret', max_age=1)
old_cookie = short_manager.create_session({'user_id': 999})

# Attendre 2 secondes
import time
time.sleep(2)

expired_data = short_manager.read_session(old_cookie)
print(f"Expired data: {expired_data}")  # {} - expiré !


# ============================================================================
# [GUIDE] CHAPITRE 3 : SERVEUR HTTP AVEC SESSIONS
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Intégrer sessions dans serveur HTTP basique
[OK] Gérer cookies dans requêtes/réponses
[OK] Parser headers Cookie
[OK] Comprendre le flow complet
"""


# ----------------------------------------------------------------------------
# [WEB] SERVEUR HTTP BASIQUE AVEC SESSIONS
# ----------------------------------------------------------------------------

from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import parse_qs, urlparse
import json

class SessionHTTPRequestHandler(BaseHTTPRequestHandler):
    """
    [IDEE] SERVEUR HTTP AVEC GESTION SESSIONS
    
    Serveur basique qui gère sessions client-side
    """
    
    # Session manager partagé entre requêtes
    session_manager = ClientSideSessionManager(
        secret_key='demo-secret-key-12345',
        cookie_name='demo_session',
        max_age=3600  # 1 heure
    )
    
    def parse_cookies(self):
        """
        Parser header Cookie
        
        Returns:
            dict: {nom_cookie: valeur_cookie}
        """
        cookie_header = self.headers.get('Cookie', '')
        
        cookies = {}
        for cookie in cookie_header.split(';'):
            cookie = cookie.strip()
            if '=' in cookie:
                name, value = cookie.split('=', 1)
                cookies[name] = value
        
        return cookies
    
    def get_session(self):
        """
        Obtenir session actuelle
        
        Returns:
            dict: Données session
        """
        cookies = self.parse_cookies()
        cookie_value = cookies.get('demo_session', '')
        
        return self.session_manager.read_session(cookie_value)
    
    def set_session(self, data):
        """
        Définir nouvelle session
        
        Args:
            data: Données session
        
        Returns:
            str: Cookie value
        """
        # Lire session actuelle
        cookies = self.parse_cookies()
        current_cookie = cookies.get('demo_session', '')
        
        if current_cookie:
            # Mettre à jour existante
            return self.session_manager.update_session(current_cookie, data)
        else:
            # Créer nouvelle
            return self.session_manager.create_session(data)
    
    def send_response_with_session(self, code, content, session_cookie=None):
        """
        Envoyer réponse avec cookie session
        
        Args:
            code: Code HTTP (200, 404, etc.)
            content: Contenu réponse
            session_cookie: Cookie value (optionnel)
        """
        self.send_response(code)
        self.send_header('Content-type', 'text/html; charset=utf-8')
        
        # Ajouter cookie si fourni
        if session_cookie is not None:
            set_cookie = self.session_manager.format_set_cookie_header(
                session_cookie,
                httponly=True,
                secure=False,  # HTTP pour demo
                samesite='Lax'
            )
            self.send_header('Set-Cookie', set_cookie)
        
        self.end_headers()
        self.wfile.write(content.encode('utf-8'))
    
    # ========================================================================
    # ROUTES
    # ========================================================================
    
    def do_GET(self):
        """Gérer requêtes GET"""
        
        path = urlparse(self.path).path
        
        if path == '/':
            self.route_index()
        elif path == '/login':
            self.route_login_form()
        elif path == '/dashboard':
            self.route_dashboard()
        elif path == '/logout':
            self.route_logout()
        elif path == '/set':
            self.route_set()
        else:
            self.send_error(404, 'Page not found')
    
    def do_POST(self):
        """Gérer requêtes POST"""
        
        path = urlparse(self.path).path
        
        if path == '/login':
            self.route_login_submit()
        else:
            self.send_error(404, 'Page not found')
    
    # ========================================================================
    # HANDLERS ROUTES
    # ========================================================================
    
    def route_index(self):
        """Page d'accueil"""
        
        session = self.get_session()
        
        if 'user_id' in session:
            username = session.get('username', 'Unknown')
            content = f"""
            <html>
            <head><title>Accueil</title></head>
            <body>
                <h1>Bienvenue {username} !</h1>
                <p>Vous êtes connecté.</p>
                <p><a href="/dashboard">Dashboard</a></p>
                <p><a href="/logout">Se déconnecter</a></p>
                
                <h2>Session Debug:</h2>
                <pre>{json.dumps(session, indent=2)}</pre>
            </body>
            </html>
            """
        else:
            content = """
            <html>
            <head><title>Accueil</title></head>
            <body>
                <h1>Bienvenue !</h1>
                <p>Vous n'êtes pas connecté.</p>
                <p><a href="/login">Se connecter</a></p>
            </body>
            </html>
            """
        
        self.send_response_with_session(200, content)
    
    def route_login_form(self):
        """Formulaire de connexion"""
        
        content = """
        <html>
        <head><title>Connexion</title></head>
        <body>
            <h1>Connexion</h1>
            <form method="POST" action="/login">
                <label>Username:</label>
                <input type="text" name="username" value="alice"><br><br>
                
                <label>Password:</label>
                <input type="password" name="password" value="password"><br><br>
                
                <button type="submit">Se connecter</button>
            </form>
        </body>
        </html>
        """
        
        self.send_response_with_session(200, content)
    
    def route_login_submit(self):
        """Traiter soumission login"""
        
        # Lire POST data
        content_length = int(self.headers['Content-Length'])
        post_data = self.rfile.read(content_length).decode('utf-8')
        params = parse_qs(post_data)
        
        username = params.get('username', [''])[0]
        password = params.get('password', [''])[0]
        
        # Authentification simple (demo)
        if username and password == 'password':
            # [OK] Créer session
            session_cookie = self.set_session({
                'user_id': 123,
                'username': username,
                'role': 'user'
            })
            
            # Redirect vers dashboard
            self.send_response(302)
            self.send_header('Location', '/dashboard')
            
            # Définir cookie
            set_cookie = self.session_manager.format_set_cookie_header(
                session_cookie
            )
            self.send_header('Set-Cookie', set_cookie)
            
            self.end_headers()
        else:
            # [X] Login failed
            content = """
            <html>
            <head><title>Erreur</title></head>
            <body>
                <h1>Erreur de connexion</h1>
                <p>Identifiants incorrects.</p>
                <p><a href="/login">Réessayer</a></p>
            </body>
            </html>
            """
            self.send_response_with_session(200, content)
    
    def route_dashboard(self):
        """Dashboard (protégé)"""
        
        session = self.get_session()
        
        if 'user_id' not in session:
            # Pas connecté -> redirect login
            self.send_response(302)
            self.send_header('Location', '/login')
            self.end_headers()
            return
        
        # Connecté -> afficher dashboard
        username = session.get('username', 'Unknown')
        
        content = f"""
        <html>
        <head><title>Dashboard</title></head>
        <body>
            <h1>Dashboard de {username}</h1>
            <p>User ID: {session.get('user_id')}</p>
            <p>Role: {session.get('role')}</p>
            
            <h2>Actions:</h2>
            <ul>
                <li><a href="/set">Ajouter au panier</a></li>
                <li><a href="/logout">Se déconnecter</a></li>
            </ul>
            
            <h2>Session complète:</h2>
            <pre>{json.dumps(session, indent=2)}</pre>
        </body>
        </html>
        """
        
        self.send_response_with_session(200, content)
    
    def route_logout(self):
        """Déconnexion"""
        
        # Détruire session
        empty_cookie = self.session_manager.destroy_session()
        
        content = """
        <html>
        <head><title>Déconnexion</title></head>
        <body>
            <h1>Déconnexion réussie</h1>
            <p><a href="/">Retour accueil</a></p>
        </body>
        </html>
        """
        
        self.send_response_with_session(200, content, empty_cookie)
    
    def route_set(self):
        """Ajouter produit au panier (demo)"""
        
        session = self.get_session()
        
        if 'user_id' not in session:
            self.send_response(302)
            self.send_header('Location', '/login')
            self.end_headers()
            return
        
        # Ajouter au panier
        cart = session.get('cart', [])
        cart.append(999)  # Produit ID
        
        session_cookie = self.set_session({'cart': cart})
        
        content = f"""
        <html>
        <head><title>Panier</title></head>
        <body>
            <h1>Produit ajouté !</h1>
            <p>Panier: {cart}</p>
            <p><a href="/dashboard">Retour dashboard</a></p>
        </body>
        </html>
        """
        
        self.send_response_with_session(200, content, session_cookie)


"""
LANCER LE SERVEUR
"""

def run_demo_server(port=8000):
    """
    [IDEE] LANCER SERVEUR DEMO
    
    Example:
        >>> run_demo_server(8000)
        # Ouvrir http://localhost:8000
    """
    server_address = ('', port)
    httpd = HTTPServer(server_address, SessionHTTPRequestHandler)
    
    print(f"""
    ══════════════════════════════════════════════════════════════
    [RAPIDE] SERVEUR DEMO SESSIONS LANCÉ
    ══════════════════════════════════════════════════════════════
    
    URL: http://localhost:{port}
    
    Routes disponibles:
    - GET  /           : Accueil
    - GET  /login      : Formulaire connexion
    - POST /login      : Soumettre login
    - GET  /dashboard  : Dashboard (protégé)
    - GET  /logout     : Déconnexion
    - GET  /set        : Ajouter au panier
    
    Credentials demo:
    - Username: alice (ou n'importe quoi)
    - Password: password
    
    Appuyez sur Ctrl+C pour arrêter
    ══════════════════════════════════════════════════════════════
    """)
    
    try:
        httpd.serve_forever()
    except KeyboardInterrupt:
        print("\n\nServeur arrêté.")
        httpd.server_close()


# Décommenter pour lancer :
# run_demo_server(8000)


# Ce fichier continue dans le prochain fichier...

# ============================================================================
# [LIVRE] SESSIONS SOUS LE CAPOT - PARTIE 2 : SERVER-SIDE & FRAMEWORKS
# ============================================================================


# ============================================================================
# [GUIDE] CHAPITRE 4 : SESSIONS CÔTÉ SERVEUR FROM SCRATCH
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Implémenter sessions server-side (ID dans cookie, data sur serveur)
[OK] Storage backends : In-Memory, Redis, Fichiers
[OK] Comparaison client-side vs server-side
[OK] Gérer expiration automatique
"""


# ----------------------------------------------------------------------------
# [SYNC] CLIENT-SIDE VS SERVER-SIDE
# ----------------------------------------------------------------------------

"""
RAPPEL : DIFFÉRENCES FONDAMENTALES


CLIENT-SIDE (ce qu'on a fait au Chapitre 2)
─────────────────────────────────────────────

┌─────────────┐                              ┌─────────────┐
│  NAVIGATEUR │                              │   SERVEUR   │
└─────────────┘                              └─────────────┘
                                             
Cookie contient :                             Serveur stocke :
  {                                             RIEN !
    "user_id": 123,                           
    "cart": [1,2,3]                           Seulement vérifier signature
  }                                           
  + signature                                 

Avantages :                                   Inconvénients :
[OK] Pas de stockage serveur                    [X] Taille limitée (~4 KB)
[OK] Scalable (stateless)                       [X] Données visibles (base64)
[OK] Simple                                     [X] Envoyé à chaque requête


SERVER-SIDE (ce qu'on va faire maintenant)
───────────────────────────────────────────

┌─────────────┐                              ┌─────────────┐
│  NAVIGATEUR │                              │   SERVEUR   │
└─────────────┘                              └─────────────┘
                                             
Cookie contient :                             Serveur stocke :
  "abc123"                                      sessions = {
  (juste ID)                                      "abc123": {
                                                    "user_id": 123,
                                                    "cart": [1,2,3]
                                                  }
                                                }

Avantages :                                   Inconvénients :
[OK] Taille illimitée                           [X] Stockage serveur requis
[OK] Données cachées                            [X] Plus complexe
[OK] Révocation possible                        [ATTENTION]  Gérer expiration
"""


# ----------------------------------------------------------------------------
# [ARCHIVE] BACKEND STORAGE ABSTRACTION
# ----------------------------------------------------------------------------

from abc import ABC, abstractmethod
from datetime import datetime, timedelta
import json
import os

class SessionStorage(ABC):
    """
    [IDEE] INTERFACE ABSTRAITE POUR STORAGE BACKEND
    
    Tous les backends doivent implémenter ces méthodes
    """
    
    @abstractmethod
    def get(self, session_id):
        """
        Récupérer session par ID
        
        Args:
            session_id: ID de session
        
        Returns:
            dict or None: Données session ou None si pas trouvée
        """
        pass
    
    @abstractmethod
    def set(self, session_id, data, ttl=None):
        """
        Stocker session
        
        Args:
            session_id: ID de session
            data: Données à stocker (dict)
            ttl: Time-to-live en secondes (None = pas d'expiration)
        """
        pass
    
    @abstractmethod
    def delete(self, session_id):
        """
        Supprimer session
        
        Args:
            session_id: ID de session
        """
        pass
    
    @abstractmethod
    def cleanup(self):
        """
        Nettoyer sessions expirées
        """
        pass


# ----------------------------------------------------------------------------
# [SAUVEGARDE] BACKEND IN-MEMORY
# ----------------------------------------------------------------------------

import threading
import time

class InMemorySessionStorage(SessionStorage):
    """
    [IDEE] STOCKAGE EN MÉMOIRE (RAM)
    
    Simple et rapide, mais :
    - Perdu au redémarrage
    - Pas partagé entre processus
    
    Idéal pour : Dev, Tests
    """
    
    def __init__(self):
        self.sessions = {}  # {session_id: {'data': {}, 'expires': datetime}}
        self.lock = threading.Lock()
        
        # Thread de nettoyage automatique
        self.cleanup_thread = threading.Thread(target=self._cleanup_loop, daemon=True)
        self.cleanup_thread.start()
    
    def get(self, session_id):
        """Récupérer session"""
        with self.lock:
            session = self.sessions.get(session_id)
            
            if not session:
                return None
            
            # Vérifier expiration
            if session['expires'] and session['expires'] < datetime.utcnow():
                # Expiré
                del self.sessions[session_id]
                return None
            
            return session['data']
    
    def set(self, session_id, data, ttl=None):
        """Stocker session"""
        with self.lock:
            expires = None
            if ttl:
                expires = datetime.utcnow() + timedelta(seconds=ttl)
            
            self.sessions[session_id] = {
                'data': data.copy(),
                'expires': expires
            }
    
    def delete(self, session_id):
        """Supprimer session"""
        with self.lock:
            self.sessions.pop(session_id, None)
    
    def cleanup(self):
        """Nettoyer sessions expirées"""
        with self.lock:
            now = datetime.utcnow()
            expired = [
                sid for sid, session in self.sessions.items()
                if session['expires'] and session['expires'] < now
            ]
            
            for sid in expired:
                del self.sessions[sid]
            
            return len(expired)
    
    def _cleanup_loop(self):
        """Boucle de nettoyage automatique (toutes les 60s)"""
        while True:
            time.sleep(60)
            cleaned = self.cleanup()
            if cleaned > 0:
                print(f"[Cleanup] {cleaned} sessions expirées nettoyées")
    
    def __len__(self):
        """Nombre de sessions actives"""
        return len(self.sessions)


"""
DÉMONSTRATION IN-MEMORY
"""

# Créer storage
storage = InMemorySessionStorage()

# Stocker session
storage.set('session123', {'user_id': 123, 'username': 'alice'}, ttl=3600)

# Récupérer
data = storage.get('session123')
print(f"Data: {data}")  # -> {'user_id': 123, 'username': 'alice'}

# Session inexistante
data = storage.get('nonexistent')
print(f"Nonexistent: {data}")  # -> None

# Supprimer
storage.delete('session123')
data = storage.get('session123')
print(f"After delete: {data}")  # -> None


# ----------------------------------------------------------------------------
# [DOSSIER] BACKEND FILESYSTEM
# ----------------------------------------------------------------------------

import pickle
import glob

class FileSystemSessionStorage(SessionStorage):
    """
    [IDEE] STOCKAGE FICHIERS
    
    Chaque session = un fichier
    
    Avantages :
    - Persistence (survit redémarrage)
    - Simple (pas de dépendance externe)
    
    Inconvénients :
    - Lent (I/O disque)
    - Pas scalable (multi-serveurs)
    
    Idéal pour : Dev, petites apps mono-serveur
    """
    
    def __init__(self, directory='/tmp/sessions'):
        """
        Args:
            directory: Dossier où stocker sessions
        """
        self.directory = directory
        os.makedirs(directory, exist_ok=True)
    
    def _get_filepath(self, session_id):
        """Obtenir chemin fichier pour session"""
        # Sanitize session_id (sécurité)
        safe_id = session_id.replace('/', '_').replace('\\', '_')
        return os.path.join(self.directory, f"{safe_id}.session")
    
    def get(self, session_id):
        """Récupérer session"""
        filepath = self._get_filepath(session_id)
        
        if not os.path.exists(filepath):
            return None
        
        try:
            with open(filepath, 'rb') as f:
                session = pickle.load(f)
            
            # Vérifier expiration
            if session['expires'] and session['expires'] < datetime.utcnow():
                # Expiré -> supprimer fichier
                os.remove(filepath)
                return None
            
            return session['data']
            
        except Exception as e:
            print(f"Error reading session {session_id}: {e}")
            return None
    
    def set(self, session_id, data, ttl=None):
        """Stocker session"""
        filepath = self._get_filepath(session_id)
        
        expires = None
        if ttl:
            expires = datetime.utcnow() + timedelta(seconds=ttl)
        
        session = {
            'data': data,
            'expires': expires
        }
        
        try:
            with open(filepath, 'wb') as f:
                pickle.dump(session, f)
        except Exception as e:
            print(f"Error writing session {session_id}: {e}")
    
    def delete(self, session_id):
        """Supprimer session"""
        filepath = self._get_filepath(session_id)
        
        try:
            if os.path.exists(filepath):
                os.remove(filepath)
        except Exception as e:
            print(f"Error deleting session {session_id}: {e}")
    
    def cleanup(self):
        """Nettoyer sessions expirées"""
        now = datetime.utcnow()
        cleaned = 0
        
        for filepath in glob.glob(os.path.join(self.directory, '*.session')):
            try:
                with open(filepath, 'rb') as f:
                    session = pickle.load(f)
                
                if session['expires'] and session['expires'] < now:
                    os.remove(filepath)
                    cleaned += 1
                    
            except Exception:
                # Fichier corrompu -> supprimer
                try:
                    os.remove(filepath)
                    cleaned += 1
                except:
                    pass
        
        return cleaned


"""
DÉMONSTRATION FILESYSTEM
"""

# Créer storage
fs_storage = FileSystemSessionStorage('/tmp/demo_sessions')

# Stocker
fs_storage.set('user_abc', {'user_id': 456, 'cart': [1, 2, 3]}, ttl=3600)

# Vérifier fichier créé
import os
print(f"Fichier créé: {os.path.exists('/tmp/demo_sessions/user_abc.session')}")

# Récupérer
data = fs_storage.get('user_abc')
print(f"Data: {data}")

# Cleanup
cleaned = fs_storage.cleanup()
print(f"Cleaned: {cleaned} sessions")


# ----------------------------------------------------------------------------
# [ROUGE] BACKEND REDIS
# ----------------------------------------------------------------------------

try:
    import redis
except ImportError:
    print("[ATTENTION]  redis-py non installé. pip install redis")
    redis = None

if redis:
    class RedisSessionStorage(SessionStorage):
        """
        [IDEE] STOCKAGE REDIS
        
        Avantages :
        - TRÈS rapide (en mémoire)
        - Expiration automatique (TTL natif)
        - Scalable (cluster Redis)
        - Partagé entre serveurs
        
        Idéal pour : Production
        """
        
        def __init__(self, host='localhost', port=6379, db=0, prefix='session:'):
            """
            Args:
                host: Redis host
                port: Redis port
                db: Redis database number
                prefix: Préfixe pour clés Redis
            """
            self.redis = redis.Redis(
                host=host,
                port=port,
                db=db,
                decode_responses=True  # String, pas bytes
            )
            self.prefix = prefix
        
        def _make_key(self, session_id):
            """Créer clé Redis avec préfixe"""
            return f"{self.prefix}{session_id}"
        
        def get(self, session_id):
            """Récupérer session"""
            key = self._make_key(session_id)
            
            data = self.redis.get(key)
            
            if not data:
                return None
            
            # Désérialiser JSON
            try:
                return json.loads(data)
            except json.JSONDecodeError:
                return None
        
        def set(self, session_id, data, ttl=None):
            """Stocker session"""
            key = self._make_key(session_id)
            
            # Sérialiser en JSON
            value = json.dumps(data)
            
            if ttl:
                # Avec expiration
                self.redis.setex(key, ttl, value)
            else:
                # Sans expiration
                self.redis.set(key, value)
        
        def delete(self, session_id):
            """Supprimer session"""
            key = self._make_key(session_id)
            self.redis.delete(key)
        
        def cleanup(self):
            """
            Nettoyer sessions expirées
            
            Avec Redis, pas nécessaire car TTL automatique
            """
            # Redis gère expiration automatiquement
            return 0
        
        def get_all_sessions(self):
            """Obtenir toutes les sessions (debug)"""
            keys = self.redis.keys(f"{self.prefix}*")
            
            sessions = {}
            for key in keys:
                session_id = key.replace(self.prefix, '')
                data = self.get(session_id)
                if data:
                    sessions[session_id] = data
            
            return sessions


"""
DÉMONSTRATION REDIS
"""

if redis:
    # Créer storage Redis
    redis_storage = RedisSessionStorage(host='localhost', port=6379)
    
    # Stocker
    redis_storage.set('user_xyz', {'user_id': 789, 'role': 'admin'}, ttl=3600)
    
    # Récupérer
    data = redis_storage.get('user_xyz')
    print(f"Redis data: {data}")
    
    # Vérifier dans Redis directement
    # redis-cli
    # GET session:user_xyz


# ----------------------------------------------------------------------------
# [CONSTRUCTION] SESSION MANAGER SERVER-SIDE
# ----------------------------------------------------------------------------

import secrets

class ServerSideSessionManager:
    """
    [IDEE] GESTIONNAIRE SESSIONS SERVER-SIDE
    
    Cookie contient seulement ID
    Données stockées sur serveur (Redis, fichiers, etc.)
    """
    
    def __init__(self, storage, cookie_name='session_id', default_ttl=3600):
        """
        Args:
            storage: Backend storage (SessionStorage)
            cookie_name: Nom du cookie
            default_ttl: TTL par défaut (secondes)
        """
        self.storage = storage
        self.cookie_name = cookie_name
        self.default_ttl = default_ttl
        self.signer = CookieSigner('server-side-secret')  # Signer ID aussi
    
    def generate_session_id(self):
        """
        Générer ID session sécurisé
        
        Returns:
            str: ID unique (URL-safe)
        """
        return secrets.token_urlsafe(32)
    
    def create_session(self, data=None):
        """
        Créer nouvelle session
        
        Args:
            data: Données initiales
        
        Returns:
            str: Session ID (pour cookie)
        """
        if data is None:
            data = {}
        
        # Générer ID unique
        session_id = self.generate_session_id()
        
        # Stocker sur serveur
        self.storage.set(session_id, data, ttl=self.default_ttl)
        
        # Retourner ID signé (pour cookie)
        return self.signer.sign(session_id)
    
    def read_session(self, signed_session_id):
        """
        Lire session
        
        Args:
            signed_session_id: ID signé depuis cookie
        
        Returns:
            dict: Données session ou {} si invalide
        """
        if not signed_session_id:
            return {}
        
        try:
            # Vérifier signature
            session_id = self.signer.unsign(signed_session_id)
            
            # Récupérer depuis storage
            data = self.storage.get(session_id)
            
            return data or {}
            
        except ValueError:
            # Signature invalide
            return {}
    
    def update_session(self, signed_session_id, updates):
        """
        Mettre à jour session
        
        Args:
            signed_session_id: ID signé
            updates: Nouvelles données
        
        Returns:
            str: Même session ID (signé)
        """
        try:
            session_id = self.signer.unsign(signed_session_id)
            
            # Lire données actuelles
            current = self.storage.get(session_id) or {}
            
            # Fusionner
            current.update(updates)
            
            # Sauvegarder
            self.storage.set(session_id, current, ttl=self.default_ttl)
            
            return signed_session_id
            
        except ValueError:
            # Signature invalide -> créer nouvelle session
            return self.create_session(updates)
    
    def destroy_session(self, signed_session_id):
        """
        Détruire session
        
        Args:
            signed_session_id: ID signé
        """
        try:
            session_id = self.signer.unsign(signed_session_id)
            self.storage.delete(session_id)
        except ValueError:
            pass


"""
DÉMONSTRATION COMPLÈTE SERVER-SIDE
"""

print("\n" + "="*70)
print("DÉMONSTRATION SESSION SERVER-SIDE")
print("="*70)

# 1. Créer storage (in-memory pour demo)
storage = InMemorySessionStorage()

# 2. Créer manager
manager = ServerSideSessionManager(storage, default_ttl=3600)

# 3. Créer session
print("\n1. CRÉER SESSION")
session_id = manager.create_session({'user_id': 123, 'username': 'alice'})
print(f"Session ID (cookie): {session_id[:50]}...")

# 4. Lire session
print("\n2. LIRE SESSION")
data = manager.read_session(session_id)
print(f"Session data: {data}")

# 5. Mettre à jour
print("\n3. METTRE À JOUR")
manager.update_session(session_id, {'cart': [1, 2, 3]})
updated = manager.read_session(session_id)
print(f"Updated data: {updated}")

# 6. Vérifier storage
print("\n4. VÉRIFIER STORAGE")
print(f"Sessions en mémoire: {len(storage)}")

# 7. Détruire
print("\n5. DÉTRUIRE SESSION")
manager.destroy_session(session_id)
destroyed = manager.read_session(session_id)
print(f"After destroy: {destroyed}")  # -> {}


# ============================================================================
# [GUIDE] CHAPITRE 5 : SESSIONS DANS DIFFÉRENTS FRAMEWORKS
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Comment Django gère sessions
[OK] Comment FastAPI gère sessions
[OK] Comment implémenter sessions custom dans FastAPI
[OK] Comparaison entre frameworks
"""


# ----------------------------------------------------------------------------
# [SCENARIO] DJANGO SESSIONS
# ----------------------------------------------------------------------------

"""
COMMENT DJANGO FAIT


1. CONFIGURATION (settings.py)
──────────────────────────────
"""

DJANGO_SETTINGS = """
# settings.py

# Middleware
MIDDLEWARE = [
    'django.contrib.sessions.middleware.SessionMiddleware',
    ...
]

# Apps
INSTALLED_APPS = [
    'django.contrib.sessions',
    ...
]

# Backend
SESSION_ENGINE = 'django.contrib.sessions.backends.db'
# Ou :
# 'django.contrib.sessions.backends.cache'  # Cache (Redis/Memcached)
# 'django.contrib.sessions.backends.file'   # Fichiers
# 'django.contrib.sessions.backends.cached_db'  # Cache + DB fallback

# Configuration
SESSION_COOKIE_NAME = 'sessionid'
SESSION_COOKIE_AGE = 1209600  # 2 semaines
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SECURE = True  # Production
SESSION_COOKIE_SAMESITE = 'Lax'

# Expiration
SESSION_EXPIRE_AT_BROWSER_CLOSE = False
SESSION_SAVE_EVERY_REQUEST = False
"""


"""
2. UTILISATION (views.py)
─────────────────────────
"""

DJANGO_USAGE = """
from django.shortcuts import render, redirect

def login_view(request):
    # Lire session
    user_id = request.session.get('user_id')
    
    if request.method == 'POST':
        # Authentifier...
        
        # [OK] Créer session
        request.session['user_id'] = user.id
        request.session['username'] = user.username
        
        # Modifier durée
        request.session.set_expiry(3600)  # 1 heure
        
        return redirect('dashboard')
    
    return render(request, 'login.html')

def logout_view(request):
    # [OK] Détruire session
    request.session.flush()
    return redirect('home')

def cart_view(request):
    # Lire/Modifier
    cart = request.session.get('cart', [])
    cart.append(product_id)
    request.session['cart'] = cart
    
    # Forcer sauvegarde (si liste modifiée en place)
    request.session.modified = True
    
    return render(request, 'cart.html', {'cart': cart})
"""


"""
3. MODÈLE DATABASE (Django ORM)
───────────────────────────────

Table créée automatiquement : django_session
"""

DJANGO_MODEL = """
class Session(models.Model):
    session_key = models.CharField(max_length=40, primary_key=True)
    session_data = models.TextField()
    expire_date = models.DateTimeField(db_index=True)
    
    class Meta:
        db_table = 'django_session'

# Exemple données stockées :
# session_key: "a1b2c3d4e5f6..."
# session_data: "eyJ1c2VyX2lkIjoxMjN9:1qZ..." (encodé + signé)
# expire_date: "2024-12-31 23:59:59"
"""


"""
4. INTERNALS - Comment ça fonctionne
────────────────────────────────────
"""

DJANGO_INTERNALS = """
# django/contrib/sessions/middleware.py

class SessionMiddleware:
    def process_request(self, request):
        # 1. Lire cookie session
        session_key = request.COOKIES.get(settings.SESSION_COOKIE_NAME)
        
        # 2. Créer objet session
        request.session = SessionStore(session_key)
    
    def process_response(self, request, response):
        # 3. Sauvegarder session si modifiée
        if request.session.modified or settings.SESSION_SAVE_EVERY_REQUEST:
            request.session.save()
            
            # 4. Définir cookie
            response.set_cookie(
                settings.SESSION_COOKIE_NAME,
                request.session.session_key,
                max_age=settings.SESSION_COOKIE_AGE,
                httponly=True,
                secure=settings.SESSION_COOKIE_SECURE
            )
        
        return response


# django/contrib/sessions/backends/db.py

class SessionStore:
    def load(self):
        # Charger depuis DB
        try:
            s = Session.objects.get(session_key=self.session_key)
            return self.decode(s.session_data)
        except Session.DoesNotExist:
            return {}
    
    def save(self):
        # Sauvegarder en DB
        Session.objects.update_or_create(
            session_key=self.session_key,
            defaults={
                'session_data': self.encode(self._session),
                'expire_date': self.get_expiry_date()
            }
        )
"""


# ----------------------------------------------------------------------------
# [RAPIDE] FASTAPI SESSIONS
# ----------------------------------------------------------------------------

"""
FASTAPI N'A PAS DE SESSIONS NATIVES

Mais on peut implémenter facilement !
"""

try:
    from fastapi import FastAPI, Cookie, Response, Depends
    from fastapi.responses import HTMLResponse, RedirectResponse
    from typing import Optional
    
    # Notre session manager
    app_fastapi = FastAPI()
    
    # Storage (in-memory pour demo)
    session_storage = InMemorySessionStorage()
    session_manager = ServerSideSessionManager(session_storage)
    
    
    # ========================================================================
    # DEPENDENCY POUR RÉCUPÉRER SESSION
    # ========================================================================
    
    def get_session(session_id: Optional[str] = Cookie(None)):
        """
        [IDEE] DEPENDENCY FASTAPI POUR SESSION
        
        Injecte automatiquement session dans routes
        """
        if session_id:
            return session_manager.read_session(session_id)
        return {}
    
    
    # ========================================================================
    # ROUTES
    # ========================================================================
    
    @app_fastapi.get("/", response_class=HTMLResponse)
    def index(session: dict = Depends(get_session)):
        """Page d'accueil"""
        
        if 'user_id' in session:
            username = session.get('username', 'Unknown')
            return f"""
            <html>
            <body>
                <h1>Bienvenue {username} !</h1>
                <p>User ID: {session.get('user_id')}</p>
                <p><a href="/dashboard">Dashboard</a></p>
                <p><a href="/logout">Déconnexion</a></p>
            </body>
            </html>
            """
        else:
            return """
            <html>
            <body>
                <h1>Bienvenue !</h1>
                <p><a href="/login">Se connecter</a></p>
            </body>
            </html>
            """
    
    
    @app_fastapi.get("/login", response_class=HTMLResponse)
    def login_form():
        """Formulaire login"""
        return """
        <html>
        <body>
            <h1>Connexion</h1>
            <form method="POST" action="/login">
                <input type="text" name="username" placeholder="Username">
                <input type="password" name="password" placeholder="Password">
                <button type="submit">Se connecter</button>
            </form>
        </body>
        </html>
        """
    
    
    @app_fastapi.post("/login")
    async def login_submit(username: str, password: str, response: Response):
        """
        Traiter login
        
        [OK] Créer session et définir cookie
        """
        # Authentification (demo)
        if password == "password":
            # Créer session
            session_id = session_manager.create_session({
                'user_id': 123,
                'username': username
            })
            
            # Définir cookie
            response = RedirectResponse(url="/dashboard", status_code=302)
            response.set_cookie(
                key="session_id",
                value=session_id,
                httponly=True,
                samesite="lax"
            )
            
            return response
        
        return RedirectResponse(url="/login", status_code=302)
    
    
    @app_fastapi.get("/dashboard", response_class=HTMLResponse)
    def dashboard(session: dict = Depends(get_session)):
        """Dashboard protégé"""
        
        if 'user_id' not in session:
            return RedirectResponse(url="/login", status_code=302)
        
        username = session.get('username', 'Unknown')
        
        return f"""
        <html>
        <body>
            <h1>Dashboard de {username}</h1>
            <p>User ID: {session.get('user_id')}</p>
            <p><a href="/logout">Déconnexion</a></p>
        </body>
        </html>
        """
    
    
    @app_fastapi.get("/logout")
    def logout(session_id: Optional[str] = Cookie(None)):
        """Déconnexion"""
        
        if session_id:
            session_manager.destroy_session(session_id)
        
        # Supprimer cookie
        response = RedirectResponse(url="/", status_code=302)
        response.delete_cookie("session_id")
        
        return response
    
    
    """
    LANCER FASTAPI
    
    # uvicorn script:app_fastapi --reload
    # Ouvrir http://localhost:8000
    """

except ImportError:
    print("[ATTENTION]  FastAPI non installé. pip install fastapi uvicorn")


# Ce fichier continue dans la prochaine partie...

# ============================================================================
# [LIVRE] SESSIONS SOUS LE CAPOT - PARTIE 3 : FLASK INTERNALS & SYNTHÈSE
# ============================================================================


# ============================================================================
# [GUIDE] CHAPITRE 6 : CODE INTERNE DE FLASK
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Comment Flask implémente sessions EXACTEMENT
[OK] Code source simplifié de Flask
[OK] itsdangerous en détail
[OK] SecureCookieSessionInterface
"""


# ----------------------------------------------------------------------------
# [ANALYSE] FLASK SESSION INTERFACE
# ----------------------------------------------------------------------------

"""
ARCHITECTURE FLASK SESSIONS


flask/
├── sessions.py              <- Interface sessions
├── app.py                   <- Application Flask
└── ctx.py                   <- Contexte requête


FLOW COMPLET :

1. Requête arrive
   v
2. before_request : Lire cookie
   v
3. Créer objet session (SessionInterface.open_session)
   v
4. Route handler accède à `session`
   v
5. after_request : Sauvegarder si modifié
   v
6. Définir cookie (SessionInterface.save_session)
   v
7. Réponse envoyée
"""


"""
CODE FLASK SIMPLIFIÉ
"""

from werkzeug.datastructures import CallbackDict
from itsdangerous import URLSafeTimedSerializer, BadSignature


class FlaskSessionInterface:
    """
    [IDEE] INTERFACE SESSION FLASK (simplifié)
    
    C'est le code RÉEL de Flask, simplifié pour compréhension
    """
    
    def __init__(self, app):
        self.app = app
        
        # Signer avec itsdangerous
        self.serializer = URLSafeTimedSerializer(
            app.config['SECRET_KEY'],
            salt='cookie-session'  # Salt pour sessions
        )
    
    def open_session(self, app, request):
        """
        [IDEE] OUVRIR SESSION DEPUIS REQUÊTE
        
        Appelé au début de chaque requête
        Lit cookie et crée objet session
        
        Returns:
            SecureCookieSession: Objet session
        """
        # Nom du cookie
        cookie_name = app.config.get('SESSION_COOKIE_NAME', 'session')
        
        # Lire cookie
        cookie_value = request.cookies.get(cookie_name)
        
        if not cookie_value:
            # Pas de cookie -> nouvelle session vide
            return SecureCookieSession()
        
        # Désérialiser et vérifier signature
        try:
            # max_age = durée de vie
            max_age = app.config.get('PERMANENT_SESSION_LIFETIME')
            if max_age:
                max_age = int(max_age.total_seconds())
            
            # Désérialiser (vérifie signature + expiration)
            data = self.serializer.loads(
                cookie_value,
                max_age=max_age
            )
            
            # Créer session depuis données
            return SecureCookieSession(data)
            
        except BadSignature:
            # Signature invalide ou expiré
            return SecureCookieSession()
    
    def save_session(self, app, session, response):
        """
        [IDEE] SAUVEGARDER SESSION DANS RÉPONSE
        
        Appelé après traitement de la requête
        Définit cookie si session modifiée
        
        Args:
            app: Application Flask
            session: Objet session
            response: Réponse HTTP
        """
        cookie_name = app.config.get('SESSION_COOKIE_NAME', 'session')
        domain = app.config.get('SESSION_COOKIE_DOMAIN')
        path = app.config.get('SESSION_COOKIE_PATH', '/')
        httponly = app.config.get('SESSION_COOKIE_HTTPONLY', True)
        secure = app.config.get('SESSION_COOKIE_SECURE', False)
        samesite = app.config.get('SESSION_COOKIE_SAMESITE', 'Lax')
        
        # Session vide -> supprimer cookie
        if not session:
            if session.modified:
                response.delete_cookie(
                    cookie_name,
                    domain=domain,
                    path=path
                )
            return
        
        # Session non modifiée et pas de refresh
        if not session.modified and not app.config.get('SESSION_REFRESH_EACH_REQUEST'):
            return
        
        # Sérialiser session
        cookie_value = self.serializer.dumps(dict(session))
        
        # Calculer expiration
        expires = None
        max_age = None
        
        if session.permanent:
            max_age = app.config.get('PERMANENT_SESSION_LIFETIME')
            if max_age:
                max_age = int(max_age.total_seconds())
                
                from datetime import datetime, timedelta
                expires = datetime.utcnow() + timedelta(seconds=max_age)
        
        # Définir cookie
        response.set_cookie(
            cookie_name,
            cookie_value,
            expires=expires,
            max_age=max_age,
            path=path,
            domain=domain,
            secure=secure,
            httponly=httponly,
            samesite=samesite
        )


class SecureCookieSession(CallbackDict):
    """
    [IDEE] OBJET SESSION FLASK
    
    Dict avec fonctionnalités supplémentaires :
    - Tracking des modifications
    - Permanent flag
    - New flag
    """
    
    def __init__(self, initial=None):
        def on_update(self):
            """Callback appelé quand session modifiée"""
            self.modified = True
        
        CallbackDict.__init__(self, initial, on_update)
        
        self.modified = False  # Session modifiée ?
        self.permanent = False  # Session permanente ?
        self.new = initial is None  # Nouvelle session ?


"""
DÉMONSTRATION CODE FLASK
"""

class MockApp:
    """Mock Flask app pour demo"""
    config = {
        'SECRET_KEY': 'demo-secret-key',
        'SESSION_COOKIE_NAME': 'demo_session',
        'PERMANENT_SESSION_LIFETIME': None
    }

class MockRequest:
    """Mock Request"""
    def __init__(self, cookies=None):
        self.cookies = cookies or {}

class MockResponse:
    """Mock Response"""
    def __init__(self):
        self.cookies_set = []
    
    def set_cookie(self, name, value, **kwargs):
        self.cookies_set.append({
            'name': name,
            'value': value,
            **kwargs
        })
    
    def delete_cookie(self, name, **kwargs):
        self.cookies_set.append({
            'name': name,
            'value': None,
            'deleted': True
        })


# Test
print("\n" + "="*70)
print("DÉMONSTRATION CODE INTERNE FLASK")
print("="*70)

app = MockApp()
interface = FlaskSessionInterface(app)

# 1. OUVRIR SESSION (requête sans cookie)
print("\n1. OUVRIR SESSION (nouveau user)")
request = MockRequest()
session = interface.open_session(app, request)

print(f"Session new: {session.new}")  # True
print(f"Session data: {dict(session)}")  # {}

# 2. MODIFIER SESSION
print("\n2. MODIFIER SESSION")
session['user_id'] = 123
session['username'] = 'alice'

print(f"Modified: {session.modified}")  # True
print(f"Data: {dict(session)}")

# 3. SAUVEGARDER SESSION
print("\n3. SAUVEGARDER SESSION")
response = MockResponse()
interface.save_session(app, session, response)

print(f"Cookies set: {len(response.cookies_set)}")
if response.cookies_set:
    cookie = response.cookies_set[0]
    print(f"Cookie name: {cookie['name']}")
    print(f"Cookie value: {cookie['value'][:50]}...")

# 4. REQUÊTE SUIVANTE (avec cookie)
print("\n4. REQUÊTE SUIVANTE")
cookie_value = response.cookies_set[0]['value']
request2 = MockRequest({'demo_session': cookie_value})
session2 = interface.open_session(app, request2)

print(f"Session new: {session2.new}")  # False
print(f"Data restored: {dict(session2)}")


# ----------------------------------------------------------------------------
# [SECURISE] ITSDANGEROUS EN DÉTAIL
# ----------------------------------------------------------------------------

"""
ITSDANGEROUS = BIBLIOTHÈQUE DE SIGNATURE


Flask utilise URLSafeTimedSerializer qui fait :
1. Sérialisation (JSON)
2. Signature (HMAC)
3. Encodage (base64 URL-safe)
4. Timestamp (pour expiration)
"""

from itsdangerous import URLSafeTimedSerializer, TimestampSigner
import time

# Créer serializer
serializer = URLSafeTimedSerializer('my-secret-key')


print("\n" + "="*70)
print("ITSDANGEROUS EN DÉTAIL")
print("="*70)

# 1. SIGNER DONNÉES
print("\n1. SIGNER")
data = {'user_id': 123, 'username': 'alice'}

signed = serializer.dumps(data)
print(f"Original: {data}")
print(f"Signed: {signed}")
print(f"Length: {len(signed)} bytes")

# 2. VÉRIFIER ET DÉSIGNER
print("\n2. VÉRIFIER")
try:
    unsigned = serializer.loads(signed)
    print(f"Unsigned: {unsigned}")
except Exception as e:
    print(f"Error: {e}")

# 3. EXPIRATION
print("\n3. EXPIRATION")
# Créer token expirant dans 5 secondes
short_signed = serializer.dumps(data)

# Attendre 2 secondes
time.sleep(2)

# Vérifier avec max_age=10s -> OK
try:
    result = serializer.loads(short_signed, max_age=10)
    print(f"Within max_age: {result}")
except Exception as e:
    print(f"Expired: {e}")

# Vérifier avec max_age=1s -> Expiré
try:
    result = serializer.loads(short_signed, max_age=1)
    print(f"Should not reach here")
except Exception as e:
    print(f"Expired (expected): {e}")


# 4. ANATOMIE D'UN TOKEN
print("\n4. ANATOMIE D'UN TOKEN")
print(f"\nToken: {signed}\n")

parts = signed.split('.')
print(f"Parts count: {len(parts)}")
print(f"Part 1 (payload): {parts[0][:30]}...")
print(f"Part 2 (timestamp): {parts[1] if len(parts) > 1 else 'N/A'}")
print(f"Part 3 (signature): {parts[-1][:30]}...")

# Décoder manuellement payload
import base64
import json

try:
    payload_b64 = parts[0]
    # Ajouter padding si nécessaire
    padding = 4 - len(payload_b64) % 4
    if padding != 4:
        payload_b64 += '=' * padding
    
    payload_bytes = base64.urlsafe_b64decode(payload_b64)
    payload_json = payload_bytes.decode('utf-8')
    payload_data = json.loads(payload_json)
    
    print(f"\nPayload décodé: {payload_data}")
except Exception as e:
    print(f"Decode error: {e}")


# ============================================================================
# [GUIDE] CHAPITRE 7 : COMPARAISON COMPLÈTE
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Quand utiliser quelle approche
[OK] Avantages/Inconvénients de chaque méthode
[OK] Recommandations production
"""


# ----------------------------------------------------------------------------
# [GRAPHIQUE] TABLEAU COMPARATIF COMPLET
# ----------------------------------------------------------------------------

COMPARISON_TABLE = """
═══════════════════════════════════════════════════════════════════════════
COMPARAISON CLIENT-SIDE vs SERVER-SIDE
═══════════════════════════════════════════════════════════════════════════

┌─────────────────────┬─────────────────────┬─────────────────────────────┐
│                     │   CLIENT-SIDE       │      SERVER-SIDE            │
├─────────────────────┼─────────────────────┼─────────────────────────────┤
│ Cookie contient     │ Toutes les données  │ Seulement ID                │
│                     │ (signées)           │ (signé)                     │
├─────────────────────┼─────────────────────┼─────────────────────────────┤
│ Stockage serveur    │ [X] Aucun            │ [OK] Redis/DB/Fichiers        │
├─────────────────────┼─────────────────────┼─────────────────────────────┤
│ Taille max          │ ~4 KB               │ Illimité                    │
├─────────────────────┼─────────────────────┼─────────────────────────────┤
│ Visibilité données  │ [ATTENTION]  Visible         │ [OK] Cachées                  │
│                     │ (base64)            │                             │
├─────────────────────┼─────────────────────┼─────────────────────────────┤
│ Modification        │ [X] Impossible       │ [X] Impossible               │
│ client              │ (signature)         │ (signature ID)              │
├─────────────────────┼─────────────────────┼─────────────────────────────┤
│ Révocation          │ [X] Impossible       │ [OK] Possible                 │
│                     │                     │ (supprimer côté serveur)    │
├─────────────────────┼─────────────────────┼─────────────────────────────┤
│ Bandwidth           │ [ATTENTION]  Cookie envoyé   │ [OK] Seulement ID             │
│                     │ à chaque requête    │ (~40 bytes)                 │
├─────────────────────┼─────────────────────┼─────────────────────────────┤
│ Scalabilité         │ [OK] Excellente       │ [ATTENTION]  Dépend storage          │
│                     │ (stateless)         │                             │
├─────────────────────┼─────────────────────┼─────────────────────────────┤
│ Multi-serveurs      │ [OK] Natif            │ [OK] Avec Redis partagé       │
│                     │                     │ [X] Avec fichiers            │
├─────────────────────┼─────────────────────┼─────────────────────────────┤
│ Setup               │ [OK] Simple           │ [ATTENTION]  Moyen (Redis, etc.)     │
├─────────────────────┼─────────────────────┼─────────────────────────────┤
│ Performance         │ [OK] Très rapide      │ [ATTENTION]  Latence réseau          │
│                     │ (pas d'I/O)         │ (Redis/DB)                  │
├─────────────────────┼─────────────────────┼─────────────────────────────┤
│ Coût                │ [OK] Gratuit          │ [ATTENTION]  Redis/DB hosting        │
├─────────────────────┼─────────────────────┼─────────────────────────────┤
│ Privacy             │ [ATTENTION]  Données         │ [OK] Données serveur          │
│                     │ visibles client     │                             │
├─────────────────────┼─────────────────────┼─────────────────────────────┤
│ Use cases           │ - Petites apps      │ - Apps sensibles            │
│                     │ - Prototypes        │ - Grandes données           │
│                     │ - Données publiques │ - Révocation requise        │
│                     │                     │ - Multi-utilisateurs        │
└─────────────────────┴─────────────────────┴─────────────────────────────┘


RECOMMANDATIONS
===============

UTILISER CLIENT-SIDE QUAND :
[OK] Données non sensibles (préférences, langue, thème)
[OK] Taille < 1 KB
[OK] Simplicité prioritaire
[OK] Stateless important (microservices)
[OK] Pas de budget pour Redis

UTILISER SERVER-SIDE QUAND :
[OK] Données sensibles
[OK] Taille > 1 KB
[OK] Révocation nécessaire (logout forcé, ban user)
[OK] Audit trail requis
[OK] Multi-utilisateurs sur même machine


HYBRIDE (MEILLEUR DES DEUX MONDES)
===================================

Client-side pour :     Server-side pour :
- Préférences UI       - Authentification
- Langue               - Panier
- Thème                - Historique
                       - Données sensibles
"""

print(COMPARISON_TABLE)


# ----------------------------------------------------------------------------
# [OBJECTIF] DÉCISIONS ARCHITECTURALES
# ----------------------------------------------------------------------------

"""
GUIDE DE DÉCISION
"""

def choose_session_strategy(
    data_size_kb=1,
    sensitive_data=False,
    need_revocation=False,
    multi_server=False,
    budget_constraint=False
):
    """
    [IDEE] AIDE À LA DÉCISION SESSION STRATEGY
    
    Args:
        data_size_kb: Taille données session (KB)
        sensitive_data: Données sensibles ?
        need_revocation: Besoin révocation ?
        multi_server: Multi-serveurs ?
        budget_constraint: Budget limité ?
    
    Returns:
        str: Recommandation
    """
    
    score_client = 0
    score_server = 0
    
    # Taille
    if data_size_kb < 2:
        score_client += 2
    else:
        score_server += 3
    
    # Sensibilité
    if sensitive_data:
        score_server += 3
    else:
        score_client += 1
    
    # Révocation
    if need_revocation:
        score_server += 3
    
    # Multi-serveurs
    if multi_server:
        score_server += 1  # Les deux fonctionnent
    
    # Budget
    if budget_constraint:
        score_client += 2
    
    # Décision
    if score_server > score_client + 2:
        return "SERVER-SIDE (Redis recommandé)"
    elif score_client > score_server + 2:
        return "CLIENT-SIDE"
    else:
        return "HYBRIDE (client-side + server-side)"


# Tests
print("\n" + "="*70)
print("AIDE À LA DÉCISION")
print("="*70)

scenarios = [
    {
        'name': 'Blog personnel',
        'data_size_kb': 0.5,
        'sensitive_data': False,
        'need_revocation': False,
        'multi_server': False,
        'budget_constraint': True
    },
    {
        'name': 'E-commerce',
        'data_size_kb': 5,
        'sensitive_data': True,
        'need_revocation': True,
        'multi_server': True,
        'budget_constraint': False
    },
    {
        'name': 'SaaS application',
        'data_size_kb': 2,
        'sensitive_data': True,
        'need_revocation': True,
        'multi_server': True,
        'budget_constraint': False
    },
    {
        'name': 'Prototype MVP',
        'data_size_kb': 1,
        'sensitive_data': False,
        'need_revocation': False,
        'multi_server': False,
        'budget_constraint': True
    }
]

for scenario in scenarios:
    name = scenario.pop('name')
    recommendation = choose_session_strategy(**scenario)
    print(f"\n{name}:")
    print(f"  -> {recommendation}")


# ============================================================================
# [GUIDE] CHAPITRE 8 : EXERCICES PRATIQUES
# ============================================================================

"""
[OBJECTIF] EXERCICES POUR MAÎTRISER LES SESSIONS
"""


EXERCICE_1 = """
EXERCICE 1 : IMPLÉMENTER REMEMBER ME
════════════════════════════════════

Objectif : Créer système "Remember Me" avec :
- Session temporaire (expire à fermeture navigateur)
- Token persistant (30 jours)
- Auto-login depuis token

Concepts :
- Sessions vs Cookies
- Token generation sécurisé
- Database storage

Code à compléter :
"""

def exercice_1_solution():
    """
    Solution Exercice 1
    """
    import secrets
    from datetime import datetime, timedelta
    
    # Storage tokens (en prod : database)
    remember_tokens = {}
    
    def generate_remember_token(user_id):
        \"\"\"Générer token Remember Me\"\"\"
        token = secrets.token_urlsafe(32)
        
        remember_tokens[token] = {
            'user_id': user_id,
            'expires': datetime.utcnow() + timedelta(days=30)
        }
        
        return token
    
    def verify_remember_token(token):
        \"\"\"Vérifier token Remember Me\"\"\"
        data = remember_tokens.get(token)
        
        if not data:
            return None
        
        if data['expires'] < datetime.utcnow():
            del remember_tokens[token]
            return None
        
        return data['user_id']
    
    # Route login
    def login(username, password, remember_me=False):
        # Auth...
        user_id = 123
        
        # Session temporaire
        session = {'user_id': user_id}
        
        if remember_me:
            # Token persistant
            token = generate_remember_token(user_id)
            # set_cookie('remember_token', token, max_age=30*24*3600)
        
        return session
    
    # Auto-login
    def auto_login_from_cookie(remember_token):
        user_id = verify_remember_token(remember_token)
        
        if user_id:
            # Créer session
            return {'user_id': user_id}
        
        return None


EXERCICE_2 = """
EXERCICE 2 : SESSION AVEC EXPIRATION SLIDING
═══════════════════════════════════════════

Objectif : Session qui se renouvelle à chaque requête
- Inactivité 30 min -> logout
- Activité -> reset timer

Concepts :
- Timestamp tracking
- Sliding expiration
- Middleware pattern

Code à compléter :
"""

def exercice_2_solution():
    """
    Solution Exercice 2
    """
    from datetime import datetime, timedelta
    
    INACTIVITY_TIMEOUT = 1800  # 30 minutes
    
    def check_session_timeout(session):
        \"\"\"Vérifier timeout session\"\"\"
        
        if 'user_id' not in session:
            return session  # Pas connecté
        
        last_activity = session.get('_last_activity')
        
        if not last_activity:
            # Première requête après login
            session['_last_activity'] = datetime.utcnow().isoformat()
            return session
        
        # Calculer inactivité
        last = datetime.fromisoformat(last_activity)
        now = datetime.utcnow()
        inactive_seconds = (now - last).total_seconds()
        
        if inactive_seconds > INACTIVITY_TIMEOUT:
            # Timeout -> logout
            return {}
        
        # Reset timer
        session['_last_activity'] = now.isoformat()
        
        return session


EXERCICE_3 = """
EXERCICE 3 : STORAGE BACKEND CUSTOM
══════════════════════════════════

Objectif : Implémenter backend storage MongoDB

Concepts :
- SessionStorage interface
- MongoDB operations
- TTL indexes

Code à compléter :
"""

def exercice_3_solution():
    """
    Solution Exercice 3
    """
    from datetime import datetime, timedelta
    
    # pip install pymongo
    try:
        from pymongo import MongoClient
        
        class MongoSessionStorage:
            def __init__(self, uri='mongodb://localhost:27017/', db_name='sessions'):
                self.client = MongoClient(uri)
                self.db = self.client[db_name]
                self.collection = self.db.sessions
                
                # Créer index TTL pour auto-cleanup
                self.collection.create_index('expires', expireAfterSeconds=0)
            
            def get(self, session_id):
                doc = self.collection.find_one({'_id': session_id})
                
                if not doc:
                    return None
                
                return doc.get('data')
            
            def set(self, session_id, data, ttl=None):
                expires = None
                if ttl:
                    expires = datetime.utcnow() + timedelta(seconds=ttl)
                
                self.collection.update_one(
                    {'_id': session_id},
                    {
                        '$set': {
                            'data': data,
                            'expires': expires
                        }
                    },
                    upsert=True
                )
            
            def delete(self, session_id):
                self.collection.delete_one({'_id': session_id})
            
            def cleanup(self):
                # MongoDB TTL index fait le cleanup automatiquement
                return 0
    
    except ImportError:
        print("pymongo non installé")


# ============================================================================
# [DOCS] RÉCAPITULATIF FINAL
# ============================================================================

RECAP_FINAL = """
═══════════════════════════════════════════════════════════════════════════
[COURS] RÉCAPITULATIF : SESSIONS SOUS LE CAPOT
═══════════════════════════════════════════════════════════════════════════

CE QUE VOUS AVEZ APPRIS
────────────────────────

CHAPITRE 1 : Anatomie Session
[OK] Composants (ID, Data, Storage)
[OK] Signature HMAC-SHA256
[OK] Sérialisation JSON + Base64
[OK] CookieSigner implementation

CHAPITRE 2 : Client-Side Sessions
[OK] ClientSideSessionManager complet
[OK] Création, lecture, mise à jour
[OK] Expiration automatique
[OK] Format Set-Cookie header

CHAPITRE 3 : Serveur HTTP Basique
[OK] Intégration sessions dans HTTP server
[OK] Parser cookies
[OK] Routes protégées
[OK] Login/Logout flow complet

CHAPITRE 4 : Server-Side Sessions
[OK] SessionStorage interface
[OK] Backends : In-Memory, Filesystem, Redis
[OK] ServerSideSessionManager
[OK] Expiration et cleanup

CHAPITRE 5 : Frameworks
[OK] Django sessions (DB, cache, file)
[OK] FastAPI sessions custom
[OK] Dependency injection
[OK] Comparaison frameworks

CHAPITRE 6 : Flask Internals
[OK] FlaskSessionInterface code source
[OK] SecureCookieSession implementation
[OK] itsdangerous détaillé
[OK] URLSafeTimedSerializer

CHAPITRE 7 : Comparaison
[OK] Client-side vs Server-side
[OK] Tableaux décision
[OK] Recommandations production
[OK] Scénarios d'usage

CHAPITRE 8 : Exercices
[OK] Remember Me
[OK] Sliding expiration
[OK] Custom storage backends


COMPÉTENCES ACQUISES
─────────────────────

Vous savez maintenant :

[OUTIL] TECHNIQUE
- Implémenter sessions from scratch
- Choisir bon backend storage
- Sécuriser avec HMAC
- Gérer expiration

[CONSTRUCTION] ARCHITECTURE
- Client-side vs Server-side
- Multi-serveurs avec Redis
- Scalabilité
- Performance optimization

[VERROUILLE] SÉCURITÉ
- Signature cryptographique
- Session Fixation prevention
- HttpOnly, Secure, SameSite
- Token generation sécurisé

[CODE] PRATIQUE
- Intégrer dans frameworks
- Debugging sessions
- Monitoring
- Production deployment


NEXT STEPS
──────────

1. Pratiquez implémentations custom
2. Testez différents backends
3. Mesurez performance
4. Contribuez à projets open-source
5. Créez vos propres extensions


RESSOURCES
──────────

Code source :
- Flask: github.com/pallets/flask/blob/main/src/flask/sessions.py
- Django: github.com/django/django/tree/main/django/contrib/sessions
- itsdangerous: github.com/pallets/itsdangerous

Documentation :
- Flask Sessions API
- Django Sessions Framework
- Redis Documentation


═══════════════════════════════════════════════════════════════════════════
                    FÉLICITATIONS ! [BRAVO]
        
        Vous comprenez maintenant EXACTEMENT comment
           les sessions fonctionnent sous le capot
                
              Du code Python pur à Flask
                  En passant par Redis
                      
                  Bon développement ! [RAPIDE]
═══════════════════════════════════════════════════════════════════════════
"""

print(RECAP_FINAL)


# ============================================================================
# [OBJECTIF] RÉFÉRENCE RAPIDE
# ============================================================================

QUICK_REFERENCE = """
RÉFÉRENCE RAPIDE - IMPLÉMENTATIONS
═══════════════════════════════════

1. SIGNER UN COOKIE (HMAC)
──────────────────────────
```python
import hmac, hashlib, base64

def sign(value, secret):
    sig = hmac.new(secret.encode(), value.encode(), hashlib.sha256).digest()
    sig_b64 = base64.urlsafe_b64encode(sig).decode()
    return f"{value}.{sig_b64}"

def verify(signed, secret):
    value, sig = signed.rsplit('.', 1)
    expected = hmac.new(secret.encode(), value.encode(), hashlib.sha256).digest()
    provided = base64.urlsafe_b64decode(sig)
    if hmac.compare_digest(expected, provided):
        return value
    raise ValueError("Invalid signature")
```

2. SESSION CLIENT-SIDE
──────────────────────
```python
class ClientSession:
    def __init__(self, secret):
        self.signer = CookieSigner(secret)
        self.serializer = SessionSerializer()
    
    def create(self, data):
        serialized = self.serializer.serialize(data)
        return self.signer.sign(serialized)
    
    def read(self, cookie):
        serialized = self.signer.unsign(cookie)
        return self.serializer.deserialize(serialized)
```

3. SESSION SERVER-SIDE
─────────────────────
```python
class ServerSession:
    def __init__(self, storage):
        self.storage = storage  # Redis, DB, etc.
    
    def create(self, data):
        sid = secrets.token_urlsafe(32)
        self.storage.set(sid, data)
        return sid
    
    def read(self, sid):
        return self.storage.get(sid)
```

4. REDIS STORAGE
────────────────
```python
import redis, json

class RedisStorage:
    def __init__(self):
        self.r = redis.Redis(decode_responses=True)
    
    def get(self, sid):
        data = self.r.get(f"session:{sid}")
        return json.loads(data) if data else None
    
    def set(self, sid, data, ttl=3600):
        self.r.setex(f"session:{sid}", ttl, json.dumps(data))
```

5. FLASK-LIKE
─────────────
```python
from itsdangerous import URLSafeTimedSerializer

serializer = URLSafeTimedSerializer('secret-key')

# Sign
cookie = serializer.dumps({'user_id': 123})

# Verify
data = serializer.loads(cookie, max_age=3600)
```
"""

print(QUICK_REFERENCE)