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 | /** * Routes Clés SSH — CRUD des clés SSH de l'utilisateur. * * Endpoints : * GET /ssh-keys — Liste les clés (sans les données sensibles) * POST /ssh-keys — Ajouter une clé (chiffrée avant stockage) * DELETE /ssh-keys/:id — Supprimer une clé * POST /ssh-keys/:id/test — Tester la connexion SSH avec cette clé * * Sécurité : * - Les clés privées sont chiffrées (AES-256-GCM) avant stockage * - Les clés déchiffrées ne sont jamais renvoyées au client * - Scopé à l'user_id (multi-tenant) * * @module routes/sshKeys */ const express = require('express'); const { pool: db } = require('../db'); const { requireAuth } = require('../middleware/auth'); const { encrypt, decrypt } = require('../services/crypto'); const { testConnection } = require('../services/ssh'); const { makeDeleteHandler } = require('../helpers/routeHelpers'); const router = express.Router(); router.use(requireAuth); // ─── GET /ssh-keys ─────────────────────────────────────────────────────────── router.get('/', async (req, res, next) => { try { // Ne jamais retourner la clé privée chiffrée — seulement les métadonnées const result = await db.query( 'SELECT id, name, created_at FROM ssh_keys WHERE user_id = $1 ORDER BY created_at DESC', [req.user.id], ); res.json(result.rows); } catch (err) { next(err); } }); // ─── POST /ssh-keys ────────────────────────────────────────────────────────── router.post('/', async (req, res, next) => { try { const { name, private_key, passphrase } = req.body; if (!name || !private_key) { return res.status(400).json({ error: 'name et private_key sont requis.' }); } if (name.length > 100) { return res.status(400).json({ error: 'Nom trop long (max 100 caractères).' }); } // Chiffrer la clé privée et la passphrase avant stockage const encryptedPrivateKey = encrypt(private_key); const passphraseEncrypted = passphrase ? encrypt(passphrase) : null; const result = await db.query( `INSERT INTO ssh_keys (user_id, name, encrypted_private_key, passphrase_encrypted) VALUES ($1, $2, $3, $4) RETURNING id, name, created_at`, [req.user.id, name, encryptedPrivateKey, passphraseEncrypted], ); res.status(201).json(result.rows[0]); } catch (err) { next(err); } }); // ─── DELETE /ssh-keys/:id ──────────────────────────────────────────────────── router.delete('/:id', makeDeleteHandler('ssh_keys', 'Clé SSH introuvable.', { message: 'Clé SSH supprimée.' })); // ─── POST /ssh-keys/:id/test ───────────────────────────────────────────────── // Teste la connexion SSH d'une clé associée à un serveur existant. router.post('/:id/test', async (req, res, next) => { try { const { server_id } = req.body; if (!server_id) { return res.status(400).json({ error: 'server_id requis.' }); } const result = await db.query( `SELECT s.host, s.port, s.ssh_user, k.encrypted_private_key, k.passphrase_encrypted FROM servers s JOIN ssh_keys k ON k.id = s.ssh_key_id WHERE s.id = $1 AND s.user_id = $2 AND k.id = $3`, [server_id, req.user.id, req.params.id], ); if (result.rows.length === 0) { return res.status(404).json({ error: 'Serveur ou clé introuvable.' }); } const row = result.rows[0]; await testConnection({ host: row.host, port: row.port, ssh_user: row.ssh_user, privateKey: decrypt(row.encrypted_private_key), passphrase: row.passphrase_encrypted ? decrypt(row.passphrase_encrypted) : undefined, }); res.json({ ok: true, message: 'Connexion SSH réussie.' }); } catch { // Message générique — pas de fuite d'info SSH res.status(502).json({ ok: false, error: 'Connexion SSH échouée. Vérifiez les paramètres.' }); } }); module.exports = router; |