# 🗄️ Web Sentinel - Architecture Base de Données MySQL

*Plan d'implémentation pour serveur OVH avec infrastructure Docker existante*

## 🎯 Vue d'ensemble

Cette architecture tire parti de votre serveur OVH Linux existant avec MySQL déjà en place, offrant une solution **zéro coût supplémentaire** et **haute performance** pour le backend de monétisation de Web Sentinel.

---

## 📊 Schéma de base de données optimisé

### 🔐 **Table Users - Gestion des comptes**
```sql
CREATE DATABASE web_sentinel CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    email VARCHAR(255) UNIQUE NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    subscription_tier ENUM('FREE', 'PRO', 'ENTERPRISE') DEFAULT 'FREE',
    stripe_customer_id VARCHAR(255) UNIQUE,
    subscription_status ENUM('active', 'canceled', 'past_due', 'unpaid') DEFAULT 'active',
    subscription_expires_at TIMESTAMP NULL,
    api_key VARCHAR(64) UNIQUE,
    email_verified BOOLEAN DEFAULT FALSE,
    email_verification_token VARCHAR(255),
    language ENUM('fr', 'en') DEFAULT 'fr',
    timezone VARCHAR(50) DEFAULT 'Europe/Paris',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    last_login_at TIMESTAMP NULL,
    
    INDEX idx_email (email),
    INDEX idx_subscription (subscription_tier, subscription_status),
    INDEX idx_stripe_customer (stripe_customer_id),
    INDEX idx_api_key (api_key)
);
```

### 📊 **Table Scan Results - Historique cloud**
```sql
CREATE TABLE scan_results (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    user_id INT NOT NULL,
    domain VARCHAR(255) NOT NULL,
    scan_type ENUM('passive', 'invasive', 'scheduled') DEFAULT 'passive',
    findings JSON NOT NULL,
    scan_config JSON NOT NULL,
    modules_used JSON NOT NULL,
    scan_duration_ms INT,
    findings_count INT GENERATED ALWAYS AS (JSON_LENGTH(findings)) STORED,
    critical_count INT GENERATED ALWAYS AS (
        CAST(JSON_LENGTH(JSON_SEARCH(findings, 'all', 'critical', NULL, '$[*].severity')) AS UNSIGNED)
    ) STORED,
    high_count INT GENERATED ALWAYS AS (
        CAST(JSON_LENGTH(JSON_SEARCH(findings, 'all', 'high', NULL, '$[*].severity')) AS UNSIGNED)
    ) STORED,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    INDEX idx_user_domain (user_id, domain),
    INDEX idx_user_created (user_id, created_at DESC),
    INDEX idx_domain_created (domain, created_at DESC),
    INDEX idx_findings_severity (critical_count, high_count),
    INDEX idx_scan_type (scan_type),
    
    -- Recherche dans JSON pour findings critiques
    INDEX idx_critical_findings (
        (CASE WHEN JSON_SEARCH(findings, 'one', 'critical', NULL, '$[*].severity') IS NOT NULL THEN 1 END)
    )
);
```

### 💳 **Table Subscriptions - Historique Stripe**
```sql
CREATE TABLE subscriptions (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT NOT NULL,
    stripe_subscription_id VARCHAR(255) UNIQUE NOT NULL,
    stripe_product_id VARCHAR(255) NOT NULL,
    stripe_price_id VARCHAR(255) NOT NULL,
    status ENUM('active', 'canceled', 'incomplete', 'incomplete_expired', 'past_due', 'trialing', 'unpaid') NOT NULL,
    current_period_start TIMESTAMP NOT NULL,
    current_period_end TIMESTAMP NOT NULL,
    trial_end TIMESTAMP NULL,
    cancel_at_period_end BOOLEAN DEFAULT FALSE,
    metadata JSON,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    INDEX idx_user_subscription (user_id, status),
    INDEX idx_stripe_subscription (stripe_subscription_id),
    INDEX idx_current_period (current_period_end)
);
```

### 📅 **Table Scheduled Scans - Fonctionnalité PRO**
```sql
CREATE TABLE scheduled_scans (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT NOT NULL,
    domain VARCHAR(255) NOT NULL,
    frequency ENUM('daily', 'weekly', 'monthly') NOT NULL,
    modules JSON NOT NULL,
    invasive_enabled BOOLEAN DEFAULT FALSE,
    notification_email BOOLEAN DEFAULT TRUE,
    notification_webhook VARCHAR(500),
    is_active BOOLEAN DEFAULT TRUE,
    last_run_at TIMESTAMP NULL,
    next_run_at TIMESTAMP NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    INDEX idx_user_active (user_id, is_active),
    INDEX idx_next_run (next_run_at, is_active),
    INDEX idx_domain_user (domain, user_id)
);
```

### 📈 **Table Analytics - Métriques business**
```sql
CREATE TABLE analytics_events (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    user_id INT,
    event_type ENUM('scan_performed', 'upgrade_viewed', 'subscription_created', 'subscription_canceled', 'api_called') NOT NULL,
    event_data JSON,
    ip_address INET6,
    user_agent TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL,
    INDEX idx_event_type (event_type, created_at),
    INDEX idx_user_events (user_id, created_at),
    INDEX idx_created_at (created_at)
);
```

---

## 🐳 Architecture Docker pour OVH

### **docker-compose.yml optimisé**
```yaml
version: '3.8'

services:
  web-sentinel-api:
    build: 
      context: .
      dockerfile: docker/Dockerfile.api
    ports:
      - "8080:8080"
    environment:
      - DATABASE_URL=mysql://web_sentinel:${WS_DB_PASSWORD}@mysql:3306/web_sentinel
      - STRIPE_SECRET_KEY=${STRIPE_SECRET_KEY}
      - STRIPE_WEBHOOK_SECRET=${STRIPE_WEBHOOK_SECRET}
      - JWT_SECRET_KEY=${JWT_SECRET_KEY}
      - REDIS_URL=redis://redis:6379/0
    depends_on:
      - mysql
      - redis
    restart: unless-stopped
    networks:
      - web-sentinel-network
    volumes:
      - ./logs:/app/logs

  web-sentinel-worker:
    build: 
      context: .
      dockerfile: docker/Dockerfile.worker
    environment:
      - DATABASE_URL=mysql://web_sentinel:${WS_DB_PASSWORD}@mysql:3306/web_sentinel
      - REDIS_URL=redis://redis:6379/0
    depends_on:
      - mysql
      - redis
    restart: unless-stopped
    networks:
      - web-sentinel-network

  mysql:
    image: mysql:8.0
    environment:
      - MYSQL_DATABASE=web_sentinel
      - MYSQL_USER=web_sentinel
      - MYSQL_PASSWORD=${WS_DB_PASSWORD}
      - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
    volumes:
      - ./data/mysql:/var/lib/mysql
      - ./docker/mysql/init:/docker-entrypoint-initdb.d
      - ./docker/mysql/conf.d:/etc/mysql/conf.d
    restart: unless-stopped
    networks:
      - web-sentinel-network
    command: --default-authentication-plugin=mysql_native_password

  redis:
    image: redis:7-alpine
    volumes:
      - ./data/redis:/data
    restart: unless-stopped
    networks:
      - web-sentinel-network
    command: redis-server --appendonly yes

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./docker/nginx/nginx.conf:/etc/nginx/nginx.conf
      - ./docker/nginx/ssl:/etc/nginx/ssl
    depends_on:
      - web-sentinel-api
    restart: unless-stopped
    networks:
      - web-sentinel-network

networks:
  web-sentinel-network:
    driver: bridge

volumes:
  mysql_data:
  redis_data:
```

---

## 💻 Code d'intégration Python

### **Gestionnaire de base de données unifié**
```python
# web_sentinel/database/mysql_manager.py
import mysql.connector
import json
import hashlib
import secrets
from typing import List, Optional, Dict, Any
from datetime import datetime, timedelta
from ..model import ScanResult, Finding, ScanRequest

class MySQLManager:
    def __init__(self, config: dict):
        self.config = {
            'host': config.get('host', 'localhost'),
            'user': config.get('user', 'web_sentinel'),
            'password': config.get('password'),
            'database': config.get('database', 'web_sentinel'),
            'charset': 'utf8mb4',
            'use_unicode': True,
            'autocommit': True,
            'pool_size': 10,
            'pool_reset_session': True
        }
        self.pool = mysql.connector.pooling.MySQLConnectionPool(**self.config)
    
    def get_connection(self):
        return self.pool.get_connection()

class UserManager:
    def __init__(self, db_manager: MySQLManager):
        self.db = db_manager
    
    def create_user(self, email: str, password: str) -> int:
        """Création compte utilisateur avec hash sécurisé"""
        password_hash = hashlib.pbkdf2_hmac('sha256', 
                                          password.encode('utf-8'),
                                          secrets.token_bytes(32), 
                                          100000).hex()
        api_key = secrets.token_urlsafe(32)
        
        with self.db.get_connection() as conn:
            cursor = conn.cursor()
            query = """
                INSERT INTO users (email, password_hash, api_key)
                VALUES (%s, %s, %s)
            """
            cursor.execute(query, (email, password_hash, api_key))
            return cursor.lastrowid
    
    def authenticate_user(self, email: str, password: str) -> Optional[Dict[str, Any]]:
        """Authentification avec rate limiting"""
        with self.db.get_connection() as conn:
            cursor = conn.cursor(dictionary=True)
            query = """
                SELECT id, email, password_hash, subscription_tier, api_key
                FROM users 
                WHERE email = %s AND email_verified = TRUE
            """
            cursor.execute(query, (email,))
            user = cursor.fetchone()
            
            if user and self._verify_password(password, user['password_hash']):
                # Update last login
                cursor.execute(
                    "UPDATE users SET last_login_at = NOW() WHERE id = %s",
                    (user['id'],)
                )
                return user
        return None
    
    def upgrade_subscription(self, user_id: int, stripe_customer_id: str, 
                           tier: str, expires_at: datetime) -> bool:
        """Upgrade abonnement post-paiement Stripe"""
        with self.db.get_connection() as conn:
            cursor = conn.cursor()
            query = """
                UPDATE users 
                SET subscription_tier = %s, 
                    stripe_customer_id = %s,
                    subscription_status = 'active',
                    subscription_expires_at = %s
                WHERE id = %s
            """
            cursor.execute(query, (tier, stripe_customer_id, expires_at, user_id))
            return cursor.rowcount > 0

class ScanManager:
    def __init__(self, db_manager: MySQLManager):
        self.db = db_manager
    
    def save_scan_result(self, user_id: int, result: ScanResult) -> int:
        """Sauvegarde avec calculs optimisés des métriques"""
        with self.db.get_connection() as conn:
            cursor = conn.cursor()
            
            findings_json = json.dumps([f.to_dict() for f in result.findings])
            config_json = json.dumps(result.request.to_dict())
            modules_json = json.dumps(getattr(result, 'modules_used', []))
            
            query = """
                INSERT INTO scan_results 
                (user_id, domain, scan_type, findings, scan_config, modules_used)
                VALUES (%s, %s, %s, %s, %s, %s)
            """
            cursor.execute(query, (
                user_id, 
                result.request.domain,
                'invasive' if result.request.allow_invasive else 'passive',
                findings_json, 
                config_json,
                modules_json
            ))
            return cursor.lastrowid
    
    def get_user_dashboard_stats(self, user_id: int) -> Dict[str, Any]:
        """Stats optimisées pour dashboard PRO"""
        with self.db.get_connection() as conn:
            cursor = conn.cursor(dictionary=True)
            
            # Stats globales (30 derniers jours)
            cursor.execute("""
                SELECT 
                    COUNT(*) as total_scans,
                    COUNT(DISTINCT domain) as unique_domains,
                    SUM(findings_count) as total_findings,
                    SUM(critical_count) as critical_findings,
                    SUM(high_count) as high_findings,
                    MAX(created_at) as last_scan
                FROM scan_results 
                WHERE user_id = %s 
                AND created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)
            """, (user_id,))
            
            stats = cursor.fetchone()
            
            # Évolution temporelle (7 derniers jours)
            cursor.execute("""
                SELECT 
                    DATE(created_at) as scan_date,
                    COUNT(*) as daily_scans,
                    SUM(critical_count) as daily_critical
                FROM scan_results 
                WHERE user_id = %s 
                AND created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)
                GROUP BY DATE(created_at)
                ORDER BY scan_date
            """, (user_id,))
            
            stats['daily_trends'] = cursor.fetchall()
            return stats
    
    def sync_scan_history(self, user_id: int, local_scans: List[Dict]) -> int:
        """Sync historique local vers cloud (tier PRO)"""
        synced_count = 0
        
        with self.db.get_connection() as conn:
            cursor = conn.cursor()
            
            for scan_data in local_scans:
                # Vérifier si déjà synced
                cursor.execute("""
                    SELECT id FROM scan_results 
                    WHERE user_id = %s AND domain = %s 
                    AND ABS(TIMESTAMPDIFF(SECOND, created_at, %s)) < 60
                """, (user_id, scan_data['domain'], scan_data['timestamp']))
                
                if not cursor.fetchone():
                    # Nouveau scan à synchroniser
                    cursor.execute("""
                        INSERT INTO scan_results 
                        (user_id, domain, findings, scan_config, created_at)
                        VALUES (%s, %s, %s, %s, %s)
                    """, (
                        user_id,
                        scan_data['domain'],
                        json.dumps(scan_data['findings']),
                        json.dumps(scan_data.get('config', {})),
                        scan_data['timestamp']
                    ))
                    synced_count += 1
        
        return synced_count
```

### **Webhook Stripe sécurisé**
```python
# web_sentinel/api/webhooks.py
import stripe
import json
from flask import request, jsonify
from datetime import datetime, timedelta

class StripeWebhookHandler:
    def __init__(self, db_manager: MySQLManager, webhook_secret: str):
        self.db = db_manager
        self.webhook_secret = webhook_secret
        self.user_manager = UserManager(db_manager)
    
    def handle_webhook(self):
        """Traitement sécurisé des webhooks Stripe"""
        payload = request.get_data()
        sig_header = request.headers.get('Stripe-Signature')
        
        try:
            event = stripe.Webhook.construct_event(
                payload, sig_header, self.webhook_secret
            )
        except ValueError:
            return jsonify({'error': 'Invalid payload'}), 400
        except stripe.error.SignatureVerificationError:
            return jsonify({'error': 'Invalid signature'}), 400
        
        # Traitement des événements
        if event['type'] == 'customer.subscription.created':
            self._handle_subscription_created(event['data']['object'])
        elif event['type'] == 'customer.subscription.updated':
            self._handle_subscription_updated(event['data']['object'])
        elif event['type'] == 'customer.subscription.deleted':
            self._handle_subscription_cancelled(event['data']['object'])
        elif event['type'] == 'invoice.payment_succeeded':
            self._handle_payment_succeeded(event['data']['object'])
        
        return jsonify({'received': True}), 200
    
    def _handle_subscription_created(self, subscription):
        """Activation automatique post-paiement"""
        customer_id = subscription['customer']
        
        # Mapping des produits Stripe vers tiers
        tier_mapping = {
            'prod_pro_monthly': 'PRO',
            'prod_pro_yearly': 'PRO', 
            'prod_enterprise_monthly': 'ENTERPRISE',
            'prod_enterprise_yearly': 'ENTERPRISE'
        }
        
        # Déterminer la durée selon le pricing
        if 'year' in subscription['items']['data'][0]['price']['nickname'].lower():
            expires_at = datetime.fromtimestamp(subscription['current_period_end'])
        else:
            expires_at = datetime.fromtimestamp(subscription['current_period_end'])
        
        # Trouver l'utilisateur par customer_id
        with self.db.get_connection() as conn:
            cursor = conn.cursor()
            cursor.execute(
                "SELECT id FROM users WHERE stripe_customer_id = %s",
                (customer_id,)
            )
            user = cursor.fetchone()
            
            if user:
                product_id = subscription['items']['data'][0]['price']['product']
                tier = tier_mapping.get(product_id, 'PRO')
                
                self.user_manager.upgrade_subscription(
                    user[0], customer_id, tier, expires_at
                )
                
                # Enregistrer la subscription
                cursor.execute("""
                    INSERT INTO subscriptions 
                    (user_id, stripe_subscription_id, stripe_product_id, 
                     stripe_price_id, status, current_period_start, current_period_end)
                    VALUES (%s, %s, %s, %s, %s, %s, %s)
                """, (
                    user[0],
                    subscription['id'],
                    product_id,
                    subscription['items']['data'][0]['price']['id'],
                    subscription['status'],
                    datetime.fromtimestamp(subscription['current_period_start']),
                    datetime.fromtimestamp(subscription['current_period_end'])
                ))
```

---

## ⚡ Optimisations de performance

### **Configuration MySQL optimisée**
```ini
# docker/mysql/conf.d/web-sentinel.cnf
[mysqld]
# Buffer Pool (75% de votre RAM serveur)
innodb_buffer_pool_size = 2G
innodb_buffer_pool_instances = 8

# Optimisations JSON
innodb_default_row_format = DYNAMIC
innodb_large_prefix = ON

# Logs et performances
innodb_log_file_size = 512M
innodb_log_buffer_size = 64M
innodb_flush_log_at_trx_commit = 2

# Connexions
max_connections = 200
thread_cache_size = 50

# Query Cache pour lectures répétées
query_cache_type = 1
query_cache_size = 256M

# Optimisation JSON et full-text
ft_min_word_len = 3
innodb_ft_enable_stopword = OFF
```

### **Index avancés pour recherches complexes**
```sql
-- Index composites pour dashboard
CREATE INDEX idx_user_stats ON scan_results(user_id, created_at DESC, critical_count, high_count);

-- Index pour analytics temporelles
CREATE INDEX idx_temporal_analytics ON scan_results(
    created_at, 
    scan_type, 
    (EXTRACT(HOUR FROM created_at))
);

-- Full-text search dans les findings
ALTER TABLE scan_results ADD FULLTEXT(domain);

-- Index fonctionnel pour JSON
CREATE INDEX idx_finding_types ON scan_results(
    (CAST(JSON_EXTRACT(findings, '$[*].check') AS CHAR(50) ARRAY))
);
```

---

## 🚀 Migration et déploiement

### **Script de migration depuis historique local**
```python
# scripts/migrate_local_to_mysql.py
import json
from pathlib import Path
from web_sentinel.database.mysql_manager import MySQLManager
from web_sentinel.reporting import HistoryStore

def migrate_user_history(email: str, local_history_path: str, mysql_config: dict):
    """Migration complète historique local → MySQL cloud"""
    
    # Connexion MySQL
    db_manager = MySQLManager(mysql_config)
    scan_manager = ScanManager(db_manager)
    
    # Création/récupération utilisateur
    user_id = get_or_create_user(email, db_manager)
    
    # Chargement historique local
    history_store = HistoryStore(Path(local_history_path))
    total_migrated = 0
    
    print(f"🔄 Migration de l'historique pour {email}...")
    
    for domain, entries in history_store.entries.items():
        for entry in entries:
            try:
                # Conversion vers ScanResult
                scan_result = ScanResult.from_dict(entry)
                
                # Sauvegarde en MySQL
                scan_id = scan_manager.save_scan_result(user_id, scan_result)
                total_migrated += 1
                
                print(f"✅ {domain}: {len(scan_result.findings)} findings migrés")
                
            except Exception as e:
                print(f"❌ Erreur migration {domain}: {e}")
    
    print(f"🎉 Migration terminée: {total_migrated} scans migrés")
    return total_migrated

if __name__ == "__main__":
    mysql_config = {
        'host': 'your-ovh-server.com',
        'user': 'web_sentinel',
        'password': 'your_secure_password',
        'database': 'web_sentinel'
    }
    
    migrate_user_history(
        email="user@example.com",
        local_history_path="~/.web-sentinel/history.json",
        mysql_config=mysql_config
    )
```

### **Déploiement automatisé OVH**
```bash
#!/bin/bash
# scripts/deploy_ovh.sh

set -e

echo "🚀 Déploiement Web Sentinel sur OVH"

# 1. Backup avant déploiement
echo "📦 Backup base de données..."
docker exec web-sentinel_mysql_1 mysqldump -u root -p$MYSQL_ROOT_PASSWORD web_sentinel > backup_$(date +%Y%m%d_%H%M%S).sql

# 2. Pull dernière version
echo "📥 Récupération dernière version..."
git pull origin main

# 3. Build nouvelles images
echo "🔨 Build images Docker..."
docker-compose build --no-cache

# 4. Update rolling avec zero-downtime
echo "🔄 Déploiement rolling..."
docker-compose up -d --no-deps web-sentinel-api
docker-compose up -d --no-deps web-sentinel-worker

# 5. Tests de santé
echo "🏥 Vérification santé..."
sleep 10
curl -f http://localhost:8080/health || {
    echo "❌ Échec santé - rollback"
    docker-compose restart
    exit 1
}

# 6. Nettoyage images anciennes
echo "🧹 Nettoyage..."
docker image prune -f

echo "✅ Déploiement réussi sur OVH!"
```

---

## 💰 Impact économique

### **Comparaison coûts (mensuel)**
| Solution | Coût | Latence | Admin |
|----------|------|---------|--------|
| **MySQL OVH (recommandé)** | 0€ | <1ms | Existant |
| PostgreSQL cloud | 25-50€ | 10-50ms | Nouveau |
| MongoDB Atlas | 30-60€ | 15-60ms | Nouveau |

### **ROI calculé sur 18 mois**
- **Économies infrastructure** : 675€ - 1080€
- **Pas de migration stack** : Gain 2-3 semaines dev
- **Performance optimale** : Rétention utilisateurs +15%

---

## 🎯 Prochaines étapes d'implémentation

### **Phase 1 (Semaine 1-2) : Base**
1. ✅ Setup schema MySQL sur votre serveur OVH
2. ✅ Implémentation MySQLManager et UserManager
3. ✅ Tests unitaires complets
4. ✅ Migration script historique local

### **Phase 2 (Semaine 3-4) : Monétisation**
1. 🔄 Webhook Stripe sécurisé
2. 🔄 API authentification JWT
3. 🔄 Dashboard stats temps réel
4. 🔄 Sync cloud pour tier PRO

### **Phase 3 (Mois 2) : Avancé**
1. 🔄 Scans programmés avec cron
2. 🔄 Analytics business complètes
3. 🔄 API REST publique
4. 🔄 Monitoring et alertes

---

**Cette architecture MySQL tire le maximum de votre infrastructure OVH existante tout en supportant parfaitement vos objectifs de 170k€ ARR. Prêt pour l'implémentation !** 🚀