chore(mikrotikConfigRoutes): remove deprecated MikroTik configuration routes file
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m49s

This commit is contained in:
2026-02-03 21:02:19 +07:00
parent 76294ab2c9
commit e7c527f787
2 changed files with 1 additions and 421 deletions
-420
View File
@@ -1,420 +0,0 @@
/**
* Роуты для генерации конфигурации MikroTik (интерфейсы, маршруты)
* Поддерживает форматы: text (как раньше) и json (структурированные операции для API)
*/
const { GetObjectCommand } = require('@aws-sdk/client-s3');
const { s3, BUCKET_NAME, streamToString } = require('../services/s3Service');
const { sendError } = require('../middleware/errorHandler');
const { decrypt } = require('../utils/encryption');
const {
buildMikrotikInterfaceBlocks,
buildMikrotikRecursiveRoutes,
buildMikrotikConfig,
} = require('../utils/mikrotikInterfaceGenerator');
const { readS3TextObject } = require('../services/s3Service');
const { RouterOSAPI } = require('node-routeros');
const { applyBlock } = require('../services/mikrotikApplyService');
async function fetchJsonFromS3(key, defaultValue = null) {
try {
const data = await s3.send(new GetObjectCommand({ Bucket: BUCKET_NAME, Key: key }));
const body = await streamToString(data.Body);
const parsed = JSON.parse(body || 'null');
return parsed ?? defaultValue;
} catch (e) {
if (e?.name === 'NoSuchKey' || e?.$metadata?.httpStatusCode === 404) {
return defaultValue;
}
throw e;
}
}
async function fetchIpsecPasswordMap(passwordIds) {
const ids = [...new Set((passwordIds || []).filter(Boolean))];
if (ids.length === 0) return {};
const data = await readS3TextObject('network-config/ipsec-passwords.json').catch(() => ({ body: '[]' }));
let passwords = [];
try {
passwords = JSON.parse(data.body || '[]');
if (!Array.isArray(passwords)) passwords = [];
} catch {
passwords = [];
}
const map = {};
for (const p of passwords) {
if (ids.includes(p.id) && p.encryptedPassword) {
try {
map[p.id] = decrypt(p.encryptedPassword) || '';
} catch {
map[p.id] = '';
}
}
}
return map;
}
/**
* POST /api/mikrotik/generate
* Body: { format?: 'text'|'json', serverId?: string, type?: 'interfaces'|'recursive'|'all', config?: object, servers?: array }
* Если config/servers не переданы — загружаются из S3
*/
async function generateMikrotikConfig(req, res) {
try {
const { format = 'text', serverId, type = 'all', config: bodyConfig, servers: bodyServers } = req.body || {};
if (!['text', 'json'].includes(format)) {
return sendError(res, 400, 'format must be "text" or "json"', 'E_BAD_REQUEST');
}
let config = bodyConfig;
let servers = bodyServers;
if (!config) {
config = await fetchJsonFromS3('network-config.json', { gateways: [], tunnelInterfaces: [], ipPools: [] });
if (!config || typeof config !== 'object') config = { gateways: [], tunnelInterfaces: [], ipPools: [] };
}
if (!Array.isArray(servers)) {
servers = await fetchJsonFromS3('servers.json', []);
if (!Array.isArray(servers)) servers = [];
}
const passwordIds = (config.tunnelInterfaces || [])
.filter(i => i.ipsecPasswordId && String(i.ipsecPasswordId).trim() !== '')
.map(i => i.ipsecPasswordId.trim());
const passwordMap = await fetchIpsecPasswordMap(passwordIds);
const options = { format, serverId };
const includeInterfaces = type === 'interfaces' || type === 'all';
const includeRecursive = type === 'recursive' || type === 'all';
const blocks = await buildMikrotikConfig(config, servers, passwordMap, {
...options,
includeInterfaces,
includeRecursive,
});
res.json({ blocks });
} catch (error) {
console.error('Error generating MikroTik config:', error);
return sendError(res, 500, error.message || 'Error generating configuration', 'E_SERVER');
}
}
/**
* GET /api/mikrotik/generate-interfaces?format=text|json&serverId=...
* Генерация только интерфейсов
*/
async function generateInterfaces(req, res) {
try {
const format = (req.query.format || 'text').toLowerCase();
const serverId = req.query.serverId || null;
if (!['text', 'json'].includes(format)) {
return sendError(res, 400, 'format must be "text" or "json"', 'E_BAD_REQUEST');
}
const config = await fetchJsonFromS3('network-config.json', { gateways: [], tunnelInterfaces: [] });
const servers = await fetchJsonFromS3('servers.json', []);
const passwordIds = (config.tunnelInterfaces || [])
.filter(i => i.ipsecPasswordId && String(i.ipsecPasswordId).trim() !== '')
.map(i => i.ipsecPasswordId.trim());
const passwordMap = await fetchIpsecPasswordMap(passwordIds);
const blocks = await buildMikrotikInterfaceBlocks(config, servers, passwordMap, {
format,
serverId: serverId || undefined,
});
res.json({ blocks });
} catch (error) {
console.error('Error generating interfaces:', error);
return sendError(res, 500, error.message || 'Error generating interfaces', 'E_SERVER');
}
}
/**
* GET /api/mikrotik/generate-recursive-routes?format=text|json&serverId=...
* Генерация только рекурсивных маршрутов
*/
async function generateRecursiveRoutes(req, res) {
try {
const format = (req.query.format || 'text').toLowerCase();
const serverId = req.query.serverId || null;
if (!['text', 'json'].includes(format)) {
return sendError(res, 400, 'format must be "text" or "json"', 'E_BAD_REQUEST');
}
const config = await fetchJsonFromS3('network-config.json', { gateways: [], tunnelInterfaces: [] });
const servers = await fetchJsonFromS3('servers.json', []);
const blocks = await buildMikrotikRecursiveRoutes(config, servers, {
format,
serverId: serverId || undefined,
});
res.json({ blocks });
} catch (error) {
console.error('Error generating recursive routes:', error);
return sendError(res, 500, error.message || 'Error generating routes', 'E_SERVER');
}
}
/**
* POST /api/mikrotik/test-connection
* Body: { serverId?: string, host?, port?, user?, password? }
* Если host/port/user/password переданы — тестирует с ними (без сохранения).
* Иначе берёт credentials из сервера по serverId.
*/
async function testMikrotikConnection(req, res) {
try {
const { serverId, host: bodyHost, port: bodyPort, user: bodyUser, password: bodyPassword } = req.body || {};
let host, port, user, password;
if (bodyHost && bodyPassword) {
host = bodyHost;
port = parseInt(bodyPort || '8728', 10) || 8728;
user = bodyUser || 'admin';
password = bodyPassword;
} else if (serverId) {
const servers = await fetchJsonFromS3('servers.json', []);
const server = servers.find(s => s.id === serverId || s.dns === serverId || s.ip === serverId);
if (!server) {
return sendError(res, 404, 'Server not found', 'E_NOT_FOUND');
}
if (server.type !== 'jumphost') {
return sendError(res, 400, 'Only jumphost servers have MikroTik credentials', 'E_BAD_REQUEST');
}
if (!server.encryptedMikrotikPassword) {
return sendError(res, 400, 'MikroTik password not configured for this server', 'E_BAD_REQUEST');
}
try {
password = decrypt(server.encryptedMikrotikPassword);
} catch (decErr) {
return sendError(res, 500, 'Failed to decrypt MikroTik password', 'E_DECRYPT');
}
host = server.mikrotikHost || server.ip || server.dns;
port = parseInt(server.mikrotikPort || '8728', 10) || 8728;
user = server.mikrotikUser || 'admin';
} else {
return sendError(res, 400, 'Provide serverId or (host + password)', 'E_BAD_REQUEST');
}
const conn = new RouterOSAPI({
host: String(host),
user: String(user),
password: String(password),
port: Number(port) || 8728,
});
await conn.connect();
await conn.write('/system/resource/print', []);
conn.close();
res.json({ ok: true, message: 'Соединение успешно' });
} catch (error) {
const msg = error.message || String(error);
res.status(400).json({
ok: false,
message: msg.includes('ECONNREFUSED') ? 'Соединение отклонено. Проверьте хост и порт.' :
msg.includes('Authentication') || msg.includes('login') ? 'Неверный логин или пароль' :
msg,
});
}
}
const APPLY_TIMEOUT_MS = Number(process.env.MIKROTIK_APPLY_TIMEOUT_MS) || 60000;
/**
* POST /api/mikrotik/apply
* Body: { serverId: string, type?: 'interfaces'|'recursive'|'all', dryRun?: boolean }
* Применяет конфигурацию (интерфейсы, маршруты) на MikroTik через RouterOS API.
* dryRun=true — только показать план, не выполнять.
*/
async function applyMikrotikConfig(req, res) {
let conn = null;
const mikrotikRequests = [];
const body = req.body || {};
const streamLogs = req.headers['x-stream-logs'] === '1' || req.headers['x-stream-logs'] === 'true';
const log = (msg, data = {}) => {
console.log('[MikroTik apply]', msg, data);
if (streamLogs && res.write) {
try {
res.write(JSON.stringify({ t: 'log', msg, ...data }) + '\n');
} catch (_) {}
}
};
const sendSafe = (status, data) => {
try {
if (streamLogs && res.write) {
res.write(JSON.stringify({ t: 'result', ok: status < 400, ...data }) + '\n');
res.end();
} else if (!res.headersSent) {
res.status(status).json(data);
}
} catch (e) {
console.error('[MikroTik apply] sendSafe failed:', e);
}
};
const sendErrorSafe = (status, data) => {
try {
if (streamLogs && res.write) {
res.write(JSON.stringify({ t: 'error', ok: false, status, ...data }) + '\n');
res.end();
} else if (!res.headersSent) {
res.status(status).json(data);
}
} catch (e) {
console.error('[MikroTik apply] sendErrorSafe failed:', e);
}
};
try {
if (streamLogs) {
res.setHeader('Content-Type', 'application/x-ndjson');
res.setHeader('Transfer-Encoding', 'chunked');
res.status(200);
}
log('Request body', { serverId: body.serverId, type: body.type, dryRun: body.dryRun });
const { serverId, type = 'all', dryRun = true } = body;
if (!serverId) {
sendErrorSafe(400, { error: 'serverId is required' });
return;
}
log('Fetching servers from S3');
const servers = await fetchJsonFromS3('servers.json', []);
const server = servers.find(s => s.id === serverId || s.dns === serverId || s.ip === serverId);
if (!server) {
sendErrorSafe(404, { error: 'Server not found' });
return;
}
if (server.type !== 'jumphost') {
sendErrorSafe(400, { error: 'Only jumphost servers support apply via API' });
return;
}
if (!server.encryptedMikrotikPassword) {
sendErrorSafe(400, { error: 'MikroTik password not configured. Add credentials in Server settings.' });
return;
}
log('Decrypting password');
let password;
try {
password = decrypt(server.encryptedMikrotikPassword);
} catch (decErr) {
sendErrorSafe(500, { error: 'Failed to decrypt MikroTik password' });
return;
}
const host = server.mikrotikHost || server.ip || server.dns;
const port = parseInt(server.mikrotikPort || '8728', 10) || 8728;
const user = server.mikrotikUser || 'admin';
log('Target MikroTik', { host, port, user });
log('Fetching network config from S3');
const config = await fetchJsonFromS3('network-config.json', { gateways: [], tunnelInterfaces: [] });
const passwordIds = (config.tunnelInterfaces || [])
.filter(i => i.ipsecPasswordId && String(i.ipsecPasswordId).trim() !== '')
.map(i => i.ipsecPasswordId.trim());
log('Fetching IPSec passwords', { count: passwordIds.length });
const passwordMap = await fetchIpsecPasswordMap(passwordIds);
const includeInterfaces = type === 'interfaces' || type === 'all';
const includeRecursive = type === 'recursive' || type === 'all';
log('Building MikroTik config blocks', { includeInterfaces, includeRecursive });
const blocks = await buildMikrotikConfig(config, servers, passwordMap, {
format: 'json',
serverId,
includeInterfaces,
includeRecursive,
});
log('Blocks built', { count: blocks.length });
const runApply = async () => {
conn = new RouterOSAPI({
host: String(host),
user: String(user),
password: String(password),
port: Number(port) || 8728,
});
conn.on('error', (e) => log('RouterOS API error', { error: e?.message || String(e), errno: e?.errno }));
conn.on('close', () => log('RouterOS API connection closed'));
try {
log('Connecting to MikroTik', { host: host + ':' + port });
await conn.connect();
if (conn.connector) {
conn.connector.on('error', (e) => log('RouterOS connector error', { error: e?.message || String(e) }));
conn.connector.on('timeout', () => log('RouterOS connector timeout'));
}
log('Connected');
const originalWrite = conn.write.bind(conn);
conn.write = async function (path, args) {
const req = { path, args: args || [] };
mikrotikRequests.push(req);
log('MikroTik API', { path, args: req.args });
return originalWrite(path, args);
};
const allResults = [];
for (const block of blocks) {
log('Applying block', { blockType: block.type, serverName: block.serverName });
const blockResults = await applyBlock(conn, block, !!dryRun);
allResults.push({
blockType: block.type,
serverName: block.serverName,
results: blockResults,
});
}
conn.close();
conn = null;
const summary = {
created: allResults.flatMap(b => b.results).filter(r => r.status === 'created' || r.status === 'would_create').length,
updated: allResults.flatMap(b => b.results).filter(r => r.status === 'updated' || r.status === 'would_update').length,
skipped: allResults.flatMap(b => b.results).filter(r => r.status === 'skip').length,
errors: allResults.flatMap(b => b.results).filter(r => r.status === 'error'),
};
log('Done', { summary });
return { ok: true, dryRun: !!dryRun, summary, results: allResults, mikrotikRequests };
} catch (err) {
if (conn) try { conn.close(); } catch (_) {}
conn = null;
throw err;
}
};
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Таймаут: MikroTik недоступен или операция заняла слишком много времени. Проверьте хост, порт и доступность роутера.')), APPLY_TIMEOUT_MS);
});
const result = await Promise.race([runApply(), timeoutPromise]);
return sendSafe(200, result);
} catch (error) {
if (conn) try { conn.close(); } catch (_) {}
const msg = error.message || String(error);
log('Error', { error: msg });
const errPayload = {
ok: false,
error: msg.includes('ECONNREFUSED') ? 'Соединение отклонено. Проверьте хост и порт.' :
msg.includes('ETIMEDOUT') || msg.includes('Таймаут') ? msg :
msg.includes('Authentication') || msg.includes('login') ? 'Неверный логин или пароль' : msg,
};
if (mikrotikRequests.length > 0) errPayload.mikrotikRequests = mikrotikRequests;
sendErrorSafe(500, errPayload);
}
}
module.exports = {
generateMikrotikConfig,
generateInterfaces,
generateRecursiveRoutes,
testMikrotikConnection,
applyMikrotikConfig,
};
+1 -1
View File
@@ -2242,7 +2242,7 @@ function NetworkConfigManager() {
body: JSON.stringify(requestBody),
});
const contentType = response.headers.get('Content-Type') || '';
const isStream = streamLogs && (contentType.includes('ndjson') || contentType.includes('x-ndjson');
const isStream = streamLogs && (contentType.includes('ndjson') || contentType.includes('x-ndjson'));
let data = {};
if (isStream && response.body) {