feat(MikrotikConfig): add API endpoint for applying MikroTik configuration; update frontend to support configuration application with dry-run option and modal interface
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m56s

This commit is contained in:
2026-02-03 19:26:49 +07:00
parent 1a27217676
commit 94314c383b
5 changed files with 523 additions and 12 deletions
+98
View File
@@ -14,6 +14,7 @@ const {
} = 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 {
@@ -227,9 +228,106 @@ async function testMikrotikConnection(req, res) {
}
}
/**
* POST /api/mikrotik/apply
* Body: { serverId: string, type?: 'interfaces'|'recursive'|'all', dryRun?: boolean }
* Применяет конфигурацию (интерфейсы, маршруты) на MikroTik через RouterOS API.
* dryRun=true — только показать план, не выполнять.
*/
async function applyMikrotikConfig(req, res) {
try {
const { serverId, type = 'all', dryRun = true } = req.body || {};
if (!serverId) {
return sendError(res, 400, 'serverId is required', 'E_BAD_REQUEST');
}
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 support apply via API', 'E_BAD_REQUEST');
}
if (!server.encryptedMikrotikPassword) {
return sendError(res, 400, 'MikroTik password not configured. Add credentials in Server settings.', 'E_BAD_REQUEST');
}
let password;
try {
password = decrypt(server.encryptedMikrotikPassword);
} catch (decErr) {
return sendError(res, 500, 'Failed to decrypt MikroTik password', 'E_DECRYPT');
}
const host = server.mikrotikHost || server.ip || server.dns;
const port = parseInt(server.mikrotikPort || '8728', 10) || 8728;
const user = server.mikrotikUser || 'admin';
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());
const passwordMap = await fetchIpsecPasswordMap(passwordIds);
const includeInterfaces = type === 'interfaces' || type === 'all';
const includeRecursive = type === 'recursive' || type === 'all';
const blocks = await buildMikrotikConfig(config, servers, passwordMap, {
format: 'json',
serverId,
includeInterfaces,
includeRecursive,
});
const conn = new RouterOSAPI({
host: String(host),
user: String(user),
password: String(password),
port: Number(port) || 8728,
});
await conn.connect();
const allResults = [];
for (const block of blocks) {
const blockResults = await applyBlock(conn, block, !!dryRun);
allResults.push({
blockType: block.type,
serverName: block.serverName,
results: blockResults,
});
}
conn.close();
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'),
};
res.json({
ok: true,
dryRun: !!dryRun,
summary,
results: allResults,
});
} catch (error) {
console.error('Error applying MikroTik config:', error);
const msg = error.message || String(error);
return res.status(500).json({
ok: false,
error: msg.includes('ECONNREFUSED') ? 'Соединение отклонено' :
msg.includes('Authentication') || msg.includes('login') ? 'Неверный логин или пароль' : msg,
});
}
}
module.exports = {
generateMikrotikConfig,
generateInterfaces,
generateRecursiveRoutes,
testMikrotikConnection,
applyMikrotikConfig,
};