feat(NetworkConfig): add network configuration routes and validation; integrate into server and frontend for managing IP, interfaces, and gateways
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m27s

This commit is contained in:
2026-01-22 10:08:51 +07:00
parent dace11a407
commit d949e011c9
5 changed files with 1222 additions and 21 deletions
+57 -19
View File
@@ -9,8 +9,15 @@ const { streamToString } = require('../services/s3Service');
/**
* Создать GET эндпоинт для JSON данных
* @param {string} s3Key - Ключ в S3
* @param {Object} options - Опции
* @param {boolean} options.singleObject - Если true, возвращает объект вместо массива
* @param {any} options.defaultValue - Значение по умолчанию ([] для массива, {} для объекта)
*/
function createJsonDataGET(s3Key) {
function createJsonDataGET(s3Key, options = {}) {
const { singleObject = false, defaultValue } = options;
const fallback = defaultValue !== undefined ? defaultValue : (singleObject ? {} : []);
return async (req, res) => {
try {
const head = await s3.send(new HeadObjectCommand({ Bucket: BUCKET_NAME, Key: s3Key })).catch(() => null);
@@ -22,22 +29,26 @@ function createJsonDataGET(s3Key) {
const data = await s3.send(new GetObjectCommand({ Bucket: BUCKET_NAME, Key: s3Key }));
const fileContent = await streamToString(data.Body);
let items = [];
let items = fallback;
try {
items = JSON.parse(fileContent);
if (!Array.isArray(items)) {
items = [];
const parsed = JSON.parse(fileContent);
if (singleObject) {
// Для одиночного объекта
items = (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) ? parsed : fallback;
} else {
// Для массива
items = Array.isArray(parsed) ? parsed : fallback;
}
} catch (parseError) {
console.error(`Error parsing ${s3Key}:`, parseError);
items = [];
items = fallback;
}
res.json(items);
} catch (error) {
if (error?.code === 'NoSuchKey' || error?.$metadata?.httpStatusCode === 404) {
res.json([]);
res.json(fallback);
} else {
console.error(error);
return sendError(res, 500, 'Error reading from S3', 'E_S3');
@@ -48,23 +59,45 @@ function createJsonDataGET(s3Key) {
/**
* Создать POST эндпоинт для JSON данных
* @param {string} s3Key - Ключ в S3
* @param {Function} validateItem - Функция валидации (для массива - валидирует каждый элемент, для объекта - весь объект)
* @param {Object} options - Опции
* @param {boolean} options.singleObject - Если true, ожидает объект вместо массива
*/
function createJsonDataPOST(s3Key, validateItem = null) {
function createJsonDataPOST(s3Key, validateItem = null, options = {}) {
const { singleObject = false } = options;
return async (req, res) => {
const { domains: items } = req.body; // Используем 'domains' для обратной совместимости
if (!Array.isArray(items)) {
return sendError(res, 400, 'Data must be an array', 'E_BAD_REQUEST');
}
// Валидация элементов, если предоставлена
if (validateItem) {
for (let i = 0; i < items.length; i++) {
const error = validateItem(items[i], i);
if (singleObject) {
// Для одиночного объекта
if (typeof items !== 'object' || items === null || Array.isArray(items)) {
return sendError(res, 400, 'Data must be an object', 'E_BAD_REQUEST');
}
// Валидация объекта целиком
if (validateItem) {
const error = validateItem(items);
if (error) {
return sendError(res, 400, error, 'E_SCHEMA');
}
}
} else {
// Для массива
if (!Array.isArray(items)) {
return sendError(res, 400, 'Data must be an array', 'E_BAD_REQUEST');
}
// Валидация каждого элемента массива
if (validateItem) {
for (let i = 0; i < items.length; i++) {
const error = validateItem(items[i], i);
if (error) {
return sendError(res, 400, error, 'E_SCHEMA');
}
}
}
}
try {
@@ -79,11 +112,16 @@ function createJsonDataPOST(s3Key, validateItem = null) {
/**
* Создать роуты для JSON данных (GET + POST)
* @param {string} s3Key - Ключ в S3
* @param {Function} validateItem - Функция валидации
* @param {Object} options - Опции
* @param {boolean} options.singleObject - Если true, работает с объектом вместо массива
* @param {any} options.defaultValue - Значение по умолчанию
*/
function createJsonDataRoutes(s3Key, validateItem = null) {
function createJsonDataRoutes(s3Key, validateItem = null, options = {}) {
return {
get: createJsonDataGET(s3Key),
post: createJsonDataPOST(s3Key, validateItem)
get: createJsonDataGET(s3Key, options),
post: createJsonDataPOST(s3Key, validateItem, options)
};
}
+24
View File
@@ -393,6 +393,30 @@ const simpleFiltersRoutes = createJsonDataRoutes('filter-manager/simple-filters.
app.get('/api/simple-filters', simpleFiltersRoutes.get);
app.post('/api/simple-filters', simpleFiltersRoutes.post);
// Network Config (справочник IP, интерфейсов и gateway)
const networkConfigRoutes = createJsonDataRoutes('network-config.json', (config) => {
// Валидация структуры конфига
if (typeof config !== 'object' || config === null) {
return 'Network config must be an object';
}
// Валидация gateways
if (config.gateways && !Array.isArray(config.gateways)) {
return 'gateways must be an array';
}
// Валидация tunnelInterfaces
if (config.tunnelInterfaces && !Array.isArray(config.tunnelInterfaces)) {
return 'tunnelInterfaces must be an array';
}
// Валидация ipPools
if (config.ipPools && !Array.isArray(config.ipPools)) {
return 'ipPools must be an array';
}
return null;
}, { singleObject: true });
app.get('/api/network-config', networkConfigRoutes.get);
app.post('/api/network-config', networkConfigRoutes.post);
// === COMMUNITIES ROUTES ===
app.get('/api/communities', communitiesRoutes.getCommunities);
app.post('/api/communities', writeLimiter, communitiesRoutes.postCommunities);