实现连接配置的导入/导出功能

This commit is contained in:
Baobhan Sith
2025-04-15 08:27:42 +08:00
parent fa27d40eb2
commit b58f5da52b
8 changed files with 762 additions and 28 deletions
+204 -21
View File
@@ -745,7 +745,6 @@
"version": "1.19.5",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
"integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/connect": "*",
@@ -756,7 +755,6 @@
"version": "3.4.38",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
"integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
@@ -782,7 +780,6 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.1.tgz",
"integrity": "sha512-UZUw8vjpWFXuDnjFTh7/5c2TWDlQqeXHi6hcN7F2XSVT5P+WmUnnbFS3KA6Jnc6IsEqI2qCVu2bK0R0J4A8ZQQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/body-parser": "*",
@@ -794,7 +791,6 @@
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz",
"integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*",
@@ -817,21 +813,27 @@
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
"integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/mime": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/multer": {
"version": "1.4.12",
"resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.12.tgz",
"integrity": "sha512-pQ2hoqvXiJt2FP9WQVLPRO+AmiIm/ZYkavPlIQnx282u4ZrVdztx0pkh3jjpQt0Kz+YI0YhSG264y08UJKoUQg==",
"license": "MIT",
"dependencies": {
"@types/express": "*"
}
},
"node_modules/@types/node": {
"version": "20.17.30",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.30.tgz",
"integrity": "sha512-7zf4YyHA+jvBNfVrk2Gtvs6x7E8V+YDW05bNfG2XkWDJfYRXrTiP/DsB2zSYTaHX0bGIujTBQdMVAhb+j7mwpg==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.19.2"
@@ -841,21 +843,18 @@
"version": "6.9.18",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz",
"integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/range-parser": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/send": {
"version": "0.17.4",
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz",
"integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/mime": "^1",
@@ -866,7 +865,6 @@
"version": "1.15.7",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz",
"integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/http-errors": "*",
@@ -1271,6 +1269,12 @@
"node": ">= 8"
}
},
"node_modules/append-field": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
"license": "MIT"
},
"node_modules/aproba": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
@@ -1485,7 +1489,6 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true,
"license": "MIT"
},
"node_modules/buildcheck": {
@@ -1497,6 +1500,17 @@
"node": ">=10.0.0"
}
},
"node_modules/busboy": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
"dependencies": {
"streamsearch": "^1.1.0"
},
"engines": {
"node": ">=10.16.0"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@@ -1725,6 +1739,51 @@
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"license": "MIT"
},
"node_modules/concat-stream": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
"integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
"engines": [
"node >= 0.8"
],
"license": "MIT",
"dependencies": {
"buffer-from": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^2.2.2",
"typedarray": "^0.0.6"
}
},
"node_modules/concat-stream/node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/concat-stream/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/concat-stream/node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/confbox": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz",
@@ -1811,6 +1870,12 @@
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"license": "MIT"
},
"node_modules/cpu-features": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz",
@@ -2824,7 +2889,6 @@
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",
"integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==",
"license": "MIT",
"optional": true,
"dependencies": {
"jsbn": "1.1.0",
"sprintf-js": "^1.1.3"
@@ -2935,6 +2999,12 @@
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"license": "MIT"
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -2961,8 +3031,7 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
"integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==",
"license": "MIT",
"optional": true
"license": "MIT"
},
"node_modules/klona": {
"version": "2.0.6",
@@ -3360,6 +3429,79 @@
"dev": true,
"license": "MIT"
},
"node_modules/multer": {
"version": "1.4.5-lts.2",
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz",
"integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==",
"license": "MIT",
"dependencies": {
"append-field": "^1.0.0",
"busboy": "^1.0.0",
"concat-stream": "^1.5.2",
"mkdirp": "^0.5.4",
"object-assign": "^4.1.1",
"type-is": "^1.6.4",
"xtend": "^4.0.0"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/multer/node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/multer/node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/multer/node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/multer/node_modules/mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"license": "MIT",
"dependencies": {
"minimist": "^1.2.6"
},
"bin": {
"mkdirp": "bin/cmd.js"
}
},
"node_modules/multer/node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"license": "MIT",
"dependencies": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/nan": {
"version": "2.22.2",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.22.2.tgz",
@@ -3862,6 +4004,12 @@
"node": ">=10"
}
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"license": "MIT"
},
"node_modules/promise-inflight": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz",
@@ -4398,7 +4546,6 @@
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">= 6.0.0",
"npm": ">= 3.0.0"
@@ -4409,7 +4556,6 @@
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz",
"integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==",
"license": "MIT",
"optional": true,
"dependencies": {
"ip-address": "^9.0.5",
"smart-buffer": "^4.2.0"
@@ -4477,8 +4623,7 @@
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
"license": "BSD-3-Clause",
"optional": true
"license": "BSD-3-Clause"
},
"node_modules/sqlite3": {
"version": "5.1.7",
@@ -4555,6 +4700,14 @@
"integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==",
"license": "MIT"
},
"node_modules/streamsearch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
@@ -4915,6 +5068,12 @@
"node": ">= 0.6"
}
},
"node_modules/typedarray": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
"license": "MIT"
},
"node_modules/typescript": {
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
@@ -4972,7 +5131,6 @@
"version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
"dev": true,
"license": "MIT"
},
"node_modules/unicorn-magic": {
@@ -5394,7 +5552,6 @@
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.4"
@@ -5447,10 +5604,14 @@
"name": "@nexus-terminal/backend",
"version": "0.1.0",
"dependencies": {
"@types/multer": "^1.4.12",
"bcrypt": "^5.1.1",
"connect-sqlite3": "^0.9.15",
"express": "^5.1.0",
"express-session": "^1.18.1",
"https-proxy-agent": "^7.0.6",
"multer": "^1.4.5-lts.2",
"socks": "^2.8.4",
"sqlite3": "^5.1.7",
"ssh2": "^1.16.0",
"ws": "^8.18.1"
@@ -5468,6 +5629,28 @@
"typescript": "^5.0.0"
}
},
"packages/backend/node_modules/agent-base": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
"integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
"license": "MIT",
"engines": {
"node": ">= 14"
}
},
"packages/backend/node_modules/https-proxy-agent": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.2",
"debug": "4"
},
"engines": {
"node": ">= 14"
}
},
"packages/frontend": {
"name": "@nexus-terminal/frontend",
"version": "0.1.0",
+2
View File
@@ -9,11 +9,13 @@
"dev": "npx ts-node-dev --respawn --transpile-only src/index.ts"
},
"dependencies": {
"@types/multer": "^1.4.12",
"bcrypt": "^5.1.1",
"connect-sqlite3": "^0.9.15",
"express": "^5.1.0",
"express-session": "^1.18.1",
"https-proxy-agent": "^7.0.6",
"multer": "^1.4.5-lts.2",
"socks": "^2.8.4",
"sqlite3": "^5.1.7",
"ssh2": "^1.16.0",
@@ -746,7 +746,349 @@ export const testConnection = async (req: Request, res: Response): Promise<void>
}
} catch (error: any) {
console.error(`测试连接 ${connectionId} 时发生内部错误:`, error);
res.status(500).json({ success: false, message: error.message || '测试连接时发生内部服务器错误。' });
console.error(`测试连接 ${connectionId} 时发生内部错误:`, error);
res.status(500).json({ success: false, message: error.message || '测试连接时发生内部服务器错误。' });
}
};
// --- 新增:导出连接配置 ---
/**
* 导出所有连接配置 (GET /api/v1/connections/export)
*/
export const exportConnections = async (req: Request, res: Response): Promise<void> => {
const userId = req.session.userId; // 保留以备将来多用户
try {
// 1. 查询所有连接及其关联的代理信息
const connectionsWithProxies = await new Promise<any[]>((resolve, reject) => {
db.all(
`SELECT
c.id, c.name, c.host, c.port, c.username, c.auth_method,
c.encrypted_password, c.encrypted_private_key, c.encrypted_passphrase,
c.proxy_id,
p.id as proxy_db_id, p.name as proxy_name, p.type as proxy_type,
p.host as proxy_host, p.port as proxy_port, p.username as proxy_username,
-- p.auth_method as proxy_auth_method, -- Removed: Column likely doesn't exist based on error
p.encrypted_password as proxy_encrypted_password
-- p.encrypted_private_key as proxy_encrypted_private_key, -- Removed: Column likely doesn't exist
-- p.encrypted_passphrase as proxy_encrypted_passphrase -- Removed: Column likely doesn't exist
FROM connections c
LEFT JOIN proxies p ON c.proxy_id = p.id
ORDER BY c.name ASC`,
(err, rows: any[]) => {
if (err) {
console.error('查询连接和代理信息以供导出时出错:', err.message);
return reject(new Error('导出连接失败:查询连接信息出错'));
}
resolve(rows);
}
);
});
// 2. 查询所有连接的标签信息
const connectionTags = await new Promise<{[connId: number]: number[]}>((resolve, reject) => {
db.all('SELECT connection_id, tag_id FROM connection_tags', (err, rows: {connection_id: number, tag_id: number}[]) => {
if (err) {
console.error('查询连接标签以供导出时出错:', err.message);
return reject(new Error('导出连接失败:查询标签信息出错'));
}
const tagsMap: {[connId: number]: number[]} = {};
rows.forEach(row => {
if (!tagsMap[row.connection_id]) {
tagsMap[row.connection_id] = [];
}
tagsMap[row.connection_id].push(row.tag_id);
});
resolve(tagsMap);
});
});
// 3. 格式化数据以供导出
const formattedData = connectionsWithProxies.map(row => {
const connection: any = {
// 不导出 id,因为导入时需要重新创建
name: row.name,
host: row.host,
port: row.port,
username: row.username,
auth_method: row.auth_method,
encrypted_password: row.encrypted_password,
encrypted_private_key: row.encrypted_private_key,
encrypted_passphrase: row.encrypted_passphrase,
tag_ids: connectionTags[row.id] || [], // 从 map 中获取标签 ID
// 不导出 created_at, updated_at, last_connected_at
};
// 添加代理信息(如果存在)
if (row.proxy_db_id) {
connection.proxy = {
// 不导出代理的 id,因为导入时可能需要重新创建或匹配
name: row.proxy_name,
type: row.proxy_type,
host: row.proxy_host,
port: row.proxy_port,
username: row.proxy_username,
// auth_method: row.proxy_auth_method, // Removed
encrypted_password: row.proxy_encrypted_password
// encrypted_private_key: row.proxy_encrypted_private_key, // Removed
// encrypted_passphrase: row.proxy_encrypted_passphrase, // Removed
};
} else {
connection.proxy = null; // 明确设为 null
}
return connection;
});
// 4. 设置响应头,提示浏览器下载文件
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const filename = `nexus-terminal-connections-${timestamp}.json`;
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
res.setHeader('Content-Type', 'application/json');
// 发送 JSON 数据
res.status(200).json(formattedData);
} catch (error: any) {
console.error('导出连接时发生错误:', error);
res.status(500).json({ message: error.message || '导出连接时发生内部服务器错误。' });
}
};
// --- 新增:导入连接配置 ---
/**
* 导入连接配置 (POST /api/v1/connections/import)
*/
export const importConnections = async (req: Request, res: Response): Promise<void> => {
const userId = req.session.userId; // 保留以备将来多用户
if (!req.file) {
res.status(400).json({ message: '未找到上传的文件 (需要名为 "connectionsFile" 的文件)。' });
return;
}
let importedData: any[];
try {
const fileContent = req.file.buffer.toString('utf8');
importedData = JSON.parse(fileContent);
if (!Array.isArray(importedData)) {
throw new Error('JSON 文件内容必须是一个数组。');
}
} catch (error: any) {
res.status(400).json({ message: `解析 JSON 文件失败: ${error.message}` });
return;
}
let successCount = 0;
let failureCount = 0;
const errors: { connectionName?: string; message: string }[] = [];
const now = Math.floor(Date.now() / 1000);
// 准备数据库语句
const findProxyStmt = db.prepare(`SELECT id FROM proxies WHERE name = ? AND type = ? AND host = ? AND port = ?`);
// 恢复为文档定义的列
const insertProxyStmt = db.prepare(`INSERT INTO proxies (name, type, host, port, username, auth_method, encrypted_password, encrypted_private_key, encrypted_passphrase, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
const insertConnStmt = db.prepare(`INSERT INTO connections (name, host, port, username, auth_method, encrypted_password, encrypted_private_key, encrypted_passphrase, proxy_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
const insertTagStmt = db.prepare(`INSERT INTO connection_tags (connection_id, tag_id) VALUES (?, ?)`);
try { // Wrap the entire database operation in a try block
await new Promise<void>((resolveOuter, rejectOuter) => {
// 使用事务处理导入
db.serialize(() => { // Removed async here
db.run('BEGIN TRANSACTION', async (beginErr: Error | null) => { // <--- Added async here
if (beginErr) return rejectOuter(new Error(`开始事务失败: ${beginErr.message}`));
try {
// 使用 Promise.allSettled 来处理所有连接的导入
const importPromises = importedData.map(connData => (async () => { // async IIFE
// 1. 验证基本连接数据结构
if (!connData.name || !connData.host || !connData.port || !connData.username || !connData.auth_method) {
// failureCount++; // 由 allSettled 结果判断
// errors.push({ connectionName: connData.name || '未知连接', message: '缺少必要的连接字段 (name, host, port, username, auth_method)。' });
throw new Error('缺少必要的连接字段 (name, host, port, username, auth_method)。');
}
// 验证 auth_method 和凭证
if (connData.auth_method === 'password' && !connData.encrypted_password) {
throw new Error('密码认证缺少 encrypted_password。');
}
if (connData.auth_method === 'key' && !connData.encrypted_private_key) {
throw new Error('密钥认证缺少 encrypted_private_key。');
}
let proxyIdToUse: number | null = null;
// 2. 处理代理信息(如果存在)
if (connData.proxy) {
const proxyData = connData.proxy;
// 验证代理数据
if (!proxyData.name || !proxyData.type || !proxyData.host || !proxyData.port /* || !proxyData.auth_method */) { // auth_method 可能不存在,暂时移除强制校验
throw new Error('代理信息不完整 (缺少 name, type, host, port)。');
}
// 验证代理凭证存在性 (如果 auth_method 存在)
if (proxyData.auth_method === 'password' && !proxyData.encrypted_password) {
throw new Error('代理密码认证缺少 encrypted_password。');
}
if (proxyData.auth_method === 'key' && !proxyData.encrypted_private_key) {
throw new Error('代理密钥认证缺少 encrypted_private_key。');
}
// 尝试查找现有代理
const existingProxy = await new Promise<{ id: number } | undefined>((resolve, reject) => {
findProxyStmt.get(proxyData.name, proxyData.type, proxyData.host, proxyData.port, (err: Error | null, row: { id: number } | undefined) => {
if (err) return reject(new Error(`查找代理时出错: ${err.message}`));
resolve(row);
});
});
if (existingProxy) {
proxyIdToUse = existingProxy.id;
console.log(`导入连接 ${connData.name}: 找到现有代理 ${proxyData.name} (ID: ${proxyIdToUse})`);
} else {
// 代理不存在,创建新代理
console.log(`导入连接 ${connData.name}: 代理 ${proxyData.name} 不存在,正在创建...`);
const proxyResult = await new Promise<{ lastID: number }>((resolve, reject) => {
insertProxyStmt.run(
proxyData.name, proxyData.type, proxyData.host, proxyData.port,
proxyData.username || null,
// 恢复为文档定义的参数
proxyData.auth_method || 'none', // 提供默认值 'none' 如果不存在
proxyData.encrypted_password || null,
proxyData.encrypted_private_key || null,
proxyData.encrypted_passphrase || null,
now, now,
function (this: Statement, err: Error | null) {
if (err) return reject(new Error(`创建代理时出错: ${err.message}`));
resolve({ lastID: (this as any).lastID });
}
);
});
proxyIdToUse = proxyResult.lastID;
console.log(`导入连接 ${connData.name}: 新代理 ${proxyData.name} 创建成功 (ID: ${proxyIdToUse})`);
}
} // 结束代理处理
// 3. 插入连接信息
const connResult = await new Promise<{ lastID: number }>((resolve, reject) => {
insertConnStmt.run(
connData.name, connData.host, connData.port, connData.username, connData.auth_method,
connData.encrypted_password || null,
connData.encrypted_private_key || null,
connData.encrypted_passphrase || null,
proxyIdToUse, // 使用找到或创建的代理 ID
now, now,
function (this: Statement, err: Error | null) {
if (err) return reject(new Error(`插入连接时出错: ${err.message}`));
resolve({ lastID: (this as any).lastID });
}
);
});
const newConnectionId = connResult.lastID;
// 4. 处理标签关联
if (Array.isArray(connData.tag_ids) && connData.tag_ids.length > 0) {
for (const tagId of connData.tag_ids) {
if (typeof tagId === 'number' && tagId > 0) {
// 注意:这里假设 tagId 在 tags 表中已存在。
await new Promise<void>((resolve, reject) => {
insertTagStmt.run(newConnectionId, tagId, (err: Error | null) => {
if (err) {
console.warn(`导入连接 ${connData.name}: 关联标签 ID ${tagId} 失败: ${err.message}`);
// 决定是否因此失败
// reject(new Error(`关联标签 ID ${tagId} 失败: ${err.message}`));
}
resolve(); // 继续处理下一个标签
});
});
}
}
}
// 如果 IIFE 成功完成,返回 null 或 undefined 表示成功
return null;
})().catch(err => { // 捕获 async IIFE 中的错误
// 返回一个包含错误信息的对象,以便 Promise.allSettled 处理
return { connectionName: connData.name || '未知连接', error: err };
})); // 结束 map 和 async IIFE
// 等待所有导入 Promise 完成
const results = await Promise.allSettled(importPromises);
// 处理结果
results.forEach(result => {
if (result.status === 'fulfilled' && result.value?.error) {
// IIFE 成功执行但内部捕获并返回了错误
failureCount++;
errors.push({ connectionName: result.value.connectionName, message: result.value.error.message });
} else if (result.status === 'fulfilled') {
// IIFE 成功执行且没有返回错误
successCount++;
} else { // status === 'rejected' - IIFE 本身抛出未捕获错误
failureCount++;
const reason = result.reason as any;
// 尝试获取 connectionName,如果 IIFE 在早期失败可能没有
const name = importedData[results.indexOf(result)]?.name || '未知连接';
errors.push({ connectionName: name, message: reason?.message || '未知导入错误' });
}
});
// 根据是否有失败决定提交或回滚
if (failureCount > 0) {
console.warn(`导入连接存在 ${failureCount} 个错误,正在回滚事务...`);
db.run('ROLLBACK', (rollbackErr: Error | null) => {
if (rollbackErr) console.error("回滚事务失败:", rollbackErr);
// 即使回滚失败,仍需告知前端导入失败
rejectOuter(new Error(`导入失败,存在 ${failureCount} 个错误。`)); // 使用 rejectOuter 传递错误
});
} else {
// 所有记录处理完毕,提交事务
db.run('COMMIT', (commitErr: Error | null) => {
if (commitErr) {
console.error('提交导入事务时出错:', commitErr);
rejectOuter(new Error(`提交导入事务失败: ${commitErr.message}`));
} else {
resolveOuter(); // 事务成功,resolve 外层 Promise
}
});
}
} catch (innerError: any) {
// 捕获 Promise.allSettled 或其他同步错误
console.error('导入事务内部出错:', innerError);
db.run('ROLLBACK', (rollbackErr: Error | null) => {
if (rollbackErr) console.error("回滚事务失败:", rollbackErr);
rejectOuter(innerError); // 将内部错误传递出去
});
}
}); // 结束 BEGIN TRANSACTION 回调
}); // 结束 db.serialize
}); // 结束 new Promise
// 如果 Promise 成功 resolve (事务提交成功)
res.status(200).json({
message: `导入成功完成。共导入 ${successCount} 条连接。`,
successCount,
failureCount: 0
});
} catch (error: any) { // 捕获外层 try 或 rejectOuter 传递的错误
console.error('导入连接时发生错误:', error);
// 如果错误是由 rejectOuter 传递的,并且包含失败计数,则使用它
if (failureCount > 0) {
res.status(400).json({
message: error.message || `导入失败,存在 ${failureCount} 个错误。`,
successCount,
failureCount,
errors
});
} else {
// 其他错误 (如文件解析、开始事务失败等)
res.status(500).json({ message: error.message || '导入连接时发生内部服务器错误。' });
}
} finally {
// Finalize prepared statements regardless of success or failure
// Ensure statements are finalized even if db.serialize wasn't fully entered
findProxyStmt?.finalize();
insertProxyStmt?.finalize();
insertConnStmt?.finalize();
insertTagStmt?.finalize();
}
};
@@ -1,19 +1,68 @@
import { Router } from 'express';
import { Router, Request, Response, NextFunction } from 'express'; // 引入 Request, Response, NextFunction
import { isAuthenticated } from '../auth/auth.middleware'; // 引入认证中间件
import multer from 'multer'; // 引入 multer 用于文件上传
import {
createConnection,
getConnections,
getConnectionById, // 引入获取单个连接的控制器
updateConnection, // 引入更新连接的控制器
deleteConnection, // 引入删除连接的控制器
testConnection // 引入测试连接的控制器
testConnection, // 引入测试连接的控制器
exportConnections, // 引入导出连接的控制器
importConnections // 引入导入连接的控制器
} from './connections.controller';
const router = Router();
// 配置 multer 用于处理 JSON 文件上传 (存储在内存中)
const storage = multer.memoryStorage(); // 将文件存储在内存中作为 Buffer
const upload = multer({
storage: storage,
limits: { fileSize: 5 * 1024 * 1024 }, // 限制文件大小为 5MB
fileFilter: (req: Request, file, cb) => { // Add type for req
if (file.mimetype === 'application/json') {
cb(null, true);
} else {
// Attach error to request instead of calling cb with error directly
// This makes it easier to handle consistently and return JSON
(req as any).fileValidationError = '只允许上传 JSON 文件!';
cb(null, false); // Reject the file
}
}
});
// 应用认证中间件到所有 /connections 路由
router.use(isAuthenticated); // 恢复认证检查
// --- Specific routes before parameterized routes ---
// GET /api/v1/connections/export - 导出连接配置
router.get('/export', exportConnections);
// POST /api/v1/connections/import - 导入连接配置
router.post('/import', (req: Request, res: Response, next: NextFunction) => {
// Use multer middleware, but handle errors specifically
upload.single('connectionsFile')(req, res, (err: any) => {
// Check for file filter validation error first
if ((req as any).fileValidationError) {
return res.status(400).json({ message: (req as any).fileValidationError });
}
// Check for other multer errors (e.g., file size limit)
if (err instanceof multer.MulterError) {
return res.status(400).json({ message: `文件上传错误: ${err.message}` });
} else if (err) {
// Other unexpected errors during upload
console.error("Unexpected error during file upload:", err);
return res.status(500).json({ message: '文件上传处理失败' });
}
// If no errors, proceed to the controller
next();
});
}, importConnections);
// --- General CRUD and other routes ---
// GET /api/v1/connections - 获取连接列表
router.get('/', getConnections);
Binary file not shown.
+9 -1
View File
@@ -86,7 +86,15 @@
"test": {
"success": "Connection test successful!",
"failed": "Connection test failed: {error}"
}
},
"exportConnections": "Export Connections",
"importConnections": "Import Connections",
"exportError": "Failed to export connections: {message}",
"importError": "Failed to import connections: {message}",
"importErrorFileType": "Invalid file type. Please select a JSON file.",
"importErrorUnknown": "Unknown import error occurred.",
"importErrorNetwork": "Network error during import.",
"importSuccess": "Import completed. Successful: {successCount}, Failed: {failureCount}."
},
"proxies": {
"title": "Proxy Management",
+9 -1
View File
@@ -86,7 +86,15 @@
"test": {
"success": "连接测试成功!",
"failed": "连接测试失败: {error}"
}
},
"exportConnections": "导出连接",
"importConnections": "导入连接",
"exportError": "导出连接失败: {message}",
"importError": "导入连接失败: {message}",
"importErrorFileType": "文件类型无效。请选择 JSON 文件。",
"importErrorUnknown": "发生未知导入错误。",
"importErrorNetwork": "导入过程中发生网络错误。",
"importSuccess": "导入完成。成功: {successCount}, 失败: {failureCount}."
},
"proxies": {
"title": "代理管理",
+143 -1
View File
@@ -62,6 +62,143 @@ const closeForm = () => {
editingConnection.value = null; //
showForm.value = false;
};
//
const handleExportConnections = async () => {
try {
// API
const response = await fetch('/api/v1/connections/export', {
method: 'GET',
headers: {
// Authorization
// 'Authorization': `Bearer ${token}`, //
'Accept': 'application/json',
},
});
if (!response.ok) {
//
const errorData = await response.json();
console.error('导出连接失败:', errorData.message || response.statusText);
alert(t('connections.exportError', { message: errorData.message || response.statusText })); //
return;
}
// ( Content-Disposition )
const disposition = response.headers.get('Content-Disposition');
let filename = 'nexus-terminal-connections.json'; //
if (disposition && disposition.includes('attachment')) {
const filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
const matches = filenameRegex.exec(disposition);
if (matches != null && matches[1]) {
filename = matches[1].replace(/['"]/g, '');
}
}
// Blob
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a); // DOM
a.click();
a.remove(); //
window.URL.revokeObjectURL(url); // URL
} catch (error: any) {
console.error('导出连接时发生网络或处理错误:', error);
alert(t('connections.exportError', { message: error.message || '未知错误' }));
}
};
// --- Import/Export Logic ---
const fileInput = ref<HTMLInputElement | null>(null); // Ref for the hidden file input
//
const handleImportClick = () => {
fileInput.value?.click(); // input
};
//
const handleFileChange = async (event: Event) => {
const target = event.target as HTMLInputElement;
const file = target.files?.[0];
if (!file) {
return; //
}
if (file.type !== 'application/json') {
alert(t('connections.importErrorFileType')); //
target.value = ''; // 便
return;
}
const formData = new FormData();
formData.append('connectionsFile', file); // 'connectionsFile' multer
try {
const response = await fetch('/api/v1/connections/import', {
method: 'POST',
body: formData,
headers: {
// 'Content-Type': 'multipart/form-data'
// Authorization
'Accept': 'application/json', // JSON
},
});
//
if (!response.ok) {
let errorMsg = `${response.status} ${response.statusText}`;
try {
// JSON
const errorResult = await response.json();
console.error('导入连接失败 (JSON):', errorResult.message || response.statusText, errorResult.errors);
errorMsg = errorResult.message || errorMsg;
if (errorResult.errors && Array.isArray(errorResult.errors)) {
errorMsg += '\n' + errorResult.errors.map((e: any) => `- ${e.connectionName || '未知'}: ${e.message}`).join('\n');
}
} catch (jsonError) {
// JSON
try {
const textError = await response.text();
console.error('导入连接失败 (Text):', textError);
// 使/
if (textError) {
errorMsg = textError.substring(0, 500); // HTML
}
} catch (textReadError) {
console.error('读取错误响应文本失败:', textReadError);
// /
}
}
alert(t('connections.importError', { message: errorMsg }));
} else {
// JSON
try {
const result = await response.json();
console.log('导入连接成功:', result);
alert(t('connections.importSuccess', { successCount: result.successCount, failureCount: result.failureCount }));
//
connectionsStore.fetchConnections();
} catch (jsonParseError: any) {
console.error('解析成功响应 JSON 时出错:', jsonParseError);
alert(t('connections.importError', { message: `无法解析服务器成功响应: ${jsonParseError.message}` }));
}
}
} catch (error: any) {
console.error('导入连接时发生网络或处理错误:', error);
alert(t('connections.importError', { message: error.message || t('connections.importErrorNetwork') }));
} finally {
// 便
if (target) {
target.value = '';
}
}
};
</script>
<template>
@@ -69,7 +206,12 @@ const closeForm = () => {
<h2>{{ t('connections.title') }}</h2>
<div class="actions-bar">
<button @click="openAddForm" v-if="!showForm">{{ t('connections.addConnection') }}</button>
<div> <!-- Wrap buttons -->
<button @click="openAddForm" v-if="!showForm" style="margin-right: 0.5rem;">{{ t('connections.addConnection') }}</button>
<button @click="handleExportConnections" style="margin-right: 0.5rem;">{{ t('connections.exportConnections') }}</button> <!-- Export Button -->
<button @click="handleImportClick">{{ t('connections.importConnections') }}</button> <!-- Import Button -->
<input type="file" ref="fileInput" @change="handleFileChange" accept=".json" style="display: none;" /> <!-- Hidden File Input -->
</div>
<!-- 标签筛选下拉框 -->
<select v-model="selectedTagId" class="tag-filter-select">
<option :value="null">{{ t('connections.filterAllTags') }}</option>