/** * Кастомные роуты для серверов с поддержкой зашифрованных MikroTik учётных данных * Для jumphost: mikrotikHost, mikrotikPort, mikrotikUser, encryptedMikrotikPassword */ const { GetObjectCommand, PutObjectCommand, HeadObjectCommand } = require('@aws-sdk/client-s3'); const { s3, BUCKET_NAME, streamToString } = require('../services/s3Service'); const { sendError, sendOk, checkIfNoneMatch } = require('../middleware/errorHandler'); const { encrypt, decrypt } = require('../utils/encryption'); const S3_KEY = 'servers.json'; const SERVER_TYPES = ['jumphost', 'exit', 'bgp', 'dns', 'home']; const TYPES_NEED_TUNNEL = ['jumphost', 'exit']; const TYPES_NEED_GATEWAYS = ['jumphost', 'exit']; function validateServer(server, i) { if (!server.ip || !server.dns || !server.country || !server.provider) { return `Server at index ${i} is missing required fields (ip, dns, country, provider)`; } const normalizedType = String(server.type || '').toLowerCase(); if (!SERVER_TYPES.includes(normalizedType)) { return `Server at index ${i} has invalid type (allowed: ${SERVER_TYPES.join(', ')})`; } if (TYPES_NEED_TUNNEL.includes(normalizedType) && !server.tunnel) { return `Server at index ${i} (${normalizedType}) requires tunnel type`; } if (TYPES_NEED_GATEWAYS.includes(normalizedType)) { if (!Array.isArray(server.gateways) || server.gateways.length === 0) { return `Server at index ${i} must have gateways for type ${normalizedType}`; } const primaries = server.gateways.filter(g => g && g.primary); if (primaries.length !== 1) { return `Server at index ${i} must have exactly one primary gateway`; } for (let j = 0; j < server.gateways.length; j++) { const gw = server.gateways[j] || {}; if (!gw.name || String(gw.name).trim().length === 0) { return `Server at index ${i} gateway at index ${j} is missing name`; } } } server.type = normalizedType; return null; } async function readServersFromS3() { try { const data = await s3.send(new GetObjectCommand({ Bucket: BUCKET_NAME, Key: S3_KEY })); const body = await streamToString(data.Body); const parsed = JSON.parse(body || '[]'); return Array.isArray(parsed) ? parsed : []; } catch (e) { if (e?.name === 'NoSuchKey' || e?.$metadata?.httpStatusCode === 404) return []; throw e; } } async function writeServersToS3(servers) { const body = JSON.stringify(servers, null, 2); await s3.send(new PutObjectCommand({ Bucket: BUCKET_NAME, Key: S3_KEY, Body: body, ContentType: 'application/json', })); } /** * GET /api/servers — возвращает серверы, для jumphost заменяет encryptedMikrotikPassword на hasMikrotikPassword */ async function getServers(req, res) { try { const head = await s3.send(new HeadObjectCommand({ Bucket: BUCKET_NAME, Key: S3_KEY })).catch(() => null); const etag = head?.ETag || null; if (etag) res.set('ETag', String(etag)); if (head?.LastModified) res.set('Last-Modified', new Date(head.LastModified).toUTCString()); if (typeof head?.ContentLength === 'number') res.set('Content-Length-Source', String(head.ContentLength)); if (checkIfNoneMatch(req, res, etag)) return; const servers = await readServersFromS3(); const sanitized = servers.map(s => { const out = { ...s }; if (s.type === 'jumphost' && out.encryptedMikrotikPassword) { out.hasMikrotikPassword = true; delete out.encryptedMikrotikPassword; } else if (s.type === 'jumphost') { out.hasMikrotikPassword = false; } return out; }); res.json(sanitized); } catch (error) { if (error?.name === 'NoSuchKey' || error?.$metadata?.httpStatusCode === 404) { return res.json([]); } console.error(error); return sendError(res, 500, 'Error reading servers', 'E_S3'); } } /** * POST /api/servers — сохраняет серверы, шифрует mikrotikPassword при наличии */ async function postServers(req, res) { const items = req.body?.domains ?? req.body?.servers; if (!Array.isArray(items)) { return sendError(res, 400, 'Data must be an array (domains or servers)', 'E_BAD_REQUEST'); } for (let i = 0; i < items.length; i++) { const err = validateServer(items[i], i); if (err) return sendError(res, 400, err, 'E_SCHEMA'); } try { const currentServers = await readServersFromS3(); const findCurrent = (srv) => currentServers.find(c => (srv.id && c.id === srv.id) || (srv.dns && c.dns === srv.dns) || (srv.ip && c.ip === srv.ip) ); const toSave = items.map((srv) => { const current = findCurrent(srv); if (srv.type === 'jumphost') { const out = { ...srv }; if (srv.mikrotikPassword !== undefined && srv.mikrotikPassword !== null && String(srv.mikrotikPassword).trim() !== '') { try { out.encryptedMikrotikPassword = encrypt(String(srv.mikrotikPassword).trim()); } catch (encErr) { console.error('MikroTik password encryption failed:', encErr); } delete out.mikrotikPassword; } else if (current?.encryptedMikrotikPassword) { out.encryptedMikrotikPassword = current.encryptedMikrotikPassword; } return out; } return srv; }); await writeServersToS3(toSave); const head = await s3.send(new HeadObjectCommand({ Bucket: BUCKET_NAME, Key: S3_KEY })).catch(() => null); return sendOk(res, { etag: head?.ETag || null, lastModified: head?.LastModified ? head.LastModified.toISOString() : null, contentLength: typeof head?.ContentLength === 'number' ? head.ContentLength : null, }); } catch (error) { console.error(error); return sendError(res, 500, error.message || 'Error saving servers', 'E_S3'); } } module.exports = { getServers, postServers, readServersFromS3, };