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; |