# LeBonCoup - Spécifications Techniques

## Architecture Générale

### Stack recommandée pour MVP

**Backend :**
- Node.js + Express (ou NestJS pour plus de structure)
- Alternative : Laravel (si préférence PHP)

**Base de données :**
- PostgreSQL 14+ avec extension PostGIS (géolocalisation)

**Frontend :**
- React ou Vue.js 3
- PWA (Progressive Web App) pour notifications
- Alternative : WordPress headless (si expertise existante)

**Paiement :**
- Stripe (Checkout ou Elements)

**Hébergement :**
- Backend : Railway, Render, DigitalOcean, ou AWS
- BDD : Managed PostgreSQL (même providers)
- Stockage médias (v2) : AWS S3 ou Cloudflare R2

**CDN :**
- Cloudflare (gratuit pour commencer)

---

## 1. Authentification et Identité

### 1.1 Système de hash d'email

**Génération du user_id :**
```javascript
// Côté serveur : salt secret
const SERVER_SALT = process.env.SECRET_SALT; // stocké en env var

// Génération
const userId = SHA256(email.toLowerCase().trim() + SERVER_SALT);
```

**Pourquoi un salt ?**
- Empêche les attaques par rainbow table
- Même avec la BDD leakée, impossible de retrouver l'email sans le salt
- Le salt reste secret côté serveur

### 1.2 Dérivation de clés pour le chiffrement

**Côté client uniquement (JavaScript) :**

```javascript
// À la connexion
async function deriveKeys(email, password) {
  const emailLower = email.toLowerCase().trim();

  // Pour l'authentification (envoyé au serveur)
  const userId = await sha256(emailLower + SERVER_SALT);

  // Pour le chiffrement E2E (reste dans le navigateur)
  const encryptionKey = await crypto.subtle.deriveKey(
    {
      name: "PBKDF2",
      salt: new TextEncoder().encode(emailLower),
      iterations: 100000,
      hash: "SHA-256"
    },
    await crypto.subtle.importKey(
      "raw",
      new TextEncoder().encode(password),
      "PBKDF2",
      false,
      ["deriveKey"]
    ),
    { name: "AES-GCM", length: 256 },
    false,
    ["encrypt", "decrypt"]
  );

  // Stockée en sessionStorage (volatile)
  sessionStorage.setItem('encryptionKey', encryptionKey);

  return { userId, encryptionKey };
}
```

**Clés dérivées :**
- `userId` : identifiant pour le serveur (hash SHA256)
- `encryptionKey` : clé AES-256 pour chiffrer/déchiffrer les messages
- La `encryptionKey` **ne quitte JAMAIS** le navigateur

### 1.3 Authentification

**Inscription :**
```javascript
POST /api/auth/signup
Body: {
  userId: "8f7a9b2c...",  // SHA256(email + salt)
  passwordHash: "bcrypt..." // password hashé côté client
}

Serveur :
1. Vérifie que userId n'existe pas
2. Hash à nouveau le passwordHash (double hash)
3. Stocke dans users table
4. Renvoie JWT
```

**Login :**
```javascript
POST /api/auth/login
Body: {
  userId: "8f7a9b2c...",
  password: "plaintext" // ou passwordHash si préférence
}

Serveur :
1. Trouve user par userId
2. Vérifie password avec bcrypt.compare()
3. Génère JWT (expiré 7 jours)
4. Renvoie token
```

**JWT Payload :**
```json
{
  "userId": "8f7a9b2c...",
  "type": "pro" | "demandeur",
  "iat": 1234567890,
  "exp": 1234999999
}
```

---

## 2. Base de Données

### 2.1 Schéma PostgreSQL

```sql
-- Users (pros et demandeurs)
CREATE TABLE users (
  id VARCHAR(64) PRIMARY KEY,           -- SHA256(email + salt)
  password_hash VARCHAR(255) NOT NULL,  -- bcrypt
  user_type VARCHAR(20) NOT NULL,       -- 'pro' ou 'demandeur'
  token_balance INT DEFAULT 0,
  token_premium_balance INT DEFAULT 0,  -- optionnel v2
  created_at TIMESTAMP DEFAULT NOW(),
  banned BOOLEAN DEFAULT FALSE,
  ban_reason TEXT
);

-- Demandes
CREATE TABLE demandes (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  author_id VARCHAR(64) REFERENCES users(id),
  title VARCHAR(100) NOT NULL,
  description TEXT NOT NULL,
  category VARCHAR(50) NOT NULL,
  location GEOGRAPHY(POINT, 4326),      -- PostGIS pour géoloc
  location_display VARCHAR(100),        -- "Paris 11e" (flouté)
  budget_min INT,
  budget_max INT,
  status VARCHAR(20) DEFAULT 'active',  -- active/full/resolved/expired
  slots_used INT DEFAULT 0,             -- nombre de pros ayant débloqué
  slots_premium_used INT DEFAULT 0,     -- optionnel v2
  created_at TIMESTAMP DEFAULT NOW(),
  expires_at TIMESTAMP,                 -- created_at + 30 jours
  flagged BOOLEAN DEFAULT FALSE,
  flag_reason TEXT
);

CREATE INDEX idx_demandes_status ON demandes(status);
CREATE INDEX idx_demandes_location ON demandes USING GIST(location);
CREATE INDEX idx_demandes_category ON demandes(category);

-- Transactions (paiements externes)
CREATE TABLE transactions (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id VARCHAR(64) REFERENCES users(id),
  stripe_payment_id VARCHAR(255) NOT NULL,
  amount_eur DECIMAL(10, 2) NOT NULL,
  tokens_credited INT NOT NULL,
  created_at TIMESTAMP DEFAULT NOW()
);

CREATE INDEX idx_transactions_user ON transactions(user_id);
CREATE INDEX idx_transactions_stripe ON transactions(stripe_payment_id);

-- Unlocks (qui a débloqué quoi)
CREATE TABLE unlocks (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  pro_id VARCHAR(64) REFERENCES users(id),
  demande_id UUID REFERENCES demandes(id),
  tokens_spent INT NOT NULL,
  is_premium BOOLEAN DEFAULT FALSE,
  unlocked_at TIMESTAMP DEFAULT NOW(),
  refunded BOOLEAN DEFAULT FALSE,
  refund_reason TEXT
);

CREATE INDEX idx_unlocks_pro ON unlocks(pro_id);
CREATE INDEX idx_unlocks_demande ON unlocks(demande_id);

-- Conversations (blobs chiffrés)
CREATE TABLE conversations (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_a_id VARCHAR(64) REFERENCES users(id),
  user_b_id VARCHAR(64) REFERENCES users(id),
  blob_for_a TEXT,                      -- JSON chiffré pour A
  blob_for_b TEXT,                      -- JSON chiffré pour B
  message_count INT DEFAULT 0,
  last_updated TIMESTAMP DEFAULT NOW(),
  created_at TIMESTAMP DEFAULT NOW(),
  auto_delete_at TIMESTAMP              -- created_at + 6 mois
);

CREATE INDEX idx_conv_user_a ON conversations(user_a_id);
CREATE INDEX idx_conv_user_b ON conversations(user_b_id);
CREATE INDEX idx_conv_auto_delete ON conversations(auto_delete_at);

-- Flags (modération)
CREATE TABLE conversation_flags (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  conversation_id UUID REFERENCES conversations(id),
  user_id VARCHAR(64) REFERENCES users(id), -- user qui a généré les flags
  flag_type VARCHAR(50) NOT NULL,           -- contact_externe, paiement_externe, suspect
  count INT DEFAULT 1,
  last_flagged TIMESTAMP DEFAULT NOW()
);

CREATE INDEX idx_flags_conv ON conversation_flags(conversation_id);
CREATE INDEX idx_flags_user ON conversation_flags(user_id);

-- Signalements (user reports)
CREATE TABLE reports (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  reporter_id VARCHAR(64) REFERENCES users(id),
  reported_id VARCHAR(64) REFERENCES users(id),
  reason TEXT NOT NULL,
  conversation_id UUID REFERENCES conversations(id),
  status VARCHAR(20) DEFAULT 'pending',     -- pending/reviewed/actioned
  created_at TIMESTAMP DEFAULT NOW()
);

-- Médias (optionnel v2)
CREATE TABLE media_files (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  conversation_id UUID REFERENCES conversations(id),
  uploaded_by VARCHAR(64) REFERENCES users(id),
  file_encrypted BYTEA,                     -- ou URL S3
  file_type VARCHAR(10),                    -- image/video
  file_size INT,
  uploaded_at TIMESTAMP DEFAULT NOW(),
  expires_at TIMESTAMP,                     -- uploaded_at + 14 jours
  deleted BOOLEAN DEFAULT FALSE
);

CREATE INDEX idx_media_expires ON media_files(expires_at) WHERE deleted = FALSE;
```

### 2.2 Tâches cron automatiques

**Script à exécuter quotidiennement (3h du matin) :**

```javascript
// Suppression conversations > 6 mois
DELETE FROM conversations
WHERE last_updated < NOW() - INTERVAL '6 months';

// Suppression demandes expirées
DELETE FROM demandes
WHERE expires_at < NOW();

// Suppression médias expirés
DELETE FROM media_files
WHERE expires_at < NOW() AND deleted = FALSE;

// Warning 30 jours avant suppression conversation
SELECT user_a_id, user_b_id, id
FROM conversations
WHERE last_updated < NOW() - INTERVAL '5 months'
  AND last_updated > NOW() - INTERVAL '5 months 1 day';
// → Envoyer notifications

// Ban automatique si trop de flags
UPDATE users
SET banned = TRUE, ban_reason = 'Auto-ban: excessive flags'
WHERE id IN (
  SELECT user_id
  FROM conversation_flags
  GROUP BY user_id
  HAVING SUM(count) > 50
);
```

---

## 3. Paiements Stripe

### 3.1 Intégration Stripe Checkout (recommandé pour MVP)

**Flow :**

```javascript
// Frontend : User clique "Acheter 50 tokens"
const response = await fetch('/api/tokens/create-checkout', {
  method: 'POST',
  headers: { 'Authorization': `Bearer ${jwt}` },
  body: JSON.stringify({ pack: '50' })
});

const { sessionId } = await response.json();

// Redirige vers Stripe Checkout
const stripe = Stripe(STRIPE_PUBLIC_KEY);
await stripe.redirectToCheckout({ sessionId });
```

**Backend :**

```javascript
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

app.post('/api/tokens/create-checkout', authenticateJWT, async (req, res) => {
  const { pack } = req.body;
  const userId = req.user.userId;

  // Définir les packs
  const packs = {
    '10': { price: 10, tokens: 10 },
    '25': { price: 25, tokens: 30 },
    '50': { price: 50, tokens: 70 },
    '100': { price: 100, tokens: 150 }
  };

  const selectedPack = packs[pack];
  if (!selectedPack) return res.status(400).json({ error: 'Invalid pack' });

  // Créer session Stripe
  const session = await stripe.checkout.sessions.create({
    payment_method_types: ['card'],
    line_items: [{
      price_data: {
        currency: 'eur',
        product_data: {
          name: `${selectedPack.tokens} tokens LeBonCoup`
        },
        unit_amount: selectedPack.price * 100 // en centimes
      },
      quantity: 1
    }],
    mode: 'payment',
    success_url: `${process.env.FRONTEND_URL}/tokens/success`,
    cancel_url: `${process.env.FRONTEND_URL}/tokens/cancel`,
    metadata: {
      userId,
      tokens: selectedPack.tokens
    }
  });

  res.json({ sessionId: session.id });
});
```

### 3.2 Webhook Stripe (CRITIQUE)

**Configuration Stripe :**
- Dashboard Stripe → Developers → Webhooks
- Ajouter endpoint : `https://tonsite.com/api/webhooks/stripe`
- Événements à écouter : `checkout.session.completed`

**Backend :**

```javascript
app.post('/api/webhooks/stripe', express.raw({ type: 'application/json' }), async (req, res) => {
  const sig = req.headers['stripe-signature'];
  const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;

  let event;

  try {
    // VÉRIFICATION DE LA SIGNATURE (CRITIQUE)
    event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);
  } catch (err) {
    console.error('Webhook signature verification failed:', err.message);
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }

  // Traiter l'événement
  if (event.type === 'checkout.session.completed') {
    const session = event.data.object;
    const { userId, tokens } = session.metadata;

    // Créditer les tokens
    await db.query(
      'UPDATE users SET token_balance = token_balance + $1 WHERE id = $2',
      [tokens, userId]
    );

    // Enregistrer la transaction
    await db.query(
      'INSERT INTO transactions (user_id, stripe_payment_id, amount_eur, tokens_credited) VALUES ($1, $2, $3, $4)',
      [userId, session.payment_intent, session.amount_total / 100, tokens]
    );

    console.log(`✅ Credited ${tokens} tokens to user ${userId}`);
  }

  res.json({ received: true });
});
```

**⚠️ SÉCURITÉ CRITIQUE :**
- **TOUJOURS** vérifier la signature Stripe
- Sans ça, n'importe qui peut envoyer un faux webhook et s'auto-créditer des tokens
- Ne JAMAIS faire confiance à un webhook non signé

---

## 4. Messagerie Chiffrée (Système de Blobs)

### 4.1 Chiffrement côté client

**Chiffrement d'un message :**

```javascript
async function encryptBlob(plaintextJSON, encryptionKey) {
  const iv = crypto.getRandomValues(new Uint8Array(12));

  const encrypted = await crypto.subtle.encrypt(
    { name: "AES-GCM", iv },
    encryptionKey,
    new TextEncoder().encode(JSON.stringify(plaintextJSON))
  );

  // Retourner IV + ciphertext encodé en base64
  return {
    iv: btoa(String.fromCharCode(...iv)),
    data: btoa(String.fromCharCode(...new Uint8Array(encrypted)))
  };
}

async function decryptBlob(encryptedBlob, encryptionKey) {
  const iv = Uint8Array.from(atob(encryptedBlob.iv), c => c.charCodeAt(0));
  const data = Uint8Array.from(atob(encryptedBlob.data), c => c.charCodeAt(0));

  const decrypted = await crypto.subtle.decrypt(
    { name: "AES-GCM", iv },
    encryptionKey,
    data
  );

  return JSON.parse(new TextDecoder().decode(decrypted));
}
```

### 4.2 Structure du blob (déchiffré)

```json
{
  "conversation_id": "uuid",
  "participants": {
    "user_a": "user_id_a",
    "user_b": "user_id_b"
  },
  "messages": [
    {
      "id": "msg_uuid",
      "from": "user_id_a",
      "type": "text",
      "content": "Bonjour, je suis intéressé...",
      "timestamp": 1234567890,
      "read": false
    },
    {
      "id": "msg_uuid2",
      "from": "user_id_b",
      "type": "text",
      "content": "Parfait, voici mes disponibilités...",
      "timestamp": 1234567920,
      "read": true
    }
  ],
  "last_read": {
    "user_a": 1234567920,
    "user_b": 1234567890
  }
}
```

### 4.3 Envoi d'un message

**Frontend :**

```javascript
async function sendMessage(conversationId, recipientId, content) {
  // 1. Récupérer les blobs existants
  const response = await fetch(`/api/conversations/${conversationId}`, {
    headers: { 'Authorization': `Bearer ${jwt}` }
  });
  const { blob_for_a, blob_for_b, user_a_id, user_b_id } = await response.json();

  // 2. Déterminer qui est qui
  const currentUserId = getCurrentUserId();
  const myBlob = currentUserId === user_a_id ? blob_for_a : blob_for_b;
  const theirBlob = currentUserId === user_a_id ? blob_for_b : blob_for_a;

  // 3. Déchiffrer mon blob
  const myKey = sessionStorage.getItem('encryptionKey');
  const myData = await decryptBlob(JSON.parse(myBlob), myKey);

  // 4. Ajouter le nouveau message
  const newMessage = {
    id: generateUUID(),
    from: currentUserId,
    type: 'text',
    content,
    timestamp: Date.now(),
    read: false
  };
  myData.messages.push(newMessage);

  // 5. Re-chiffrer pour moi
  const newMyBlob = await encryptBlob(myData, myKey);

  // 6. Dériver la clé du destinataire et chiffrer pour lui
  // NOTE: Ici on suppose qu'on a la clé du destinataire
  // En réalité, chaque user chiffre avec SA clé, donc on doit :
  // - Soit récupérer leur clé publique (si on passe en asymétrique)
  // - Soit re-construire leur blob côté serveur (impossible car on a pas leur clé)
  //
  // SOLUTION RÉELLE : Chaque user chiffre son propre blob
  // Donc User A met à jour son blob, User B met à jour le sien

  // Version simplifiée : on envoie juste notre blob mis à jour
  await fetch(`/api/conversations/${conversationId}/message`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${jwt}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      myBlob: newMyBlob,
      recipientId,
      messagePreview: content.substring(0, 50) // pour notif
    })
  });
}
```

**Backend :**

```javascript
app.post('/api/conversations/:id/message', authenticateJWT, async (req, res) => {
  const { id } = req.params;
  const { myBlob, recipientId, messagePreview } = req.body;
  const senderId = req.user.userId;

  // 1. Mettre à jour le blob de l'expéditeur
  await db.query(
    `UPDATE conversations
     SET ${senderId === 'user_a_id' ? 'blob_for_a' : 'blob_for_b'} = $1,
         message_count = message_count + 1,
         last_updated = NOW()
     WHERE id = $2`,
    [JSON.stringify(myBlob), id]
  );

  // 2. Envoyer notification push au destinataire
  await sendPushNotification(recipientId, {
    title: 'Nouveau message',
    body: messagePreview,
    conversationId: id
  });

  res.json({ success: true });
});
```

**⚠️ PROBLÈME IDENTIFIÉ :**

Avec des clés symétriques dérivées, **chaque user ne peut chiffrer QUE pour lui-même**.

**Solutions :**

**Option A : Chiffrement asymétrique (RSA/ECDH)**
- Chaque user a une paire clé publique/privée
- Clé publique stockée sur le serveur
- Messages chiffrés avec la clé publique du destinataire
- Plus complexe

**Option B : Blob personnel uniquement**
- Chaque user gère uniquement SON blob
- Quand User A envoie un message, il met à jour son blob
- Quand User B se connecte, il récupère son blob (qui contient les messages que A lui a envoyés)
- Le serveur reçoit deux blobs séparés et les synchronise

**Recommandation pour MVP : Option B (plus simple)**

---

## 5. Système de Flags (Modération)

### 5.1 Scan côté client

**Patterns de détection :**

```javascript
const DETECTION_PATTERNS = {
  phone: /\b0[67]\d{8}\b|\b\+33\s?[67]\d{8}\b/,
  email: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/i,
  social: /@(instagram|telegram|whatsapp|snapchat|facebook)/i,
  payment: /\b(paypal|venmo|cash|espèces|liquide|virement|western\s?union)\b/i,
  suspect: /\b(discret|sans facture|urgent|livraison rapide|entre nous)\b/i
};

function scanMessage(content) {
  const flags = [];

  if (DETECTION_PATTERNS.phone.test(content)) {
    flags.push('contact_externe');
  }
  if (DETECTION_PATTERNS.email.test(content)) {
    flags.push('contact_externe');
  }
  if (DETECTION_PATTERNS.social.test(content)) {
    flags.push('contact_externe');
  }
  if (DETECTION_PATTERNS.payment.test(content)) {
    flags.push('paiement_externe');
  }
  if (DETECTION_PATTERNS.suspect.test(content)) {
    flags.push('activité_suspecte');
  }

  return flags;
}
```

### 5.2 Envoi des flags au serveur

```javascript
async function sendMessage(conversationId, content) {
  // 1. Scan du message
  const flags = scanMessage(content);

  // 2. Si flags détectés, envoyer au serveur (asynchrone)
  if (flags.length > 0) {
    fetch('/api/flags', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${jwt}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        conversationId,
        flags,
        timestamp: Date.now()
      })
    }).catch(err => console.error('Failed to send flags:', err));
  }

  // 3. Chiffrer et envoyer le message normalement
  // (ne pas bloquer l'envoi)
  await encryptAndSendMessage(conversationId, content);
}
```

### 5.3 Traitement serveur

```javascript
app.post('/api/flags', authenticateJWT, async (req, res) => {
  const { conversationId, flags } = req.body;
  const userId = req.user.userId;

  // Incrémenter les compteurs
  for (const flagType of flags) {
    await db.query(
      `INSERT INTO conversation_flags (conversation_id, user_id, flag_type, count, last_flagged)
       VALUES ($1, $2, $3, 1, NOW())
       ON CONFLICT (conversation_id, user_id, flag_type)
       DO UPDATE SET count = conversation_flags.count + 1, last_flagged = NOW()`,
      [conversationId, userId, flagType]
    );
  }

  // Calculer le total de flags pour cet user
  const result = await db.query(
    'SELECT SUM(count) as total FROM conversation_flags WHERE user_id = $1',
    [userId]
  );
  const totalFlags = parseInt(result.rows[0].total);

  // Actions automatiques
  if (totalFlags >= 50) {
    await db.query('UPDATE users SET banned = TRUE, ban_reason = $1 WHERE id = $2',
      ['Auto-ban: 50+ flags', userId]);
  } else if (totalFlags >= 25) {
    // Restriction temporaire (TODO: implémenter système de restrictions)
  } else if (totalFlags >= 10) {
    // Warning (TODO: notification in-app)
  }

  res.json({ success: true });
});
```

---

## 6. Géolocalisation (PostGIS)

### 6.1 Stockage d'une demande avec localisation

```javascript
app.post('/api/demandes', authenticateJWT, async (req, res) => {
  const { title, description, category, lat, lon, budget_min, budget_max } = req.body;
  const authorId = req.user.userId;

  // Créer un point géographique
  const result = await db.query(
    `INSERT INTO demandes (author_id, title, description, category, location, location_display, budget_min, budget_max, expires_at)
     VALUES ($1, $2, $3, $4, ST_SetSRID(ST_MakePoint($5, $6), 4326), $7, $8, $9, NOW() + INTERVAL '30 days')
     RETURNING *`,
    [authorId, title, description, category, lon, lat, getLocationDisplay(lat, lon), budget_min, budget_max]
  );

  res.json(result.rows[0]);
});
```

### 6.2 Filtrage par distance

```javascript
app.get('/api/demandes/nearby', authenticateJWT, async (req, res) => {
  const { lat, lon, radius_km, category } = req.query;

  const result = await db.query(
    `SELECT *, ST_Distance(location, ST_SetSRID(ST_MakePoint($1, $2), 4326)) / 1000 as distance_km
     FROM demandes
     WHERE status = 'active'
       AND category = $3
       AND ST_DWithin(location::geography, ST_SetSRID(ST_MakePoint($1, $2), 4326)::geography, $4 * 1000)
     ORDER BY distance_km ASC`,
    [lon, lat, category, radius_km]
  );

  res.json(result.rows);
});
```

---

## 7. Sécurité

### 7.1 Checklist sécurité

**Backend :**
- ✅ HTTPS obligatoire (certificat SSL)
- ✅ Helmet.js (headers de sécurité)
- ✅ Rate limiting (express-rate-limit)
- ✅ CORS configuré strictement
- ✅ SQL injection : utiliser parameterized queries
- ✅ XSS : sanitize inputs (DOMPurify côté client)
- ✅ CSRF : tokens CSRF ou SameSite cookies
- ✅ JWT avec expiration courte (7 jours max)
- ✅ Webhook Stripe : vérification signature

**Frontend :**
- ✅ CSP (Content Security Policy)
- ✅ Pas de données sensibles en localStorage (sessionStorage OK)
- ✅ Clés de chiffrement en mémoire uniquement
- ✅ Logout = clear sessionStorage

**BDD :**
- ✅ Pas de port exposé publiquement
- ✅ Credentials forts
- ✅ Backups réguliers
- ✅ Chiffrement at-rest si possible

### 7.2 Rate limiting

```javascript
const rateLimit = require('express-rate-limit');

const apiLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // 100 requêtes max
  message: 'Too many requests, please try again later'
});

app.use('/api/', apiLimiter);

const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5, // 5 tentatives de login max
  message: 'Too many login attempts, please try again later'
});

app.use('/api/auth/login', authLimiter);
```

---

## 8. Déploiement

### 8.1 Variables d'environnement

```bash
# .env
NODE_ENV=production

# Database
DATABASE_URL=postgresql://user:password@host:5432/leboncoup

# Secrets
JWT_SECRET=random_string_very_long_and_secure
SECRET_SALT=another_random_string_for_email_hashing

# Stripe
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...
STRIPE_PUBLIC_KEY=pk_live_...

# Frontend
FRONTEND_URL=https://leboncoup.fr

# Optionnel (v2)
AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...
S3_BUCKET_NAME=leboncoup-media
```

### 8.2 Dockerfile (optionnel)

```dockerfile
FROM node:18-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY . .

EXPOSE 3000

CMD ["node", "server.js"]
```

### 8.3 Procédure de déploiement

**Option 1 : Railway (recommandé pour MVP)**
1. Connecter repo GitHub
2. Railway détecte Node.js automatiquement
3. Ajouter PostgreSQL addon
4. Configurer les env vars
5. Deploy automatique sur chaque push

**Option 2 : DigitalOcean App Platform**
- Similaire à Railway
- Managed PostgreSQL inclus

**Option 3 : AWS (plus complexe)**
- EC2 pour backend
- RDS PostgreSQL
- S3 pour médias
- CloudFront CDN
- Route53 DNS

---

## 9. Monitoring et Logs

### 9.1 Logs essentiels

```javascript
const winston = require('winston');

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' })
  ]
});

// Logs critiques
logger.info('User login', { userId, timestamp });
logger.info('Token purchase', { userId, amount, tokens });
logger.warn('Flag threshold reached', { userId, totalFlags });
logger.error('Webhook verification failed', { error });
```

### 9.2 Alertes

**Mettre en place des alertes pour :**
- Webhook Stripe échoué
- User atteint 50+ flags
- Erreurs serveur (500)
- Usage disque > 80%
- CPU > 90%

**Outils recommandés :**
- Sentry (monitoring d'erreurs)
- Better Uptime (uptime monitoring)
- Logtail (logs centralisés)

---

## 10. Tests

### 10.1 Tests critiques

**Backend :**
```javascript
// Test webhook Stripe
test('Webhook Stripe valide crédite les tokens', async () => {
  const event = createMockStripeEvent();
  const response = await request(app)
    .post('/api/webhooks/stripe')
    .set('stripe-signature', generateValidSignature(event))
    .send(event);

  expect(response.status).toBe(200);

  const user = await db.getUserById(userId);
  expect(user.token_balance).toBe(50);
});

// Test flags auto-ban
test('User avec 50 flags est automatiquement banni', async () => {
  await createFlagsForUser(userId, 50);

  const user = await db.getUserById(userId);
  expect(user.banned).toBe(true);
});
```

**Frontend :**
```javascript
// Test chiffrement/déchiffrement
test('Message chiffré puis déchiffré = message original', async () => {
  const key = await deriveEncryptionKey('test@example.com', 'password123');
  const message = { content: 'Hello', timestamp: Date.now() };

  const encrypted = await encryptBlob(message, key);
  const decrypted = await decryptBlob(encrypted, key);

  expect(decrypted.content).toBe('Hello');
});
```

---

## 11. Performance

### 11.1 Optimisations BDD

**Index critiques :**
```sql
CREATE INDEX CONCURRENTLY idx_demandes_active ON demandes(status) WHERE status = 'active';
CREATE INDEX CONCURRENTLY idx_conversations_recent ON conversations(last_updated DESC);
CREATE INDEX CONCURRENTLY idx_unlocks_pro_recent ON unlocks(pro_id, unlocked_at DESC);
```

**Requêtes optimisées :**
- Utiliser `LIMIT` partout
- Pagination (offset/limit ou cursor-based)
- Éviter les `SELECT *` (spécifier les colonnes)

### 11.2 Cache

**Redis (optionnel pour scale) :**
```javascript
// Cache du nombre de demandes actives
const cachedCount = await redis.get('demandes:active:count');
if (cachedCount) return parseInt(cachedCount);

const count = await db.query('SELECT COUNT(*) FROM demandes WHERE status = \'active\'');
await redis.set('demandes:active:count', count, 'EX', 300); // 5 min
```

---

## 12. Migration et Maintenance

### 12.1 Scripts de migration

**Utiliser un outil comme `node-pg-migrate` :**

```javascript
// migrations/001_initial_schema.js
exports.up = (pgm) => {
  pgm.createTable('users', {
    id: { type: 'varchar(64)', primaryKey: true },
    password_hash: { type: 'varchar(255)', notNull: true },
    // ...
  });
};

exports.down = (pgm) => {
  pgm.dropTable('users');
};
```

### 12.2 Backups

**PostgreSQL :**
```bash
# Backup quotidien automatique
pg_dump -U user -h host leboncoup > backup_$(date +%Y%m%d).sql

# Restauration
psql -U user -h host leboncoup < backup_20250116.sql
```

---

**Date de création :** 2025-01-16
**Version :** 1.0
**Statut :** Draft pour implémentation
