feat(serverConfigs): implement GET and POST routes for jumphost server management, including validation and filtering logic
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m22s

This commit is contained in:
2026-01-20 15:42:10 +07:00
parent f2e237840b
commit fda6e936aa
3 changed files with 188 additions and 56 deletions
+123 -25
View File
@@ -1,39 +1,137 @@
/**
* Роуты для управления конфигурациями серверов
*
* ВАЖНО: /api/server-configs возвращает только jumphost серверы из servers.json
* Это нужно для EasySwitch, который работает только с jumphost серверами
*/
const { s3, BUCKET_NAME, writeS3TextObject, deleteS3Object, headMeta } = require('../services/s3Service');
const { sendError, sendOk } = require('../middleware/errorHandler');
const { GetObjectCommand, DeleteObjectCommand } = require('@aws-sdk/client-s3');
const { s3, BUCKET_NAME, writeS3TextObject, deleteS3Object, headMeta, writeS3JsonObject } = require('../services/s3Service');
const { sendError, sendOk, checkIfNoneMatch } = require('../middleware/errorHandler');
const { GetObjectCommand, HeadObjectCommand } = require('@aws-sdk/client-s3');
const { streamToString } = require('../services/s3Service');
const { createJsonDataRoutes } = require('./jsonDataRoutes');
const { getS3TextWithHeaders, getS3JsonWithHeaders } = require('../utils/s3Helpers');
// GET /api/server-configs (список серверов)
const serverConfigsListRoutes = createJsonDataRoutes('servers.json', (server, i) => {
if (!server.id || !server.name) {
return `Server at index ${i} is missing required fields`;
const S3_KEY = 'servers.json';
// Валидация jumphost сервера
function validateJumphostServer(server, i) {
if (!server.ip || !server.dns || !server.country || !server.provider || !server.tunnel) {
return `Server at index ${i} is missing required fields (ip, dns, country, provider, tunnel)`;
}
if (server.gateways !== undefined) {
if (!Array.isArray(server.gateways)) {
return `Server at index ${i} has invalid gateways (must be array)`;
}
if (server.gateways.length === 0) {
return `Server at index ${i} must have at least one gateway`;
}
const defaults = server.gateways.filter(g => g && g.primary);
if (defaults.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`;
}
if (String(server.type || '').toLowerCase() !== 'jumphost') {
return `Server at index ${i} must have type 'jumphost'`;
}
if (!Array.isArray(server.gateways) || server.gateways.length === 0) {
return `Server at index ${i} must have at least one gateway`;
}
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`;
}
}
return null;
});
}
// GET /api/server-configs (список только jumphost серверов)
async function getServerConfigsList(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 data = await s3.send(new GetObjectCommand({ Bucket: BUCKET_NAME, Key: S3_KEY }));
const fileContent = await streamToString(data.Body);
let allServers = [];
try {
allServers = JSON.parse(fileContent);
if (!Array.isArray(allServers)) {
allServers = [];
}
} catch (parseError) {
console.error(`Error parsing ${S3_KEY}:`, parseError);
allServers = [];
}
// Фильтруем только jumphost серверы и добавляем id/name для совместимости с фронтендом
const jumphostServers = allServers
.filter(s => String(s.type || '').toLowerCase() === 'jumphost')
.map(s => ({
...s,
// Для совместимости с EasySwitchManager добавляем id и name из dns
id: s.id || s.dns,
name: s.name || s.dns
}));
res.json(jumphostServers);
} catch (error) {
if (error?.code === 'NoSuchKey' || error?.$metadata?.httpStatusCode === 404) {
res.json([]);
} else {
console.error(error);
return sendError(res, 500, 'Error reading from S3', 'E_S3');
}
}
}
// POST /api/server-configs (сохранить jumphost серверы, сохраняя остальные)
async function postServerConfigsList(req, res) {
const { servers: jumphostServers } = req.body;
if (!Array.isArray(jumphostServers)) {
return sendError(res, 400, 'Servers must be an array', 'E_BAD_REQUEST');
}
// Валидация jumphost серверов
for (let i = 0; i < jumphostServers.length; i++) {
const error = validateJumphostServer(jumphostServers[i], i);
if (error) {
return sendError(res, 400, error, 'E_SCHEMA');
}
}
try {
// Читаем текущие данные
let allServers = [];
try {
const data = await s3.send(new GetObjectCommand({ Bucket: BUCKET_NAME, Key: S3_KEY }));
const fileContent = await streamToString(data.Body);
allServers = JSON.parse(fileContent);
if (!Array.isArray(allServers)) allServers = [];
} catch (e) {
// Файл не существует или пустой - это нормально
allServers = [];
}
// Оставляем только НЕ-jumphost серверы (exit и другие)
const nonJumphostServers = allServers.filter(s =>
String(s.type || '').toLowerCase() !== 'jumphost'
);
// Объединяем: jumphost из запроса + остальные из файла
const updatedServers = [...jumphostServers, ...nonJumphostServers];
const meta = await writeS3JsonObject(S3_KEY, updatedServers);
return sendOk(res, meta);
} catch (error) {
console.error(error);
return sendError(res, 500, 'Error writing to S3', 'E_S3');
}
}
// Объект с GET и POST для совместимости с server.js
const serverConfigsListRoutes = {
get: getServerConfigsList,
post: postServerConfigsList
};
// GET /api/server-configs/:serverId (конкретная конфигурация)
async function getServerConfig(req, res) {