Files
2026-04-20 01:17:00 +07:00

153 lines
5.5 KiB
JavaScript

/**
* Кастомные роуты для серверов с поддержкой зашифрованных MikroTik учётных данных
* Для jumphost и home (входной роутер): mikrotikHost, mikrotikPort, mikrotikUser, encryptedMikrotikPassword
*/
const { readS3TextObject, writeS3JsonObject, headMeta } = 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 readS3TextObject(S3_KEY);
const parsed = JSON.parse(data.body || '[]');
return Array.isArray(parsed) ? parsed : [];
} catch (e) {
if (e?.code === 'NoSuchKey') return [];
throw e;
}
}
async function writeServersToS3(servers) {
await writeS3JsonObject(S3_KEY, servers);
}
/**
* GET /api/servers — возвращает серверы, для jumphost заменяет encryptedMikrotikPassword на hasMikrotikPassword
*/
async function getServers(req, res) {
try {
const head = await headMeta(S3_KEY);
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 };
const hasMikrotikApi = s.type === 'jumphost' || s.type === 'home';
if (hasMikrotikApi && out.encryptedMikrotikPassword) {
out.hasMikrotikPassword = true;
delete out.encryptedMikrotikPassword;
} else if (hasMikrotikApi) {
out.hasMikrotikPassword = false;
}
return out;
});
res.json(sanitized);
} catch (error) {
if (error?.code === 'NoSuchKey') {
return res.json([]);
}
console.error(error);
return sendError(res, 500, 'Error reading servers', 'E_STORAGE');
}
}
/**
* 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' || srv.type === 'home') {
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 meta = await headMeta(S3_KEY);
return sendOk(res, {
etag: meta?.etag || null,
lastModified: meta?.lastModified || null,
contentLength: typeof meta?.contentLength === 'number' ? meta.contentLength : null,
});
} catch (error) {
console.error(error);
return sendError(res, 500, error.message || 'Error saving servers', 'E_STORAGE');
}
}
module.exports = {
getServers,
postServers,
readServersFromS3,
};