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 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 | /** * Routes Serveurs — CRUD des serveurs SSH de l'utilisateur. * * Endpoints : * GET /servers — Liste les serveurs de l'utilisateur * GET /servers/:id — Détail d'un serveur * POST /servers — Créer un serveur (avec test SSH immédiat) * PATCH /servers/:id — Modifier un serveur * DELETE /servers/:id — Supprimer un serveur * * Sécurité : * - Toutes les routes sont protégées par requireAuth * - Chaque requête est scopée à l'user_id (multi-tenant) * - Le test SSH à la création évite les configs invalides * - La connexion SSH est fermée à la modification/suppression * * @module routes/servers */ const express = require('express'); const { pool: db } = require('../db'); const { requireAuth } = require('../middleware/auth'); const { checkQuota } = require('../middleware/planLimits'); const { requireGroupContext, requireGroupPermission, requireGroupServer } = require('../middleware/groupContext'); const { decrypt } = require('../services/crypto'); const { testConnection, closeConnection, exec } = require('../services/ssh'); const { parseCpu, parseFree, parseDf } = require('../utils/parsers'); const router = express.Router(); router.use(requireAuth); router.use(requireGroupContext()); // ─── Validation des entrées ────────────────────────────────────────────────── /** * Valide les champs d'un serveur. * @returns {string|null} — Message d'erreur ou null si valide */ function validateServerInput({ name, host, port, ssh_user }) { if (!name || typeof name !== 'string' || name.length > 100) { return 'Nom invalide (requis, max 100 caractères).'; } if (!host || typeof host !== 'string' || host.length > 255) { return 'Host invalide (requis, max 255 caractères).'; } if (!ssh_user || typeof ssh_user !== 'string' || ssh_user.length > 64) { return 'User SSH invalide (requis, max 64 caractères).'; } if (port != null) { const p = Number.parseInt(port); if (!Number.isFinite(p) || p < 1 || p > 65535) { return 'Port invalide (1-65535).'; } } return null; } // ─── GET /servers ──────────────────────────────────────────────────────────── router.get('/', async (req, res, next) => { try { let result; if (req.access) { // Mode groupe — seulement les serveurs lies au groupe result = await db.query( `SELECT s.id, s.name, s.host, s.port, s.ssh_user, s.ssh_key_id, k.name AS ssh_key_name, s.created_at FROM servers s LEFT JOIN ssh_keys k ON k.id = s.ssh_key_id JOIN access_group_servers ags ON ags.server_id = s.id WHERE ags.group_id = $1 ORDER BY s.created_at DESC`, [req.access.groupId], ); } else { // Mode global owner result = await db.query( `SELECT s.id, s.name, s.host, s.port, s.ssh_user, s.ssh_key_id, k.name AS ssh_key_name, s.created_at FROM servers s LEFT JOIN ssh_keys k ON k.id = s.ssh_key_id WHERE s.user_id = $1 ORDER BY s.created_at DESC`, [req.user.id], ); } res.json(result.rows); } catch (err) { next(err); } }); // ─── GET /servers/overview ─────────────────────────────────────────────────── /** * Récupère un résumé des métriques de tous les serveurs de l'utilisateur. * Pour chaque serveur, tente une connexion SSH et collecte CPU + RAM. * Les serveurs injoignables sont marqués 'offline'. * * Réponse : [{ id, name, host, status, cpu, mem }] * status : 'online' | 'warning' | 'critical' | 'offline' */ router.get('/overview', async (req, res, next) => { try { let result; if (req.access) { result = await db.query( `SELECT s.id, s.name, s.host, s.port, s.ssh_user FROM servers s JOIN access_group_servers ags ON ags.server_id = s.id WHERE ags.group_id = $1 ORDER BY s.created_at DESC`, [req.access.groupId], ); } else { result = await db.query( `SELECT id, name, host, port, ssh_user FROM servers WHERE user_id = $1 ORDER BY created_at DESC`, [req.user.id], ); } const servers = result.rows; // Collecter les métriques de chaque serveur en parallèle const overview = await Promise.all( servers.map(async (server) => { try { const [cpuRes, memRes, diskRes] = await Promise.allSettled([ exec(req.user.id, server.id, String.raw`top -bn1 | grep '%Cpu\|cpu'`), exec(req.user.id, server.id, 'free -m'), exec(req.user.id, server.id, 'df -h'), ]); const getData = (r) => (r.status === 'fulfilled' ? r.value.stdout : ''); const cpu = parseCpu(getData(cpuRes)); const mem = parseFree(getData(memRes)); const diskPartitions = parseDf(getData(diskRes)); // Prendre la partition racine ou la plus remplie const rootDisk = diskPartitions.find((d) => d.mount === '/') || diskPartitions[0]; const disk = rootDisk ? { use_percent: rootDisk.use_percent, size: rootDisk.size, used: rootDisk.used, mount: rootDisk.mount } : null; // Déterminer le statut en fonction des seuils let status = 'online'; const cpuPercent = cpu?.use_percent ?? 0; const memPercent = mem?.use_percent ?? 0; const diskPercent = disk?.use_percent ?? 0; if (cpuPercent > 90 || memPercent > 90 || diskPercent > 95) status = 'critical'; else if (cpuPercent > 70 || memPercent > 70 || diskPercent > 85) status = 'warning'; return { id: server.id, name: server.name, host: server.host, status, cpu, mem, disk }; } catch { return { id: server.id, name: server.name, host: server.host, status: 'offline', cpu: null, mem: null, disk: null }; } }), ); res.json(overview); } catch (err) { next(err); } }); // ─── GET /servers/:id ──────────────────────────────────────────────────────── router.get('/:id', async (req, res, next) => { try { let result; if (req.access) { // Mode groupe — verifier que le serveur est dans le groupe result = await db.query( `SELECT s.id, s.name, s.host, s.port, s.ssh_user, s.ssh_key_id, k.name AS ssh_key_name, s.created_at FROM servers s LEFT JOIN ssh_keys k ON k.id = s.ssh_key_id JOIN access_group_servers ags ON ags.server_id = s.id WHERE s.id = $1 AND ags.group_id = $2`, [req.params.id, req.access.groupId], ); } else { result = await db.query( `SELECT s.id, s.name, s.host, s.port, s.ssh_user, s.ssh_key_id, k.name AS ssh_key_name, s.created_at FROM servers s LEFT JOIN ssh_keys k ON k.id = s.ssh_key_id WHERE s.id = $1 AND s.user_id = $2`, [req.params.id, req.user.id], ); } if (result.rows.length === 0) { return res.status(404).json({ error: 'Serveur introuvable.' }); } res.json(result.rows[0]); } catch (err) { next(err); } }); // ─── POST /servers ─────────────────────────────────────────────────────────── router.post('/', checkQuota('servers', 'servers'), async (req, res, next) => { try { const { name, host, port = 22, ssh_user, ssh_key_id } = req.body; // Validation des champs if (!ssh_key_id) { return res.status(400).json({ error: 'ssh_key_id est requis.' }); } const validationError = validateServerInput({ name, host, port, ssh_user }); if (validationError) { return res.status(400).json({ error: validationError }); } // Vérifier que la clé SSH appartient à l'utilisateur const keyCheck = await db.query( 'SELECT id, encrypted_private_key, passphrase_encrypted FROM ssh_keys WHERE id = $1 AND user_id = $2', [ssh_key_id, req.user.id], ); if (keyCheck.rows.length === 0) { return res.status(404).json({ error: 'Clé SSH introuvable.' }); } // Test de connexion SSH immédiat (évite les configs invalides) const key = keyCheck.rows[0]; try { await testConnection({ host, port: Number.parseInt(port), ssh_user, privateKey: decrypt(key.encrypted_private_key), passphrase: key.passphrase_encrypted ? decrypt(key.passphrase_encrypted) : undefined, }); } catch { return res.status(502).json({ error: 'Impossible de se connecter via SSH. Vérifiez les paramètres.', }); } const result = await db.query( `INSERT INTO servers (user_id, ssh_key_id, name, host, port, ssh_user) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id, name, host, port, ssh_user, ssh_key_id, created_at`, [req.user.id, ssh_key_id, name, host, Number.parseInt(port), ssh_user], ); res.status(201).json(result.rows[0]); } catch (err) { next(err); } }); // ─── PATCH /servers/:id ────────────────────────────────────────────────────── router.patch('/:id', async (req, res, next) => { try { const { name, host, port, ssh_user, ssh_key_id } = req.body; const existing = await db.query( 'SELECT * FROM servers WHERE id = $1 AND user_id = $2', [req.params.id, req.user.id], ); if (existing.rows.length === 0) { return res.status(404).json({ error: 'Serveur introuvable.' }); } const s = existing.rows[0]; const updated = { name: name ?? s.name, host: host ?? s.host, port: port == null ? s.port : Number.parseInt(port), ssh_user: ssh_user ?? s.ssh_user, ssh_key_id: ssh_key_id ?? s.ssh_key_id, }; // Valider les nouvelles valeurs const validationError = validateServerInput(updated); if (validationError) { return res.status(400).json({ error: validationError }); } // Si la clé SSH change, vérifier qu'elle appartient à l'utilisateur if (ssh_key_id && ssh_key_id !== s.ssh_key_id) { const keyCheck = await db.query( 'SELECT id FROM ssh_keys WHERE id = $1 AND user_id = $2', [ssh_key_id, req.user.id], ); if (keyCheck.rows.length === 0) { return res.status(404).json({ error: 'Clé SSH introuvable.' }); } } const result = await db.query( `UPDATE servers SET name=$1, host=$2, port=$3, ssh_user=$4, ssh_key_id=$5 WHERE id=$6 AND user_id=$7 RETURNING id, name, host, port, ssh_user, ssh_key_id, created_at`, [updated.name, updated.host, updated.port, updated.ssh_user, updated.ssh_key_id, req.params.id, req.user.id], ); // Fermer la connexion SSH si les paramètres de connexion ont changé closeConnection(req.user.id, req.params.id); res.json(result.rows[0]); } catch (err) { next(err); } }); // ─── DELETE /servers/:id ───────────────────────────────────────────────────── router.delete('/:id', async (req, res, next) => { try { const result = await db.query( 'DELETE FROM servers WHERE id = $1 AND user_id = $2 RETURNING id', [req.params.id, req.user.id], ); if (result.rows.length === 0) { return res.status(404).json({ error: 'Serveur introuvable.' }); } // Fermer et supprimer la connexion SSH du pool closeConnection(req.user.id, req.params.id); res.json({ message: 'Serveur supprimé.' }); } catch (err) { next(err); } }); module.exports = router; |