diff --git a/backend/routes/mikrotikConfigRoutes.js b/backend/routes/mikrotikConfigRoutes.js index f5b2948..9d4bb29 100644 --- a/backend/routes/mikrotikConfigRoutes.js +++ b/backend/routes/mikrotikConfigRoutes.js @@ -228,6 +228,8 @@ async function testMikrotikConnection(req, res) { } } +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 } @@ -235,6 +237,7 @@ async function testMikrotikConnection(req, res) { * dryRun=true — только показать план, не выполнять. */ async function applyMikrotikConfig(req, res) { + let conn = null; try { const { serverId, type = 'all', dryRun = true } = req.body || {}; if (!serverId) { @@ -279,47 +282,55 @@ async function applyMikrotikConfig(req, res) { 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, + const runApply = async () => { + conn = new RouterOSAPI({ + host: String(host), + user: String(user), + password: String(password), + port: Number(port) || 8728, }); - } - - 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'), + try { + 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(); + 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'), + }; + return { ok: true, dryRun: !!dryRun, summary, results: allResults }; + } catch (err) { + if (conn) try { conn.close(); } catch (_) {} + conn = null; + throw err; + } }; - res.json({ - ok: true, - dryRun: !!dryRun, - summary, - results: allResults, + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => reject(new Error('Таймаут: MikroTik недоступен или операция заняла слишком много времени. Проверьте хост, порт и доступность роутера.')), APPLY_TIMEOUT_MS); }); + + const result = await Promise.race([runApply(), timeoutPromise]); + return res.json(result); } catch (error) { + if (conn) try { conn.close(); } catch (_) {} 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, + error: msg.includes('ECONNREFUSED') ? 'Соединение отклонено. Проверьте хост и порт.' : + msg.includes('ETIMEDOUT') || msg.includes('Таймаут') ? msg : + msg.includes('Authentication') || msg.includes('login') ? 'Неверный логин или пароль' : msg, }); } }