This commit is contained in:
Baobhan Sith
2025-04-20 15:23:58 +08:00
parent 1160f8a514
commit 77cd9272ba
31 changed files with 2781 additions and 2113 deletions
+412 -21
View File
@@ -10,13 +10,18 @@
"dependencies": {
"@simplewebauthn/server": "^13.1.1",
"@types/multer": "^1.4.12",
"@types/session-file-store": "^1.2.5",
"@types/uuid": "^10.0.0",
"axios": "^1.8.4",
"bcrypt": "^5.1.1",
"connect-sqlite3": "^0.9.15",
"express": "^5.1.0",
"express-session": "^1.18.1",
"https-proxy-agent": "^7.0.6",
"i18next": "^25.0.0",
"i18next-fs-backend": "^2.6.0",
"multer": "^1.4.5-lts.2",
"nodemailer": "^6.10.1",
"session-file-store": "^1.5.0",
"socks": "^2.8.4",
"sqlite3": "^5.1.7",
"ssh2": "^1.16.0",
@@ -29,13 +34,27 @@
"@types/express": "^5.0.1",
"@types/express-session": "^1.18.1",
"@types/node": "^20.0.0",
"@types/nodemailer": "^6.4.17",
"@types/sqlite3": "^3.1.11",
"@types/ssh2": "^1.15.5",
"@types/ws": "^8.18.1",
"cross-env": "^7.0.3",
"ts-node-dev": "^2.0.0",
"typescript": "^5.0.0"
}
},
"node_modules/@babel/runtime": {
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz",
"integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==",
"license": "MIT",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@cspotcode/source-map-support": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
@@ -335,7 +354,6 @@
"version": "1.18.1",
"resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.18.1.tgz",
"integrity": "sha512-S6TkD/lljxDlQ2u/4A70luD8/ZxZcrU5pQwI1rVXCiaVIywoFgbA+PIUNDjPhQpPdK0dGleLtYc/y7XWBfclBg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/express": "*"
@@ -371,6 +389,16 @@
"undici-types": "~6.19.2"
}
},
"node_modules/@types/nodemailer": {
"version": "6.4.17",
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.17.tgz",
"integrity": "sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/qs": {
"version": "6.9.18",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz",
@@ -404,6 +432,16 @@
"@types/send": "*"
}
},
"node_modules/@types/session-file-store": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/@types/session-file-store/-/session-file-store-1.2.5.tgz",
"integrity": "sha512-xjIyh40IznXLrvbAY/nmxu5cMcPcE3ZoDrSDvd02m6p8UjUgOtZAGI7Os5DDd6THuxClLWNhFo/awy1tYp64Bg==",
"license": "MIT",
"dependencies": {
"@types/express": "*",
"@types/express-session": "*"
}
},
"node_modules/@types/sqlite3": {
"version": "3.1.11",
"resolved": "https://registry.npmjs.org/@types/sqlite3/-/sqlite3-3.1.11.tgz",
@@ -620,6 +658,18 @@
"safer-buffer": "~2.1.0"
}
},
"node_modules/asn1.js": {
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz",
"integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==",
"license": "MIT",
"dependencies": {
"bn.js": "^4.0.0",
"inherits": "^2.0.1",
"minimalistic-assert": "^1.0.0",
"safer-buffer": "^2.1.0"
}
},
"node_modules/asn1js": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.6.tgz",
@@ -634,6 +684,29 @@
"node": ">=12.0.0"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/axios": {
"version": "1.8.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz",
"integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/bagpipe": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/bagpipe/-/bagpipe-0.3.5.tgz",
"integrity": "sha512-42sAlmPDKes1nLm/aly+0VdaopSU9br+jkRELedhQxI5uXHgtk47I83Mpmf4zoNTRMASdLFtUkimlu/Z9zQ8+g==",
"license": "MIT"
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -716,6 +789,12 @@
"readable-stream": "^3.4.0"
}
},
"node_modules/bn.js": {
"version": "4.12.1",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz",
"integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==",
"license": "MIT"
},
"node_modules/body-parser": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
@@ -930,6 +1009,18 @@
"color-support": "bin.js"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -981,17 +1072,6 @@
"safe-buffer": "~5.1.0"
}
},
"node_modules/connect-sqlite3": {
"version": "0.9.15",
"resolved": "https://registry.npmjs.org/connect-sqlite3/-/connect-sqlite3-0.9.15.tgz",
"integrity": "sha512-aJGDtASX8DTUZ++7iTN97vR0vGFpm8jDFew/qHK3veISkCpVpPS0tMdqs7i9fiHLaqaU0Jh3c4sUvNxsizaSTA==",
"dependencies": {
"sqlite3": "^5.0.2"
},
"engines": {
"node": ">=0.4.x"
}
},
"node_modules/console-control-strings": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
@@ -1064,6 +1144,40 @@
"dev": true,
"license": "MIT"
},
"node_modules/cross-env": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
"integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
"dev": true,
"license": "MIT",
"dependencies": {
"cross-spawn": "^7.0.1"
},
"bin": {
"cross-env": "src/bin/cross-env.js",
"cross-env-shell": "src/bin/cross-env-shell.js"
},
"engines": {
"node": ">=10.14",
"npm": ">=6",
"yarn": ">=1"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
@@ -1105,6 +1219,15 @@
"node": ">=4.0.0"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/delegates": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
@@ -1250,6 +1373,21 @@
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@@ -1392,6 +1530,62 @@
"node": ">= 0.8"
}
},
"node_modules/follow-redirects": {
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/form-data/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/form-data/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/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -1416,6 +1610,20 @@
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
"license": "MIT"
},
"node_modules/fs-extra": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^4.0.0",
"universalify": "^0.1.0"
},
"engines": {
"node": ">=6 <7 || >=8"
}
},
"node_modules/fs-minipass": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
@@ -1572,8 +1780,7 @@
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"license": "ISC",
"optional": true
"license": "ISC"
},
"node_modules/has-symbols": {
"version": "1.1.0",
@@ -1587,6 +1794,21 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-unicode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
@@ -1675,6 +1897,43 @@
"ms": "^2.0.0"
}
},
"node_modules/i18next": {
"version": "25.0.0",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.0.0.tgz",
"integrity": "sha512-POPvwjOPR1GQvRnbikTMPEhQD+ekd186MHE6NtVxl3Lby+gPp0iq60eCqGrY6wfRnp1lejjFNu0EKs1afA322w==",
"funding": [
{
"type": "individual",
"url": "https://locize.com"
},
{
"type": "individual",
"url": "https://locize.com/i18next.html"
},
{
"type": "individual",
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
}
],
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.10"
},
"peerDependencies": {
"typescript": "^5"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/i18next-fs-backend": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/i18next-fs-backend/-/i18next-fs-backend-2.6.0.tgz",
"integrity": "sha512-3ZlhNoF9yxnM8pa8bWp5120/Ob6t4lVl1l/tbLmkml/ei3ud8IWySCHt2lrY5xWRlSU5D9IV2sm5bEbGuTqwTw==",
"license": "MIT"
},
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@@ -1712,7 +1971,6 @@
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
"integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=0.8.19"
}
@@ -1863,6 +2121,12 @@
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
"license": "MIT"
},
"node_modules/is-typedarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
"integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==",
"license": "MIT"
},
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
@@ -1873,8 +2137,8 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"license": "ISC",
"optional": true
"devOptional": true,
"license": "ISC"
},
"node_modules/jsbn": {
"version": "1.1.0",
@@ -1882,6 +2146,27 @@
"integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==",
"license": "MIT"
},
"node_modules/jsonfile": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
"integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
"license": "MIT",
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/kruptein": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/kruptein/-/kruptein-2.2.3.tgz",
"integrity": "sha512-BTwprBPTzkFT9oTugxKd3WnWrX630MqUDsnmBuoa98eQs12oD4n4TeI0GbpdGcYn/73Xueg2rfnw+oK4dovnJg==",
"license": "MIT",
"dependencies": {
"asn1.js": "^5.4.1"
},
"engines": {
"node": ">6"
}
},
"node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
@@ -2041,6 +2326,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
"license": "ISC"
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -2392,6 +2683,15 @@
"node": "^12.13.0 || ^14.15.0 || >=16.0.0"
}
},
"node_modules/nodemailer": {
"version": "6.10.1",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz",
"integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==",
"license": "MIT-0",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/nopt": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
@@ -2515,6 +2815,16 @@
"node": ">=0.10.0"
}
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
@@ -2610,6 +2920,12 @@
"node": ">= 0.10"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/pump": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
@@ -2728,6 +3044,12 @@
"node": ">=8.10.0"
}
},
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
"license": "MIT"
},
"node_modules/resolve": {
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
@@ -2754,7 +3076,6 @@
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
"integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">= 4"
}
@@ -2866,6 +3187,23 @@
"node": ">= 18"
}
},
"node_modules/session-file-store": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/session-file-store/-/session-file-store-1.5.0.tgz",
"integrity": "sha512-60IZaJNzyu2tIeHutkYE8RiXVx3KRvacOxfLr2Mj92SIsRIroDsH0IlUUR6fJAjoTW4RQISbaOApa2IZpIwFdQ==",
"license": "Apache-2.0",
"dependencies": {
"bagpipe": "^0.3.5",
"fs-extra": "^8.0.1",
"kruptein": "^2.0.4",
"object-assign": "^4.1.1",
"retry": "^0.12.0",
"write-file-atomic": "3.0.3"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
@@ -2878,6 +3216,29 @@
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC"
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"shebang-regex": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/side-channel": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
@@ -3459,11 +3820,20 @@
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
"license": "MIT"
},
"node_modules/typedarray-to-buffer": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
"integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
"license": "MIT",
"dependencies": {
"is-typedarray": "^1.0.0"
}
},
"node_modules/typescript": {
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"dev": true,
"devOptional": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
@@ -3511,6 +3881,15 @@
"imurmurhash": "^0.1.4"
}
},
"node_modules/universalify": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
"license": "MIT",
"engines": {
"node": ">= 4.0.0"
}
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@@ -3575,8 +3954,8 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"devOptional": true,
"license": "ISC",
"optional": true,
"dependencies": {
"isexe": "^2.0.0"
},
@@ -3602,6 +3981,18 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC"
},
"node_modules/write-file-atomic": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz",
"integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==",
"license": "ISC",
"dependencies": {
"imurmurhash": "^0.1.4",
"is-typedarray": "^1.0.0",
"signal-exit": "^3.0.2",
"typedarray-to-buffer": "^3.1.5"
}
},
"node_modules/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz",
+2 -1
View File
@@ -11,10 +11,10 @@
"dependencies": {
"@simplewebauthn/server": "^13.1.1",
"@types/multer": "^1.4.12",
"@types/session-file-store": "^1.2.5",
"@types/uuid": "^10.0.0",
"axios": "^1.8.4",
"bcrypt": "^5.1.1",
"connect-sqlite3": "^0.9.15",
"express": "^5.1.0",
"express-session": "^1.18.1",
"https-proxy-agent": "^7.0.6",
@@ -22,6 +22,7 @@
"i18next-fs-backend": "^2.6.0",
"multer": "^1.4.5-lts.2",
"nodemailer": "^6.10.1",
"session-file-store": "^1.5.0",
"socks": "^2.8.4",
"sqlite3": "^5.1.7",
"ssh2": "^1.16.0",
@@ -0,0 +1 @@
{"cookie":{"secure":false,"httpOnly":true,"path":"/"},"userId":1,"username":"admin","requiresTwoFactor":false,"__lastAccess":1745133821105}
+116 -100
View File
@@ -1,7 +1,8 @@
import { Request, Response } from 'express';
import bcrypt from 'bcrypt';
import { getDb } from '../database';
import sqlite3, { RunResult } from 'sqlite3';
// Import the instance getter and promisified helpers
import { getDbInstance, runDb, getDb, allDb } from '../database/connection';
import sqlite3, { RunResult } from 'sqlite3'; // Keep sqlite3 for type hints if needed
import speakeasy from 'speakeasy';
import qrcode from 'qrcode';
import { PasskeyService } from '../services/passkey.service'; // 导入 PasskeyService
@@ -9,7 +10,8 @@ import { NotificationService } from '../services/notification.service'; // 导
import { AuditLogService } from '../services/audit.service'; // 导入 AuditLogService
import { ipBlacklistService } from '../services/ip-blacklist.service'; // 导入 IP 黑名单服务
const db = getDb();
// Remove top-level db instance acquisition
// const db = getDb();
const passkeyService = new PasskeyService(); // 实例化 PasskeyService
const notificationService = new NotificationService(); // 实例化 NotificationService
const auditLogService = new AuditLogService(); // 实例化 AuditLogService
@@ -49,6 +51,11 @@ export const login = async (req: Request, res: Response): Promise<void> => {
}
try {
const db = await getDbInstance(); // Get DB instance inside the function
// Use the promisified getDb helper
const user = await getDb<User>(db, 'SELECT id, username, hashed_password, two_factor_secret FROM users WHERE username = ?', [username]);
/* Original callback logic replaced by await getDb
const user = await new Promise<User | undefined>((resolve, reject) => {
// 查询用户,包含 2FA 密钥
db.get('SELECT id, username, hashed_password, two_factor_secret FROM users WHERE username = ?', [username], (err, row: User) => {
@@ -59,6 +66,7 @@ export const login = async (req: Request, res: Response): Promise<void> => {
resolve(row);
});
});
*/
if (!user) {
console.log(`登录尝试失败: 用户未找到 - ${username}`);
@@ -144,7 +152,11 @@ export const getAuthStatus = async (req: Request, res: Response): Promise<void>
}
try {
// 查询用户的 2FA 状态
const db = await getDbInstance(); // Get DB instance
// 查询用户的 2FA 状态 using promisified getDb
const user = await getDb<{ two_factor_secret: string | null }>(db, 'SELECT two_factor_secret FROM users WHERE id = ?', [userId]);
/* Original callback logic replaced by await getDb
const user = await new Promise<{ two_factor_secret: string | null } | undefined>((resolve, reject) => {
db.get('SELECT two_factor_secret FROM users WHERE id = ?', [userId], (err, row: { two_factor_secret: string | null }) => {
if (err) {
@@ -154,6 +166,7 @@ export const getAuthStatus = async (req: Request, res: Response): Promise<void>
resolve(row);
});
});
*/
// 如果找不到用户(理论上不应发生),也视为未认证
if (!user) {
@@ -194,7 +207,11 @@ export const verifyLogin2FA = async (req: Request, res: Response): Promise<void>
}
try {
// 获取用户的 2FA 密钥
const db = await getDbInstance(); // Get DB instance
// 获取用户的 2FA 密钥 using promisified getDb
const user = await getDb<User>(db, 'SELECT id, username, two_factor_secret FROM users WHERE id = ?', [userId]);
/* Original callback logic replaced by await getDb
const user = await new Promise<User | undefined>((resolve, reject) => {
db.get('SELECT id, username, two_factor_secret FROM users WHERE id = ?', [userId], (err, row: User) => {
if (err) {
@@ -204,6 +221,7 @@ export const verifyLogin2FA = async (req: Request, res: Response): Promise<void>
resolve(row);
});
});
*/
if (!user || !user.two_factor_secret) {
console.error(`2FA 验证错误: 未找到用户 ${userId} 或未设置密钥。`);
@@ -290,6 +308,11 @@ export const changePassword = async (req: Request, res: Response): Promise<void>
}
try {
const db = await getDbInstance(); // Get DB instance
// Use promisified getDb
const user = await getDb<User>(db, 'SELECT id, hashed_password FROM users WHERE id = ?', [userId]);
/* Original callback logic replaced by await getDb
const user = await new Promise<User | undefined>((resolve, reject) => {
db.get('SELECT id, hashed_password FROM users WHERE id = ?', [userId], (err, row: User) => {
if (err) {
@@ -299,6 +322,7 @@ export const changePassword = async (req: Request, res: Response): Promise<void>
resolve(row);
});
});
*/
if (!user) {
console.error(`修改密码错误: 未找到 ID 为 ${userId} 的用户。`);
@@ -317,27 +341,21 @@ export const changePassword = async (req: Request, res: Response): Promise<void>
const newHashedPassword = await bcrypt.hash(newPassword, saltRounds);
const now = Math.floor(Date.now() / 1000);
await new Promise<void>((resolveUpdate, rejectUpdate) => {
const stmt = db.prepare(
'UPDATE users SET hashed_password = ?, updated_at = ? WHERE id = ?'
);
stmt.run(newHashedPassword, now, userId, function (this: RunResult, err: Error | null) {
if (err) {
console.error(`更新用户 ${userId} 密码时出错:`, err.message);
return rejectUpdate(new Error('更新密码失败'));
}
if (this.changes === 0) {
console.error(`修改密码错误: 更新影响行数为 0 - 用户 ID ${userId}`);
return rejectUpdate(new Error('未找到要更新的用户'));
}
console.log(`用户 ${userId} 密码已成功修改。`);
const clientIp = req.ip || req.socket?.remoteAddress || 'unknown'; // 获取客户端 IP
// 记录审计日志 (添加 IP)
auditLogService.logAction('PASSWORD_CHANGED', { userId, ip: clientIp });
resolveUpdate();
});
stmt.finalize();
});
// Use promisified runDb instead of prepare/run/finalize
const result = await runDb(db,
'UPDATE users SET hashed_password = ?, updated_at = ? WHERE id = ?',
[newHashedPassword, now, userId]
);
if (result.changes === 0) {
console.error(`修改密码错误: 更新影响行数为 0 - 用户 ID ${userId}`);
throw new Error('未找到要更新的用户'); // Throw error to be caught below
}
console.log(`用户 ${userId} 密码已成功修改。`);
const clientIp = req.ip || req.socket?.remoteAddress || 'unknown'; // 获取客户端 IP
// 记录审计日志 (添加 IP)
auditLogService.logAction('PASSWORD_CHANGED', { userId, ip: clientIp });
res.status(200).json({ message: '密码已成功修改。' });
@@ -361,13 +379,19 @@ export const setup2FA = async (req: Request, res: Response): Promise<void> => {
}
try {
// 检查用户是否已启用 2FA
const db = await getDbInstance(); // Get DB instance
// 检查用户是否已启用 2FA using promisified getDb
const user = await getDb<{ two_factor_secret: string | null }>(db, 'SELECT two_factor_secret FROM users WHERE id = ?', [userId]);
const existingSecret = user ? user.two_factor_secret : null;
/* Original callback logic replaced by await getDb
const existingSecret = await new Promise<string | null>((resolve, reject) => {
db.get('SELECT two_factor_secret FROM users WHERE id = ?', [userId], (err, row: { two_factor_secret: string | null }) => {
if (err) reject(err);
else resolve(row ? row.two_factor_secret : null);
});
});
*/
if (existingSecret) {
res.status(400).json({ message: '两步验证已启用。如需重置,请先禁用。' });
@@ -510,6 +534,7 @@ export const verifyAndActivate2FA = async (req: Request, res: Response): Promise
}
try {
const db = await getDbInstance(); // <<< Add this line to get the db instance
// 使用临时密钥验证用户提交的令牌
const verified = speakeasy.totp.verify({
secret: tempSecret,
@@ -519,29 +544,22 @@ export const verifyAndActivate2FA = async (req: Request, res: Response): Promise
});
if (verified) {
// 验证成功,将密钥永久存储到数据库
// 验证成功,将密钥永久存储到数据库 using promisified runDb
const now = Math.floor(Date.now() / 1000);
await new Promise<void>((resolveUpdate, rejectUpdate) => {
const stmt = db.prepare(
'UPDATE users SET two_factor_secret = ?, updated_at = ? WHERE id = ?'
);
stmt.run(tempSecret, now, userId, function (this: RunResult, err: Error | null) {
if (err) {
console.error(`更新用户 ${userId} 的 2FA 密钥时出错:`, err.message);
return rejectUpdate(new Error('激活两步验证失败'));
}
if (this.changes === 0) {
console.error(`激活 2FA 错误: 更新影响行数为 0 - 用户 ID ${userId}`);
return rejectUpdate(new Error('未找到要更新的用户'));
}
console.log(`用户 ${userId} 已成功激活两步验证。`);
const clientIp = req.ip || req.socket?.remoteAddress || 'unknown'; // 获取客户端 IP
// 记录审计日志 (添加 IP)
auditLogService.logAction('2FA_ENABLED', { userId, ip: clientIp });
resolveUpdate();
});
stmt.finalize();
});
const result = await runDb(db,
'UPDATE users SET two_factor_secret = ?, updated_at = ? WHERE id = ?',
[tempSecret, now, userId]
);
if (result.changes === 0) {
console.error(`激活 2FA 错误: 更新影响行数为 0 - 用户 ID ${userId}`);
throw new Error('未找到要更新的用户');
}
console.log(`用户 ${userId} 已成功激活两步验证。`);
const clientIp = req.ip || req.socket?.remoteAddress || 'unknown'; // 获取客户端 IP
// 记录审计日志 (添加 IP)
auditLogService.logAction('2FA_ENABLED', { userId, ip: clientIp });
// 清除 session 中的临时密钥
delete req.session.tempTwoFactorSecret;
@@ -577,12 +595,17 @@ export const disable2FA = async (req: Request, res: Response): Promise<void> =>
}
try {
// 1. 验证当前密码
const db = await getDbInstance(); // Get DB instance
// 1. 验证当前密码 using promisified getDb
const user = await getDb<User>(db, 'SELECT id, hashed_password FROM users WHERE id = ?', [userId]);
/* Original callback logic replaced by await getDb
const user = await new Promise<User | undefined>((resolve, reject) => {
db.get('SELECT id, hashed_password FROM users WHERE id = ?', [userId], (err, row: User) => {
if (err) reject(err); else resolve(row);
});
});
*/
if (!user) {
res.status(404).json({ message: '用户不存在。' }); return;
}
@@ -591,29 +614,22 @@ export const disable2FA = async (req: Request, res: Response): Promise<void> =>
res.status(400).json({ message: '当前密码不正确。' }); return;
}
// 2. 清除数据库中的 2FA 密钥
// 2. 清除数据库中的 2FA 密钥 using promisified runDb
const now = Math.floor(Date.now() / 1000);
await new Promise<void>((resolveUpdate, rejectUpdate) => {
const stmt = db.prepare(
'UPDATE users SET two_factor_secret = NULL, updated_at = ? WHERE id = ?'
);
stmt.run(now, userId, function (this: RunResult, err: Error | null) {
if (err) {
console.error(`清除用户 ${userId} 的 2FA 密钥时出错:`, err.message);
return rejectUpdate(new Error('禁用两步验证失败'));
}
if (this.changes === 0) {
console.error(`禁用 2FA 错误: 更新影响行数为 0 - 用户 ID ${userId}`);
return rejectUpdate(new Error('未找到要更新的用户'));
}
console.log(`用户 ${userId} 已成功禁用两步验证。`);
const clientIp = req.ip || req.socket?.remoteAddress || 'unknown'; // 获取客户端 IP
// 记录审计日志 (添加 IP)
auditLogService.logAction('2FA_DISABLED', { userId, ip: clientIp });
resolveUpdate();
});
stmt.finalize();
});
const result = await runDb(db,
'UPDATE users SET two_factor_secret = NULL, updated_at = ? WHERE id = ?',
[now, userId]
);
if (result.changes === 0) {
console.error(`禁用 2FA 错误: 更新影响行数为 0 - 用户 ID ${userId}`);
throw new Error('未找到要更新的用户');
}
console.log(`用户 ${userId} 已成功禁用两步验证。`);
const clientIp = req.ip || req.socket?.remoteAddress || 'unknown'; // 获取客户端 IP
// 记录审计日志 (添加 IP)
auditLogService.logAction('2FA_DISABLED', { userId, ip: clientIp });
res.status(200).json({ message: '两步验证已成功禁用。' });
@@ -629,6 +645,12 @@ export const disable2FA = async (req: Request, res: Response): Promise<void> =>
*/
export const needsSetup = async (req: Request, res: Response): Promise<void> => {
try {
const db = await getDbInstance(); // Get DB instance
// Use promisified getDb
const row = await getDb<{ count: number }>(db, 'SELECT COUNT(*) as count FROM users');
const userCount = row ? row.count : 0;
/* Original callback logic replaced by await getDb
const userCount = await new Promise<number>((resolve, reject) => {
db.get('SELECT COUNT(*) as count FROM users', (err, row: { count: number }) => {
if (err) {
@@ -638,6 +660,7 @@ export const needsSetup = async (req: Request, res: Response): Promise<void> =>
resolve(row ? row.count : 0); // 如果表为空,row 可能为 undefined
});
});
*/
res.status(200).json({ needsSetup: userCount === 0 });
@@ -670,7 +693,12 @@ export const setupAdmin = async (req: Request, res: Response): Promise<void> =>
try {
// 2. 检查数据库中是否已存在用户 (关键安全检查)
const db = await getDbInstance(); // Get DB instance
// 2. 检查数据库中是否已存在用户 (关键安全检查) using promisified getDb
const row = await getDb<{ count: number }>(db, 'SELECT COUNT(*) as count FROM users');
const userCount = row ? row.count : 0;
/* Original callback logic replaced by await getDb
const userCount = await new Promise<number>((resolve, reject) => {
db.get('SELECT COUNT(*) as count FROM users', (err, row: { count: number }) => {
if (err) {
@@ -680,6 +708,7 @@ export const setupAdmin = async (req: Request, res: Response): Promise<void> =>
resolve(row ? row.count : 0);
});
});
*/
if (userCount > 0) {
console.warn('尝试在已有用户的情况下执行初始设置。');
@@ -692,33 +721,20 @@ export const setupAdmin = async (req: Request, res: Response): Promise<void> =>
const hashedPassword = await bcrypt.hash(password, saltRounds);
const now = Math.floor(Date.now() / 1000);
// 4. 插入新用户
const newUser = await new Promise<{ id: number }>((resolveInsert, rejectInsert) => {
const stmt = db.prepare(
`INSERT INTO users (username, hashed_password, created_at, updated_at)
VALUES (?, ?, ?, ?)`
);
// 使用 function(this: RunResult) 来获取 lastID
stmt.run(username, hashedPassword, now, now, function (this: RunResult, err: Error | null) {
if (err) {
console.error('创建初始管理员时出错:', err.message);
// 检查是否是唯一约束错误
if (err.message.includes('UNIQUE constraint failed: users.username')) {
return rejectInsert(new Error('用户名已存在。')); // 虽然理论上不应发生,但以防万一
}
return rejectInsert(new Error('创建初始管理员失败'));
}
// 获取新插入用户的 ID
resolveInsert({ id: this.lastID });
});
stmt.finalize((finalizeErr) => {
if (finalizeErr) {
console.error('Finalizing statement failed:', finalizeErr.message);
// 如果 finalize 失败,可能插入已完成,但最好还是通知错误
rejectInsert(new Error('创建初始管理员时发生错误 (finalize)'));
}
});
});
// 4. 插入新用户 using promisified runDb
const result = await runDb(db,
`INSERT INTO users (username, hashed_password, created_at, updated_at)
VALUES (?, ?, ?, ?)`,
[username, hashedPassword, now, now]
);
// Check if insertion was successful and get the ID
if (typeof result.lastID !== 'number' || result.lastID <= 0) {
// This might happen due to UNIQUE constraint or other issues caught by runDb's error handling
console.error('创建初始管理员后未能获取有效的 lastID。可能原因:用户名已存在或其他数据库错误。');
throw new Error('创建初始管理员失败,可能用户名已存在。');
}
const newUser = { id: result.lastID };
console.log(`初始管理员账号 '${username}' (ID: ${newUser.id}) 已成功创建。`);
@@ -9,7 +9,7 @@ const auditLogService = new AuditLogService(); // 实例化 AuditLogService
// --- 移除所有不再需要的导入和变量 ---
// import { Statement } from 'sqlite3';
// import { getDb } from '../database';
// import { getDb } from '../database/connection'; // Updated import path in comment
// const db = getDb();
// --- 清理结束 ---
-113
View File
@@ -1,113 +0,0 @@
import sqlite3 from 'sqlite3';
import path from 'path';
import fs from 'fs';
// 导入 Repository 模块以调用其初始化相关函数
import * as appearanceRepository from './repositories/appearance.repository';
import * as terminalThemeRepository from './repositories/terminal-theme.repository';
// 导入预设主题定义
import { presetTerminalThemes } from './config/preset-themes-definition';
// 数据库文件路径
const dbDir = path.resolve(__dirname, '../../data');
const dbPath = path.join(dbDir, 'nexus-terminal.db');
// 确保数据库目录存在
if (!fs.existsSync(dbDir)) {
fs.mkdirSync(dbDir, { recursive: true });
console.log(`数据库目录已创建: ${dbDir}`);
}
const verboseSqlite3 = sqlite3.verbose();
let dbInstance: sqlite3.Database | null = null;
/**
* 执行数据库初始化序列:创建表、插入预设主题、设置默认外观。
* @param db 数据库实例
*/
const runDatabaseInitializations = (db: sqlite3.Database) => {
db.serialize(() => {
console.log('[DB Init] 开始数据库初始化序列...');
// 1. 启用外键约束 (必须在事务之外或序列化块的开始处)
db.run('PRAGMA foreign_keys = ON;', (pragmaErr) => {
if (pragmaErr) {
console.error('[DB Init] 启用外键约束失败:', pragmaErr.message);
// 根据需要处理错误,可能需要阻止后续初始化
} else {
console.log('[DB Init] 外键约束已启用。');
}
});
// 2. 创建 terminal_themes 表
db.run(terminalThemeRepository.SQL_CREATE_TABLE, (err) => {
if (err) {
console.error('[DB Init] 创建 terminal_themes 表失败:', err.message);
} else {
console.log('[DB Init] terminal_themes 表已存在或已创建。');
// 3. 初始化预设主题 (只有在表创建成功或已存在后才执行)
terminalThemeRepository.initializePresetThemes(presetTerminalThemes)
.then(() => console.log('[DB Init] 预设主题初始化检查完成。'))
.catch(initErr => console.error('[DB Init] 初始化预设主题时出错:', initErr));
}
});
// 4. 创建 appearance_settings 表
db.run(appearanceRepository.SQL_CREATE_TABLE, (err) => {
if (err) {
console.error('[DB Init] 创建 appearance_settings 表失败:', err.message);
} else {
console.log('[DB Init] appearance_settings 表已存在或已创建。');
// 5. 确保默认设置存在并设置默认激活主题 (只有在表创建成功或已存在后才执行)
// 注意:这需要等待预设主题初始化完成,但 serialize 会保证顺序
appearanceRepository.ensureDefaultSettingsExist()
.then(() => console.log('[DB Init] 外观设置初始化检查完成。'))
.catch(initErr => console.error('[DB Init] 初始化外观设置时出错:', initErr));
}
});
// TODO: 在这里添加其他表的创建和初始化逻辑...
console.log('[DB Init] 数据库初始化序列提交。');
});
};
export const getDb = (): sqlite3.Database => {
if (!dbInstance) {
console.log(`[DB] 尝试连接到数据库: ${dbPath}`);
dbInstance = new verboseSqlite3.Database(dbPath, (err) => {
if (err) {
console.error('[DB] 打开数据库时出错:', err.message);
process.exit(1);
} else {
console.log(`[DB] 已连接到 SQLite 数据库: ${dbPath}`);
// 连接成功后运行初始化逻辑
runDatabaseInitializations(dbInstance as sqlite3.Database);
}
});
}
return dbInstance;
};
// 优雅停机
process.on('SIGINT', () => {
if (dbInstance) {
console.log('[DB] 收到 SIGINT,正在关闭数据库连接...');
dbInstance.close((err) => {
if (err) {
console.error('[DB] 关闭数据库时出错:', err.message);
} else {
console.log('[DB] 数据库连接已关闭。');
}
process.exit(err ? 1 : 0);
});
} else {
process.exit(0);
}
});
export default getDb;
// 注意:为了让这个文件工作,需要修改 repository 文件:
// 1. 从 repository 文件中移除顶层的 createTableIfNotExists() 调用。
// 2. 从 repository 文件中导出 SQL_CREATE_TABLE 字符串常量。
// 3. 从 appearance.repository.ts 导出 ensureDefaultSettingsExist 函数。
+213
View File
@@ -0,0 +1,213 @@
// packages/backend/src/database/connection.ts
import sqlite3, { OPEN_READWRITE, OPEN_CREATE } from 'sqlite3'; // Import flags
import path from 'path';
import fs from 'fs';
import * as schema from './schema';
// Import the table definitions registry instead of individual repositories here
import { tableDefinitions } from './schema.registry';
// presetTerminalThemes might still be needed if passed directly, but likely handled in registry now
// import { presetTerminalThemes } from '../config/preset-themes-definition';
// --- Revert to original path and filename ---
// 使用 process.cwd() 获取项目根目录,然后拼接路径,确保路径一致性
console.log('[Connection CWD]', process.cwd()); // 添加 CWD 日志
const dbDir = path.join(process.cwd(), 'data'); // Correct path relative to CWD (packages/backend)
const dbFilename = 'nexus-terminal.db'; // Revert to original filename
const dbPath = path.join(dbDir, dbFilename);
console.log(`[DB Path] Determined database directory: ${dbDir}`);
console.log(`[DB Path] Determined database file path: ${dbPath}`);
// Add logging before checking/creating directory
console.log(`[DB FS] Checking existence of directory: ${dbDir}`);
if (!fs.existsSync(dbDir)) {
console.log(`[DB FS] Directory does not exist. Attempting to create: ${dbDir}`);
try {
fs.mkdirSync(dbDir, { recursive: true });
console.log(`[DB FS] Directory successfully created: ${dbDir}`);
} catch (mkdirErr: any) {
console.error(`[DB FS] Failed to create directory ${dbDir}:`, mkdirErr.message);
// Consider throwing error here to prevent proceeding if directory creation fails
throw new Error(`Failed to create database directory: ${mkdirErr.message}`);
}
} else {
console.log(`[DB FS] Directory already exists: ${dbDir}`);
}
const verboseSqlite3 = sqlite3.verbose();
let dbInstancePromise: Promise<sqlite3.Database> | null = null;
// --- Promisified Database Operations ---
interface RunResult {
lastID: number;
changes: number;
}
/**
* Promisified version of db.run(). Resolves with { lastID, changes }.
*/
export const runDb = (db: sqlite3.Database, sql: string, params: any[] = []): Promise<RunResult> => {
return new Promise((resolve, reject) => {
db.run(sql, params, function (err: Error | null) { // Use function() to access this
if (err) {
console.error(`[DB Error] SQL: ${sql.substring(0, 100)}... Params: ${JSON.stringify(params)} Error: ${err.message}`);
reject(err);
} else {
// 'this' context provides lastID and changes for INSERT/UPDATE/DELETE
resolve({ lastID: this.lastID, changes: this.changes });
}
});
});
};
/**
* Promisified version of db.get(). Resolves with the row found, or undefined.
*/
export const getDb = <T = any>(db: sqlite3.Database, sql: string, params: any[] = []): Promise<T | undefined> => {
return new Promise((resolve, reject) => {
db.get(sql, params, (err: Error | null, row: T) => { // Add type annotation for row
if (err) {
console.error(`[DB Error] SQL: ${sql.substring(0, 100)}... Params: ${JSON.stringify(params)} Error: ${err.message}`);
reject(err);
} else {
resolve(row); // row will be undefined if not found
}
});
});
};
/**
* Promisified version of db.all(). Resolves with an array of rows found.
*/
export const allDb = <T = any>(db: sqlite3.Database, sql: string, params: any[] = []): Promise<T[]> => {
return new Promise((resolve, reject) => {
db.all(sql, params, (err: Error | null, rows: T[]) => { // Add type annotation for rows
if (err) {
console.error(`[DB Error] SQL: ${sql.substring(0, 100)}... Params: ${JSON.stringify(params)} Error: ${err.message}`);
reject(err);
} else {
resolve(rows); // rows will be an empty array if no matches
}
});
});
};
/**
* Executes the database initialization sequence: creates all tables, inserts preset/default data.
* Now returns a Promise that resolves when all initializations are complete.
* @param db The database instance
*/
const runDatabaseInitializations = async (db: sqlite3.Database): Promise<void> => {
console.log('[DB Init] 开始数据库初始化序列...');
try {
// 1. Enable foreign key constraints
await runDb(db, 'PRAGMA foreign_keys = ON;'); // Use promisified runDb
console.log('[DB Init] 外键约束已启用。');
// 2. Create tables and run initializations based on the registry
for (const tableDef of tableDefinitions) {
await runDb(db, tableDef.sql); // Create table (IF NOT EXISTS)
console.log(`[DB Init] ${tableDef.name} 表已存在或已创建。`);
if (tableDef.init) {
// Pass the db instance to the init function
await tableDef.init(db);
}
}
// Migrations (if any) would run after initial schema setup
// import { runMigrations } from './migrations';
// await runMigrations(db);
// console.log('[DB Init] 迁移检查完成。');
console.log('[DB Init] 数据库初始化序列成功完成。');
} catch (error) {
console.error('[DB Init] 数据库初始化序列失败:', error);
// Propagate the error to stop the application startup in index.ts
throw error;
}
};
/**
* Gets the database instance. Initializes the connection and runs initializations if not already done.
* Returns a Promise that resolves with the database instance once ready.
*/
// Renamed original getDb to getDbInstance to avoid confusion with the promisified getDb helper
export const getDbInstance = (): Promise<sqlite3.Database> => {
if (!dbInstancePromise) {
dbInstancePromise = new Promise((resolve, reject) => {
// Remove connectionFailed flag and double check logic
// Add logging before attempting connection
console.log(`[DB Connection] Attempting to connect/open database file with explicit create flag: ${dbPath}`);
// Explicitly add OPEN_READWRITE and OPEN_CREATE flags
const db = new verboseSqlite3.Database(dbPath, sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE, async (err) => { // Mark callback as async
// --- Strict Error Check FIRST ---
if (err) {
console.error(`[DB Connection] Error opening database file ${dbPath}:`, err.message);
// connectionFailed = true; // Remove flag setting
dbInstancePromise = null; // Reset promise on error
reject(err); // Reject the main promise
return; // Explicitly return
}
// --- End Strict Error Check ---
// Remove Double Check Flag logic
// If no error, proceed with success logging and initialization
console.log(`[DB Connection] Successfully connected to SQLite database: ${dbPath}`);
try {
// Wait for initializations to complete
await runDatabaseInitializations(db);
console.log('[DB] Database initialization complete. Ready.');
resolve(db); // Resolve the main promise with the db instance
} catch (initError) {
console.error('[DB] Initialization failed after connection, closing connection...');
// connectionFailed = true; // Remove flag setting
dbInstancePromise = null; // Reset promise on error
db.close((closeErr) => {
if (closeErr) console.error('[DB] Error closing connection after init failure:', closeErr.message);
reject(initError); // Reject with the initialization error
});
// process.exit(1); // Consider exiting on init failure
}
});
});
}
return dbInstancePromise;
};
// Graceful shutdown remains the same, but it might need access to the resolved instance
// Consider a way to get the instance if needed during shutdown, e.g., a global variable set after promise resolution.
// For now, it checks the promise state indirectly.
process.on('SIGINT', async () => { // Mark as async if needed
if (dbInstancePromise) {
console.log('[DB] 收到 SIGINT,尝试关闭数据库连接...');
try {
// We need the actual instance, not the promise, to close
// Let's assume if the promise exists, we try to resolve it to get the instance
const db = await dbInstancePromise;
db.close((err) => {
if (err) {
console.error('[DB] 关闭数据库时出错:', err.message);
} else {
console.log('[DB] 数据库连接已关闭。');
}
process.exit(err ? 1 : 0);
});
} catch (error) {
console.error('[DB] 获取数据库实例以关闭时出错 (可能初始化失败):', error);
process.exit(1);
}
} else {
console.log('[DB] 收到 SIGINT,但数据库连接从未初始化或已失败。');
process.exit(0);
}
});
// Note: We now export getDbInstance (the promise for the connection)
// and the helper functions runDb, getDb, allDb.
// Files needing the db instance will call `const db = await getDbInstance();`
// and then use `await runDb(db, ...)` etc.
@@ -0,0 +1,24 @@
// packages/backend/src/migrations.ts
import { Database } from 'sqlite3';
// import { getDb } from './database'; // 可能不再需要直接从这里获取 db
/**
* 运行数据库迁移。
* 注意:此函数目前为空,仅作为未来迁移的占位符。
* 数据库的初始模式创建在 database.ts 的初始化逻辑中处理。
* @param db 数据库实例
*/
export const runMigrations = (db: Database): Promise<void> => {
return new Promise<void>((resolve) => {
console.log('[Migrations] 检查数据库迁移(当前无操作)。');
// 在这里添加未来的迁移逻辑,例如:
// db.serialize(() => {
// db.run("ALTER TABLE users ADD COLUMN last_login INTEGER;", (err) => { ... });
// // 更多迁移步骤...
// });
resolve(); // 立即解决,因为没有迁移要运行
});
};
// 可以保留一个默认导出或根据需要移除
// export default runMigrations;
@@ -0,0 +1,99 @@
import { Database } from 'sqlite3';
import * as schemaSql from './schema';
import * as appearanceRepository from '../repositories/appearance.repository';
import * as terminalThemeRepository from '../repositories/terminal-theme.repository';
import { presetTerminalThemes } from '../config/preset-themes-definition';
import { runDb } from './connection'; // Import runDb for init functions
/**
* Interface describing a database table definition for initialization.
*/
export interface TableDefinition {
name: string;
sql: string;
init?: (db: Database) => Promise<void>; // Optional initialization function
}
// --- Initialization Functions ---
/**
* Initializes default settings in the settings table.
*/
const initSettingsTable = async (db: Database): Promise<void> => {
const defaultSettings = [
{ key: 'ipWhitelistEnabled', value: 'false' },
{ key: 'ipWhitelist', value: '' }
];
for (const setting of defaultSettings) {
// Use INSERT OR IGNORE to avoid errors if settings already exist
await runDb(db, "INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)", [setting.key, setting.value]);
}
console.log('[DB Init] 默认 settings 初始化检查完成。');
};
/**
* Initializes preset terminal themes.
* Assumes terminalThemeRepository.initializePresetThemes might need the db instance.
*/
const initTerminalThemesTable = async (db: Database): Promise<void> => {
// Pass the db instance to the repository function
// Note: This might require modifying initializePresetThemes if it doesn't accept db
await terminalThemeRepository.initializePresetThemes(db, presetTerminalThemes);
console.log('[DB Init] 预设主题初始化检查完成。');
};
/**
* Ensures default appearance settings exist.
* Assumes appearanceRepository.ensureDefaultSettingsExist might need the db instance.
*/
const initAppearanceSettingsTable = async (db: Database): Promise<void> => {
// Pass the db instance to the repository function
// Note: This might require modifying ensureDefaultSettingsExist if it doesn't accept db
await appearanceRepository.ensureDefaultSettingsExist(db);
console.log('[DB Init] 外观设置初始化检查完成。');
};
// --- Table Definitions Registry ---
/**
* Array containing definitions for all tables to be created and initialized.
* The order might matter if there are strict foreign key dependencies without ON DELETE/UPDATE clauses,
* but CREATE IF NOT EXISTS makes it generally safe. Initialization order might also matter.
*/
export const tableDefinitions: TableDefinition[] = [
// Core settings and logs first
{
name: 'settings',
sql: schemaSql.createSettingsTableSQL,
init: initSettingsTable
},
{ name: 'audit_logs', sql: schemaSql.createAuditLogsTableSQL },
{ name: 'api_keys', sql: schemaSql.createApiKeysTableSQL },
{ name: 'passkeys', sql: schemaSql.createPasskeysTableSQL },
{ name: 'notification_settings', sql: schemaSql.createNotificationSettingsTableSQL },
{ name: 'users', sql: schemaSql.createUsersTableSQL },
// Features like proxies, connections, tags
{ name: 'proxies', sql: schemaSql.createProxiesTableSQL },
{ name: 'connections', sql: schemaSql.createConnectionsTableSQL }, // Depends on proxies
{ name: 'tags', sql: schemaSql.createTagsTableSQL },
{ name: 'connection_tags', sql: schemaSql.createConnectionTagsTableSQL }, // Depends on connections, tags
// Other utilities
{ name: 'ip_blacklist', sql: schemaSql.createIpBlacklistTableSQL },
{ name: 'command_history', sql: schemaSql.createCommandHistoryTableSQL },
{ name: 'quick_commands', sql: schemaSql.createQuickCommandsTableSQL },
// Appearance related tables (often depend on others or have init logic)
{
name: 'terminal_themes',
sql: schemaSql.createTerminalThemesTableSQL,
init: initTerminalThemesTable
},
{
name: 'appearance_settings',
sql: schemaSql.createAppearanceSettingsTableSQL,
init: initAppearanceSettingsTable
}, // Depends on terminal_themes
];
+190
View File
@@ -0,0 +1,190 @@
// packages/backend/src/schema.ts
export const createSettingsTableSQL = `
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY NOT NULL,
value TEXT NOT NULL,
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);
`;
export const createAuditLogsTableSQL = `
CREATE TABLE IF NOT EXISTS audit_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp INTEGER NOT NULL,
action_type TEXT NOT NULL,
details TEXT NULL
);
`;
export const createApiKeysTableSQL = `
CREATE TABLE IF NOT EXISTS api_keys (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
hashed_key TEXT UNIQUE NOT NULL,
created_at INTEGER NOT NULL
);
`;
export const createPasskeysTableSQL = `
CREATE TABLE IF NOT EXISTS passkeys (
id INTEGER PRIMARY KEY AUTOINCREMENT,
credential_id TEXT UNIQUE NOT NULL, -- Base64URL encoded
public_key TEXT NOT NULL, -- Base64URL encoded
counter INTEGER NOT NULL,
transports TEXT, -- JSON array as string, e.g., '["internal", "usb"]'
name TEXT, -- User-provided name for the key
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);
`;
export const createNotificationSettingsTableSQL = `
CREATE TABLE IF NOT EXISTS notification_settings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
channel_type TEXT NOT NULL CHECK(channel_type IN ('webhook', 'email', 'telegram')),
name TEXT NOT NULL DEFAULT '',
enabled BOOLEAN NOT NULL DEFAULT false,
config TEXT NOT NULL DEFAULT '{}', -- JSON string for channel-specific config
enabled_events TEXT NOT NULL DEFAULT '[]', -- JSON array of event names
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);
`;
export const createUsersTableSQL = `
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
hashed_password TEXT NOT NULL,
two_factor_secret TEXT NULL, -- 添加 2FA 密钥列,允许为空
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);
`;
export const createProxiesTableSQL = `
CREATE TABLE IF NOT EXISTS proxies (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
type TEXT NOT NULL CHECK(type IN ('SOCKS5', 'HTTP')),
host TEXT NOT NULL,
port INTEGER NOT NULL,
username TEXT NULL,
auth_method TEXT NOT NULL DEFAULT 'none' CHECK(auth_method IN ('none', 'password', 'key')),
encrypted_password TEXT NULL,
encrypted_private_key TEXT NULL,
encrypted_passphrase TEXT NULL,
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
UNIQUE(name, type, host, port)
);
`;
export const createConnectionsTableSQL = `
CREATE TABLE IF NOT EXISTS connections (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NULL, -- 允许 name 为空
host TEXT NOT NULL,
port INTEGER NOT NULL,
username TEXT NOT NULL,
auth_method TEXT NOT NULL CHECK(auth_method IN ('password', 'key')),
encrypted_password TEXT NULL,
encrypted_private_key TEXT NULL,
encrypted_passphrase TEXT NULL,
proxy_id INTEGER NULL,
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
last_connected_at INTEGER NULL,
FOREIGN KEY (proxy_id) REFERENCES proxies(id) ON DELETE SET NULL
);
`;
export const createTagsTableSQL = `
CREATE TABLE IF NOT EXISTS tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE NOT NULL,
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);
`;
export const createConnectionTagsTableSQL = `
CREATE TABLE IF NOT EXISTS connection_tags (
connection_id INTEGER NOT NULL,
tag_id INTEGER NOT NULL,
PRIMARY KEY (connection_id, tag_id),
FOREIGN KEY (connection_id) REFERENCES connections(id) ON DELETE CASCADE,
FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
);
`;
export const createIpBlacklistTableSQL = `
CREATE TABLE IF NOT EXISTS ip_blacklist (
ip TEXT PRIMARY KEY NOT NULL,
attempts INTEGER NOT NULL DEFAULT 1,
last_attempt_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
blocked_until INTEGER NULL -- 封禁截止时间戳 (秒),NULL 表示未封禁或永久封禁 (根据逻辑决定)
);
`;
export const createCommandHistoryTableSQL = `
CREATE TABLE IF NOT EXISTS command_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
command TEXT NOT NULL,
timestamp INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);
`;
export const createQuickCommandsTableSQL = `
CREATE TABLE IF NOT EXISTS quick_commands (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NULL, -- 名称可选
command TEXT NOT NULL, -- 指令必选
usage_count INTEGER NOT NULL DEFAULT 0, -- 使用频率
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);
`;
// 从 database.ts 移动过来的,保持一致性
export const createTerminalThemesTableSQL = `
CREATE TABLE IF NOT EXISTS terminal_themes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE NOT NULL,
theme_type TEXT NOT NULL CHECK(theme_type IN ('preset', 'user')),
foreground TEXT,
background TEXT,
cursor TEXT,
cursor_accent TEXT,
selection_background TEXT,
black TEXT,
red TEXT,
green TEXT,
yellow TEXT,
blue TEXT,
magenta TEXT,
cyan TEXT,
white TEXT,
bright_black TEXT,
bright_red TEXT,
bright_green TEXT,
bright_yellow TEXT,
bright_blue TEXT,
bright_magenta TEXT,
bright_cyan TEXT,
bright_white TEXT,
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);
`;
export const createAppearanceSettingsTableSQL = `
CREATE TABLE IF NOT EXISTS appearance_settings (
key TEXT PRIMARY KEY NOT NULL,
value TEXT NOT NULL,
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);
`;
+86 -69
View File
@@ -2,12 +2,13 @@ import express = require('express');
// import express = require('express'); // 移除重复导入
import { Request, Response, NextFunction, RequestHandler } from 'express'; // 添加 RequestHandler
import http from 'http'; // 引入 http 模块
import fs from 'fs'; // 导入 fs 模块用于创建目录
import session from 'express-session';
import connectSqlite3 from 'connect-sqlite3';
import sessionFileStore from 'session-file-store'; // 替换为 session-file-store
import path from 'path'; // 需要 path 模块
import bcrypt from 'bcrypt'; // 引入 bcrypt 用于哈希密码
import { getDb } from './database';
import { runMigrations } from './migrations';
import { getDbInstance } from './database/connection'; // Updated import path, use getDbInstance
import { runMigrations } from './database/migrations'; // Updated import path
import authRouter from './auth/auth.routes'; // 导入认证路由
import connectionsRouter from './connections/connections.routes';
import sftpRouter from './sftp/sftp.routes';
@@ -23,6 +24,7 @@ import appearanceRoutes from './appearance/appearance.routes'; // 导入外观
import { initializeWebSocket } from './websocket';
import { ipWhitelistMiddleware } from './auth/ipWhitelist.middleware'; // 导入 IP 白名单中间件
// 基础 Express 应用设置 (后续会扩展)
const app = express();
const server = http.createServer(app); // 创建 HTTP 服务器实例
@@ -34,8 +36,10 @@ app.set('trust proxy', true);
// --- 结束信任代理设置 ---
// --- 会话存储设置 ---
const SQLiteStore = connectSqlite3(session);
const dbPath = path.resolve(__dirname, '../../data'); // 数据库目录路径
// const SQLiteStore = connectSqlite3(session); // 移除旧的 Store 初始化
// 使用 process.cwd() 获取项目根目录,然后拼接路径,确保路径一致性
console.log('[Index CWD 1]', process.cwd()); // 添加 CWD 日志
const dbPath = path.join(process.cwd(), 'data'); // Correct path relative to CWD (packages/backend)
// --- 中间件 ---
// !! 重要:IP 白名单应尽可能早地应用,通常在其他中间件之前 !!
@@ -50,40 +54,10 @@ if (sessionSecret === 'a-very-insecure-secret-for-dev') {
console.warn('警告:正在使用默认的不安全会话密钥,请在生产环境中设置 SESSION_SECRET 环境变量!');
}
app.use(session({
// 使用类型断言 (as any) 来解决 @types/connect-sqlite3 和 @types/express-session 的类型冲突
store: new SQLiteStore({
db: 'nexus-terminal.db', // 数据库文件名
dir: dbPath, // 数据库文件所在目录
table: 'sessions' // 存储会话的表名 (会自动创建)
}) as any,
secret: sessionSecret,
resave: false, // 强制保存 session 即使它没有变化 (通常为 false)
saveUninitialized: false, // 强制将未初始化的 session 存储 (通常为 false)
cookie: {
maxAge: 1000 * 60 * 60 * 24 * 7, // Cookie 有效期:7天 (毫秒)
httpOnly: true, // 防止客户端脚本访问 cookie
secure: process.env.NODE_ENV === 'production' // 仅在 HTTPS 下发送 cookie (生产环境)
}
}));
// 将 session 中间件保存到一个变量,以便传递给 WebSocket 初始化函数
const sessionMiddleware = session({
// 使用类型断言 (as any) 来解决 @types/connect-sqlite3 和 @types/express-session 的类型冲突
store: new SQLiteStore({
db: 'nexus-terminal.db', // 数据库文件名
dir: dbPath, // 数据库文件所在目录
table: 'sessions' // 存储会话的表名 (会自动创建)
}) as any,
secret: sessionSecret,
resave: false, // 强制保存 session 即使它没有变化 (通常为 false)
saveUninitialized: false, // 强制将未初始化的 session 存储 (通常为 false)
cookie: {
maxAge: 1000 * 60 * 60 * 24 * 7, // Cookie 有效期:7天 (毫秒)
httpOnly: true, // 防止客户端脚本访问 cookie
secure: process.env.NODE_ENV === 'production' // 仅在 HTTPS 下发送 cookie (生产环境)
}
});
app.use(sessionMiddleware); // 应用会话中间件
// !! 移除顶层的 session 中间件应用,将其移至 startServer 内部 !!
// !! 将 sessionMiddleware 的创建和应用移到 startServer 函数内部 !!
// const sessionMiddleware = session({ ... }); // 不在这里创建
// app.use(sessionMiddleware); // 不在这里应用
// --- 静态文件服务 ---
// 提供上传的背景图片等静态资源
@@ -104,38 +78,25 @@ declare module 'express-session' {
const port = process.env.PORT || 3001; // 示例端口,可配置
// --- API 路由 ---
app.use('/api/v1/auth', authRouter);
app.use('/api/v1/connections', connectionsRouter);
app.use('/api/v1/sftp', sftpRouter);
app.use('/api/v1/proxies', proxyRoutes); // 挂载代理相关的路由
app.use('/api/v1/tags', tagsRouter); // 挂载标签相关的路由
app.use('/api/v1/settings', settingsRoutes); // 挂载设置相关的路由
app.use('/api/v1/notifications', notificationRoutes); // 挂载通知相关的路由
app.use('/api/v1/audit-logs', auditRoutes); // 挂载审计日志相关的路由
app.use('/api/v1/command-history', commandHistoryRoutes); // 挂载命令历史记录相关的路由
app.use('/api/v1/quick-commands', quickCommandsRoutes); // 挂载快捷指令相关的路由
app.use('/api/v1/terminal-themes', terminalThemeRoutes); // 挂载终端主题路由
app.use('/api/v1/appearance', appearanceRoutes); // 挂载外观设置路由
// 状态检查接口
app.get('/api/v1/status', (req: Request, res: Response) => {
res.json({ status: '后端服务运行中!' }); // 响应也改为中文
});
// --- API 路由 (移动到 startServer 内部,在 session 中间件之后应用) ---
// 在服务器启动前初始化数据库并执行迁移
const initializeDatabase = async () => {
try {
const db = getDb(); // 获取数据库实例 (同时会建立连接)
await runMigrations(db); // 执行数据库迁移 (创建表)
// console.log('数据库迁移执行成功。'); // 日志已移至 migrations.ts
// getDb() now returns a Promise and handles initialization internally
const db = await getDbInstance(); // Correctly await the Promise, use getDbInstance
console.log('数据库实例已获取并初始化完成。');
// runMigrations is now just a placeholder and initialization is done within getDb
// await runMigrations(db); // Removed call to placeholder runMigrations
// 检查管理员用户是否存在
const userCount = await new Promise<number>((resolve, reject) => {
db.get('SELECT COUNT(*) as count FROM users', (err, row: { count: number }) => { // 查询用户数量
// Use the resolved db instance here
db.get('SELECT COUNT(*) as count FROM users', (err: Error | null, row: { count: number }) => { // Add type for err
if (err) {
console.error('检查 users 表时出错:', err.message);
return reject(err);
return reject(err); // Reject the promise on error
}
resolve(row.count);
});
@@ -143,20 +104,76 @@ const initializeDatabase = async () => {
// 检查用户数量后不再执行任何操作 (移除了自动创建和日志记录)
console.log('数据库初始化检查完成。');
console.log(`数据库中找到 ${userCount} 个用户。`); // Log user count
console.log('数据库初始化后检查完成。');
} catch (error) {
console.error('数据库初始化失败:', error);
console.error('数据库初始化或检查失败:', error); // More specific error message
process.exit(1); // 如果数据库初始化失败,则退出进程
}
};
// 启动 HTTP 服务器 (而不是直接 app.listen)
const startServer = () => {
server.listen(port, () => { // 使用 server.listen
console.log(`后端服务器正在监听 http://localhost:${port}`);
// 初始化 WebSocket 服务器,并传入 HTTP 服务器实例和会话解析器
initializeWebSocket(server, sessionMiddleware as RequestHandler);
});
// !! 在服务器启动前,但在数据库初始化后,设置会话中间件 !!
console.log('数据库初始化成功,现在设置会话存储...');
const FileStore = sessionFileStore(session); // 使用新的 FileStore
// 使用 process.cwd() 获取项目根目录,然后拼接路径,确保路径一致性
console.log('[Index CWD 2]', process.cwd()); // 添加 CWD 日志
const dataPath = path.join(process.cwd(), 'data'); // 数据库文件目录保持不变 (重命名变量以便区分)
const sessionsPath = path.join(process.cwd(), 'sessions'); // 新建 sessions 目录存储会话文件
// 确保 sessions 目录存在
if (!fs.existsSync(sessionsPath)) {
fs.mkdirSync(sessionsPath, { recursive: true });
console.log(`[Session Store] 已创建会话目录: ${sessionsPath}`);
}
console.log(`[Session Store] 使用文件存储,路径: ${sessionsPath}`);
const sessionMiddleware = session({
store: new FileStore({
path: sessionsPath, // 指定会话文件存储目录
ttl: 60 * 60 * 24 * 7, // 会话有效期 (秒)7天,匹配 cookie maxAge (需要秒)
logFn: (message) => { console.log('[SessionFileStore]', message); } // 可选:启用日志
// reapInterval: 3600 // 清理过期会话间隔 (秒),默认1小时
}),
secret: sessionSecret,
resave: false,
saveUninitialized: false,
cookie: {
maxAge: 1000 * 60 * 60 * 24 * 7,
httpOnly: true,
secure: process.env.NODE_ENV === 'production'
}
});
app.use(sessionMiddleware); // 在这里应用会话中间件
console.log('会话中间件已应用。');
// --- 应用 API 路由 ---
console.log('应用 API 路由...');
app.use('/api/v1/auth', authRouter);
app.use('/api/v1/connections', connectionsRouter);
app.use('/api/v1/sftp', sftpRouter);
app.use('/api/v1/proxies', proxyRoutes);
app.use('/api/v1/tags', tagsRouter);
app.use('/api/v1/settings', settingsRoutes);
app.use('/api/v1/notifications', notificationRoutes);
app.use('/api/v1/audit-logs', auditRoutes);
app.use('/api/v1/command-history', commandHistoryRoutes);
app.use('/api/v1/quick-commands', quickCommandsRoutes);
app.use('/api/v1/terminal-themes', terminalThemeRoutes);
app.use('/api/v1/appearance', appearanceRoutes);
// 状态检查接口 (如果不需要 session 可以保留在外面,但移入更安全)
app.get('/api/v1/status', (req: Request, res: Response) => {
res.json({ status: '后端服务运行中!' });
});
console.log('API 路由已应用。');
server.listen(port, () => { // 使用 server.listen
console.log(`后端服务器正在监听 http://localhost:${port}`);
// 初始化 WebSocket 服务器,并传入 HTTP 服务器实例和会话解析器
initializeWebSocket(server, sessionMiddleware as RequestHandler); // 传递新创建的 sessionMiddleware
});
};
// 先执行数据库初始化,成功后再启动服务器
-286
View File
@@ -1,286 +0,0 @@
import { Database } from 'sqlite3';
import { getDb } from './database';
const createSettingsTableSQL = `
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY NOT NULL,
value TEXT NOT NULL,
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);
`;
const createAuditLogsTableSQL = `
CREATE TABLE IF NOT EXISTS audit_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp INTEGER NOT NULL,
action_type TEXT NOT NULL,
details TEXT NULL
);
`;
const createApiKeysTableSQL = `
CREATE TABLE IF NOT EXISTS api_keys (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
hashed_key TEXT UNIQUE NOT NULL,
created_at INTEGER NOT NULL
);
`;
const createPasskeysTableSQL = `
CREATE TABLE IF NOT EXISTS passkeys (
id INTEGER PRIMARY KEY AUTOINCREMENT,
credential_id TEXT UNIQUE NOT NULL, -- Base64URL encoded
public_key TEXT NOT NULL, -- Base64URL encoded
counter INTEGER NOT NULL,
transports TEXT, -- JSON array as string, e.g., '["internal", "usb"]'
name TEXT, -- User-provided name for the key
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);
`;
const createNotificationSettingsTableSQL = `
CREATE TABLE IF NOT EXISTS notification_settings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
channel_type TEXT NOT NULL CHECK(channel_type IN ('webhook', 'email', 'telegram')),
name TEXT NOT NULL DEFAULT '',
enabled BOOLEAN NOT NULL DEFAULT false,
config TEXT NOT NULL DEFAULT '{}', -- JSON string for channel-specific config
enabled_events TEXT NOT NULL DEFAULT '[]', -- JSON array of event names
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);
`;
// --- 新增表结构定义 ---
const createUsersTableSQL = `
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
hashed_password TEXT NOT NULL,
two_factor_secret TEXT NULL, -- 添加 2FA 密钥列,允许为空
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);
`;
const createProxiesTableSQL = `
CREATE TABLE IF NOT EXISTS proxies (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
type TEXT NOT NULL CHECK(type IN ('SOCKS5', 'HTTP')),
host TEXT NOT NULL,
port INTEGER NOT NULL,
username TEXT NULL,
auth_method TEXT NOT NULL DEFAULT 'none' CHECK(auth_method IN ('none', 'password', 'key')),
encrypted_password TEXT NULL,
encrypted_private_key TEXT NULL,
encrypted_passphrase TEXT NULL,
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
UNIQUE(name, type, host, port)
);
`;
const createConnectionsTableSQL = `
CREATE TABLE IF NOT EXISTS connections (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NULL, -- 允许 name 为空
host TEXT NOT NULL,
port INTEGER NOT NULL,
username TEXT NOT NULL,
auth_method TEXT NOT NULL CHECK(auth_method IN ('password', 'key')),
encrypted_password TEXT NULL,
encrypted_private_key TEXT NULL,
encrypted_passphrase TEXT NULL,
proxy_id INTEGER NULL,
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
last_connected_at INTEGER NULL,
FOREIGN KEY (proxy_id) REFERENCES proxies(id) ON DELETE SET NULL
);
`;
const createTagsTableSQL = `
CREATE TABLE IF NOT EXISTS tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE NOT NULL,
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);
`;
const createConnectionTagsTableSQL = `
CREATE TABLE IF NOT EXISTS connection_tags (
connection_id INTEGER NOT NULL,
tag_id INTEGER NOT NULL,
PRIMARY KEY (connection_id, tag_id),
FOREIGN KEY (connection_id) REFERENCES connections(id) ON DELETE CASCADE,
FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
);
`;
const createIpBlacklistTableSQL = `
CREATE TABLE IF NOT EXISTS ip_blacklist (
ip TEXT PRIMARY KEY NOT NULL,
attempts INTEGER NOT NULL DEFAULT 1,
last_attempt_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
blocked_until INTEGER NULL -- 封禁截止时间戳 (秒),NULL 表示未封禁或永久封禁 (根据逻辑决定)
);
`;
const createCommandHistoryTableSQL = `
CREATE TABLE IF NOT EXISTS command_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
command TEXT NOT NULL,
timestamp INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);
`;
const createQuickCommandsTableSQL = `
CREATE TABLE IF NOT EXISTS quick_commands (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NULL, -- 名称可选
command TEXT NOT NULL, -- 指令必选
usage_count INTEGER NOT NULL DEFAULT 0, -- 使用频率
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);
`;
// --- 结束新增表结构定义 ---
export const runMigrations = (db: Database): Promise<void> => {
// 使用 Promise 包装 serialize,以便在所有操作完成后解析
return new Promise<void>((resolveOuter, rejectOuter) => {
db.serialize(() => {
// 定义一个统一的错误处理函数
const handleError = (operation: string, err: Error | null) => {
if (err) {
const errorMsg = `${operation} 时出错: ${err.message}`;
console.error(errorMsg);
// 停止序列并拒绝外部 Promise
rejectOuter(new Error(errorMsg));
return true; // 表示有错误发生
}
return false; // 表示没有错误
};
// 创建 settings 表
db.run(createSettingsTableSQL, (err: Error | null) => {
if (handleError('创建 settings 表', err)) return;
console.log('Settings 表已检查/创建。');
});
// 插入默认的 IP 白名单设置 (使用 INSERT OR IGNORE)
db.run("INSERT OR IGNORE INTO settings (key, value) VALUES ('ipWhitelistEnabled', 'false')", (err: Error | null) => {
// 对于 INSERT OR IGNORE,即使发生 UNIQUE constraint 错误,err 也可能为 null 或特定错误
// 但 SQLITE_BUSY 是需要处理的
if (err && (err as any).code !== 'SQLITE_CONSTRAINT') { // 忽略约束错误,处理其他错误
if (handleError('插入默认 ipWhitelistEnabled 设置', err)) return;
} else if (err && (err as any).code === 'SQLITE_CONSTRAINT') {
console.log('默认 ipWhitelistEnabled 设置已存在,跳过插入。');
} else {
console.log('默认 ipWhitelistEnabled 设置已插入或已存在。');
}
});
db.run("INSERT OR IGNORE INTO settings (key, value) VALUES ('ipWhitelist', '')", (err: Error | null) => {
if (err && (err as any).code !== 'SQLITE_CONSTRAINT') {
if (handleError('插入默认 ipWhitelist 设置', err)) return;
} else if (err && (err as any).code === 'SQLITE_CONSTRAINT') {
console.log('默认 ipWhitelist 设置已存在,跳过插入。');
} else {
console.log('默认 ipWhitelist 设置已插入或已存在。');
}
});
// 创建 audit_logs 表
db.run(createAuditLogsTableSQL, (err: Error | null) => {
if (handleError('创建 audit_logs 表', err)) return;
console.log('Audit_Logs 表已检查/创建。');
});
// 创建 api_keys 表
db.run(createApiKeysTableSQL, (err: Error | null) => {
if (handleError('创建 api_keys 表', err)) return;
console.log('Api_Keys 表已检查/创建。');
});
// 创建 passkeys 表
db.run(createPasskeysTableSQL, (err: Error | null) => {
if (handleError('创建 passkeys 表', err)) return;
console.log('Passkeys 表已检查/创建。');
});
// 创建 notification_settings 表
db.run(createNotificationSettingsTableSQL, (err: Error | null) => {
if (handleError('创建 notification_settings 表', err)) return;
console.log('Notification_Settings 表已检查/创建。');
});
// --- 新增表创建逻辑 ---
// 创建 users 表
db.run(createUsersTableSQL, (err: Error | null) => {
if (handleError('创建 users 表', err)) return;
console.log('Users 表已检查/创建。');
});
// 创建 proxies 表
db.run(createProxiesTableSQL, (err: Error | null) => {
if (handleError('创建 proxies 表', err)) return;
console.log('Proxies 表已检查/创建。');
});
// 创建 connections 表
db.run(createConnectionsTableSQL, (err: Error | null) => {
if (handleError('创建 connections 表', err)) return;
console.log('Connections 表已检查/创建。');
});
// 创建 tags 表
db.run(createTagsTableSQL, (err: Error | null) => {
if (handleError('创建 tags 表', err)) return;
console.log('Tags 表已检查/创建。');
});
// 创建 connection_tags 表
db.run(createConnectionTagsTableSQL, (err: Error | null) => {
if (handleError('创建 connection_tags 表', err)) return;
console.log('Connection_Tags 表已检查/创建。');
});
// 创建 ip_blacklist 表
db.run(createIpBlacklistTableSQL, (err: Error | null) => {
if (handleError('创建 ip_blacklist 表', err)) return;
console.log('Ip_Blacklist 表已检查/创建。');
});
// 创建 command_history 表
db.run(createCommandHistoryTableSQL, (err: Error | null) => {
if (handleError('创建 command_history 表', err)) return;
console.log('Command_History 表已检查/创建。');
});
// 创建 quick_commands 表 - 这是最后一个操作
db.run(createQuickCommandsTableSQL, (err: Error | null) => {
if (handleError('创建 quick_commands 表', err)) {
// 如果最后一个操作失败,serialize 会停止,rejectOuter 已被调用
return;
}
console.log('Quick_Commands 表已检查/创建。');
// 所有操作成功完成
console.log('所有数据库迁移已成功完成。');
resolveOuter(); // 解析外部 Promise
});
// --- 结束新增表创建逻辑 ---
}); // 结束 db.serialize
}); // 结束 new Promise
};
@@ -1,164 +1,175 @@
import { getDb } from '../database';
// packages/backend/src/repositories/appearance.repository.ts
// Import new async helpers and the instance getter, ensuring getDb is included
import { getDbInstance, runDb, getDb, allDb } from '../database/connection';
import { AppearanceSettings, UpdateAppearanceDto } from '../types/appearance.types';
import { defaultUiTheme } from '../config/default-themes'; // Assuming default UI theme is here too
import { defaultUiTheme } from '../config/default-themes';
// Import findThemeById from terminal theme repository for validation
import { findThemeById as findTerminalThemeById } from './terminal-theme.repository';
import * as sqlite3 from 'sqlite3'; // Import sqlite3 for Database type hint
// const db = getDb(); // Removed top-level call to avoid circular dependency issues
const TABLE_NAME = 'appearance_settings';
const SETTINGS_ID = 1; // Use a fixed ID for the single row of global settings
// Remove SETTINGS_ID as the table is key-value based
// const SETTINGS_ID = 1;
/**
* SQL语句:创建 appearance_settings 表
*/
export const SQL_CREATE_TABLE = `
CREATE TABLE IF NOT EXISTS ${TABLE_NAME} (
id INTEGER PRIMARY KEY, -- Fixed ID for the single settings row
custom_ui_theme TEXT,
active_terminal_theme_id INTEGER NULL, -- 修改为 INTEGER NULL
terminal_font_family TEXT,
terminal_font_size INTEGER,
editor_font_size INTEGER, -- 新增:编辑器字体大小
terminal_background_image TEXT,
page_background_image TEXT,
updated_at INTEGER NOT NULL,
FOREIGN KEY(active_terminal_theme_id) REFERENCES terminal_themes(id) -- 添加外键约束
);
`;
// Define the expected row structure from the database (key-value)
interface DbAppearanceSettingsRow {
key: string;
value: string;
created_at: number;
updated_at: number;
}
/**
* 创建 appearance_settings 表 (如果不存在) - 不再自动调用
*/
const createTableIfNotExists = () => {
// This function is no longer called automatically, initialization is handled in database.ts
getDb().run(SQL_CREATE_TABLE, (err) => {
if (err) {
console.error(`创建 ${TABLE_NAME} 表失败:`, err.message);
} else {
console.log(`${TABLE_NAME} 表已存在或已创建。`);
// 确保默认设置行存在 - 这个调用也应该移到 database.ts
// ensureDefaultSettingsExist();
// Helper function to map DB rows (key-value pairs) to AppearanceSettings object
const mapRowsToAppearanceSettings = (rows: DbAppearanceSettingsRow[]): AppearanceSettings => {
const settings: Partial<AppearanceSettings> = {};
let latestUpdatedAt = 0;
for (const row of rows) {
// Update latestUpdatedAt
if (row.updated_at > latestUpdatedAt) {
latestUpdatedAt = row.updated_at;
}
switch (row.key) {
case 'customUiTheme':
settings.customUiTheme = row.value;
break;
case 'activeTerminalThemeId':
// Ensure value is parsed as number or null
const parsedId = parseInt(row.value, 10);
settings.activeTerminalThemeId = isNaN(parsedId) ? null : parsedId;
break;
case 'terminalFontFamily':
settings.terminalFontFamily = row.value;
break;
case 'terminalFontSize':
settings.terminalFontSize = parseInt(row.value, 10);
break;
case 'editorFontSize':
settings.editorFontSize = parseInt(row.value, 10);
break;
case 'terminalBackgroundImage':
settings.terminalBackgroundImage = row.value || undefined; // Use undefined if empty string
break;
case 'pageBackgroundImage':
settings.pageBackgroundImage = row.value || undefined; // Use undefined if empty string
break;
// Add cases for other potential keys if needed
}
}
});
};
// 辅助函数:将数据库行转换为 AppearanceSettings 对象
const mapRowToAppearanceSettings = (row: any): AppearanceSettings => {
if (!row) return getDefaultAppearanceSettings(); // Return default if no row found
// Merge with defaults for any missing keys and add _id and updatedAt
const defaults = getDefaultAppearanceSettings(); // Get defaults
return {
_id: row.id.toString(),
customUiTheme: row.custom_ui_theme,
activeTerminalThemeId: row.active_terminal_theme_id, // 直接返回数字或 null
terminalFontFamily: row.terminal_font_family,
terminalFontSize: row.terminal_font_size,
editorFontSize: row.editor_font_size, // 新增:编辑器字体大小映射
terminalBackgroundImage: row.terminal_background_image,
// terminalBackgroundOpacity: row.terminal_background_opacity, // Removed
pageBackgroundImage: row.page_background_image,
// pageBackgroundOpacity: row.page_background_opacity, // Removed
updatedAt: row.updated_at,
_id: 'global_appearance', // Use a fixed string ID for the conceptual global settings
customUiTheme: settings.customUiTheme ?? defaults.customUiTheme,
activeTerminalThemeId: settings.activeTerminalThemeId ?? defaults.activeTerminalThemeId,
terminalFontFamily: settings.terminalFontFamily ?? defaults.terminalFontFamily,
terminalFontSize: settings.terminalFontSize ?? defaults.terminalFontSize,
editorFontSize: settings.editorFontSize ?? defaults.editorFontSize,
terminalBackgroundImage: settings.terminalBackgroundImage ?? defaults.terminalBackgroundImage,
pageBackgroundImage: settings.pageBackgroundImage ?? defaults.pageBackgroundImage,
updatedAt: latestUpdatedAt || defaults.updatedAt, // Use latest DB timestamp or default
};
};
// 获取默认外观设置
const getDefaultAppearanceSettings = (): AppearanceSettings => {
// TODO: Find the ID of the default preset theme from terminal_themes table later
// For now, leave activeTerminalThemeId null or undefined
// 获取默认外观设置 (Simplified, _id is no longer relevant here)
const getDefaultAppearanceSettings = (): Omit<AppearanceSettings, '_id'> => {
return {
_id: SETTINGS_ID.toString(),
customUiTheme: JSON.stringify(defaultUiTheme), // Use default UI theme
activeTerminalThemeId: null, // 初始应为 null,待 findAndSetDefaultThemeId 设置
terminalFontFamily: 'Consolas, "Courier New", monospace, "Microsoft YaHei", "微软雅黑"', // Default font
customUiTheme: JSON.stringify(defaultUiTheme),
activeTerminalThemeId: null, // Default should be null initially
terminalFontFamily: 'Consolas, "Courier New", monospace, "Microsoft YaHei", "微软雅黑"',
terminalFontSize: 14,
editorFontSize: 14, // 新增:默认编辑器字体大小
editorFontSize: 14,
terminalBackgroundImage: undefined,
// terminalBackgroundOpacity: 1.0, // Removed
pageBackgroundImage: undefined,
// pageBackgroundOpacity: 1.0, // Removed
updatedAt: Date.now(),
updatedAt: Date.now(), // Provide a default timestamp
};
};
/**
* 确保默认设置行存在,并在需要时设置默认激活主题 ID。
* 这个函数应该在数据库初始化时,在预设主题初始化之后调用。
* Ensures default settings exist in the key-value table.
* This function is called during database initialization.
*/
export const ensureDefaultSettingsExist = async () => { // 改为 async 以便内部 await
export const ensureDefaultSettingsExist = async (db: sqlite3.Database): Promise<void> => {
const defaults = getDefaultAppearanceSettings();
const sqlSelect = `SELECT id, active_terminal_theme_id FROM ${TABLE_NAME} WHERE id = ?`; // 同时查询当前 ID
// 将回调函数改为 async
getDb().get(sqlSelect, [SETTINGS_ID], async (err, row) => {
if (err) {
console.error(`检查默认外观设置时出错:`, err.message);
return;
const nowSeconds = Math.floor(Date.now() / 1000);
const sqlInsertOrIgnore = `INSERT OR IGNORE INTO ${TABLE_NAME} (key, value, created_at, updated_at) VALUES (?, ?, ?, ?)`;
// Define default key-value pairs to ensure existence
const defaultEntries: Array<{ key: keyof Omit<AppearanceSettings, '_id' | 'updatedAt'>, value: any }> = [
{ key: 'customUiTheme', value: defaults.customUiTheme },
{ key: 'activeTerminalThemeId', value: null }, // Start with null
{ key: 'terminalFontFamily', value: defaults.terminalFontFamily },
{ key: 'terminalFontSize', value: defaults.terminalFontSize },
{ key: 'editorFontSize', value: defaults.editorFontSize },
{ key: 'terminalBackgroundImage', value: defaults.terminalBackgroundImage ?? '' }, // Use empty string for DB
{ key: 'pageBackgroundImage', value: defaults.pageBackgroundImage ?? '' }, // Use empty string for DB
];
try {
for (const entry of defaultEntries) {
// Convert value to string for DB storage, handle null/undefined
let dbValue: string;
if (entry.value === null || entry.value === undefined) {
dbValue = entry.key === 'activeTerminalThemeId' ? 'null' : ''; // Store null specifically for theme ID, empty otherwise
} else if (typeof entry.value === 'object') {
dbValue = JSON.stringify(entry.value);
} else {
dbValue = String(entry.value);
}
// Special handling for activeTerminalThemeId: store null as 'null' string or the number as string
if (entry.key === 'activeTerminalThemeId') {
dbValue = entry.value === null ? 'null' : String(entry.value);
}
await runDb(db, sqlInsertOrIgnore, [entry.key, dbValue, nowSeconds, nowSeconds]);
}
if (!row) {
const sqlInsert = `
INSERT INTO ${TABLE_NAME} (
id, custom_ui_theme, active_terminal_theme_id, terminal_font_family, terminal_font_size, editor_font_size, -- 添加 editor_font_size 列
terminal_background_image, -- terminal_background_opacity, -- Removed
page_background_image, -- page_background_opacity, -- Removed
updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) -- 调整占位符数量
`;
getDb().run(sqlInsert, [
SETTINGS_ID,
defaults.customUiTheme,
defaults.activeTerminalThemeId, // Initially null
defaults.terminalFontFamily,
defaults.terminalFontSize,
defaults.editorFontSize, // 添加 editor_font_size 默认值参数
defaults.terminalBackgroundImage,
// defaults.terminalBackgroundOpacity, // Removed
defaults.pageBackgroundImage,
// defaults.pageBackgroundOpacity, // Removed
defaults.updatedAt
], (insertErr) => {
if (insertErr) {
console.error('插入默认外观设置失败:', insertErr.message);
} else {
console.log('默认外观设置已初始化。');
// Now try to find and set the default theme ID
findAndSetDefaultThemeId();
}
});
} else {
// 如果行已存在,直接调用 findAndSetDefaultThemeId 检查并设置默认 ID
await findAndSetDefaultThemeId(); // 使用 await
}
});
console.log('[AppearanceRepo] 默认外观设置键值对检查完成。');
// After ensuring keys exist, try to set the default theme ID if it's currently null
await findAndSetDefaultThemeIdIfNull(db);
} catch (err: any) {
console.error(`[AppearanceRepo] 检查或插入默认外观设置键值对时出错:`, err.message);
throw new Error(`检查或插入默认外观设置失败: ${err.message}`);
}
};
/**
* 查找默认终端主题 ID 并更新外观设置
* Finds the default terminal theme ID and updates the 'activeTerminalThemeId' setting if it's currently null.
* @param db - The active database instance
*/
const findAndSetDefaultThemeId = async () => {
const findAndSetDefaultThemeIdIfNull = async (db: sqlite3.Database): Promise<void> => {
try {
// Find the default theme from the other table
const defaultThemeSql = `SELECT id FROM terminal_themes WHERE is_system_default = 1 LIMIT 1`;
// Explicitly type the row or use type assertion
getDb().get(defaultThemeSql, [], async (err, defaultThemeRow: { id: number } | undefined) => {
if (err) {
console.error("查找默认终端主题 ID 失败:", err.message);
return;
}
// Check the current value of activeTerminalThemeId
const currentSetting = await getDb<{ value: string }>(db, `SELECT value FROM ${TABLE_NAME} WHERE key = ?`, ['activeTerminalThemeId']);
// Proceed only if the setting exists and its value represents null ('null' string)
if (currentSetting && currentSetting.value === 'null') {
// Find the default theme from the terminal_themes table (assuming name 'default' marks the default)
const defaultThemeSql = `SELECT id FROM terminal_themes WHERE name = 'default' AND theme_type = 'preset' LIMIT 1`;
const defaultThemeRow = await getDb<{ id: number }>(db, defaultThemeSql);
if (defaultThemeRow) {
const defaultThemeIdNum = defaultThemeRow.id; // 直接使用数字 ID
// Check current appearance settings
const currentSettings = await getAppearanceSettings();
// Only set the default theme ID if no active theme ID is currently set (i.e., it's null in the DB)
if (currentSettings && currentSettings.activeTerminalThemeId === null) {
console.log(`数据库中未设置激活终端主题,设置为默认数字 ID: ${defaultThemeIdNum}`);
// 更新时传递数字 ID
await updateAppearanceSettings({ activeTerminalThemeId: defaultThemeIdNum });
} else {
console.log(`数据库中已设置激活终端主题数字 ID (${currentSettings?.activeTerminalThemeId}) 或未找到默认主题,跳过设置默认 ID。`);
}
const defaultThemeIdNum = defaultThemeRow.id;
console.log(`[AppearanceRepo] activeTerminalThemeId 为 null,尝试设置为默认主题 ID: ${defaultThemeIdNum}`);
// Update the setting using INSERT OR REPLACE
const sqlReplace = `INSERT OR REPLACE INTO ${TABLE_NAME} (key, value, updated_at) VALUES (?, ?, ?)`;
await runDb(db, sqlReplace, ['activeTerminalThemeId', String(defaultThemeIdNum), Math.floor(Date.now() / 1000)]);
} else {
console.warn("未找到系统默认终端主题,无法设置 activeTerminalThemeId。");
console.warn("[AppearanceRepo] 未找到名为 'default' 的预设终端主题,无法设置默认 activeTerminalThemeId。");
}
});
} catch (error) {
console.error("设置默认终端主题 ID 时出错:", error);
} else {
// console.log(`[AppearanceRepo] activeTerminalThemeId 已设置 (${currentSetting?.value}) 或键不存在,跳过设置默认 ID。`);
}
} catch (error: any) {
console.error("[AppearanceRepo] 设置默认终端主题 ID 时出错:", error.message);
// Don't throw here, just log
}
};
@@ -168,72 +179,90 @@ const findAndSetDefaultThemeId = async () => {
* @returns Promise<AppearanceSettings>
*/
export const getAppearanceSettings = async (): Promise<AppearanceSettings> => {
return new Promise((resolve, reject) => {
getDb().get(`SELECT * FROM ${TABLE_NAME} WHERE id = ?`, [SETTINGS_ID], (err, row) => {
if (err) {
console.error('获取外观设置失败:', err.message);
reject(new Error('获取外观设置失败'));
} else {
resolve(mapRowToAppearanceSettings(row));
}
});
});
try {
const db = await getDbInstance();
// Fetch all rows from the key-value table
const rows = await allDb<DbAppearanceSettingsRow>(db, `SELECT key, value, updated_at FROM ${TABLE_NAME}`);
return mapRowsToAppearanceSettings(rows); // Map the key-value pairs to the settings object
} catch (err: any) {
console.error('获取外观设置失败:', err.message);
throw new Error('获取外观设置失败');
}
};
/**
* 更新外观设置
* 更新外观设置 (Public API)
* @param settingsDto 更新的数据
* @returns Promise<boolean> 是否成功更新
*/
export const updateAppearanceSettings = async (settingsDto: UpdateAppearanceDto): Promise<boolean> => {
const now = Date.now();
let sql = `UPDATE ${TABLE_NAME} SET updated_at = ?`;
const params: any[] = [now];
const db = await getDbInstance();
// Perform validation or complex logic if needed before calling internal update
// Example validation (already present in service, but could be here too):
if (settingsDto.activeTerminalThemeId !== undefined && settingsDto.activeTerminalThemeId !== null) {
try {
const themeExists = await findTerminalThemeById(settingsDto.activeTerminalThemeId);
if (!themeExists) {
throw new Error(`指定的终端主题 ID 不存在: ${settingsDto.activeTerminalThemeId}`);
}
} catch (validationError: any) {
console.error(`[AppearanceRepo] 验证主题 ID ${settingsDto.activeTerminalThemeId} 时出错:`, validationError.message);
throw new Error(`验证主题 ID 失败: ${validationError.message}`);
}
}
// ... other validations ...
// Dynamically build the SET part of the query
const updates: string[] = [];
const validDbKeys = ['custom_ui_theme', 'active_terminal_theme_id', 'terminal_font_family', 'terminal_font_size', 'editor_font_size', 'terminal_background_image', 'page_background_image'];
// Iterate over potential keys to update
for (const key of Object.keys(settingsDto) as Array<keyof UpdateAppearanceDto>) {
const dbKey = key.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`); // Convert camelCase to snake_case
if (validDbKeys.includes(dbKey)) {
const value = settingsDto[key];
// active_terminal_theme_id 应该是数字或 null
if (dbKey === 'active_terminal_theme_id' && typeof value !== 'number' && value !== null) {
console.error(`[AppearanceRepo] 更新 active_terminal_theme_id 时收到无效类型值: ${value} (类型: ${typeof value}),应为数字或 null。跳过此字段。`);
continue; // 跳过无效类型
}
updates.push(`${dbKey} = ?`);
// 直接推入值 (数字或 null)
params.push(value);
}
}
if (updates.length === 0) {
return true; // Nothing to update
}
sql += `, ${updates.join(', ')} WHERE id = ?`;
params.push(SETTINGS_ID);
return new Promise((resolve, reject) => {
// --- 增加详细日志 ---
console.log(`[AppearanceRepo] Executing SQL: ${sql}`);
console.log(`[AppearanceRepo] With Params: ${JSON.stringify(params)}`);
// --- 日志结束 ---
getDb().run(sql, params, function (err) {
if (err) {
console.error('更新外观设置失败:', err.message);
reject(new Error('更新外观设置失败'));
} else {
console.log(`[AppearanceRepo] 更新外观设置成功,影响行数: ${this.changes}`);
resolve(this.changes > 0);
}
});
});
return updateAppearanceSettingsInternal(db, settingsDto);
};
// 初始化时创建表 - Removed: Initialization is now handled in database.ts
// createTableIfNotExists();
/**
* 内部更新外观设置函数 (供内部调用,如初始化)
* @param db - Active database instance
* @param settingsDto - Data to update
* @returns Promise<boolean> - Success status
*/
// Internal function to update settings in the key-value table
const updateAppearanceSettingsInternal = async (db: sqlite3.Database, settingsDto: UpdateAppearanceDto): Promise<boolean> => {
const nowSeconds = Math.floor(Date.now() / 1000);
const sqlReplace = `INSERT OR REPLACE INTO ${TABLE_NAME} (key, value, updated_at) VALUES (?, ?, ?)`;
let changesMade = false;
try {
for (const key of Object.keys(settingsDto) as Array<keyof UpdateAppearanceDto>) {
const value = settingsDto[key];
let dbValue: string;
// Convert value to string for DB, handle null/undefined
if (value === null || value === undefined) {
dbValue = key === 'activeTerminalThemeId' ? 'null' : ''; // Store null specifically for theme ID
} else if (typeof value === 'object') {
dbValue = JSON.stringify(value);
} else {
dbValue = String(value);
}
// Special handling for activeTerminalThemeId to store 'null' string or number string
if (key === 'activeTerminalThemeId') {
dbValue = value === null ? 'null' : String(value);
}
// Validation for active_terminal_theme_id type before saving
if (key === 'activeTerminalThemeId' && value !== null && typeof value !== 'number') {
console.error(`[AppearanceRepo] 更新 activeTerminalThemeId 时收到无效类型值: ${value} (类型: ${typeof value}),应为数字或 null。跳过此字段。`);
continue; // Skip this key
}
// Execute INSERT OR REPLACE for each key-value pair
const result = await runDb(db, sqlReplace, [key, dbValue, nowSeconds]);
if (result.changes > 0) {
changesMade = true;
}
}
console.log(`[AppearanceRepo] 更新外观设置完成。是否有更改: ${changesMade}`);
return changesMade; // Return true if any row was inserted or replaced
} catch (err: any) {
console.error('更新外观设置失败:', err.message);
throw new Error('更新外观设置失败');
}
};
@@ -1,13 +1,15 @@
// packages/backend/src/repositories/audit.repository.ts
import { Database } from 'sqlite3';
import { getDb } from '../database';
// Import new async helpers and the instance getter
import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection';
import { AuditLogEntry, AuditLogActionType } from '../types/audit.types';
export class AuditLogRepository {
private db: Database;
// Define the expected row structure from the database if it matches AuditLogEntry
type DbAuditLogRow = AuditLogEntry;
constructor() {
this.db = getDb();
}
export class AuditLogRepository {
// Remove constructor or leave it empty
// constructor() { }
/**
* 添加一条审计日志记录
@@ -21,28 +23,24 @@ export class AuditLogRepository {
if (details) {
try {
detailsString = typeof details === 'string' ? details : JSON.stringify(details);
} catch (error) {
console.error(`[Audit Log] Failed to stringify details for action ${actionType}:`, error);
detailsString = JSON.stringify({ error: 'Failed to stringify details', originalDetails: details });
} catch (error: any) {
console.error(`[Audit Log] Failed to stringify details for action ${actionType}:`, error.message);
detailsString = JSON.stringify({ error: 'Failed to stringify details', originalDetails: String(details) }); // Ensure originalDetails is stringifiable
}
}
const sql = 'INSERT INTO audit_logs (timestamp, action_type, details) VALUES (?, ?, ?)';
const params = [timestamp, actionType, detailsString];
return new Promise((resolve, reject) => {
this.db.run(sql, params, (err) => {
if (err) {
console.error(`[Audit Log] Error adding log entry for action ${actionType}: ${err.message}`);
// 不拒绝 Promise,记录日志失败不应阻止核心操作
// 但可以在这里触发一个 SERVER_ERROR 通知或日志
resolve(); // Or potentially reject if logging is critical
} else {
// console.log(`[Audit Log] Logged action: ${actionType}`); // Optional: verbose logging
resolve();
}
});
});
try {
const db = await getDbInstance();
await runDb(db, sql, params);
// console.log(`[Audit Log] Logged action: ${actionType}`); // Optional: verbose logging
} catch (err: any) {
console.error(`[Audit Log] Error adding log entry for action ${actionType}: ${err.message}`);
// Decide if logging failure should throw an error or just be logged
// throw new Error(`Error adding log entry: ${err.message}`); // Uncomment to make it critical
}
}
/**
@@ -66,21 +64,9 @@ export class AuditLogRepository {
const params: (string | number)[] = [];
const countParams: (string | number)[] = [];
if (actionType) {
whereClauses.push('action_type = ?');
params.push(actionType);
countParams.push(actionType);
}
if (startDate) {
whereClauses.push('timestamp >= ?');
params.push(startDate);
countParams.push(startDate);
}
if (endDate) {
whereClauses.push('timestamp <= ?');
params.push(endDate);
countParams.push(endDate);
}
if (actionType) { whereClauses.push('action_type = ?'); params.push(actionType); countParams.push(actionType); }
if (startDate) { whereClauses.push('timestamp >= ?'); params.push(startDate); countParams.push(startDate); }
if (endDate) { whereClauses.push('timestamp <= ?'); params.push(endDate); countParams.push(endDate); }
if (whereClauses.length > 0) {
const whereSql = ` WHERE ${whereClauses.join(' AND ')}`;
@@ -91,22 +77,22 @@ export class AuditLogRepository {
baseSql += ' ORDER BY timestamp DESC LIMIT ? OFFSET ?';
params.push(limit, offset);
return new Promise((resolve, reject) => {
try {
const db = await getDbInstance();
// First get the total count
this.db.get(countSql, countParams, (err, row: { total: number }) => {
if (err) {
return reject(new Error(`Error counting audit logs: ${err.message}`));
}
const total = row.total;
const countRow = await getDbRow<{ total: number }>(db, countSql, countParams);
const total = countRow?.total ?? 0;
// Then get the paginated logs
this.db.all(baseSql, params, (err, rows: AuditLogEntry[]) => {
if (err) {
return reject(new Error(`Error fetching audit logs: ${err.message}`));
}
resolve({ logs: rows, total });
});
});
});
// Then get the paginated logs
const logs = await allDb<DbAuditLogRow>(db, baseSql, params);
return { logs, total };
} catch (err: any) {
console.error(`Error fetching audit logs:`, err.message);
throw new Error(`Error fetching audit logs: ${err.message}`);
}
}
}
// Export the class (Removed redundant export below as class is already exported)
// export { AuditLogRepository };
@@ -1,4 +1,5 @@
import { getDb } from '../database';
// packages/backend/src/repositories/command-history.repository.ts
import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection'; // Import new async helpers
// 定义命令历史记录的接口
export interface CommandHistoryEntry {
@@ -7,109 +8,95 @@ export interface CommandHistoryEntry {
timestamp: number; // Unix 时间戳 (秒)
}
// Define the expected row structure from the database if it matches CommandHistoryEntry
type DbCommandHistoryRow = CommandHistoryEntry;
/**
* 插入或更新一条命令历史记录。
* 如果命令已存在,则更新其时间戳;否则,插入新记录。
* @param command - 要添加或更新的命令字符串
* @returns 返回插入或更新记录的 ID
*/
export const upsertCommand = (command: string): Promise<number> => {
const db = getDb();
// 使用 INSERT ... ON CONFLICT DO UPDATE 语法 (SQLite 3.24.0+)
// 如果 command 列冲突 (假设我们为 command 列添加了 UNIQUE 约束,或者手动检查)
// 这里我们先不加 UNIQUE 约束,而是先尝试 UPDATE,再尝试 INSERT
export const upsertCommand = async (command: string): Promise<number> => {
const now = Math.floor(Date.now() / 1000); // 获取当前时间戳
const db = await getDbInstance();
return new Promise((resolve, reject) => {
try {
// 1. 尝试更新现有记录的时间戳
const updateSql = `UPDATE command_history SET timestamp = ? WHERE command = ?`;
db.run(updateSql, [now, command], function (updateErr) {
if (updateErr) {
console.error('更新命令历史记录时间戳时出错:', updateErr);
return reject(new Error('无法更新命令历史记录'));
}
const updateResult = await runDb(db, updateSql, [now, command]);
if (this.changes > 0) {
// 更新成功,需要获取被更新记录的 ID
const selectSql = `SELECT id FROM command_history WHERE command = ? ORDER BY timestamp DESC LIMIT 1`;
db.get(selectSql, [command], (selectErr, row: { id: number } | undefined) => {
if (selectErr) {
console.error('获取更新后记录 ID 时出错:', selectErr);
return reject(new Error('无法获取更新后的记录 ID'));
}
if (row) {
resolve(row.id);
} else {
// 理论上不应该发生,因为我们刚更新了它
reject(new Error('更新成功但无法找到记录 ID'));
}
});
if (updateResult.changes > 0) {
// 更新成功,需要获取被更新记录的 ID
const selectSql = `SELECT id FROM command_history WHERE command = ? ORDER BY timestamp DESC LIMIT 1`;
const row = await getDbRow<{ id: number }>(db, selectSql, [command]);
if (row) {
return row.id;
} else {
// 2. 没有记录被更新,说明命令不存在,执行插入
const insertSql = `INSERT INTO command_history (command, timestamp) VALUES (?, ?)`;
db.run(insertSql, [command, now], function (insertErr) {
if (insertErr) {
console.error('插入新命令历史记录时出错:', insertErr);
return reject(new Error('无法插入新命令历史记录'));
}
resolve(this.lastID); // 返回新插入的行 ID
});
// This case should theoretically not happen if update succeeded
throw new Error('更新成功但无法找到记录 ID');
}
});
});
} else {
// 2. 没有记录被更新,说明命令不存在,执行插入
const insertSql = `INSERT INTO command_history (command, timestamp) VALUES (?, ?)`;
const insertResult = await runDb(db, insertSql, [command, now]);
// Ensure lastID is valid before returning
if (typeof insertResult.lastID !== 'number' || insertResult.lastID <= 0) {
throw new Error('插入新命令历史记录后未能获取有效的 lastID');
}
return insertResult.lastID;
}
} catch (err: any) {
console.error('Upsert 命令历史记录时出错:', err.message);
throw new Error('无法更新或插入命令历史记录');
}
};
/**
* 获取所有命令历史记录,按时间戳升序排列(最旧的在前)
* @returns 返回包含所有历史记录条目的数组
*/
export const getAllCommands = (): Promise<CommandHistoryEntry[]> => {
const db = getDb();
export const getAllCommands = async (): Promise<CommandHistoryEntry[]> => {
const sql = `SELECT id, command, timestamp FROM command_history ORDER BY timestamp ASC`;
return new Promise((resolve, reject) => {
db.all(sql, [], (err, rows: CommandHistoryEntry[]) => {
if (err) {
console.error('获取命令历史记录时出错:', err);
return reject(new Error('无法获取命令历史记录'));
}
resolve(rows);
});
});
try {
const db = await getDbInstance();
const rows = await allDb<DbCommandHistoryRow>(db, sql);
return rows;
} catch (err: any) {
console.error('获取命令历史记录时出错:', err.message);
throw new Error('无法获取命令历史记录');
}
};
/**
* 根据 ID 删除指定的命令历史记录
* @param id - 要删除的记录 ID
* @returns 返回删除的行数 (通常是 1 或 0)
* @returns 返回是否成功删除 (true/false)
*/
export const deleteCommandById = (id: number): Promise<number> => {
const db = getDb();
export const deleteCommandById = async (id: number): Promise<boolean> => {
const sql = `DELETE FROM command_history WHERE id = ?`;
return new Promise((resolve, reject) => {
db.run(sql, [id], function (err) {
if (err) {
console.error('删除命令历史记录时出错:', err);
return reject(new Error('无法删除命令历史记录'));
}
resolve(this.changes); // 返回受影响的行数
});
});
try {
const db = await getDbInstance();
const result = await runDb(db, sql, [id]);
return result.changes > 0;
} catch (err: any) {
console.error('删除命令历史记录时出错:', err.message);
throw new Error('无法删除命令历史记录');
}
};
/**
* 清空所有命令历史记录
* @returns 返回删除的行数
*/
export const clearAllCommands = (): Promise<number> => {
const db = getDb();
export const clearAllCommands = async (): Promise<number> => {
const sql = `DELETE FROM command_history`;
return new Promise((resolve, reject) => {
db.run(sql, [], function (err) {
if (err) {
console.error('清空命令历史记录时出错:', err);
return reject(new Error('无法清空命令历史记录'));
}
resolve(this.changes); // 返回受影响的行数
});
});
try {
const db = await getDbInstance();
const result = await runDb(db, sql);
return result.changes; // Return the number of deleted rows
} catch (err: any) {
console.error('清空命令历史记录时出错:', err.message);
throw new Error('无法清空命令历史记录');
}
};
@@ -1,9 +1,12 @@
// packages/backend/src/repositories/connection.repository.ts
import { Database, Statement } from 'sqlite3';
import { getDb } from '../database';
// Import new async helpers and the instance getter
import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection';
const db = getDb();
// Remove top-level db instance
// const db = getDb();
// 定义 Connection 类型 (可以从 controller 或 types 文件导入,暂时在此定义)
// Define Connection 类型 (可以从 controller 或 types 文件导入,暂时在此定义)
// 注意:这里不包含加密字段,因为 Repository 不应处理解密
interface ConnectionBase {
id: number;
@@ -18,15 +21,36 @@ interface ConnectionBase {
last_connected_at: number | null;
}
interface ConnectionWithTags extends ConnectionBase {
// Type for the result of the JOIN query in findAllConnectionsWithTags and findConnectionByIdWithTags
interface ConnectionWithTagsRow extends ConnectionBase {
tag_ids_str: string | null; // Raw string from GROUP_CONCAT
}
export interface ConnectionWithTags extends ConnectionBase {
tag_ids: number[];
}
// 包含加密字段的完整类型,用于插入/更新
export interface FullConnectionData extends ConnectionBase { // <-- Added export
export interface FullConnectionData extends ConnectionBase {
encrypted_password?: string | null;
encrypted_private_key?: string | null;
encrypted_passphrase?: string | null;
// Include tag_ids for creation/update convenience if needed, handled separately
tag_ids?: number[];
}
// Type for the result of the JOIN query in findFullConnectionById
// Define a more specific type for the complex row structure
interface FullConnectionDbRow extends FullConnectionData {
proxy_db_id: number | null;
proxy_name: string | null;
proxy_type: string | null;
proxy_host: string | null;
proxy_port: number | null;
proxy_username: string | null;
proxy_encrypted_password?: string | null;
proxy_encrypted_private_key?: string | null;
proxy_encrypted_passphrase?: string | null;
}
@@ -34,162 +58,147 @@ export interface FullConnectionData extends ConnectionBase { // <-- Added export
* 获取所有连接及其标签
*/
export const findAllConnectionsWithTags = async (): Promise<ConnectionWithTags[]> => {
return new Promise((resolve, reject) => {
db.all(
`SELECT
c.id, c.name, c.host, c.port, c.username, c.auth_method, c.proxy_id,
c.created_at, c.updated_at, c.last_connected_at,
GROUP_CONCAT(ct.tag_id) as tag_ids_str
FROM connections c
LEFT JOIN connection_tags ct ON c.id = ct.connection_id
GROUP BY c.id
ORDER BY c.name ASC`,
(err, rows: any[]) => {
if (err) {
console.error('Repository: 查询连接列表时出错:', err.message);
return reject(new Error('获取连接列表失败'));
}
const processedRows = rows.map(row => ({
...row,
tag_ids: row.tag_ids_str ? row.tag_ids_str.split(',').map(Number) : []
}));
resolve(processedRows);
}
);
});
const sql = `
SELECT
c.id, c.name, c.host, c.port, c.username, c.auth_method, c.proxy_id,
c.created_at, c.updated_at, c.last_connected_at,
GROUP_CONCAT(ct.tag_id) as tag_ids_str
FROM connections c
LEFT JOIN connection_tags ct ON c.id = ct.connection_id
GROUP BY c.id
ORDER BY c.name ASC`;
try {
const db = await getDbInstance();
const rows = await allDb<ConnectionWithTagsRow>(db, sql);
// Safely map rows, handling potential null tag_ids_str
return rows.map(row => ({
...row,
tag_ids: row.tag_ids_str ? row.tag_ids_str.split(',').map(Number).filter(id => !isNaN(id)) : []
}));
} catch (err: any) {
console.error('Repository: 查询连接列表时出错:', err.message);
throw new Error('获取连接列表失败');
}
};
/**
* 根据 ID 获取单个连接及其标签
*/
export const findConnectionByIdWithTags = async (id: number): Promise<ConnectionWithTags | null> => {
return new Promise((resolve, reject) => {
db.get(
`SELECT
c.id, c.name, c.host, c.port, c.username, c.auth_method, c.proxy_id,
c.created_at, c.updated_at, c.last_connected_at,
GROUP_CONCAT(ct.tag_id) as tag_ids_str
FROM connections c
LEFT JOIN connection_tags ct ON c.id = ct.connection_id
WHERE c.id = ?
GROUP BY c.id`,
[id],
(err, row: any) => {
if (err) {
console.error(`Repository: 查询连接 ${id} 时出错:`, err.message);
return reject(new Error('获取连接信息失败'));
}
if (row) {
row.tag_ids = row.tag_ids_str ? row.tag_ids_str.split(',').map(Number) : [];
delete row.tag_ids_str;
resolve(row);
} else {
resolve(null);
}
}
);
});
const sql = `
SELECT
c.id, c.name, c.host, c.port, c.username, c.auth_method, c.proxy_id,
c.created_at, c.updated_at, c.last_connected_at,
GROUP_CONCAT(ct.tag_id) as tag_ids_str
FROM connections c
LEFT JOIN connection_tags ct ON c.id = ct.connection_id
WHERE c.id = ?
GROUP BY c.id`;
try {
const db = await getDbInstance();
const row = await getDbRow<ConnectionWithTagsRow>(db, sql, [id]);
if (row && typeof row.id !== 'undefined') { // Check if a valid row was found
return {
...row,
tag_ids: row.tag_ids_str ? row.tag_ids_str.split(',').map(Number).filter(id => !isNaN(id)) : []
};
} else {
return null;
}
} catch (err: any) {
console.error(`Repository: 查询连接 ${id} 时出错:`, err.message);
throw new Error('获取连接信息失败');
}
};
/**
* 根据 ID 获取单个连接的完整信息 (包括加密字段)
* 用于更新或测试连接等需要完整信息的场景
* 根据 ID 获取单个连接的完整信息 (包括加密字段和代理信息)
*/
export const findFullConnectionById = async (id: number): Promise<any | null> => {
return new Promise((resolve, reject) => {
// 查询连接信息,并 LEFT JOIN 代理信息 (因为测试连接需要)
// 注意:这里返回的结构比较复杂,服务层需要处理
db.get(
`SELECT
c.*, -- 选择 connections 表所有列
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.encrypted_password as proxy_encrypted_password,
p.encrypted_private_key as proxy_encrypted_private_key, -- 包含代理的 key
p.encrypted_passphrase as proxy_encrypted_passphrase -- 包含代理的 passphrase
FROM connections c
LEFT JOIN proxies p ON c.proxy_id = p.id
WHERE c.id = ?`,
[id],
(err, row: any) => {
if (err) {
console.error(`Repository: 查询连接 ${id} 详细信息时出错:`, err.message);
return reject(new Error('获取连接详细信息失败'));
}
resolve(row || null);
}
);
});
export const findFullConnectionById = async (id: number): Promise<FullConnectionDbRow | null> => {
const sql = `
SELECT
c.*, -- 选择 connections 表所有列
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.encrypted_password as proxy_encrypted_password,
p.encrypted_private_key as proxy_encrypted_private_key,
p.encrypted_passphrase as proxy_encrypted_passphrase
FROM connections c
LEFT JOIN proxies p ON c.proxy_id = p.id
WHERE c.id = ?`;
try {
const db = await getDbInstance();
const row = await getDbRow<FullConnectionDbRow>(db, sql, [id]);
return row || null;
} catch (err: any) {
console.error(`Repository: 查询连接 ${id} 详细信息时出错:`, err.message);
throw new Error('获取连接详细信息失败');
}
};
/**
* 创建新连接
* 创建新连接 (不处理标签)
*/
// Update function signature to accept name as string | null
export const createConnection = async (data: Omit<FullConnectionData, 'id' | 'created_at' | 'updated_at' | 'last_connected_at'> & { name: string | null }): Promise<number> => {
return new Promise((resolve, reject) => {
const now = Math.floor(Date.now() / 1000);
const stmt = 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
);
stmt.run(
data.name ?? null, // Ensure null is passed if name is null/undefined
data.host, data.port, data.username, data.auth_method,
data.encrypted_password ?? null, data.encrypted_private_key ?? null, data.encrypted_passphrase ?? null,
data.proxy_id ?? null,
now, now,
function (this: Statement, err: Error | null) {
stmt.finalize(); // 确保 finalize 被调用
if (err) {
console.error('Repository: 插入连接时出错:', err.message);
return reject(new Error('创建连接记录失败'));
}
resolve((this as any).lastID);
}
);
});
export const createConnection = async (data: Omit<FullConnectionData, 'id' | 'created_at' | 'updated_at' | 'last_connected_at' | 'tag_ids'>): Promise<number> => {
const now = Math.floor(Date.now() / 1000);
const sql = `
INSERT INTO connections (name, host, port, username, auth_method, encrypted_password, encrypted_private_key, encrypted_passphrase, proxy_id, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
const params = [
data.name ?? null,
data.host, data.port, data.username, data.auth_method,
data.encrypted_password ?? null, data.encrypted_private_key ?? null, data.encrypted_passphrase ?? null,
data.proxy_id ?? null,
now, now
];
try {
const db = await getDbInstance();
const result = await runDb(db, sql, params);
// Ensure lastID is valid before returning
if (typeof result.lastID !== 'number' || result.lastID <= 0) {
throw new Error('创建连接后未能获取有效的 lastID');
}
return result.lastID;
} catch (err: any) {
console.error('Repository: 插入连接时出错:', err.message);
throw new Error('创建连接记录失败');
}
};
/**
* 更新连接信息
* 更新连接信息 (不处理标签)
*/
// Update function signature to accept name as string | null | undefined
export const updateConnection = async (id: number, data: Partial<Omit<FullConnectionData, 'id' | 'created_at' | 'last_connected_at'> & { name?: string | null }>): Promise<boolean> => {
export const updateConnection = async (id: number, data: Partial<Omit<FullConnectionData, 'id' | 'created_at' | 'last_connected_at' | 'tag_ids'>>): Promise<boolean> => {
const fieldsToUpdate: { [key: string]: any } = { ...data };
const params: any[] = [];
// 移除 id, created_at, last_connected_at (不应通过此方法更新)
delete fieldsToUpdate.id;
delete fieldsToUpdate.created_at;
delete fieldsToUpdate.last_connected_at;
delete fieldsToUpdate.tag_ids; // Tags handled separately
// 设置 updated_at
fieldsToUpdate.updated_at = Math.floor(Date.now() / 1000);
const setClauses = Object.keys(fieldsToUpdate).map(key => `${key} = ?`).join(', ');
Object.values(fieldsToUpdate).forEach(value => params.push(value ?? null)); // 处理 undefined 为 null
Object.values(fieldsToUpdate).forEach(value => params.push(value ?? null));
if (!setClauses) {
return false; // 没有要更新的字段
console.warn(`[Repository] updateConnection called for ID ${id} with no fields to update.`);
return false;
}
params.push(id); // 添加 WHERE id = ? 的参数
params.push(id);
const sql = `UPDATE connections SET ${setClauses} WHERE id = ?`;
return new Promise((resolve, reject) => {
const stmt = db.prepare(
`UPDATE connections SET ${setClauses} WHERE id = ?`
);
stmt.run(...params, function (this: Statement, err: Error | null) {
stmt.finalize();
if (err) {
console.error(`Repository: 更新连接 ${id} 时出错:`, err.message);
return reject(new Error('更新连接记录失败'));
}
resolve((this as any).changes > 0);
});
});
try {
const db = await getDbInstance();
const result = await runDb(db, sql, params);
return result.changes > 0;
} catch (err: any) {
console.error(`Repository: 更新连接 ${id} 时出错:`, err.message);
throw new Error('更新连接记录失败');
}
};
@@ -197,116 +206,97 @@ export const updateConnection = async (id: number, data: Partial<Omit<FullConnec
* 删除连接
*/
export const deleteConnection = async (id: number): Promise<boolean> => {
return new Promise((resolve, reject) => {
const stmt = db.prepare(
`DELETE FROM connections WHERE id = ?`
);
stmt.run(id, function (this: Statement, err: Error | null) {
stmt.finalize();
if (err) {
console.error(`Repository: 删除连接 ${id} 时出错:`, err.message);
return reject(new Error('删除连接记录失败'));
}
resolve((this as any).changes > 0);
});
});
const sql = `DELETE FROM connections WHERE id = ?`;
try {
const db = await getDbInstance();
// ON DELETE CASCADE in connection_tags and ON DELETE SET NULL for proxy_id handle related data
const result = await runDb(db, sql, [id]);
return result.changes > 0;
} catch (err: any) {
console.error(`Repository: 删除连接 ${id} 时出错:`, err.message);
throw new Error('删除连接记录失败');
}
};
/**
* 更新连接的标签关联
* 更新连接的标签关联 (使用事务)
* @param connectionId 连接 ID
* @param tagIds 新的标签 ID 数组 (空数组表示清除所有标签)
*/
export const updateConnectionTags = async (connectionId: number, tagIds: number[]): Promise<void> => {
const deleteStmt = db.prepare(`DELETE FROM connection_tags WHERE connection_id = ?`);
const insertStmt = db.prepare(`INSERT INTO connection_tags (connection_id, tag_id) VALUES (?, ?)`);
const db = await getDbInstance();
// Use a transaction to ensure atomicity
try {
await runDb(db, 'BEGIN TRANSACTION');
return new Promise((resolve, reject) => {
db.serialize(() => {
db.run('BEGIN TRANSACTION');
try {
// 1. 删除旧关联
deleteStmt.run(connectionId, (err: Error | null) => {
if (err) throw err;
});
deleteStmt.finalize();
// 1. Delete old associations
await runDb(db, `DELETE FROM connection_tags WHERE connection_id = ?`, [connectionId]);
// 2. 插入新关联 (如果 tagIds 不为空)
if (tagIds.length > 0) {
tagIds.forEach((tagId: any) => {
if (typeof tagId === 'number' && tagId > 0) {
insertStmt.run(connectionId, tagId, (err: Error | null) => {
if (err) throw err;
});
} else {
console.warn(`Repository: 更新连接 ${connectionId} 标签时,提供的 tag_id 无效: ${tagId}`);
}
});
}
insertStmt.finalize();
db.run('COMMIT', (commitErr: Error | null) => {
if (commitErr) throw commitErr;
resolve(); // 事务成功
});
} catch (tagError: any) {
console.error(`Repository: 更新连接 ${connectionId} 的标签关联时出错:`, tagError);
db.run('ROLLBACK');
reject(new Error('处理标签关联失败'));
}
});
});
// 2. Insert new associations (if any)
if (tagIds.length > 0) {
const insertSql = `INSERT INTO connection_tags (connection_id, tag_id) VALUES (?, ?)`;
// Use Promise.all for potentially better performance, though sequential inserts are safer for constraints
const insertPromises = tagIds
.filter(tagId => typeof tagId === 'number' && tagId > 0) // Basic validation
.map(tagId => runDb(db, insertSql, [connectionId, tagId]).catch(err => {
// Log warning but don't fail the whole transaction for a single tag insert error (e.g., invalid tag ID)
console.warn(`Repository: 更新连接 ${connectionId} 标签时,插入 tag_id ${tagId} 失败: ${err.message}`);
}));
await Promise.all(insertPromises);
}
await runDb(db, 'COMMIT');
} catch (err: any) {
console.error(`Repository: 更新连接 ${connectionId} 的标签关联时出错:`, err.message);
try {
await runDb(db, 'ROLLBACK'); // Attempt to rollback on error
} catch (rollbackErr: any) {
console.error(`Repository: 回滚连接 ${connectionId} 的标签更新事务失败:`, rollbackErr.message);
}
throw new Error('处理标签关联失败'); // Re-throw original error
}
};
/**
* 批量插入连接(用于导入)
* 注意:此函数应在事务中调用
* 注意:此函数应在事务中调用 (由调用者负责事务)
* Returns an array mapping new connection IDs to their original import data (for tag association)
*/
export const bulkInsertConnections = async (connections: Omit<FullConnectionData, 'id' | 'created_at' | 'updated_at' | 'last_connected_at'>[]): Promise<{ connectionId: number, originalData: any }[]> => {
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 (?, ?)`);
export const bulkInsertConnections = async (
db: Database, // Pass the transaction-aware db instance
connections: Array<Omit<FullConnectionData, 'id' | 'created_at' | 'updated_at' | 'last_connected_at'> & { tag_ids?: number[] }>
): Promise<{ connectionId: number, originalData: any }[]> => {
const insertConnSql = `INSERT INTO connections (name, host, port, username, auth_method, encrypted_password, encrypted_private_key, encrypted_passphrase, proxy_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
const results: { connectionId: number, originalData: any }[] = [];
const now = Math.floor(Date.now() / 1000);
try {
for (const connData of connections) {
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,
connData.proxy_id || null,
now, now,
function (this: Statement, err: Error | null) {
if (err) return reject(new Error(`插入连接 "${connData.name}" 时出错: ${err.message}`));
resolve({ lastID: (this as any).lastID });
}
);
});
const newConnectionId = connResult.lastID;
results.push({ connectionId: newConnectionId, originalData: connData }); // Store ID and original data for tag association
// Prepare statement outside the loop for efficiency (though sqlite3 might cache implicitly)
// Using direct runDb might be simpler here unless performance is critical
// 处理标签关联 (在同一个事务中)
if (Array.isArray((connData as any).tag_ids) && (connData as any).tag_ids.length > 0) {
for (const tagId of (connData as any).tag_ids) {
if (typeof tagId === 'number' && tagId > 0) {
await new Promise<void>((resolve, reject) => {
insertTagStmt.run(newConnectionId, tagId, (err: Error | null) => {
if (err) {
// 警告但不中断整个导入
console.warn(`Repository: 导入连接 ${connData.name}: 关联标签 ID ${tagId} 失败: ${err.message}`);
}
resolve();
});
});
}
}
for (const connData of connections) {
const params = [
connData.name ?? null, connData.host, connData.port, connData.username, connData.auth_method,
connData.encrypted_password || null,
connData.encrypted_private_key || null,
connData.encrypted_passphrase || null,
connData.proxy_id || null,
now, now
];
try {
// Use the passed db instance (which should be in a transaction)
const connResult = await runDb(db, insertConnSql, params);
if (typeof connResult.lastID !== 'number' || connResult.lastID <= 0) {
throw new Error(`插入连接 "${connData.name}" 后未能获取有效的 lastID`);
}
results.push({ connectionId: connResult.lastID, originalData: connData });
} catch (err: any) {
// Log error but continue with other connections? Or re-throw to fail the whole batch?
console.error(`Repository: 批量插入连接 "${connData.name}" 时出错: ${err.message}`);
// Decide on error handling strategy for batch operations
throw new Error(`批量插入连接 "${connData.name}" 失败`); // Fail fast for now
}
return results;
} finally {
// Finalize statements after the loop
insertConnStmt.finalize();
insertTagStmt.finalize();
}
return results;
// Tag insertion should be handled separately after connections are inserted, using the returned IDs
};
@@ -1,5 +1,7 @@
// packages/backend/src/repositories/notification.repository.ts
import { Database } from 'sqlite3';
import { getDb } from '../database';
// Import new async helpers and the instance getter
import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection';
import { NotificationSetting, RawNotificationSetting, NotificationChannelType, NotificationEvent, NotificationChannelConfig } from '../types/notification.types';
// Helper to parse raw data from DB
@@ -11,85 +13,81 @@ const parseRawSetting = (raw: RawNotificationSetting): NotificationSetting => {
config: JSON.parse(raw.config || '{}'),
enabled_events: JSON.parse(raw.enabled_events || '[]'),
};
} catch (error) {
console.error(`Error parsing notification setting ID ${raw.id}:`, error);
// Return a default/error state or re-throw, depending on desired handling
// For now, return partially parsed with defaults for JSON fields
// Cast to satisfy type checker, but this indicates a parsing error.
} catch (error: any) { // Add type annotation
console.error(`Error parsing notification setting ID ${raw.id}:`, error.message);
return {
...raw,
enabled: Boolean(raw.enabled),
config: {} as NotificationChannelConfig, // Config is invalid due to parsing error
config: {} as NotificationChannelConfig, // Indicate parsing error
enabled_events: [],
};
}
};
export class NotificationSettingsRepository {
private db: Database;
constructor() {
this.db = getDb();
}
// Remove constructor or leave it empty
// constructor() { }
async getAll(): Promise<NotificationSetting[]> {
return new Promise((resolve, reject) => {
this.db.all('SELECT * FROM notification_settings ORDER BY created_at ASC', (err, rows: RawNotificationSetting[]) => {
if (err) {
return reject(new Error(`Error fetching notification settings: ${err.message}`));
}
resolve(rows.map(parseRawSetting));
});
});
try {
const db = await getDbInstance();
const rows = await allDb<RawNotificationSetting>(db, 'SELECT * FROM notification_settings ORDER BY created_at ASC');
return rows.map(parseRawSetting);
} catch (err: any) {
console.error(`Error fetching notification settings:`, err.message);
throw new Error(`Error fetching notification settings: ${err.message}`);
}
}
async getById(id: number): Promise<NotificationSetting | null> {
return new Promise((resolve, reject) => {
this.db.get('SELECT * FROM notification_settings WHERE id = ?', [id], (err, row: RawNotificationSetting) => {
if (err) {
return reject(new Error(`Error fetching notification setting by ID ${id}: ${err.message}`));
}
resolve(row ? parseRawSetting(row) : null);
});
});
try {
const db = await getDbInstance();
const row = await getDbRow<RawNotificationSetting>(db, 'SELECT * FROM notification_settings WHERE id = ?', [id]);
return row ? parseRawSetting(row) : null;
} catch (err: any) {
console.error(`Error fetching notification setting by ID ${id}:`, err.message);
throw new Error(`Error fetching notification setting by ID ${id}: ${err.message}`);
}
}
async getEnabledByEvent(event: NotificationEvent): Promise<NotificationSetting[]> {
return new Promise((resolve, reject) => {
// Note: This query is inefficient as it fetches all enabled settings and filters in code.
// For better performance with many settings, consider normalizing enabled_events
// or using JSON functions if the SQLite version supports them well.
this.db.all('SELECT * FROM notification_settings WHERE enabled = 1', (err, rows: RawNotificationSetting[]) => {
if (err) {
return reject(new Error(`Error fetching enabled notification settings: ${err.message}`));
}
const parsedRows = rows.map(parseRawSetting);
const filteredRows = parsedRows.filter(setting => setting.enabled_events.includes(event));
resolve(filteredRows);
});
});
// Note: Query remains inefficient, consider optimization later if needed.
try {
const db = await getDbInstance();
const rows = await allDb<RawNotificationSetting>(db, 'SELECT * FROM notification_settings WHERE enabled = 1');
const parsedRows = rows.map(parseRawSetting);
const filteredRows = parsedRows.filter(setting => setting.enabled_events.includes(event));
return filteredRows;
} catch (err: any) {
console.error(`Error fetching enabled notification settings:`, err.message);
throw new Error(`Error fetching enabled notification settings: ${err.message}`);
}
}
async create(setting: Omit<NotificationSetting, 'id' | 'created_at' | 'updated_at'>): Promise<number> {
const sql = `
INSERT INTO notification_settings (channel_type, name, enabled, config, enabled_events)
VALUES (?, ?, ?, ?, ?)
`;
INSERT INTO notification_settings (channel_type, name, enabled, config, enabled_events, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, strftime('%s', 'now'), strftime('%s', 'now'))
`; // Added created_at, updated_at
const params = [
setting.channel_type,
setting.name,
setting.name ?? '', // Ensure name is not undefined
setting.enabled ? 1 : 0,
JSON.stringify(setting.config || {}),
JSON.stringify(setting.enabled_events || [])
];
return new Promise((resolve, reject) => {
this.db.run(sql, params, function (err) { // Use function() to access this.lastID
if (err) {
return reject(new Error(`Error creating notification setting: ${err.message}`));
}
resolve(this.lastID);
});
});
try {
const db = await getDbInstance();
const result = await runDb(db, sql, params);
// Ensure lastID is valid before returning
if (typeof result.lastID !== 'number' || result.lastID <= 0) {
throw new Error('创建通知设置后未能获取有效的 lastID');
}
return result.lastID;
} catch (err: any) {
console.error(`Error creating notification setting:`, err.message);
throw new Error(`Error creating notification setting: ${err.message}`);
}
}
async update(id: number, setting: Partial<Omit<NotificationSetting, 'id' | 'created_at' | 'updated_at'>>): Promise<boolean> {
@@ -97,29 +95,16 @@ export class NotificationSettingsRepository {
const fields: string[] = [];
const params: (string | number | null)[] = [];
if (setting.channel_type !== undefined) {
fields.push('channel_type = ?');
params.push(setting.channel_type);
}
if (setting.name !== undefined) {
fields.push('name = ?');
params.push(setting.name);
}
if (setting.enabled !== undefined) {
fields.push('enabled = ?');
params.push(setting.enabled ? 1 : 0);
}
if (setting.config !== undefined) {
fields.push('config = ?');
params.push(JSON.stringify(setting.config || {}));
}
if (setting.enabled_events !== undefined) {
fields.push('enabled_events = ?');
params.push(JSON.stringify(setting.enabled_events || []));
}
// Dynamically build SET clauses
if (setting.channel_type !== undefined) { fields.push('channel_type = ?'); params.push(setting.channel_type); }
if (setting.name !== undefined) { fields.push('name = ?'); params.push(setting.name); }
if (setting.enabled !== undefined) { fields.push('enabled = ?'); params.push(setting.enabled ? 1 : 0); }
if (setting.config !== undefined) { fields.push('config = ?'); params.push(JSON.stringify(setting.config || {})); }
if (setting.enabled_events !== undefined) { fields.push('enabled_events = ?'); params.push(JSON.stringify(setting.enabled_events || [])); }
if (fields.length === 0) {
return Promise.resolve(true); // Nothing to update
console.warn(`[NotificationRepo] update called for ID ${id} with no fields to update.`);
return true; // Or false, depending on desired behavior for no-op update
}
fields.push('updated_at = strftime(\'%s\', \'now\')'); // Always update timestamp
@@ -127,25 +112,28 @@ export class NotificationSettingsRepository {
const sql = `UPDATE notification_settings SET ${fields.join(', ')} WHERE id = ?`;
params.push(id);
return new Promise((resolve, reject) => {
this.db.run(sql, params, function (err) { // Use function() to access this.changes
if (err) {
return reject(new Error(`Error updating notification setting ID ${id}: ${err.message}`));
}
resolve(this.changes > 0);
});
});
try {
const db = await getDbInstance();
const result = await runDb(db, sql, params);
return result.changes > 0;
} catch (err: any) {
console.error(`Error updating notification setting ID ${id}:`, err.message);
throw new Error(`Error updating notification setting ID ${id}: ${err.message}`);
}
}
async delete(id: number): Promise<boolean> {
const sql = 'DELETE FROM notification_settings WHERE id = ?';
return new Promise((resolve, reject) => {
this.db.run(sql, [id], function (err) { // Use function() to access this.changes
if (err) {
return reject(new Error(`Error deleting notification setting ID ${id}: ${err.message}`));
}
resolve(this.changes > 0);
});
});
try {
const db = await getDbInstance();
const result = await runDb(db, sql, [id]);
return result.changes > 0;
} catch (err: any) {
console.error(`Error deleting notification setting ID ${id}:`, err.message);
throw new Error(`Error deleting notification setting ID ${id}: ${err.message}`);
}
}
}
// Export the class (Removed redundant export below as class is already exported)
// export { NotificationSettingsRepository };
@@ -1,5 +1,7 @@
// packages/backend/src/repositories/passkey.repository.ts
import { Database } from 'sqlite3';
import { getDb } from '../database';
// Import new async helpers and the instance getter
import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection';
// 定义 Passkey 数据库记录的接口
export interface PasskeyRecord {
@@ -13,20 +15,15 @@ export interface PasskeyRecord {
updated_at: number;
}
export class PasskeyRepository {
private db: Database;
// Define the expected row structure from the database if it matches PasskeyRecord
type DbPasskeyRow = PasskeyRecord;
constructor() {
this.db = getDb();
}
export class PasskeyRepository {
// Remove constructor or leave it empty, db instance will be fetched in each method
// constructor() { }
/**
* 保存新的 Passkey 凭证
* @param credentialId Base64URL 编码的凭证 ID
* @param publicKey Base64URL 编码的公钥
* @param counter 签名计数器
* @param transports 传输方式 (JSON 字符串)
* @param name 用户提供的名称 (可选)
* @returns Promise<number> 新插入记录的 ID
*/
async savePasskey(
@@ -40,136 +37,147 @@ export class PasskeyRepository {
INSERT INTO passkeys (credential_id, public_key, counter, transports, name, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, strftime('%s', 'now'), strftime('%s', 'now'))
`;
return new Promise((resolve, reject) => {
this.db.run(sql, [credentialId, publicKey, counter, transports, name ?? null], function (err) {
if (err) {
console.error('保存 Passkey 时出错:', err.message);
return reject(new Error(`保存 Passkey 时出错: ${err.message}`));
}
resolve(this.lastID);
});
});
const params = [credentialId, publicKey, counter, transports, name ?? null];
try {
const db = await getDbInstance();
const result = await runDb(db, sql, params);
// Ensure lastID is valid before returning
if (typeof result.lastID !== 'number' || result.lastID <= 0) {
throw new Error('保存 Passkey 后未能获取有效的 lastID');
}
return result.lastID;
} catch (err: any) {
console.error('保存 Passkey 时出错:', err.message);
// Handle potential UNIQUE constraint errors on credential_id
if (err.message.includes('UNIQUE constraint failed')) {
throw new Error(`Credential ID "${credentialId}" 已存在。`);
}
throw new Error(`保存 Passkey 时出错: ${err.message}`);
}
}
/**
* 根据 Credential ID 获取 Passkey 记录
* @param credentialId Base64URL 编码的凭证 ID
* @returns Promise<PasskeyRecord | null> 找到的记录或 null
*/
async getPasskeyByCredentialId(credentialId: string): Promise<PasskeyRecord | null> {
const sql = `SELECT * FROM passkeys WHERE credential_id = ?`;
return new Promise((resolve, reject) => {
this.db.get(sql, [credentialId], (err, row: PasskeyRecord) => {
if (err) {
console.error('按 Credential ID 获取 Passkey 时出错:', err.message);
return reject(new Error(`按 Credential ID 获取 Passkey 时出错: ${err.message}`));
}
resolve(row || null);
});
});
try {
const db = await getDbInstance();
const row = await getDbRow<DbPasskeyRow>(db, sql, [credentialId]);
return row || null;
} catch (err: any) {
console.error('按 Credential ID 获取 Passkey 时出错:', err.message);
throw new Error(`按 Credential ID 获取 Passkey 时出错: ${err.message}`);
}
}
/**
* 获取所有已注册的 Passkey 记录
* @returns Promise<PasskeyRecord[]> 所有记录的数组
* 获取所有已注册的 Passkey 记录 (仅选择必要字段)
* @returns Promise<Partial<PasskeyRecord>[]> 所有记录的部分信息的数组
*/
async getAllPasskeys(): Promise<PasskeyRecord[]> {
const sql = `SELECT id, credential_id, name, transports, created_at FROM passkeys ORDER BY created_at DESC`; // 仅选择必要字段
return new Promise((resolve, reject) => {
this.db.all(sql, [], (err, rows: PasskeyRecord[]) => {
if (err) {
console.error('获取所有 Passkey 时出错:', err.message);
return reject(new Error(`获取所有 Passkey 时出错: ${err.message}`));
}
resolve(rows);
});
});
// Adjust return type based on selected columns
async getAllPasskeys(): Promise<Array<Pick<PasskeyRecord, 'id' | 'credential_id' | 'name' | 'transports' | 'created_at'>>> {
const sql = `SELECT id, credential_id, name, transports, created_at FROM passkeys ORDER BY created_at DESC`;
try {
const db = await getDbInstance();
// Adjust the generic type for allDb to match the selected columns
const rows = await allDb<Pick<PasskeyRecord, 'id' | 'credential_id' | 'name' | 'transports' | 'created_at'>>(db, sql);
return rows;
} catch (err: any) {
console.error('获取所有 Passkey 时出错:', err.message);
throw new Error(`获取所有 Passkey 时出错: ${err.message}`);
}
}
/**
* 更新 Passkey 的签名计数器
* @param credentialId Base64URL 编码的凭证 ID
* @param newCounter 新的计数器值
* @returns Promise<void>
*/
async updatePasskeyCounter(credentialId: string, newCounter: number): Promise<void> {
const sql = `UPDATE passkeys SET counter = ?, updated_at = strftime('%s', 'now') WHERE credential_id = ?`;
return new Promise((resolve, reject) => {
this.db.run(sql, [newCounter, credentialId], function (err) {
if (err) {
console.error('更新 Passkey 计数器时出错:', err.message);
return reject(new Error(`更新 Passkey 计数器时出错: ${err.message}`));
}
if (this.changes === 0) {
return reject(new Error(`未找到 Credential ID 为 ${credentialId} 的 Passkey 进行更新`));
}
resolve();
});
});
try {
const db = await getDbInstance();
const result = await runDb(db, sql, [newCounter, credentialId]);
if (result.changes === 0) {
// Consider if this should be an error or just a warning/no-op
console.warn(`未找到 Credential ID 为 ${credentialId} 的 Passkey 进行计数器更新`);
// throw new Error(`未找到 Credential ID 为 ${credentialId} 的 Passkey 进行更新`);
}
} catch (err: any) {
console.error('更新 Passkey 计数器时出错:', err.message);
throw new Error(`更新 Passkey 计数器时出错: ${err.message}`);
}
}
/**
* 根据 ID 删除 Passkey
* @param id Passkey 记录的 ID
* @returns Promise<void>
* @returns Promise<boolean> 是否成功删除
*/
async deletePasskeyById(id: number): Promise<void> {
async deletePasskeyById(id: number): Promise<boolean> {
const sql = `DELETE FROM passkeys WHERE id = ?`;
return new Promise((resolve, reject) => {
this.db.run(sql, [id], function (err) {
if (err) {
console.error('按 ID 删除 Passkey 时出错:', err.message);
return reject(new Error(`ID 删除 Passkey 时出错: ${err.message}`));
}
if (this.changes === 0) {
return reject(new Error(`未找到 ID ${id} 的 Passkey 进行删除`));
}
console.log(`ID 为 ${id} 的 Passkey 已删除。`);
resolve();
});
});
try {
const db = await getDbInstance();
const result = await runDb(db, sql, [id]);
if (result.changes > 0) {
console.log(`ID ${id} 的 Passkey 已删除。`);
return true;
} else {
console.warn(`尝试删除不存在的 Passkey ID: ${id}`);
return false;
}
} catch (err: any) {
console.error('按 ID 删除 Passkey 时出错:', err.message);
throw new Error(`按 ID 删除 Passkey 时出错: ${err.message}`);
}
}
/**
* 根据 Credential ID 删除 Passkey
* @param credentialId Base64URL 编码的凭证 ID
* @returns Promise<void>
* @returns Promise<boolean> 是否成功删除
*/
async deletePasskeyByCredentialId(credentialId: string): Promise<void> {
async deletePasskeyByCredentialId(credentialId: string): Promise<boolean> {
const sql = `DELETE FROM passkeys WHERE credential_id = ?`;
return new Promise((resolve, reject) => {
this.db.run(sql, [credentialId], function (err) {
if (err) {
console.error('按 Credential ID 删除 Passkey 时出错:', err.message);
return reject(new Error(`Credential ID 删除 Passkey 时出错: ${err.message}`));
}
if (this.changes === 0) {
// It's possible the user tries to delete a non-existent key, maybe not an error?
console.warn(`尝试删除不存在的 Credential ID: ${credentialId}`);
} else {
console.log(`Credential ID 为 ${credentialId} 的 Passkey 已删除。`);
}
resolve();
});
});
try {
const db = await getDbInstance();
const result = await runDb(db, sql, [credentialId]);
if (result.changes > 0) {
console.log(`Credential ID ${credentialId} 的 Passkey 已删除。`);
return true;
} else {
console.warn(`尝试删除不存在的 Credential ID: ${credentialId}`);
return false;
}
} catch (err: any) {
console.error('按 Credential ID 删除 Passkey 时出错:', err.message);
throw new Error(`按 Credential ID 删除 Passkey 时出错: ${err.message}`);
}
}
/**
* 根据 credential_id 或 name 前缀模糊查找 Passkey 记录(自动补全)
* @param prefix 前缀字符串
* @returns Promise<PasskeyRecord[]> 匹配的记录数组
*/
async searchPasskeyByPrefix(prefix: string): Promise<PasskeyRecord[]> {
// Adjust return type based on selected columns if not selecting all (*)
async searchPasskeyByPrefix(prefix: string): Promise<DbPasskeyRow[]> {
const sql = `SELECT * FROM passkeys WHERE credential_id LIKE ? OR name LIKE ? ORDER BY created_at DESC`;
const likePrefix = `${prefix}%`;
return new Promise((resolve, reject) => {
this.db.all(sql, [likePrefix, likePrefix], (err, rows: PasskeyRecord[]) => {
if (err) {
console.error('模糊查找 Passkey 时出错:', err.message);
return reject(new Error(`模糊查找 Passkey 时出错: ${err.message}`));
}
resolve(rows);
});
});
try {
const db = await getDbInstance();
const rows = await allDb<DbPasskeyRow>(db, sql, [likePrefix, likePrefix]);
return rows;
} catch (err: any) {
console.error('模糊查找 Passkey 时出错:', err.message);
throw new Error(`模糊查找 Passkey 时出错: ${err.message}`);
}
}
}
// Export an instance or the class itself depending on usage pattern
// If used as a singleton service, export an instance:
// export const passkeyRepository = new PasskeyRepository();
// If instantiated elsewhere (e.g., dependency injection), export the class:
// export { PasskeyRepository };
// For now, let's assume it's used like other repositories (exporting functions/class)
// Exporting the class seems more appropriate given its structure
// Removed redundant export below as the class is already exported with 'export class'
@@ -1,7 +1,10 @@
// packages/backend/src/repositories/proxy.repository.ts
import { Database, Statement } from 'sqlite3';
import { getDb } from '../database';
// Import new async helpers and the instance getter
import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection';
const db = getDb();
// Remove top-level db instance
// const db = getDb();
// 定义 Proxy 类型 (可以共享到 types 文件)
export interface ProxyData {
@@ -19,83 +22,83 @@ export interface ProxyData {
updated_at: number;
}
// Define the expected row structure from the database if it matches ProxyData
type DbProxyRow = ProxyData;
/**
* 根据名称、类型、主机和端口查找代理
*/
export const findProxyByNameTypeHostPort = async (name: string, type: string, host: string, port: number): Promise<{ id: number } | undefined> => {
return new Promise((resolve, reject) => {
db.get(
`SELECT id FROM proxies WHERE name = ? AND type = ? AND host = ? AND port = ?`,
[name, type, host, port],
(err: Error | null, row: { id: number } | undefined) => {
if (err) {
console.error(`Repository: 查找代理时出错 (name=${name}, type=${type}, host=${host}, port=${port}):`, err.message);
return reject(new Error(`查找代理时出错: ${err.message}`));
}
resolve(row);
}
);
});
const sql = `SELECT id FROM proxies WHERE name = ? AND type = ? AND host = ? AND port = ?`;
try {
const db = await getDbInstance();
const row = await getDbRow<{ id: number }>(db, sql, [name, type, host, port]);
return row;
} catch (err: any) {
console.error(`Repository: 查找代理时出错 (name=${name}, type=${type}, host=${host}, port=${port}):`, err.message);
throw new Error(`查找代理时出错: ${err.message}`);
}
};
/**
* 创建新代理
*/
export const createProxy = async (data: Omit<ProxyData, 'id' | 'created_at' | 'updated_at'>): Promise<number> => {
return new Promise((resolve, reject) => {
const now = Math.floor(Date.now() / 1000);
const stmt = db.prepare(
`INSERT INTO proxies (name, type, host, port, username, auth_method, encrypted_password, encrypted_private_key, encrypted_passphrase, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
);
stmt.run(
data.name, data.type, data.host, data.port,
data.username || null,
data.auth_method || 'none',
data.encrypted_password || null,
data.encrypted_private_key || null,
data.encrypted_passphrase || null,
now, now,
function (this: Statement, err: Error | null) {
stmt.finalize();
if (err) {
console.error('Repository: 创建代理时出错:', err.message);
return reject(new Error(`创建代理时出错: ${err.message}`));
}
resolve((this as any).lastID);
}
);
});
const now = Math.floor(Date.now() / 1000);
const sql = `INSERT INTO proxies (name, type, host, port, username, auth_method, encrypted_password, encrypted_private_key, encrypted_passphrase, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
const params = [
data.name, data.type, data.host, data.port,
data.username || null,
data.auth_method || 'none',
data.encrypted_password || null,
data.encrypted_private_key || null,
data.encrypted_passphrase || null,
now, now
];
try {
const db = await getDbInstance();
const result = await runDb(db, sql, params);
// Ensure lastID is valid before returning
if (typeof result.lastID !== 'number' || result.lastID <= 0) {
throw new Error('创建代理后未能获取有效的 lastID');
}
return result.lastID;
} catch (err: any) {
console.error('Repository: 创建代理时出错:', err.message);
// Handle potential UNIQUE constraint errors if needed (e.g., on name)
throw new Error(`创建代理时出错: ${err.message}`);
}
};
/**
* 获取所有代理
*/
export const findAllProxies = async (): Promise<ProxyData[]> => {
return new Promise((resolve, reject) => {
db.all(`SELECT * FROM proxies ORDER BY name ASC`, (err, rows: ProxyData[]) => {
if (err) {
console.error('Repository: 查询代理列表时出错:', err.message);
return reject(new Error('获取代理列表失败'));
}
resolve(rows);
});
});
const sql = `SELECT * FROM proxies ORDER BY name ASC`;
try {
const db = await getDbInstance();
const rows = await allDb<DbProxyRow>(db, sql);
return rows;
} catch (err: any) {
console.error('Repository: 查询代理列表时出错:', err.message);
throw new Error('获取代理列表失败');
}
};
/**
* 根据 ID 获取单个代理
*/
export const findProxyById = async (id: number): Promise<ProxyData | null> => {
return new Promise((resolve, reject) => {
db.get(`SELECT * FROM proxies WHERE id = ?`, [id], (err, row: ProxyData) => {
if (err) {
console.error(`Repository: 查询代理 ${id} 时出错:`, err.message);
return reject(new Error('获取代理信息失败'));
}
resolve(row || null);
});
});
const sql = `SELECT * FROM proxies WHERE id = ?`;
try {
const db = await getDbInstance();
const row = await getDbRow<DbProxyRow>(db, sql, [id]);
return row || null;
} catch (err: any) {
console.error(`Repository: 查询代理 ${id} 时出错:`, err.message);
throw new Error('获取代理信息失败');
}
};
@@ -106,8 +109,10 @@ export const updateProxy = async (id: number, data: Partial<Omit<ProxyData, 'id'
const fieldsToUpdate: { [key: string]: any } = { ...data };
const params: any[] = [];
// Remove fields that should not be updated directly
delete fieldsToUpdate.id;
delete fieldsToUpdate.created_at;
// updated_at will be set explicitly
fieldsToUpdate.updated_at = Math.floor(Date.now() / 1000);
@@ -115,39 +120,37 @@ export const updateProxy = async (id: number, data: Partial<Omit<ProxyData, 'id'
Object.values(fieldsToUpdate).forEach(value => params.push(value ?? null));
if (!setClauses) {
return false;
console.warn(`[Repository] updateProxy called for ID ${id} with no fields to update.`);
return false; // Nothing to update
}
params.push(id);
params.push(id); // Add the ID for the WHERE clause
return new Promise((resolve, reject) => {
const stmt = db.prepare(`UPDATE proxies SET ${setClauses} WHERE id = ?`);
stmt.run(...params, function (this: Statement, err: Error | null) {
stmt.finalize();
if (err) {
console.error(`Repository: 更新代理 ${id} 时出错:`, err.message);
return reject(new Error('更新代理记录失败'));
}
resolve((this as any).changes > 0);
});
});
const sql = `UPDATE proxies SET ${setClauses} WHERE id = ?`;
try {
const db = await getDbInstance();
const result = await runDb(db, sql, params);
return result.changes > 0;
} catch (err: any) {
console.error(`Repository: 更新代理 ${id} 时出错:`, err.message);
// Handle potential UNIQUE constraint errors if needed
throw new Error('更新代理记录失败');
}
};
/**
* 删除代理
*/
export const deleteProxy = async (id: number): Promise<boolean> => {
return new Promise((resolve, reject) => {
// 注意:connections 表中的 proxy_id 外键设置了 ON DELETE SET NULL
// 所以删除代理时,关联的连接会自动将 proxy_id 设为 NULL。
const stmt = db.prepare(`DELETE FROM proxies WHERE id = ?`);
stmt.run(id, function (this: Statement, err: Error | null) {
stmt.finalize();
if (err) {
console.error(`Repository: 删除代理 ${id} 时出错:`, err.message);
return reject(new Error('删除代理记录失败'));
}
resolve((this as any).changes > 0);
});
});
// Note: connections table proxy_id foreign key has ON DELETE SET NULL.
const sql = `DELETE FROM proxies WHERE id = ?`;
try {
const db = await getDbInstance();
const result = await runDb(db, sql, [id]);
return result.changes > 0;
} catch (err: any) {
console.error(`Repository: 删除代理 ${id} 时出错:`, err.message);
throw new Error('删除代理记录失败');
}
};
@@ -1,4 +1,5 @@
import { getDb } from '../database';
// packages/backend/src/repositories/quick-commands.repository.ts
import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection'; // Import new async helpers
// 定义快捷指令的接口
export interface QuickCommand {
@@ -10,124 +11,118 @@ export interface QuickCommand {
updated_at: number; // Unix 时间戳 (秒)
}
// Define the expected row structure from the database if it matches QuickCommand
type DbQuickCommandRow = QuickCommand;
/**
* 添加一条新的快捷指令
* @param name - 指令名称 (可选)
* @param command - 指令内容
* @returns 返回插入记录的 ID
*/
export const addQuickCommand = (name: string | null, command: string): Promise<number> => {
const db = getDb();
export const addQuickCommand = async (name: string | null, command: string): Promise<number> => {
const sql = `INSERT INTO quick_commands (name, command, created_at, updated_at) VALUES (?, ?, strftime('%s', 'now'), strftime('%s', 'now'))`;
return new Promise((resolve, reject) => {
db.run(sql, [name, command], function (err) {
if (err) {
console.error('添加快捷指令时出错:', err);
return reject(new Error('无法添加快捷指令'));
}
resolve(this.lastID);
});
});
};
try {
const db = await getDbInstance();
const result = await runDb(db, sql, [name, command]);
// Ensure lastID is valid before returning
if (typeof result.lastID !== 'number' || result.lastID <= 0) {
throw new Error('添加快捷指令后未能获取有效的 lastID');
}
return result.lastID;
} catch (err: any) {
console.error('添加快捷指令时出错:', err.message);
throw new Error('无法添加快捷指令');
}
}; // End of addQuickCommand
/**
* 更新指定的快捷指令
* @param id - 要更新的记录 ID
* @param name - 新的指令名称 (可选)
* @param command - 新的指令内容
* @returns 返回更新的行数 (通常是 1 或 0)
* @returns 返回是否成功更新 (true/false)
*/
export const updateQuickCommand = (id: number, name: string | null, command: string): Promise<number> => {
const db = getDb();
export const updateQuickCommand = async (id: number, name: string | null, command: string): Promise<boolean> => {
const sql = `UPDATE quick_commands SET name = ?, command = ?, updated_at = strftime('%s', 'now') WHERE id = ?`;
return new Promise((resolve, reject) => {
db.run(sql, [name, command, id], function (err) {
if (err) {
console.error('更新快捷指令时出错:', err);
return reject(new Error('无法更新快捷指令'));
}
resolve(this.changes);
});
});
};
try {
const db = await getDbInstance();
const result = await runDb(db, sql, [name, command, id]);
return result.changes > 0;
} catch (err: any) {
console.error('更新快捷指令时出错:', err.message);
throw new Error('无法更新快捷指令');
}
}; // End of updateQuickCommand
/**
* 根据 ID 删除指定的快捷指令
* @param id - 要删除的记录 ID
* @returns 返回删除的行数 (通常是 1 或 0)
* @returns 返回是否成功删除 (true/false)
*/
export const deleteQuickCommand = (id: number): Promise<number> => {
const db = getDb();
export const deleteQuickCommand = async (id: number): Promise<boolean> => {
const sql = `DELETE FROM quick_commands WHERE id = ?`;
return new Promise((resolve, reject) => {
db.run(sql, [id], function (err) {
if (err) {
console.error('删除快捷指令时出错:', err);
return reject(new Error('无法删除快捷指令'));
}
resolve(this.changes);
});
});
};
try {
const db = await getDbInstance();
const result = await runDb(db, sql, [id]);
return result.changes > 0;
} catch (err: any) {
console.error('删除快捷指令时出错:', err.message);
throw new Error('无法删除快捷指令');
}
}; // End of deleteQuickCommand
/**
* 获取所有快捷指令
* @param sortBy - 排序字段 ('name' 或 'usage_count')
* @returns 返回包含所有快捷指令条目的数组
*/
export const getAllQuickCommands = (sortBy: 'name' | 'usage_count' = 'name'): Promise<QuickCommand[]> => {
const db = getDb();
export const getAllQuickCommands = async (sortBy: 'name' | 'usage_count' = 'name'): Promise<QuickCommand[]> => {
let orderByClause = 'ORDER BY name ASC'; // 默认按名称升序
if (sortBy === 'usage_count') {
orderByClause = 'ORDER BY usage_count DESC, name ASC'; // 按使用频率降序,同频率按名称升序
}
// SQLite 中 NULLS LAST/FIRST 的支持可能不一致,这里简单处理 NULL 名称排在前面
const sql = `SELECT id, name, command, usage_count, created_at, updated_at FROM quick_commands ${orderByClause}`;
return new Promise((resolve, reject) => {
db.all(sql, [], (err, rows: QuickCommand[]) => {
if (err) {
console.error('获取快捷指令时出错:', err);
return reject(new Error('无法获取快捷指令'));
}
resolve(rows);
});
});
};
try {
const db = await getDbInstance();
const rows = await allDb<DbQuickCommandRow>(db, sql);
return rows;
} catch (err: any) {
console.error('获取快捷指令时出错:', err.message);
throw new Error('无法获取快捷指令');
}
}; // End of getAllQuickCommands
/**
* 增加指定快捷指令的使用次数
* @param id - 要增加次数的记录 ID
* @returns 返回更新的行数 (通常是 1 或 0)
* @returns 返回是否成功更新 (true/false)
*/
export const incrementUsageCount = (id: number): Promise<number> => {
const db = getDb();
export const incrementUsageCount = async (id: number): Promise<boolean> => {
const sql = `UPDATE quick_commands SET usage_count = usage_count + 1, updated_at = strftime('%s', 'now') WHERE id = ?`;
return new Promise((resolve, reject) => {
db.run(sql, [id], function (err) {
if (err) {
console.error('增加快捷指令使用次数时出错:', err);
return reject(new Error('无法增加快捷指令使用次数'));
}
resolve(this.changes);
});
});
};
try {
const db = await getDbInstance();
const result = await runDb(db, sql, [id]);
return result.changes > 0;
} catch (err: any) {
console.error('增加快捷指令使用次数时出错:', err.message);
throw new Error('无法增加快捷指令使用次数');
}
}; // End of incrementUsageCount
/**
* 根据 ID 查找快捷指令 (用于编辑前获取数据)
* @param id - 要查找的记录 ID
* @returns 返回找到的快捷指令条目,如果未找到则返回 undefined
*/
export const findQuickCommandById = (id: number): Promise<QuickCommand | undefined> => {
const db = getDb();
export const findQuickCommandById = async (id: number): Promise<QuickCommand | undefined> => {
const sql = `SELECT id, name, command, usage_count, created_at, updated_at FROM quick_commands WHERE id = ?`;
return new Promise((resolve, reject) => {
db.get(sql, [id], (err, row: QuickCommand | undefined) => {
if (err) {
console.error('查找快捷指令时出错:', err);
return reject(new Error('无法查找快捷指令'));
}
resolve(row);
});
});
};
try {
const db = await getDbInstance();
const row = await getDbRow<DbQuickCommandRow>(db, sql, [id]);
return row; // Returns undefined if not found
} catch (err: any) {
console.error('查找快捷指令时出错:', err.message);
throw new Error('无法查找快捷指令');
}
}; // End of findQuickCommandById
@@ -1,92 +1,95 @@
import { getDb } from '../database'; // 正确导入 getDb 函数
// packages/backend/src/repositories/settings.repository.ts
import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection'; // Import new async helpers
const db = getDb(); // 获取数据库实例
// Remove top-level db instance
// const db = getDb();
export interface Setting {
key: string;
value: string;
}
// Define the expected row structure from the database if different from Setting
// In this case, it seems Setting matches the SELECT columns.
type DbSettingRow = Setting;
export const settingsRepository = {
async getAllSettings(): Promise<Setting[]> {
return new Promise((resolve, reject) => {
db.all('SELECT key, value FROM settings', (err: any, rows: Setting[]) => { // 添加 err 类型
if (err) {
console.error('[Repository] 获取所有设置时出错:', err); // 更新日志为中文
reject(new Error('获取设置失败')); // 更新错误消息为中文
} else {
resolve(rows);
}
});
});
try {
const db = await getDbInstance();
const rows = await allDb<DbSettingRow>(db, 'SELECT key, value FROM settings');
return rows;
} catch (err: any) {
console.error('[Repository] 获取所有设置时出错:', err.message);
throw new Error('获取设置失败');
}
},
async getSetting(key: string): Promise<string | null> {
return new Promise((resolve, reject) => {
console.log(`[Repository] Attempting to get setting with key: ${key}`); // +++ 添加日志 +++
db.get('SELECT value FROM settings WHERE key = ?', [key], (err: any, row: { value: string } | undefined) => { // 添加 err 类型
if (err) {
console.error(`[Repository] 获取设置项 ${key} 时出错:`, err); // 更新日志为中文
reject(new Error(`获取设置项 ${key} 失败`)); // 更新错误消息为中文
} else {
console.log(`[Repository] Found value for key ${key}:`, row ? row.value : null); // +++ 添加日志 +++
resolve(row ? row.value : null);
}
});
});
console.log(`[Repository] Attempting to get setting with key: ${key}`);
try {
const db = await getDbInstance();
// Use the correct type for the expected row structure
const row = await getDbRow<{ value: string }>(db, 'SELECT value FROM settings WHERE key = ?', [key]);
const value = row ? row.value : null;
console.log(`[Repository] Found value for key ${key}:`, value);
return value;
} catch (err: any) {
console.error(`[Repository] 获取设置项 ${key} 时出错:`, err.message);
throw new Error(`获取设置项 ${key} 失败`);
}
},
async setSetting(key: string, value: string): Promise<void> {
return new Promise((resolve, reject) => {
const now = Math.floor(Date.now() / 1000); // 获取当前 Unix 时间戳
const sql = `INSERT INTO settings (key, value, created_at, updated_at)
const now = Math.floor(Date.now() / 1000); // Use seconds
const sql = `INSERT INTO settings (key, value, created_at, updated_at)
VALUES (?, ?, ?, ?)
ON CONFLICT(key) DO UPDATE SET
value = excluded.value,
updated_at = excluded.updated_at`;
const params = [key, value, now, now];
const params = [key, value, now, now];
console.log(`[Repository] Attempting to set setting. Key: ${key}, Value: ${value}`); // +++ 添加日志 +++
console.log(`[Repository] Executing SQL: ${sql} with params: ${JSON.stringify(params)}`); // +++ 添加日志 +++
console.log(`[Repository] Attempting to set setting. Key: ${key}, Value: ${value}`);
console.log(`[Repository] Executing SQL: ${sql} with params: ${JSON.stringify(params)}`);
db.run(
sql,
params,
function (this: any, err: any) { // 使用 this 需要 function 声明, 添加 err 类型
if (err) {
console.error(`[Repository] 设置设置项 ${key} 时出错:`, err); // 更新日志为中文
reject(new Error(`设置设置项 ${key} 失败`)); // 更新错误消息为中文
} else {
// this.changes 提供了受影响的行数 (对于 INSERT/UPDATE)
console.log(`[Repository] Successfully set setting for key: ${key}. Rows affected: ${this.changes}`); // +++ 添加日志 +++
resolve();
}
}
);
});
try {
const db = await getDbInstance();
const result = await runDb(db, sql, params);
console.log(`[Repository] Successfully set setting for key: ${key}. Rows affected: ${result.changes}`);
} catch (err: any) {
console.error(`[Repository] 设置设置项 ${key} 时出错:`, err.message);
throw new Error(`设置设置项 ${key} 失败`);
}
},
async deleteSetting(key: string): Promise<void> {
return new Promise((resolve, reject) => {
console.log(`[Repository] Attempting to delete setting with key: ${key}`); // +++ 添加日志 +++
db.run('DELETE FROM settings WHERE key = ?', [key], function (this: any, err: any) { // 添加 err 类型
if (err) {
console.error(`[Repository] 删除设置项 ${key} 时出错:`, err); // 更新日志为中文
reject(new Error(`删除设置项 ${key} 失败`)); // 更新错误消息为中文
} else {
console.log(`[Repository] Successfully deleted setting for key: ${key}. Rows affected: ${this.changes}`); // +++ 添加日志 +++
resolve();
}
});
});
async deleteSetting(key: string): Promise<boolean> { // Return boolean indicating success
console.log(`[Repository] Attempting to delete setting with key: ${key}`);
const sql = 'DELETE FROM settings WHERE key = ?';
try {
const db = await getDbInstance();
const result = await runDb(db, sql, [key]);
console.log(`[Repository] Successfully deleted setting for key: ${key}. Rows affected: ${result.changes}`);
return result.changes > 0; // Return true if a row was deleted
} catch (err: any) {
console.error(`[Repository] 删除设置项 ${key} 时出错:`, err.message);
throw new Error(`删除设置项 ${key} 失败`);
}
},
async setMultipleSettings(settings: Record<string, string>): Promise<void> {
console.log('[Repository] setMultipleSettings called with:', JSON.stringify(settings)); // +++ 添加日志 +++
console.log('[Repository] setMultipleSettings called with:', JSON.stringify(settings));
// Use Promise.all with the async setSetting method
// Note: 'this' inside map refers to the settingsRepository object correctly here
const promises = Object.entries(settings).map(([key, value]) =>
this.setSetting(key, value) // this 指向 settingsRepository 对象
this.setSetting(key, value)
);
await Promise.all(promises);
console.log('[Repository] setMultipleSettings finished.'); // +++ 添加日志 +++
try {
await Promise.all(promises);
console.log('[Repository] setMultipleSettings finished successfully.');
} catch (error) {
console.error('[Repository] setMultipleSettings failed:', error);
// Re-throw the error or handle it as needed
throw new Error('批量设置失败');
}
},
};
@@ -1,44 +1,46 @@
import { Database, Statement } from 'sqlite3';
import { getDb } from '../database';
// packages/backend/src/repositories/tag.repository.ts
import { Database, Statement } from 'sqlite3'; // Keep Statement if using prepare directly, otherwise remove
// Import new async helpers and the instance getter
import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection';
const db = getDb();
// Remove top-level db instance
// const db = getDb();
// 定义 Tag 类型 (可以共享到 types 文件)
// Let's assume TagData is the correct interface for a row from the 'tags' table
export interface TagData {
id: number;
name: string;
created_at: number;
updated_at: number; // Assuming tags also have updated_at based on migrations
updated_at: number;
}
/**
* 获取所有标签
*/
export const findAllTags = async (): Promise<TagData[]> => {
return new Promise((resolve, reject) => {
db.all(`SELECT * FROM tags ORDER BY name ASC`, [], (err, rows: TagData[]) => {
if (err) {
console.error('Repository: 查询标签列表时出错:', err.message);
return reject(new Error('获取标签列表失败'));
}
resolve(rows);
});
});
try {
const db = await getDbInstance();
const rows = await allDb<TagData>(db, `SELECT * FROM tags ORDER BY name ASC`);
return rows;
} catch (err: any) {
console.error('Repository: 查询标签列表时出错:', err.message);
throw new Error('获取标签列表失败');
}
};
/**
* 根据 ID 获取单个标签
*/
export const findTagById = async (id: number): Promise<TagData | null> => {
return new Promise((resolve, reject) => {
db.get(`SELECT * FROM tags WHERE id = ?`, [id], (err, row: TagData) => {
if (err) {
console.error(`Repository: 查询标签 ${id} 时出错:`, err.message);
return reject(new Error('获取标签信息失败'));
}
resolve(row || null);
});
});
try {
const db = await getDbInstance();
const row = await getDbRow<TagData>(db, `SELECT * FROM tags WHERE id = ?`, [id]);
return row || null;
} catch (err: any) {
console.error(`Repository: 查询标签 ${id} 时出错:`, err.message);
throw new Error('获取标签信息失败');
}
};
@@ -46,59 +48,59 @@ export const findTagById = async (id: number): Promise<TagData | null> => {
* 创建新标签
*/
export const createTag = async (name: string): Promise<number> => {
return new Promise((resolve, reject) => {
const now = Math.floor(Date.now() / 1000);
const stmt = db.prepare(
`INSERT INTO tags (name, created_at, updated_at) VALUES (?, ?, ?)`
);
stmt.run(name, now, now, function (this: Statement, err: Error | null) {
stmt.finalize();
if (err) {
// Handle unique constraint error specifically if needed
console.error('Repository: 创建标签时出错:', err.message);
return reject(new Error(`创建标签失败: ${err.message}`));
}
resolve((this as any).lastID);
});
});
const now = Math.floor(Date.now() / 1000); // Use seconds for consistency? Check table definition
const sql = `INSERT INTO tags (name, created_at, updated_at) VALUES (?, ?, ?)`;
try {
const db = await getDbInstance();
const result = await runDb(db, sql, [name, now, now]);
// Ensure lastID is valid before returning
if (typeof result.lastID !== 'number' || result.lastID <= 0) {
throw new Error('创建标签后未能获取有效的 lastID');
}
return result.lastID;
} catch (err: any) {
console.error('Repository: 创建标签时出错:', err.message);
// Handle unique constraint error specifically if needed
if (err.message.includes('UNIQUE constraint failed')) {
throw new Error(`标签名称 "${name}" 已存在。`);
}
throw new Error(`创建标签失败: ${err.message}`);
}
};
/**
* 更新标签名称
*/
export const updateTag = async (id: number, name: string): Promise<boolean> => {
return new Promise((resolve, reject) => {
const now = Math.floor(Date.now() / 1000);
const stmt = db.prepare(
`UPDATE tags SET name = ?, updated_at = ? WHERE id = ?`
);
stmt.run(name, now, id, function (this: Statement, err: Error | null) {
stmt.finalize();
if (err) {
// Handle unique constraint error specifically if needed
console.error(`Repository: 更新标签 ${id} 时出错:`, err.message);
return reject(new Error(`更新标签失败: ${err.message}`));
}
resolve((this as any).changes > 0);
});
});
const now = Math.floor(Date.now() / 1000);
const sql = `UPDATE tags SET name = ?, updated_at = ? WHERE id = ?`;
try {
const db = await getDbInstance();
const result = await runDb(db, sql, [name, now, id]);
return result.changes > 0;
} catch (err: any) {
console.error(`Repository: 更新标签 ${id} 时出错:`, err.message);
// Handle unique constraint error specifically if needed
if (err.message.includes('UNIQUE constraint failed')) {
throw new Error(`标签名称 "${name}" 已存在。`);
}
throw new Error(`更新标签失败: ${err.message}`);
}
};
/**
* 删除标签
*/
export const deleteTag = async (id: number): Promise<boolean> => {
return new Promise((resolve, reject) => {
// Note: connection_tags junction table has ON DELETE CASCADE for tag_id,
// so related entries there will be deleted automatically.
const stmt = db.prepare(`DELETE FROM tags WHERE id = ?`);
stmt.run(id, function (this: Statement, err: Error | null) {
stmt.finalize();
if (err) {
console.error(`Repository: 删除标签 ${id} 时出错:`, err.message);
return reject(new Error('删除标签失败'));
}
resolve((this as any).changes > 0);
});
});
// Note: connection_tags junction table has ON DELETE CASCADE for tag_id,
// so related entries there will be deleted automatically.
const sql = `DELETE FROM tags WHERE id = ?`;
try {
const db = await getDbInstance();
const result = await runDb(db, sql, [id]);
return result.changes > 0;
} catch (err: any) {
console.error(`Repository: 删除标签 ${id} 时出错:`, err.message);
throw new Error('删除标签失败');
}
};
@@ -1,50 +1,94 @@
import { getDb } from '../database';
// packages/backend/src/repositories/terminal-theme.repository.ts
import { Database } from 'sqlite3'; // Import Database type if needed for type hints
import { getDbInstance, runDb, getDb, allDb } from '../database/connection'; // Import new async helpers, including getDb
// Remove the incorrect import of DbTerminalThemeRow
import { TerminalTheme, CreateTerminalThemeDto, UpdateTerminalThemeDto } from '../types/terminal-theme.types';
import { defaultXtermTheme } from '../config/default-themes'; // 假设默认主题配置在此
import { defaultXtermTheme } from '../config/default-themes';
// const db = getDb(); // Removed top-level call to avoid circular dependency issues
// Define the interface for the raw database row structure
// Interface matching the schema in schema.ts
interface DbTerminalThemeRow {
id: number;
name: string;
theme_type: 'preset' | 'user';
foreground?: string | null;
background?: string | null;
cursor?: string | null;
cursor_accent?: string | null;
selection_background?: string | null;
black?: string | null;
red?: string | null;
green?: string | null;
yellow?: string | null;
blue?: string | null;
magenta?: string | null;
cyan?: string | null;
white?: string | null;
bright_black?: string | null;
bright_red?: string | null;
bright_green?: string | null;
bright_yellow?: string | null;
bright_blue?: string | null;
bright_magenta?: string | null;
bright_cyan?: string | null;
bright_white?: string | null;
created_at: number;
updated_at: number;
}
/**
* SQL语句:创建 terminal_themes 表
*/
export const SQL_CREATE_TABLE = `
CREATE TABLE IF NOT EXISTS terminal_themes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
theme_data TEXT NOT NULL, -- Store ITheme as JSON string
is_preset BOOLEAN NOT NULL DEFAULT 0,
preset_key TEXT NULL UNIQUE, -- 可选,用于识别预设主题
is_system_default BOOLEAN NOT NULL DEFAULT 0, -- 新增:标记是否为系统默认主题
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
`;
/**
* 创建 terminal_themes 表 (如果不存在) - 不再自动调用
*/
const createTableIfNotExists = () => {
// This function is no longer called automatically, initialization is handled in database.ts
getDb().run(SQL_CREATE_TABLE, (err) => {
if (err) {
console.error('创建 terminal_themes 表失败:', err.message);
} else {
console.log('terminal_themes 表已存在或已创建。');
}
});
};
// SQL_CREATE_TABLE and createTableIfNotExists removed as initialization is handled in database/connection.ts
// 辅助函数:将数据库行转换为 TerminalTheme 对象
const mapRowToTerminalTheme = (row: any): TerminalTheme => {
return {
_id: row.id.toString(), // SQLite ID 是数字,转换为字符串以匹配 NeDB 风格
name: row.name,
themeData: JSON.parse(row.theme_data), // 解析 JSON 字符串
isPreset: !!row.is_preset, // 转换为布尔值
isSystemDefault: !!row.is_system_default, // 映射新增的列
createdAt: row.created_at,
updatedAt: row.updated_at,
};
// Add type annotation for the input row
const mapRowToTerminalTheme = (row: DbTerminalThemeRow): TerminalTheme => {
// Basic check if row exists and has id property
if (!row || typeof row.id === 'undefined') {
console.error("mapRowToTerminalTheme received invalid row:", row);
// Return a default or throw an error, depending on desired behavior
// For now, let's throw an error to make the issue visible
throw new Error("Invalid database row provided to mapRowToTerminalTheme");
}
try {
return {
_id: row.id.toString(),
name: row.name,
// Reconstruct themeData from individual columns
themeData: {
foreground: row.foreground ?? undefined,
background: row.background ?? undefined,
cursor: row.cursor ?? undefined,
cursorAccent: row.cursor_accent ?? undefined,
selectionBackground: row.selection_background ?? undefined,
black: row.black ?? undefined,
red: row.red ?? undefined,
green: row.green ?? undefined,
yellow: row.yellow ?? undefined,
blue: row.blue ?? undefined,
magenta: row.magenta ?? undefined,
cyan: row.cyan ?? undefined,
white: row.white ?? undefined,
brightBlack: row.bright_black ?? undefined,
brightRed: row.bright_red ?? undefined,
brightGreen: row.bright_green ?? undefined,
brightYellow: row.bright_yellow ?? undefined,
brightBlue: row.bright_blue ?? undefined,
brightMagenta: row.bright_magenta ?? undefined,
brightCyan: row.bright_cyan ?? undefined,
brightWhite: row.bright_white ?? undefined,
},
isPreset: row.theme_type === 'preset',
// isSystemDefault needs to be handled differently, maybe based on name 'default'?
// For now, let's assume it's not directly mapped or needed here.
isSystemDefault: row.name === 'default', // Tentative mapping based on name
createdAt: row.created_at,
updatedAt: row.updated_at,
};
} catch (e: any) {
// Log the entire row for debugging instead of the non-existent theme_data
console.error(`Error mapping theme data for theme ID ${row.id}:`, e.message, "Raw row:", row);
// Return a partially mapped object or throw error
throw new Error(`Failed to map theme data for theme ID ${row.id}`);
}
};
/**
@@ -52,34 +96,45 @@ const mapRowToTerminalTheme = (row: any): TerminalTheme => {
* @returns Promise<TerminalTheme[]>
*/
export const findAllThemes = async (): Promise<TerminalTheme[]> => {
return new Promise((resolve, reject) => {
getDb().all('SELECT * FROM terminal_themes ORDER BY is_preset DESC, name ASC', [], (err, rows) => {
if (err) {
console.error('查询所有终端主题失败:', err.message);
reject(new Error('查询终端主题失败'));
} else {
resolve(rows.map(mapRowToTerminalTheme));
}
});
});
try {
const db = await getDbInstance();
// Specify the expected row type for allDb
const rows = await allDb<DbTerminalThemeRow>(db, 'SELECT * FROM terminal_themes ORDER BY is_preset DESC, name ASC');
// Filter out potential errors during mapping
return rows.map(row => {
try {
return mapRowToTerminalTheme(row);
} catch (mapError: any) {
console.error(`Error mapping row ID ${row?.id}:`, mapError.message);
return null; // Or handle differently
}
}).filter((theme): theme is TerminalTheme => theme !== null);
} catch (err: any) { // Add type annotation for err
console.error('查询所有终端主题失败:', err.message);
throw new Error('查询终端主题失败'); // Re-throw or handle error appropriately
}
};
/**
* 根据 ID 查找终端主题
* @param id 主题 ID (注意:这里是 SQLite 数字 ID)
* @param id 主题 ID (SQLite 数字 ID)
* @returns Promise<TerminalTheme | null>
*/
export const findThemeById = async (id: number): Promise<TerminalTheme | null> => {
return new Promise((resolve, reject) => {
getDb().get('SELECT * FROM terminal_themes WHERE id = ?', [id], (err, row) => {
if (err) {
console.error(`查询 ID 为 ${id} 的终端主题失败:`, err.message);
reject(new Error('查询终端主题失败'));
} else {
resolve(row ? mapRowToTerminalTheme(row) : null);
}
});
});
if (isNaN(id) || id <= 0) {
console.error("findThemeById called with invalid ID:", id);
return null; // Return null for invalid IDs
}
try {
const db = await getDbInstance();
// Specify the expected row type for getDbRow
// Use getDb instead of the non-existent getDbRow
const row = await getDb<DbTerminalThemeRow>(db, 'SELECT * FROM terminal_themes WHERE id = ?', [id]);
return row ? mapRowToTerminalTheme(row) : null;
} catch (err: any) { // Add type annotation for err
console.error(`查询 ID 为 ${id} 的终端主题失败:`, err.message);
throw new Error('查询终端主题失败');
}
};
/**
@@ -89,36 +144,33 @@ export const findThemeById = async (id: number): Promise<TerminalTheme | null> =
*/
export const createTheme = async (themeDto: CreateTerminalThemeDto): Promise<TerminalTheme> => {
const now = Date.now();
const themeDataJson = JSON.stringify(themeDto.themeData); // 将 ITheme 转换为 JSON 字符串
const themeDataJson = JSON.stringify(themeDto.themeData);
const sql = `
INSERT INTO terminal_themes (name, theme_data, is_preset, created_at, updated_at)
VALUES (?, ?, 0, ?, ?)
`;
return new Promise((resolve, reject) => {
getDb().run(sql, [themeDto.name, themeDataJson, now, now], function (err) {
if (err) {
console.error('创建新终端主题失败:', err.message);
// 特别处理唯一约束错误
if (err.message.includes('UNIQUE constraint failed')) {
reject(new Error(`主题名称 "${themeDto.name}" 已存在。`));
} else {
reject(new Error('创建终端主题失败'));
}
} else {
// 获取新插入行的 ID 并查询返回完整对象
findThemeById(this.lastID)
.then(newTheme => {
if (newTheme) {
resolve(newTheme);
} else {
// 理论上不应该发生,但作为回退
reject(new Error('创建主题后未能检索到该主题'));
}
})
.catch(reject);
}
});
});
try {
const db = await getDbInstance();
const result = await runDb(db, sql, [themeDto.name, themeDataJson, now, now]);
// Ensure lastID is valid before trying to find the theme
if (typeof result.lastID !== 'number' || result.lastID <= 0) {
throw new Error('创建主题后未能获取有效的 lastID');
}
const newTheme = await findThemeById(result.lastID);
if (newTheme) {
return newTheme;
} else {
// This case might happen if findThemeById fails for some reason
throw new Error(`创建主题后未能检索到 ID 为 ${result.lastID} 的主题`);
}
} catch (err: any) { // Add type annotation for err
console.error('创建新终端主题失败:', err.message);
if (err.message.includes('UNIQUE constraint failed')) {
throw new Error(`主题名称 "${themeDto.name}" 已存在。`);
} else {
throw new Error('创建终端主题失败');
}
}
};
/**
@@ -130,26 +182,23 @@ export const createTheme = async (themeDto: CreateTerminalThemeDto): Promise<Ter
export const updateTheme = async (id: number, themeDto: UpdateTerminalThemeDto): Promise<boolean> => {
const now = Date.now();
const themeDataJson = JSON.stringify(themeDto.themeData);
// 只允许更新非预设主题的 name 和 theme_data
const sql = `
UPDATE terminal_themes
SET name = ?, theme_data = ?, updated_at = ?
WHERE id = ? AND is_preset = 0
`;
return new Promise((resolve, reject) => {
getDb().run(sql, [themeDto.name, themeDataJson, now, id], function (err) {
if (err) {
console.error(`更新 ID 为 ${id} 的终端主题失败:`, err.message);
if (err.message.includes('UNIQUE constraint failed')) {
reject(new Error(`主题名称 "${themeDto.name}" 已存在。`));
} else {
reject(new Error('更新终端主题失败'));
}
} else {
resolve(this.changes > 0); // 如果有行被改变,则更新成功
}
});
});
try {
const db = await getDbInstance();
const result = await runDb(db, sql, [themeDto.name, themeDataJson, now, id]);
return result.changes > 0;
} catch (err: any) { // Add type annotation for err
console.error(`更新 ID 为 ${id} 的终端主题失败:`, err.message);
if (err.message.includes('UNIQUE constraint failed')) {
throw new Error(`主题名称 "${themeDto.name}" 已存在。`);
} else {
throw new Error('更新终端主题失败');
}
}
};
/**
@@ -158,77 +207,66 @@ export const updateTheme = async (id: number, themeDto: UpdateTerminalThemeDto):
* @returns Promise<boolean> 是否成功删除
*/
export const deleteTheme = async (id: number): Promise<boolean> => {
// 只允许删除非预设主题
const sql = 'DELETE FROM terminal_themes WHERE id = ? AND is_preset = 0';
return new Promise((resolve, reject) => {
getDb().run(sql, [id], function (err) {
if (err) {
console.error(`删除 ID 为 ${id} 的终端主题失败:`, err.message);
reject(new Error('删除终端主题失败'));
} else {
resolve(this.changes > 0); // 如果有行被改变,则删除成功
}
});
});
try {
const db = await getDbInstance();
const result = await runDb(db, sql, [id]);
return result.changes > 0;
} catch (err: any) { // Add type annotation for err
console.error(`删除 ID 为 ${id} 的终端主题失败:`, err.message);
throw new Error('删除终端主题失败');
}
};
/**
* 初始化预设主题到数据库 (如果不存在)
* 这个函数应该在数据库连接成功后,由应用初始化逻辑调用。
* @param presets 预设主题定义数组 (包含 name, themeData, isPreset=true, 可选 preset_key)
* @param presets 预设主题定义数组
*/
export const initializePresetThemes = async (presets: Array<Omit<TerminalTheme, '_id' | 'createdAt' | 'updatedAt'> & { preset_key?: string }>) => {
export const initializePresetThemes = async (db: Database, presets: Array<Omit<TerminalTheme, '_id' | 'createdAt' | 'updatedAt' | 'isSystemDefault'> & { name: string }>) => {
console.log('[DB Init] 开始检查并初始化预设主题...');
const now = Date.now();
const nowSeconds = Math.floor(Date.now() / 1000); // Use seconds for DB consistency
// const db = await getDbInstance(); // Use the passed db instance
// 使用 for...of 循环确保顺序执行检查和插入(避免并发 UNIQUE 约束问题)
for (const preset of presets) {
await new Promise<void>((resolve, reject) => {
// 优先使用 preset_key 检查,如果提供了的话
const checkColumn = preset.preset_key ? 'preset_key' : 'name';
const checkValue = preset.preset_key ?? preset.name;
try {
// Check using name and theme_type
const existing = await getDb<{ id: number }>(db, `SELECT id FROM terminal_themes WHERE name = ? AND theme_type = 'preset'`, [preset.name]);
getDb().get(`SELECT id FROM terminal_themes WHERE ${checkColumn} = ? AND is_preset = 1`, [checkValue], (err, row) => {
if (err) {
console.error(`[DB Init] 检查预设主题 "${preset.name}" (Key: ${checkValue}) 时出错:`, err.message);
return reject(err);
}
if (!row) {
const themeDataJson = JSON.stringify(preset.themeData);
const isDefault = preset.preset_key === 'default' ? 1 : 0;
// 始终包含 preset_key 列,如果不存在则插入 NULL
const columns = ['name', 'theme_data', 'is_preset', 'is_system_default', 'preset_key', 'created_at', 'updated_at']; // 7 columns
const values = [preset.name, themeDataJson, 1, isDefault, preset.preset_key ?? null, now, now]; // 7 values
const placeholders = ['?', '?', '?', '?', '?', '?', '?']; // 7 placeholders
if (!existing) {
// Map preset.themeData to individual columns
const theme = preset.themeData;
const columns = [
'name', 'theme_type', 'foreground', 'background', 'cursor', 'cursor_accent',
'selection_background', 'black', 'red', 'green', 'yellow', 'blue',
'magenta', 'cyan', 'white', 'bright_black', 'bright_red', 'bright_green',
'bright_yellow', 'bright_blue', 'bright_magenta', 'bright_cyan', 'bright_white',
'created_at', 'updated_at'
];
const values = [
preset.name, 'preset', theme?.foreground, theme?.background, theme?.cursor, theme?.cursorAccent,
theme?.selectionBackground, theme?.black, theme?.red, theme?.green, theme?.yellow, theme?.blue,
theme?.magenta, theme?.cyan, theme?.white, theme?.brightBlack, theme?.brightRed, theme?.brightGreen,
theme?.brightYellow, theme?.brightBlue, theme?.brightMagenta, theme?.brightCyan, theme?.brightWhite,
nowSeconds, nowSeconds
];
const placeholders = columns.map(() => '?').join(', ');
// 移除动态添加 preset_key 的逻辑
// if (preset.preset_key) {
// values.push(preset.preset_key);
// placeholders.push('?');
// }
const insertSql = `
INSERT INTO terminal_themes (${columns.join(', ')})
VALUES (${placeholders.join(', ')})
`;
getDb().run(insertSql, values, (insertErr) => {
if (insertErr) {
console.error(`[DB Init] 初始化预设主题 "${preset.name}" (Key: ${preset.preset_key ?? 'N/A'}) 失败:`, insertErr.message); // 调整日志输出
return reject(insertErr);
} else {
console.log(`[DB Init] 预设主题 "${preset.name}" (Key: ${checkValue}) 已初始化到数据库。`);
resolve();
}
});
} else {
// console.log(`[DB Init] 预设主题 "${preset.name}" (Key: ${checkValue}) 已存在,跳过初始化。`);
resolve();
}
});
});
const insertSql = `
INSERT INTO terminal_themes (${columns.join(', ')})
VALUES (${placeholders})
`;
await runDb(db, insertSql, values);
console.log(`[DB Init] 预设主题 "${preset.name}" 已初始化到数据库。`);
} else {
// console.log(`[DB Init] 预设主题 "${preset.name}" 已存在,跳过初始化。`);
}
} catch (err: any) { // Add type annotation for err
// Remove reference to non-existent preset_key
console.error(`[DB Init] 处理预设主题 "${preset.name}" 时出错:`, err.message);
// Decide if one error should stop the whole process or just log and continue
// throw err; // Uncomment to stop on first error
}
}
console.log('[DB Init] 预设主题检查和初始化完成。');
};
// 移除所有在此文件中的初始化调用和相关导入,它们应该在 database.ts 或 app.ts 中进行
@@ -33,8 +33,9 @@ export const getAllCommandHistory = async (): Promise<CommandHistoryEntry[]> =>
* @returns 返回是否成功删除 (删除行数 > 0)
*/
export const deleteCommandHistoryById = async (id: number): Promise<boolean> => {
const changes = await CommandHistoryRepository.deleteCommandById(id);
return changes > 0;
// deleteCommandById now directly returns boolean indicating success
const success = await CommandHistoryRepository.deleteCommandById(id);
return success;
};
/**
@@ -1,10 +1,13 @@
// packages/backend/src/services/import-export.service.ts
import * as ConnectionRepository from '../repositories/connection.repository';
import * as ProxyRepository from '../repositories/proxy.repository';
import { getDb } from '../database'; // Need db instance for transaction
// Import the instance getter and helpers
import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection';
import { Database } from 'sqlite3'; // Import Database type
const db = getDb(); // Get db instance for transaction management
// Remove top-level db instance
// Define structure for imported connection data (can be shared in types)
// --- Interface definitions remain the same ---
interface ImportedConnectionData {
name: string;
host: string;
@@ -27,35 +30,40 @@ interface ImportedConnectionData {
encrypted_passphrase?: string | null;
} | null;
}
// Define structure for exported connection data (can be shared in types)
interface ExportedConnectionData extends Omit<ImportedConnectionData, 'id'> {
// Exclude fields not needed for export like id, created_at etc.
}
// Define structure for import results
interface ExportedConnectionData extends Omit<ImportedConnectionData, 'id'> {}
export interface ImportResult {
successCount: number;
failureCount: number;
errors: { connectionName?: string; message: string }[];
}
// --- End Interface definitions ---
/**
* 导出所有连接配置
*/
export const exportConnections = async (): Promise<ExportedConnectionData[]> => {
// 1. Fetch all connections with tags (basic info)
// We need full connection info including encrypted fields and proxy details for export
// Let's adapt the repository or add a new method if needed.
// For now, let's assume findFullConnectionById can be adapted or a similar findAll method exists.
// Re-using the logic from controller for now, ideally repo handles joins.
try {
const db = await getDbInstance(); // Get DB instance
const connectionsWithProxies = await new Promise<any[]>((resolve, reject) => {
db.all(
// Define a more specific type for the row structure
type ExportRow = ConnectionRepository.FullConnectionData & {
proxy_db_id: number | null;
proxy_name: string | null;
proxy_type: 'SOCKS5' | 'HTTP' | null;
proxy_host: string | null;
proxy_port: number | null;
proxy_username: string | null;
proxy_auth_method: 'none' | 'password' | 'key' | null;
proxy_encrypted_password?: string | null;
proxy_encrypted_private_key?: string | null;
proxy_encrypted_passphrase?: string | null;
};
// Fetch connections joined with proxies using await allDb
const connectionsWithProxies = await allDb<ExportRow>(db,
`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,
c.*,
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,
@@ -64,64 +72,58 @@ export const exportConnections = async (): Promise<ExportedConnectionData[]> =>
p.encrypted_passphrase as proxy_encrypted_passphrase
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('Service: 查询连接和代理信息以供导出时出错:', err.message);
return reject(new Error('导出连接失败:查询连接信息出错'));
}
resolve(rows);
}
ORDER BY c.name ASC`
);
});
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('Service: 查询连接标签以供导出时出错:', 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);
// Fetch all tag associations using await allDb
const tagRows = await allDb<{ connection_id: number, tag_id: number }>(db,
'SELECT connection_id, tag_id FROM connection_tags'
);
// Create a map for easy tag lookup
const tagsMap: { [connId: number]: number[] } = {};
tagRows.forEach(row => {
if (!tagsMap[row.connection_id]) tagsMap[row.connection_id] = [];
tagsMap[row.connection_id].push(row.tag_id);
});
});
// 2. Format data for export
const formattedData: ExportedConnectionData[] = connectionsWithProxies.map(row => {
const connection: ExportedConnectionData = {
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] || [],
proxy: null // Initialize proxy as null
};
if (row.proxy_db_id) {
connection.proxy = {
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,
encrypted_password: row.proxy_encrypted_password,
encrypted_private_key: row.proxy_encrypted_private_key,
encrypted_passphrase: row.proxy_encrypted_passphrase,
// Format data for export
const formattedData: ExportedConnectionData[] = connectionsWithProxies.map(row => {
const connection: ExportedConnectionData = {
name: row.name ?? 'Unnamed', // Provide default if name is null
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: tagsMap[row.id] || [],
proxy: null
};
}
return connection;
});
return formattedData;
if (row.proxy_db_id) {
connection.proxy = {
name: row.proxy_name ?? 'Unnamed Proxy', // Provide default
type: row.proxy_type ?? 'SOCKS5', // Provide default or handle error
host: row.proxy_host ?? '', // Provide default or handle error
port: row.proxy_port ?? 0, // Provide default or handle error
username: row.proxy_username,
auth_method: row.proxy_auth_method ?? 'none', // Provide default
encrypted_password: row.proxy_encrypted_password,
encrypted_private_key: row.proxy_encrypted_private_key,
encrypted_passphrase: row.proxy_encrypted_passphrase,
};
}
return connection;
});
return formattedData;
} catch (err: any) {
console.error('Service: 导出连接时出错:', err.message);
throw new Error(`导出连接失败: ${err.message}`); // Re-throw for controller
}
};
@@ -139,153 +141,127 @@ export const importConnections = async (fileBuffer: Buffer): Promise<ImportResul
}
} catch (error: any) {
console.error('Service: 解析导入文件失败:', error);
throw new Error(`解析 JSON 文件失败: ${error.message}`); // Re-throw for controller
throw new Error(`解析 JSON 文件失败: ${error.message}`);
}
let successCount = 0;
let failureCount = 0;
const errors: { connectionName?: string; message: string }[] = [];
const connectionsToInsert: Omit<ConnectionRepository.FullConnectionData, 'id' | 'created_at' | 'updated_at' | 'last_connected_at'>[] = [];
const db = await getDbInstance(); // Get DB instance once for the transaction
// Use a transaction for atomicity
return new Promise<ImportResult>((resolveOuter, rejectOuter) => {
db.serialize(() => {
db.run('BEGIN TRANSACTION', async (beginErr: Error | null) => {
if (beginErr) {
console.error('Service: 开始导入事务失败:', beginErr);
return rejectOuter(new Error(`开始事务失败: ${beginErr.message}`));
try {
await runDb(db, 'BEGIN TRANSACTION'); // Start transaction using await runDb
const connectionsToInsert: Array<Omit<ConnectionRepository.FullConnectionData, 'id' | 'created_at' | 'updated_at' | 'last_connected_at'> & { tag_ids?: number[] }> = [];
const proxyCache: { [key: string]: number } = {}; // Cache for created/found proxy IDs
// --- Pass 1: Validate data and prepare for insertion ---
for (const connData of importedData) {
try {
// Basic validation
if (!connData.name || !connData.host || !connData.port || !connData.username || !connData.auth_method) {
throw new Error('缺少必要的连接字段 (name, host, port, username, auth_method)。');
}
// ... (add other validation as before) ...
try {
// Process each connection data from the imported file
for (const connData of importedData) {
try {
// 1. Validate connection data (basic)
if (!connData.name || !connData.host || !connData.port || !connData.username || !connData.auth_method) {
throw new Error('缺少必要的连接字段 (name, host, port, username, 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。');
}
// Add more validation as needed
let proxyIdToUse: number | null = null;
let proxyIdToUse: number | null = null;
// 2. Handle proxy (find or create)
if (connData.proxy) {
const proxyData = connData.proxy;
// Validate proxy data
if (!proxyData.name || !proxyData.type || !proxyData.host || !proxyData.port) {
throw new Error('代理信息不完整 (缺少 name, type, host, port)。');
}
// Add more proxy validation if needed
// Try to find existing proxy
const existingProxy = await ProxyRepository.findProxyByNameTypeHostPort(proxyData.name, proxyData.type, proxyData.host, proxyData.port);
if (existingProxy) {
proxyIdToUse = existingProxy.id;
} else {
// Proxy doesn't exist, create it
const newProxyData = {
name: proxyData.name,
type: proxyData.type,
host: proxyData.host,
port: proxyData.port,
username: proxyData.username || null,
auth_method: proxyData.auth_method || 'none',
encrypted_password: proxyData.encrypted_password || null,
encrypted_private_key: proxyData.encrypted_private_key || null,
encrypted_passphrase: proxyData.encrypted_passphrase || null,
};
proxyIdToUse = await ProxyRepository.createProxy(newProxyData);
console.log(`Service: 导入连接 ${connData.name}: 新代理 ${proxyData.name} 创建成功 (ID: ${proxyIdToUse})`);
}
}
// 3. Prepare connection data for bulk insert
connectionsToInsert.push({
name: connData.name,
host: connData.host,
port: connData.port,
username: connData.username,
auth_method: connData.auth_method,
encrypted_password: connData.encrypted_password || null,
encrypted_private_key: connData.encrypted_private_key || null,
encrypted_passphrase: connData.encrypted_passphrase || null,
proxy_id: proxyIdToUse,
// tag_ids will be handled separately after insertion
});
} catch (connError: any) {
// Error processing this specific connection
failureCount++;
errors.push({ connectionName: connData.name || '未知连接', message: connError.message });
console.warn(`Service: 处理导入连接 "${connData.name || '未知'}" 时出错: ${connError.message}`);
}
} // End for loop
// 4. Bulk insert connections
let insertedResults: { connectionId: number, originalData: any }[] = [];
if (connectionsToInsert.length > 0) {
insertedResults = await ConnectionRepository.bulkInsertConnections(connectionsToInsert);
successCount = insertedResults.length;
// Handle proxy (find or create) - uses async repository functions
if (connData.proxy) {
const proxyData = connData.proxy;
if (!proxyData.name || !proxyData.type || !proxyData.host || !proxyData.port) {
throw new Error('代理信息不完整 (缺少 name, type, host, port)。');
}
// 5. Associate tags for successfully inserted connections
for (const result of insertedResults) {
const originalTagIds = result.originalData?.tag_ids;
if (Array.isArray(originalTagIds) && originalTagIds.length > 0) {
const validTagIds = originalTagIds.filter((id: any) => typeof id === 'number' && id > 0);
if (validTagIds.length > 0) {
try {
await ConnectionRepository.updateConnectionTags(result.connectionId, validTagIds);
} catch (tagError: any) {
// Log warning but don't fail the entire import for tag association error
console.warn(`Service: 导入连接 ${result.originalData.name}: 关联标签失败 (ID: ${result.connectionId}): ${tagError.message}`);
// Optionally, add this to the 'errors' array reported back
errors.push({ connectionName: result.originalData.name, message: `关联标签失败: ${tagError.message}` });
// Decrement successCount or increment failureCount if tag failure should count as overall failure
// failureCount++; // Example: Count tag failures
}
}
}
}
// 6. Commit or Rollback
if (failureCount > 0 && successCount === 0) { // Only rollback if ALL fail, or adjust logic as needed
console.warn(`Service: 导入连接存在 ${failureCount} 个错误,且无成功记录,正在回滚事务...`);
db.run('ROLLBACK', (rollbackErr: Error | null) => {
if (rollbackErr) console.error("Service: 回滚事务失败:", rollbackErr);
// Reject outer promise with collected errors
rejectOuter(new Error(`导入失败,存在 ${failureCount} 个错误。`));
});
const cacheKey = `${proxyData.name}-${proxyData.type}-${proxyData.host}-${proxyData.port}`;
if (proxyCache[cacheKey]) {
proxyIdToUse = proxyCache[cacheKey];
} else {
// Commit even if some failed, report partial success
db.run('COMMIT', (commitErr: Error | null) => {
if (commitErr) {
console.error('Service: 提交导入事务时出错:', commitErr);
rejectOuter(new Error(`提交导入事务失败: ${commitErr.message}`));
} else {
console.log(`Service: 导入事务提交。成功: ${successCount}, 失败: ${failureCount}`);
resolveOuter({ successCount, failureCount, errors }); // Resolve outer promise
}
});
const existingProxy = await ProxyRepository.findProxyByNameTypeHostPort(proxyData.name, proxyData.type, proxyData.host, proxyData.port);
if (existingProxy) {
proxyIdToUse = existingProxy.id;
} else {
const newProxyData: Omit<ProxyRepository.ProxyData, 'id' | 'created_at' | 'updated_at'> = {
name: proxyData.name,
type: proxyData.type,
host: proxyData.host,
port: proxyData.port,
username: proxyData.username || null,
auth_method: proxyData.auth_method || 'none',
encrypted_password: proxyData.encrypted_password || null,
encrypted_private_key: proxyData.encrypted_private_key || null,
encrypted_passphrase: proxyData.encrypted_passphrase || null,
};
proxyIdToUse = await ProxyRepository.createProxy(newProxyData);
console.log(`Service: 导入连接 ${connData.name}: 新代理 ${proxyData.name} 创建成功 (ID: ${proxyIdToUse})`);
}
if (proxyIdToUse) proxyCache[cacheKey] = proxyIdToUse; // Cache the ID
}
} catch (innerError: any) {
// Catch errors during the process (e.g., bulk insert failure)
console.error('Service: 导入事务内部出错:', innerError);
db.run('ROLLBACK', (rollbackErr: Error | null) => {
if (rollbackErr) console.error("Service: 回滚事务失败:", rollbackErr);
rejectOuter(innerError); // Reject outer promise
});
}
}); // End BEGIN TRANSACTION
}); // End db.serialize
}); // End new Promise
// Prepare connection data for bulk insert (add tag_ids here)
connectionsToInsert.push({
name: connData.name,
host: connData.host,
port: connData.port,
username: connData.username,
auth_method: connData.auth_method,
encrypted_password: connData.encrypted_password || null,
encrypted_private_key: connData.encrypted_private_key || null,
encrypted_passphrase: connData.encrypted_passphrase || null,
proxy_id: proxyIdToUse,
tag_ids: connData.tag_ids || [] // Include tag_ids
});
} catch (connError: any) {
failureCount++;
errors.push({ connectionName: connData.name || '未知连接', message: connError.message });
console.warn(`Service: 处理导入连接 "${connData.name || '未知'}" 时出错: ${connError.message}`);
}
} // End for loop
// --- Pass 2: Bulk insert connections ---
let insertedResults: { connectionId: number, originalData: any }[] = [];
if (connectionsToInsert.length > 0) {
// Pass the transaction-aware db instance
insertedResults = await ConnectionRepository.bulkInsertConnections(db, connectionsToInsert);
successCount = insertedResults.length;
}
// --- Pass 3: Associate tags ---
const insertTagSql = `INSERT OR IGNORE INTO connection_tags (connection_id, tag_id) VALUES (?, ?)`; // Use INSERT OR IGNORE
for (const result of insertedResults) {
const originalTagIds = result.originalData?.tag_ids;
if (Array.isArray(originalTagIds) && originalTagIds.length > 0) {
const validTagIds = originalTagIds.filter((id: any) => typeof id === 'number' && id > 0);
if (validTagIds.length > 0) {
const tagPromises = validTagIds.map(tagId =>
runDb(db, insertTagSql, [result.connectionId, tagId]).catch(tagError => { // Use await runDb
console.warn(`Service: 导入连接 ${result.originalData.name}: 关联标签 ID ${tagId} 失败: ${tagError.message}`);
})
);
await Promise.all(tagPromises);
}
}
}
// Commit transaction using await runDb
await runDb(db, 'COMMIT');
console.log(`Service: 导入事务提交。成功: ${successCount}, 失败: ${failureCount}`);
return { successCount, failureCount, errors };
} catch (error: any) {
// Rollback transaction on any error during the process
console.error('Service: 导入事务处理出错,正在回滚:', error);
try {
await runDb(db, 'ROLLBACK'); // Use await runDb
} catch (rollbackErr: any) {
console.error("Service: 回滚事务失败:", rollbackErr);
}
// Adjust failure count and return error summary
failureCount = importedData.length;
successCount = 0;
errors.push({ message: `事务处理失败: ${error.message}` });
return { successCount, failureCount, errors };
}
};
@@ -1,9 +1,12 @@
import { getDb } from '../database';
// packages/backend/src/services/ip-blacklist.service.ts
// Import new async helpers and the instance getter
import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection';
import { settingsService } from './settings.service';
import { NotificationService } from './notification.service'; // 导入 NotificationService
import * as sqlite3 from 'sqlite3';
import * as sqlite3 from 'sqlite3'; // Keep for RunResult type if needed
const db = getDb();
// Remove top-level db instance
// const db = getDb();
const notificationService = new NotificationService(); // 实例化 NotificationService
// 黑名单相关设置的 Key
@@ -25,6 +28,9 @@ interface IpBlacklistEntry {
blocked_until: number | null;
}
// Define the expected row structure from the database if it matches IpBlacklistEntry
type DbIpBlacklistRow = IpBlacklistEntry;
export class IpBlacklistService {
/**
@@ -33,15 +39,14 @@ export class IpBlacklistService {
* @returns 黑名单记录或 undefined
*/
private async getEntry(ip: string): Promise<IpBlacklistEntry | undefined> {
return new Promise((resolve, reject) => {
db.get('SELECT * FROM ip_blacklist WHERE ip = ?', [ip], (err, row: IpBlacklistEntry) => {
if (err) {
console.error(`[IP Blacklist] 查询 IP ${ip} 时出错:`, err.message);
return reject(new Error('数据库查询失败'));
}
resolve(row);
});
});
try {
const db = await getDbInstance();
const row = await getDbRow<DbIpBlacklistRow>(db, 'SELECT * FROM ip_blacklist WHERE ip = ?', [ip]);
return row; // Returns undefined if not found
} catch (err: any) {
console.error(`[IP Blacklist] 查询 IP ${ip} 时出错:`, err.message);
throw new Error('数据库查询失败'); // Re-throw error
}
}
/**
@@ -60,11 +65,10 @@ export class IpBlacklistService {
console.log(`[IP Blacklist] IP ${ip} 当前被封禁,直到 ${new Date(entry.blocked_until * 1000).toISOString()}`);
return true; // 仍在封禁期内
}
// 如果封禁时间已过或为 null,则不再封禁
return false;
} catch (error) {
console.error(`[IP Blacklist] 检查 IP ${ip} 封禁状态时出错:`, error);
return false; // 出错时默认不封禁,避免锁死用户
} catch (error: any) { // Catch errors from getEntry
console.error(`[IP Blacklist] 检查 IP ${ip} 封禁状态时出错:`, error.message);
return false; // 出错时默认不封禁
}
}
@@ -74,7 +78,6 @@ export class IpBlacklistService {
* @param ip IP 地址
*/
async recordFailedAttempt(ip: string): Promise<void> {
// 如果是本地 IP,则不记录失败尝试,直接返回
if (LOCAL_IPS.includes(ip)) {
console.log(`[IP Blacklist] 检测到本地 IP ${ip} 登录失败,跳过黑名单处理。`);
return;
@@ -82,83 +85,75 @@ export class IpBlacklistService {
const now = Math.floor(Date.now() / 1000);
try {
// 获取设置,并提供默认值处理
const db = await getDbInstance();
const maxAttemptsStr = await settingsService.getSetting(MAX_LOGIN_ATTEMPTS_KEY);
const banDurationStr = await settingsService.getSetting(LOGIN_BAN_DURATION_KEY);
// 解析设置值,如果无效或未设置,则使用默认值
const maxAttempts = parseInt(maxAttemptsStr || '5', 10) || 5;
const banDuration = parseInt(banDurationStr || '300', 10) || 300;
const entry = await this.getEntry(ip);
if (entry) {
// 更新现有记录
// Update existing record
const newAttempts = entry.attempts + 1;
let blockedUntil = entry.blocked_until;
let shouldNotify = false;
// 检查是否达到封禁阈值
if (newAttempts >= maxAttempts && !entry.blocked_until) { // 只有在之前未被封禁时才触发通知
if (newAttempts >= maxAttempts && !entry.blocked_until) { // Only block and notify if not already blocked
blockedUntil = now + banDuration;
shouldNotify = true;
console.warn(`[IP Blacklist] IP ${ip} 登录失败次数达到 ${newAttempts} 次 (阈值 ${maxAttempts}),将被封禁 ${banDuration} 秒。`);
// 触发 IP_BLACKLISTED 通知
} else if (newAttempts >= maxAttempts && entry.blocked_until) {
console.log(`[IP Blacklist] IP ${ip} 再次登录失败,当前已处于封禁状态。`);
// Optionally extend ban duration here if needed
}
await runDb(db,
'UPDATE ip_blacklist SET attempts = ?, last_attempt_at = ?, blocked_until = ? WHERE ip = ?',
[newAttempts, now, blockedUntil, ip]
);
if (shouldNotify && blockedUntil) {
// Trigger notification after successful DB update
notificationService.sendNotification('IP_BLACKLISTED', {
ip: ip,
attempts: newAttempts,
duration: banDuration, // 封禁时长(秒)
blockedUntil: new Date(blockedUntil * 1000).toISOString() // 封禁截止时间
duration: banDuration,
blockedUntil: new Date(blockedUntil * 1000).toISOString()
}).catch(err => console.error(`[IP Blacklist] 发送 IP_BLACKLISTED 通知失败 for IP ${ip}:`, err));
} else if (newAttempts >= maxAttempts && entry.blocked_until) {
// 如果已经达到阈值且已被封禁,可能需要更新封禁时间(如果策略是每次失败都延长)
// 当前逻辑是只在首次达到阈值时设置封禁时间,后续失败只增加次数
console.log(`[IP Blacklist] IP ${ip} 再次登录失败,当前已处于封禁状态。`);
}
await new Promise<void>((resolve, reject) => {
db.run(
'UPDATE ip_blacklist SET attempts = ?, last_attempt_at = ?, blocked_until = ? WHERE ip = ?',
[newAttempts, now, blockedUntil, ip],
(err) => {
if (err) {
console.error(`[IP Blacklist] 更新 IP ${ip} 失败尝试次数时出错:`, err.message);
return reject(err);
}
resolve();
}
);
});
} else {
// 插入新记录
// Insert new record
let blockedUntil: number | null = null;
const attempts = 1; // 首次尝试
if (attempts >= maxAttempts) { // 首次尝试就达到阈值
const attempts = 1;
let shouldNotify = false;
if (attempts >= maxAttempts) {
blockedUntil = now + banDuration;
console.warn(`[IP Blacklist] IP ${ip} 首次登录失败即达到阈值 ${maxAttempts},将被封禁 ${banDuration} 秒。`);
// 触发 IP_BLACKLISTED 通知
shouldNotify = true;
console.warn(`[IP Blacklist] IP ${ip} 首次登录失败即达到阈值 ${maxAttempts},将被封禁 ${banDuration} 秒。`);
}
await runDb(db,
'INSERT INTO ip_blacklist (ip, attempts, last_attempt_at, blocked_until) VALUES (?, ?, ?, ?)',
[ip, attempts, now, blockedUntil]
);
if (shouldNotify && blockedUntil) {
// Trigger notification after successful DB insert
notificationService.sendNotification('IP_BLACKLISTED', {
ip: ip,
attempts: attempts,
duration: banDuration,
blockedUntil: new Date(blockedUntil * 1000).toISOString()
}).catch(err => console.error(`[IP Blacklist] 发送 IP_BLACKLISTED 通知失败 for IP ${ip}:`, err));
}
await new Promise<void>((resolve, reject) => {
db.run(
'INSERT INTO ip_blacklist (ip, attempts, last_attempt_at, blocked_until) VALUES (?, 1, ?, ?)',
[ip, now, blockedUntil],
(err) => {
if (err) {
console.error(`[IP Blacklist] 插入新 IP ${ip} 失败记录时出错:`, err.message);
return reject(err);
}
resolve();
}
);
});
}
}
} catch (error) {
console.error(`[IP Blacklist] 记录 IP ${ip} 失败尝试时出错:`, error);
} catch (error: any) {
console.error(`[IP Blacklist] 记录 IP ${ip} 失败尝试时出错:`, error.message);
// Avoid throwing error here to prevent login process failure due to blacklist issues
}
}
@@ -168,19 +163,12 @@ export class IpBlacklistService {
*/
async resetAttempts(ip: string): Promise<void> {
try {
await new Promise<void>((resolve, reject) => {
// 直接删除记录,或者将 attempts 重置为 0 并清除 blocked_until
db.run('DELETE FROM ip_blacklist WHERE ip = ?', [ip], (err) => {
if (err) {
console.error(`[IP Blacklist] 重置 IP ${ip} 尝试次数时出错:`, err.message);
return reject(err);
}
console.log(`[IP Blacklist] 已重置 IP ${ip} 的失败尝试记录。`);
resolve();
});
});
} catch (error) {
console.error(`[IP Blacklist] 重置 IP ${ip} 尝试次数时出错:`, error);
const db = await getDbInstance();
await runDb(db, 'DELETE FROM ip_blacklist WHERE ip = ?', [ip]);
console.log(`[IP Blacklist] 已重置 IP ${ip} 的失败尝试记录。`);
} catch (error: any) {
console.error(`[IP Blacklist] 重置 IP ${ip} 尝试次数时出错:`, error.message);
// Avoid throwing error here
}
}
@@ -190,53 +178,42 @@ export class IpBlacklistService {
* @param offset 偏移量
*/
async getBlacklist(limit: number = 50, offset: number = 0): Promise<{ entries: IpBlacklistEntry[], total: number }> {
const entries = await new Promise<IpBlacklistEntry[]>((resolve, reject) => {
db.all('SELECT * FROM ip_blacklist ORDER BY last_attempt_at DESC LIMIT ? OFFSET ?', [limit, offset], (err, rows: IpBlacklistEntry[]) => {
if (err) {
console.error('[IP Blacklist] 获取黑名单列表时出错:', err.message);
return reject(new Error('数据库查询失败'));
}
resolve(rows);
});
});
const total = await new Promise<number>((resolve, reject) => {
db.get('SELECT COUNT(*) as count FROM ip_blacklist', (err, row: { count: number }) => {
if (err) {
console.error('[IP Blacklist] 获取黑名单总数时出错:', err.message);
return reject(0); // 出错时返回 0
}
resolve(row.count);
});
});
return { entries, total };
try {
const db = await getDbInstance();
const entries = await allDb<DbIpBlacklistRow>(db,
'SELECT * FROM ip_blacklist ORDER BY last_attempt_at DESC LIMIT ? OFFSET ?',
[limit, offset]
);
const countRow = await getDbRow<{ count: number }>(db, 'SELECT COUNT(*) as count FROM ip_blacklist');
const total = countRow?.count ?? 0;
return { entries, total };
} catch (error: any) {
console.error('[IP Blacklist] 获取黑名单列表时出错:', error.message);
// Return empty list on error? Or re-throw?
// throw new Error('获取黑名单列表失败');
return { entries: [], total: 0 }; // Return empty on error
}
}
/**
* 从黑名单中删除一个 IP (解除封禁)
* @param ip IP 地址
* @returns Promise<boolean> 是否成功删除
*/
async removeFromBlacklist(ip: string): Promise<void> {
async removeFromBlacklist(ip: string): Promise<boolean> {
try {
await new Promise<void>((resolve, reject) => {
// 将 this 类型改回 RunResult 以访问 changes 属性
db.run('DELETE FROM ip_blacklist WHERE ip = ?', [ip], function(this: sqlite3.RunResult, err: Error | null) {
if (err) {
console.error(`[IP Blacklist] 从黑名单删除 IP ${ip} 时出错:`, err.message);
return reject(err);
}
if (this.changes === 0) {
console.warn(`[IP Blacklist] 尝试删除 IP ${ip},但该 IP 不在黑名单中。`);
} else {
console.log(`[IP Blacklist] 从黑名单删除 IP ${ip}`);
}
resolve();
});
});
} catch (error) {
console.error(`[IP Blacklist] 从黑名单删除 IP ${ip} 时出错:`, error);
throw error; // 重新抛出错误,以便上层处理
const db = await getDbInstance();
const result = await runDb(db, 'DELETE FROM ip_blacklist WHERE ip = ?', [ip]);
if (result.changes > 0) {
console.log(`[IP Blacklist] 已从黑名单中删除 IP ${ip}`);
return true;
} else {
console.warn(`[IP Blacklist] 尝试删除 IP ${ip},但该 IP 不在黑名单中。`);
return false;
}
} catch (error: any) {
console.error(`[IP Blacklist] 从黑名单删除 IP ${ip} 时出错:`, error.message);
throw new Error(`从黑名单删除 IP ${ip} 时出错`); // Re-throw error
}
}
}
@@ -32,7 +32,7 @@ export const updateQuickCommand = async (id: number, name: string | null, comman
}
const finalName = name && name.trim().length > 0 ? name.trim() : null;
const changes = await QuickCommandsRepository.updateQuickCommand(id, finalName, command.trim());
return changes > 0;
return changes;
};
/**
@@ -42,7 +42,7 @@ export const updateQuickCommand = async (id: number, name: string | null, comman
*/
export const deleteQuickCommand = async (id: number): Promise<boolean> => {
const changes = await QuickCommandsRepository.deleteQuickCommand(id);
return changes > 0;
return changes;
};
/**
@@ -61,7 +61,7 @@ export const getAllQuickCommands = async (sortBy: QuickCommandSortBy = 'name'):
*/
export const incrementUsageCount = async (id: number): Promise<boolean> => {
const changes = await QuickCommandsRepository.incrementUsageCount(id);
return changes > 0;
return changes;
};
/**
+24 -12
View File
@@ -49,11 +49,12 @@ export const getConnectionDetails = async (connectionId: number): Promise<Decryp
try {
const fullConnInfo: DecryptedConnectionDetails = {
id: rawConnInfo.id,
name: rawConnInfo.name,
host: rawConnInfo.host,
port: rawConnInfo.port,
username: rawConnInfo.username,
auth_method: rawConnInfo.auth_method,
// Add null check for required fields from rawConnInfo
name: rawConnInfo.name ?? (() => { throw new Error(`Connection ID ${connectionId} has null name.`); })(),
host: rawConnInfo.host ?? (() => { throw new Error(`Connection ID ${connectionId} has null host.`); })(),
port: rawConnInfo.port ?? (() => { throw new Error(`Connection ID ${connectionId} has null port.`); })(),
username: rawConnInfo.username ?? (() => { throw new Error(`Connection ID ${connectionId} has null username.`); })(),
auth_method: rawConnInfo.auth_method ?? (() => { throw new Error(`Connection ID ${connectionId} has null auth_method.`); })(),
password: (rawConnInfo.auth_method === 'password' && rawConnInfo.encrypted_password) ? decrypt(rawConnInfo.encrypted_password) : undefined,
privateKey: (rawConnInfo.auth_method === 'key' && rawConnInfo.encrypted_private_key) ? decrypt(rawConnInfo.encrypted_private_key) : undefined,
passphrase: (rawConnInfo.auth_method === 'key' && rawConnInfo.encrypted_passphrase) ? decrypt(rawConnInfo.encrypted_passphrase) : undefined,
@@ -61,14 +62,25 @@ export const getConnectionDetails = async (connectionId: number): Promise<Decryp
};
if (rawConnInfo.proxy_db_id) {
// Add null checks for required proxy fields inside the if block
const proxyName = rawConnInfo.proxy_name ?? (() => { throw new Error(`Proxy for Connection ID ${connectionId} has null name.`); })();
const proxyType = rawConnInfo.proxy_type ?? (() => { throw new Error(`Proxy for Connection ID ${connectionId} has null type.`); })();
const proxyHost = rawConnInfo.proxy_host ?? (() => { throw new Error(`Proxy for Connection ID ${connectionId} has null host.`); })();
const proxyPort = rawConnInfo.proxy_port ?? (() => { throw new Error(`Proxy for Connection ID ${connectionId} has null port.`); })();
// Ensure proxyType is one of the allowed values
if (proxyType !== 'SOCKS5' && proxyType !== 'HTTP') {
throw new Error(`Proxy for Connection ID ${connectionId} has invalid type: ${proxyType}`);
}
fullConnInfo.proxy = {
id: rawConnInfo.proxy_db_id,
name: rawConnInfo.proxy_name,
type: rawConnInfo.proxy_type,
host: rawConnInfo.proxy_host,
port: rawConnInfo.proxy_port,
username: rawConnInfo.proxy_username || undefined,
password: rawConnInfo.proxy_encrypted_password ? decrypt(rawConnInfo.proxy_encrypted_password) : undefined,
id: rawConnInfo.proxy_db_id, // Already checked by the if condition
name: proxyName,
type: proxyType, // Already validated
host: proxyHost,
port: proxyPort,
username: rawConnInfo.proxy_username || undefined, // Optional, defaults to undefined
password: rawConnInfo.proxy_encrypted_password ? decrypt(rawConnInfo.proxy_encrypted_password) : undefined, // Optional, handled by decrypt logic
// 可以根据需要解密代理的其他凭证
};
}
+7 -3
View File
@@ -1,6 +1,6 @@
import { Request, Response } from 'express';
import path from 'path'; // 需要 path 用于处理文件名
import { activeSshConnections } from '../websocket'; // 导入共享的连接 Map
import { clientStates } from '../websocket'; // Import the exported clientStates map
/**
* (GET /api/v1/sftp/download)
@@ -25,9 +25,13 @@ export const downloadFile = async (req: Request, res: Response): Promise<void> =
// 查找与当前用户会话关联的活动 WebSocket 连接和 SFTP 会话
let userSftpSession = null;
// 注意:这种查找方式效率不高,实际应用中可能需要更优化的结构来按 userId 查找连接
for (const [ws, connData] of activeSshConnections.entries()) {
// TODO: Refactor this to use SftpService instead of directly accessing clientStates
// This direct access is not ideal and couples the controller to websocket internals.
for (const [sessionId, state] of clientStates.entries()) {
const ws = state.ws; // Get the WebSocket instance from the state
const connData = state; // Use the entire state object
// 假设 AuthenticatedWebSocket 上存储了 userId
if ((ws as any).userId === userId && connData.sftp) {
if (ws.userId === userId && connData.sftp) { // Access userId directly from AuthenticatedWebSocket
// 这里简单地取第一个找到的匹配连接,没有处理 connectionId 的匹配
// TODO: 需要一种方式将 HTTP 请求与特定的 WebSocket/SSH/SFTP 会话关联起来
// 临时方案:假设一个用户只有一个活动的 SSH/SFTP 会话
+4 -4
View File
@@ -3,7 +3,7 @@ import http from 'http';
import { Request, RequestHandler } from 'express';
import { Client, ClientChannel } from 'ssh2';
import { v4 as uuidv4 } from 'uuid'; // 用于生成唯一的会话 ID
import { getDb } from './database';
import { getDbInstance } from './database/connection'; // Updated import path, use getDbInstance
import { decrypt } from './utils/crypto';
import { SftpService } from './services/sftp.service';
import { StatusMonitorService } from './services/status-monitor.service';
@@ -33,7 +33,7 @@ export interface ClientState { // 导出以便 Service 可以导入
}
// 存储所有活动客户端的状态 (key: sessionId)
const clientStates = new Map<string, ClientState>();
export const clientStates = new Map<string, ClientState>(); // Export clientStates
// --- 服务实例化 ---
// 将 clientStates 传递给需要访问共享状态的服务
@@ -76,9 +76,9 @@ const cleanupClientConnection = (sessionId: string | undefined) => {
}
};
export const initializeWebSocket = (server: http.Server, sessionParser: RequestHandler): WebSocketServer => {
export const initializeWebSocket = async (server: http.Server, sessionParser: RequestHandler): Promise<WebSocketServer> => { // Make async
const wss = new WebSocketServer({ noServer: true });
const db = getDb(); // 获取数据库实例
const db = await getDbInstance(); // 获取数据库实例 (use await and getDbInstance)
// --- 心跳检测 ---
const heartbeatInterval = setInterval(() => {