All files / src/routes apiKeys.js

0% Statements 0/40
0% Branches 0/13
0% Functions 0/3
0% Lines 0/40

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125                                                                                                                                                                                                                                                         
/**
 * Routes Clés API — Gestion des clés API IA (Anthropic, OpenAI, Mistral).
 *
 * Endpoints :
 *   GET    /api-keys     — Liste les clés (sans la valeur déchiffrée)
 *   POST   /api-keys     — Ajouter/remplacer une clé (UPSERT par provider)
 *   PATCH  /api-keys/:id — Modifier une clé
 *   DELETE /api-keys/:id — Supprimer une clé
 *
 * Sécurité :
 *   - Les clés sont chiffrées (AES-256-GCM) avant stockage
 *   - Les clés déchiffrées ne sont JAMAIS renvoyées au client
 *   - UPSERT par (user_id, provider) — une seule clé par provider
 *
 * @module routes/apiKeys
 */
 
const express = require('express');
const { pool: db } = require('../db');
const { requireAuth } = require('../middleware/auth');
const { encrypt } = require('../services/crypto');
const { makeDeleteHandler } = require('../helpers/routeHelpers');
 
const router = express.Router();
 
router.use(requireAuth);
 
// Providers autorisés — whitelist stricte
const VALID_PROVIDERS = ['anthropic', 'openai', 'mistral', 'google', 'deepseek', 'xai'];
 
// Modèles par défaut par provider
const DEFAULT_MODELS = {
  anthropic: 'claude-sonnet-4-20250514',
  openai: 'gpt-4o',
  mistral: 'mistral-large-latest',
  google: 'gemini-2.5-flash',
  deepseek: 'deepseek-chat',
  xai: 'grok-3',
};
 
// ─── GET /api-keys ───────────────────────────────────────────────────────────
 
router.get('/', async (req, res, next) => {
  try {
    // Ne jamais renvoyer la clé chiffrée — seulement les métadonnées
    const result = await db.query(
      'SELECT id, provider, default_model, created_at FROM api_keys WHERE user_id = $1 ORDER BY created_at DESC',
      [req.user.id],
    );
    res.json(result.rows);
  } catch (err) {
    next(err);
  }
});
 
// ─── POST /api-keys ──────────────────────────────────────────────────────────
 
router.post('/', async (req, res, next) => {
  try {
    const { provider = 'anthropic', api_key, default_model } = req.body;
 
    if (!api_key) {
      return res.status(400).json({ error: 'api_key est requis.' });
    }
    if (!VALID_PROVIDERS.includes(provider)) {
      return res.status(400).json({
        error: `Provider invalide. Valeurs acceptées : ${VALID_PROVIDERS.join(', ')}`,
      });
    }
 
    const model = default_model || DEFAULT_MODELS[provider];
    const encryptedKey = encrypt(api_key);
 
    // UPSERT : une seule clé par (user, provider)
    const result = await db.query(
      `INSERT INTO api_keys (user_id, provider, encrypted_key, default_model)
       VALUES ($1, $2, $3, $4)
       ON CONFLICT (user_id, provider)
       DO UPDATE SET encrypted_key = EXCLUDED.encrypted_key, default_model = EXCLUDED.default_model
       RETURNING id, provider, default_model, created_at`,
      [req.user.id, provider, encryptedKey, model],
    );
 
    res.status(201).json(result.rows[0]);
  } catch (err) {
    next(err);
  }
});
 
// ─── PATCH /api-keys/:id ─────────────────────────────────────────────────────
 
router.patch('/:id', async (req, res, next) => {
  try {
    const { api_key, default_model } = req.body;
 
    const existing = await db.query(
      'SELECT * FROM api_keys WHERE id = $1 AND user_id = $2',
      [req.params.id, req.user.id],
    );
    if (existing.rows.length === 0) {
      return res.status(404).json({ error: 'Clé API introuvable.' });
    }
 
    const k = existing.rows[0];
    const encryptedKey = api_key ? encrypt(api_key) : k.encrypted_key;
    const model = default_model ?? k.default_model;
 
    const result = await db.query(
      `UPDATE api_keys SET encrypted_key=$1, default_model=$2 WHERE id=$3 AND user_id=$4
       RETURNING id, provider, default_model, created_at`,
      [encryptedKey, model, req.params.id, req.user.id],
    );
 
    res.json(result.rows[0]);
  } catch (err) {
    next(err);
  }
});
 
// ─── DELETE /api-keys/:id ────────────────────────────────────────────────────
 
router.delete('/:id', makeDeleteHandler('api_keys', 'Clé API introuvable.', { message: 'Clé API supprimée.' }));
 
module.exports = router;