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
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m27s
This commit is contained in:
@@ -9,8 +9,15 @@ const { streamToString } = require('../services/s3Service');
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Создать GET эндпоинт для JSON данных
|
* Создать 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) => {
|
return async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const head = await s3.send(new HeadObjectCommand({ Bucket: BUCKET_NAME, Key: s3Key })).catch(() => null);
|
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 data = await s3.send(new GetObjectCommand({ Bucket: BUCKET_NAME, Key: s3Key }));
|
||||||
const fileContent = await streamToString(data.Body);
|
const fileContent = await streamToString(data.Body);
|
||||||
let items = [];
|
let items = fallback;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
items = JSON.parse(fileContent);
|
const parsed = JSON.parse(fileContent);
|
||||||
if (!Array.isArray(items)) {
|
if (singleObject) {
|
||||||
items = [];
|
// Для одиночного объекта
|
||||||
|
items = (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) ? parsed : fallback;
|
||||||
|
} else {
|
||||||
|
// Для массива
|
||||||
|
items = Array.isArray(parsed) ? parsed : fallback;
|
||||||
}
|
}
|
||||||
} catch (parseError) {
|
} catch (parseError) {
|
||||||
console.error(`Error parsing ${s3Key}:`, parseError);
|
console.error(`Error parsing ${s3Key}:`, parseError);
|
||||||
items = [];
|
items = fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json(items);
|
res.json(items);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error?.code === 'NoSuchKey' || error?.$metadata?.httpStatusCode === 404) {
|
if (error?.code === 'NoSuchKey' || error?.$metadata?.httpStatusCode === 404) {
|
||||||
res.json([]);
|
res.json(fallback);
|
||||||
} else {
|
} else {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
return sendError(res, 500, 'Error reading from S3', 'E_S3');
|
return sendError(res, 500, 'Error reading from S3', 'E_S3');
|
||||||
@@ -48,23 +59,45 @@ function createJsonDataGET(s3Key) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Создать POST эндпоинт для JSON данных
|
* Создать 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) => {
|
return async (req, res) => {
|
||||||
const { domains: items } = req.body; // Используем 'domains' для обратной совместимости
|
const { domains: items } = req.body; // Используем 'domains' для обратной совместимости
|
||||||
|
|
||||||
if (!Array.isArray(items)) {
|
if (singleObject) {
|
||||||
return sendError(res, 400, 'Data must be an array', 'E_BAD_REQUEST');
|
// Для одиночного объекта
|
||||||
}
|
if (typeof items !== 'object' || items === null || Array.isArray(items)) {
|
||||||
|
return sendError(res, 400, 'Data must be an object', 'E_BAD_REQUEST');
|
||||||
|
}
|
||||||
|
|
||||||
// Валидация элементов, если предоставлена
|
// Валидация объекта целиком
|
||||||
if (validateItem) {
|
if (validateItem) {
|
||||||
for (let i = 0; i < items.length; i++) {
|
const error = validateItem(items);
|
||||||
const error = validateItem(items[i], i);
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return sendError(res, 400, error, 'E_SCHEMA');
|
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 {
|
try {
|
||||||
@@ -79,11 +112,16 @@ function createJsonDataPOST(s3Key, validateItem = null) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Создать роуты для JSON данных (GET + POST)
|
* Создать роуты для 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 {
|
return {
|
||||||
get: createJsonDataGET(s3Key),
|
get: createJsonDataGET(s3Key, options),
|
||||||
post: createJsonDataPOST(s3Key, validateItem)
|
post: createJsonDataPOST(s3Key, validateItem, options)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -393,6 +393,30 @@ const simpleFiltersRoutes = createJsonDataRoutes('filter-manager/simple-filters.
|
|||||||
app.get('/api/simple-filters', simpleFiltersRoutes.get);
|
app.get('/api/simple-filters', simpleFiltersRoutes.get);
|
||||||
app.post('/api/simple-filters', simpleFiltersRoutes.post);
|
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 ===
|
// === COMMUNITIES ROUTES ===
|
||||||
app.get('/api/communities', communitiesRoutes.getCommunities);
|
app.get('/api/communities', communitiesRoutes.getCommunities);
|
||||||
app.post('/api/communities', writeLimiter, communitiesRoutes.postCommunities);
|
app.post('/api/communities', writeLimiter, communitiesRoutes.postCommunities);
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
{
|
||||||
|
"gateways": [
|
||||||
|
{
|
||||||
|
"id": "gw-cloudflare-swe",
|
||||||
|
"name": "SWE-IHOR",
|
||||||
|
"ip": "94.142.140.1",
|
||||||
|
"provider": "cloudflare",
|
||||||
|
"country": "SWE",
|
||||||
|
"description": "Cloudflare Sweden - основной выход"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "gw-bunny-swe",
|
||||||
|
"name": "SWE-BUNNY",
|
||||||
|
"ip": "94.142.140.2",
|
||||||
|
"provider": "bunny",
|
||||||
|
"country": "SWE",
|
||||||
|
"description": "Bunny CDN Sweden"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "gw-telegram-1",
|
||||||
|
"name": "TG-PROXY-1",
|
||||||
|
"ip": "149.154.167.50",
|
||||||
|
"provider": "telegram",
|
||||||
|
"country": "NL",
|
||||||
|
"description": "Telegram DC2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "gw-hetzner-de",
|
||||||
|
"name": "DE-HETZNER",
|
||||||
|
"ip": "195.100.30.21",
|
||||||
|
"provider": "hetzner",
|
||||||
|
"country": "DE",
|
||||||
|
"description": "Hetzner Frankfurt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "gw-fastly-us",
|
||||||
|
"name": "US-FASTLY",
|
||||||
|
"ip": "151.101.1.1",
|
||||||
|
"provider": "fastly",
|
||||||
|
"country": "US",
|
||||||
|
"description": "Fastly US East"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tunnelInterfaces": [
|
||||||
|
{
|
||||||
|
"id": "if-gre-swe-de",
|
||||||
|
"name": "gre-swe-de",
|
||||||
|
"type": "GRE",
|
||||||
|
"localIp": "10.10.0.1",
|
||||||
|
"remoteIp": "10.10.0.2",
|
||||||
|
"port": "",
|
||||||
|
"serverId": "SWE-HIPHOST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "if-wg-swe-nl",
|
||||||
|
"name": "wg0",
|
||||||
|
"type": "WireGuard",
|
||||||
|
"localIp": "10.20.0.1",
|
||||||
|
"remoteIp": "10.20.0.2",
|
||||||
|
"port": "51820",
|
||||||
|
"serverId": "SWE-HIPHOST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "if-gre-de-us",
|
||||||
|
"name": "gre-de-us",
|
||||||
|
"type": "GRE",
|
||||||
|
"localIp": "10.10.1.1",
|
||||||
|
"remoteIp": "10.10.1.2",
|
||||||
|
"port": "",
|
||||||
|
"serverId": "DE-FRANKFURT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "if-ipsec-nl-jp",
|
||||||
|
"name": "ipsec-nl-jp",
|
||||||
|
"type": "IPSec",
|
||||||
|
"localIp": "10.30.0.1",
|
||||||
|
"remoteIp": "10.30.0.2",
|
||||||
|
"port": "",
|
||||||
|
"serverId": "NL-AMSTERDAM"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ipPools": [
|
||||||
|
{
|
||||||
|
"id": "pool-gre",
|
||||||
|
"name": "GRE Tunnels",
|
||||||
|
"cidr": "10.10.0.0/16",
|
||||||
|
"description": "Пул для GRE туннелей между серверами"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pool-wg",
|
||||||
|
"name": "WireGuard",
|
||||||
|
"cidr": "10.20.0.0/16",
|
||||||
|
"description": "Пул для WireGuard интерфейсов"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pool-ipsec",
|
||||||
|
"name": "IPSec VPN",
|
||||||
|
"cidr": "10.30.0.0/16",
|
||||||
|
"description": "Пул для IPSec туннелей"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"defaults": {
|
||||||
|
"baseUrl": "https://functions.yandexcloud.net/d4eno3im0qgsr4tj5hdo",
|
||||||
|
"type": "routes",
|
||||||
|
"version": "v4.rsc"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,6 +31,7 @@ import ASNsNewManager from './ASNsNewManager';
|
|||||||
import AutoUrlManager from './AutoUrlManager';
|
import AutoUrlManager from './AutoUrlManager';
|
||||||
import BillingManager from './BillingManager';
|
import BillingManager from './BillingManager';
|
||||||
import CommunitiesManager from './CommunitiesManager';
|
import CommunitiesManager from './CommunitiesManager';
|
||||||
|
import NetworkConfigManager from './NetworkConfigManager';
|
||||||
import Dashboard from './Dashboard';
|
import Dashboard from './Dashboard';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
import { NotifyProvider } from './components/NotifyProvider.jsx';
|
import { NotifyProvider } from './components/NotifyProvider.jsx';
|
||||||
@@ -51,14 +52,14 @@ function LanguageProvider({ children }) {
|
|||||||
home: 'Главная', data: 'Данные', management: 'Управление', tools: 'Инструменты',
|
home: 'Главная', data: 'Данные', management: 'Управление', tools: 'Инструменты',
|
||||||
dashboard: 'Панель', domains: 'Домены', ipRanges: 'IP-диапазоны', asns: 'AS',
|
dashboard: 'Панель', domains: 'Домены', ipRanges: 'IP-диапазоны', asns: 'AS',
|
||||||
communities: 'Community', servers: 'Серверы', filters: 'Фильтры', billing: 'Биллинг', autoUrls: 'Авто URL',
|
communities: 'Community', servers: 'Серверы', filters: 'Фильтры', billing: 'Биллинг', autoUrls: 'Авто URL',
|
||||||
easySwitch: 'Easy Switch',
|
easySwitch: 'Easy Switch', networkConfig: 'Сетевые настройки',
|
||||||
light: 'Светлая', dark: 'Тёмная'
|
light: 'Светлая', dark: 'Тёмная'
|
||||||
},
|
},
|
||||||
en: {
|
en: {
|
||||||
home: 'Home', data: 'Data', management: 'Management', tools: 'Tools',
|
home: 'Home', data: 'Data', management: 'Management', tools: 'Tools',
|
||||||
dashboard: 'Dashboard', domains: 'Domains', ipRanges: 'IP Ranges', asns: 'ASNs',
|
dashboard: 'Dashboard', domains: 'Domains', ipRanges: 'IP Ranges', asns: 'ASNs',
|
||||||
communities: 'Communities', servers: 'Servers', filters: 'Filters', billing: 'Billing', autoUrls: 'Auto URLs',
|
communities: 'Communities', servers: 'Servers', filters: 'Filters', billing: 'Billing', autoUrls: 'Auto URLs',
|
||||||
easySwitch: 'Easy Switch',
|
easySwitch: 'Easy Switch', networkConfig: 'Network Config',
|
||||||
light: 'Light', dark: 'Dark'
|
light: 'Light', dark: 'Dark'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -190,6 +191,7 @@ function MainLayout() {
|
|||||||
items: [
|
items: [
|
||||||
{ id: 'servers', title: t('servers'), path: '/servers', icon: IconServer },
|
{ id: 'servers', title: t('servers'), path: '/servers', icon: IconServer },
|
||||||
{ id: 'filters', title: t('filters'), path: '/filters', icon: IconFilter },
|
{ id: 'filters', title: t('filters'), path: '/filters', icon: IconFilter },
|
||||||
|
{ id: 'network-config', title: t('networkConfig'), path: '/network-config', icon: IconNetwork },
|
||||||
{ id: 'easy-switch', title: t('easySwitch'), path: '/easy-switch', icon: IconArrowsExchange },
|
{ id: 'easy-switch', title: t('easySwitch'), path: '/easy-switch', icon: IconArrowsExchange },
|
||||||
{ id: 'billing', title: t('billing'), path: '/billing', icon: IconCreditCard }
|
{ id: 'billing', title: t('billing'), path: '/billing', icon: IconCreditCard }
|
||||||
]
|
]
|
||||||
@@ -397,6 +399,7 @@ function MainLayout() {
|
|||||||
<Route path="/servers" element={<ServerManager />} />
|
<Route path="/servers" element={<ServerManager />} />
|
||||||
<Route path="/billing" element={<BillingManager />} />
|
<Route path="/billing" element={<BillingManager />} />
|
||||||
<Route path="/filters" element={<FilterManager />} />
|
<Route path="/filters" element={<FilterManager />} />
|
||||||
|
<Route path="/network-config" element={<NetworkConfigManager />} />
|
||||||
<Route path="/easy-switch" element={<EasySwitchManager />} />
|
<Route path="/easy-switch" element={<EasySwitchManager />} />
|
||||||
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user