From 77cd9272baa9f4d74092aa2cbfbe788b7d5504a1 Mon Sep 17 00:00:00 2001 From: Baobhan Sith <80159437+Heavrnl@users.noreply.github.com> Date: Sun, 20 Apr 2025 15:23:58 +0800 Subject: [PATCH] update --- dist-scripts/test-db-connection.js | 116 +++++ packages/backend/package-lock.json | 433 +++++++++++++++++- packages/backend/package.json | 3 +- .../-fE-hxxfwPXFiUPuUpQOxqLjPOzWpmQX.json | 1 + packages/backend/src/auth/auth.controller.ts | 216 +++++---- .../src/connections/connections.controller.ts | 2 +- packages/backend/src/database.ts | 113 ----- packages/backend/src/database/connection.ts | 213 +++++++++ packages/backend/src/database/migrations.ts | 24 + .../backend/src/database/schema.registry.ts | 99 ++++ packages/backend/src/database/schema.ts | 190 ++++++++ packages/backend/src/index.ts | 155 ++++--- packages/backend/src/migrations.ts | 286 ------------ .../src/repositories/appearance.repository.ts | 407 ++++++++-------- .../src/repositories/audit.repository.ts | 90 ++-- .../command-history.repository.ts | 131 +++--- .../src/repositories/connection.repository.ts | 426 +++++++++-------- .../repositories/notification.repository.ts | 168 ++++--- .../src/repositories/passkey.repository.ts | 212 ++++----- .../src/repositories/proxy.repository.ts | 169 +++---- .../repositories/quick-commands.repository.ts | 149 +++--- .../src/repositories/settings.repository.ts | 127 ++--- .../src/repositories/tag.repository.ts | 132 +++--- .../repositories/terminal-theme.repository.ts | 364 ++++++++------- .../src/services/command-history.service.ts | 5 +- .../src/services/import-export.service.ts | 392 ++++++++-------- .../src/services/ip-blacklist.service.ts | 211 ++++----- .../src/services/quick-commands.service.ts | 6 +- packages/backend/src/services/ssh.service.ts | 36 +- packages/backend/src/sftp/sftp.controller.ts | 10 +- packages/backend/src/websocket.ts | 8 +- 31 files changed, 2781 insertions(+), 2113 deletions(-) create mode 100644 dist-scripts/test-db-connection.js create mode 100644 packages/backend/sessions/-fE-hxxfwPXFiUPuUpQOxqLjPOzWpmQX.json delete mode 100644 packages/backend/src/database.ts create mode 100644 packages/backend/src/database/connection.ts create mode 100644 packages/backend/src/database/migrations.ts create mode 100644 packages/backend/src/database/schema.registry.ts create mode 100644 packages/backend/src/database/schema.ts delete mode 100644 packages/backend/src/migrations.ts diff --git a/dist-scripts/test-db-connection.js b/dist-scripts/test-db-connection.js new file mode 100644 index 0000000..868b4ed --- /dev/null +++ b/dist-scripts/test-db-connection.js @@ -0,0 +1,116 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +var sqlite3_1 = __importDefault(require("sqlite3")); +var path_1 = __importDefault(require("path")); +var fs_1 = __importDefault(require("fs")); +// --- 配置 --- +// 使用与 connection.ts 中相同的路径逻辑 +var dbDir = path_1.default.resolve(__dirname, '../packages/backend/data'); +var dbPath = path_1.default.join(dbDir, 'nexus-terminal-test-connection.db'); // 使用不同的测试文件名避免冲突 +var flags = sqlite3_1.default.OPEN_READWRITE | sqlite3_1.default.OPEN_CREATE; +console.log("[Test Script] \u6D4B\u8BD5\u6570\u636E\u5E93\u76EE\u5F55: ".concat(dbDir)); +console.log("[Test Script] \u6D4B\u8BD5\u6570\u636E\u5E93\u6587\u4EF6\u8DEF\u5F84: ".concat(dbPath)); +console.log("[Test Script] \u4F7F\u7528\u7684\u6807\u5FD7: READWRITE | CREATE"); +// --- 测试函数 --- +function testConnection(filePath, openFlags) { + return new Promise(function (resolve, reject) { + var connectionFailed = false; // 跟踪连接错误 + var successPathReached = false; // 跟踪是否进入成功回调 + var errorFromCallback = null; // 存储回调中的错误 + console.log("[Test Function] \u51C6\u5907\u8FDE\u63A5: ".concat(filePath)); + // 确保目录存在 (如果不存在则创建) + var dir = path_1.default.dirname(filePath); + if (!fs_1.default.existsSync(dir)) { + console.log("[Test Function] \u76EE\u5F55\u4E0D\u5B58\u5728\uFF0C\u521B\u5EFA: ".concat(dir)); + try { + fs_1.default.mkdirSync(dir, { recursive: true }); + console.log("[Test Function] \u76EE\u5F55\u521B\u5EFA\u6210\u529F: ".concat(dir)); + } + catch (mkdirErr) { + console.error("[Test Function] \u521B\u5EFA\u76EE\u5F55\u5931\u8D25: ".concat(mkdirErr.message)); + reject("\u521B\u5EFA\u76EE\u5F55\u5931\u8D25: ".concat(mkdirErr.message)); + return; + } + } + else { + console.log("[Test Function] \u76EE\u5F55\u5DF2\u5B58\u5728: ".concat(dir)); + } + // 删除旧的测试文件(如果存在)以确保干净的测试环境 + if (fs_1.default.existsSync(filePath)) { + try { + fs_1.default.unlinkSync(filePath); + console.log("[Test Function] \u5DF2\u5220\u9664\u65E7\u7684\u6D4B\u8BD5\u6587\u4EF6: ".concat(filePath)); + } + catch (unlinkErr) { + console.warn("[Test Function] \u5220\u9664\u65E7\u6D4B\u8BD5\u6587\u4EF6\u5931\u8D25 (\u53EF\u80FD\u88AB\u9501\u5B9A?): ".concat(unlinkErr.message)); + // 不阻止测试继续,但记录警告 + } + } + var db = new sqlite3_1.default.Database(filePath, openFlags, function (err) { + console.log('[Test Callback] 回调函数被触发'); + if (err) { + console.error("[Test Callback] \u56DE\u8C03\u6536\u5230\u9519\u8BEF: ".concat(err.message)); + errorFromCallback = err; + connectionFailed = true; + // 注意:我们在这里故意不 reject 或 return,以模拟原始代码中可能的问题 + // reject(`回调错误: ${err.message}`); // 正常应该在这里 reject + // return; + } + else { + console.log('[Test Callback] 回调未收到错误 (成功路径)'); + successPathReached = true; + // db.close(); // 正常应该在这里关闭连接 + // resolve("连接成功 (回调)"); // 正常应该在这里 resolve + } + }); + // 为了模拟异步回调完成,我们设置一个短暂的延迟 + // 这不是完美的,但可以帮助我们看到回调执行后的状态 + setTimeout(function () { + var _a; + console.log('[Test Timeout] 检查最终状态...'); + console.log("[Test Timeout] connectionFailed \u6807\u5FD7: ".concat(connectionFailed)); + console.log("[Test Timeout] successPathReached \u6807\u5FD7: ".concat(successPathReached)); + console.log("[Test Timeout] errorFromCallback: ".concat((_a = errorFromCallback === null || errorFromCallback === void 0 ? void 0 : errorFromCallback.message) !== null && _a !== void 0 ? _a : 'null')); + // 根据标志判断最终结果 + if (connectionFailed && successPathReached) { + reject("\u6D4B\u8BD5\u5931\u8D25\uFF1A\u68C0\u6D4B\u5230\u9519\u8BEF (".concat(errorFromCallback === null || errorFromCallback === void 0 ? void 0 : errorFromCallback.message, ")\uFF0C\u4F46\u6210\u529F\u8DEF\u5F84\u4E5F\u88AB\u6267\u884C\uFF01")); + } + else if (connectionFailed) { + reject("\u6D4B\u8BD5\u5931\u8D25\uFF1A\u8FDE\u63A5\u65F6\u53D1\u751F\u9519\u8BEF: ".concat(errorFromCallback === null || errorFromCallback === void 0 ? void 0 : errorFromCallback.message)); + } + else if (successPathReached) { + // 尝试关闭数据库 + db.close(function (closeErr) { + if (closeErr) { + console.error("[Test Timeout] \u5173\u95ED\u6210\u529F\u8FDE\u63A5\u65F6\u51FA\u9519: ".concat(closeErr.message)); + resolve("\u6D4B\u8BD5\u6210\u529F\uFF1A\u8FDE\u63A5\u6210\u529F\uFF0C\u4F46\u5173\u95ED\u65F6\u51FA\u9519: ".concat(closeErr.message)); + } + else { + console.log('[Test Timeout] 数据库连接已成功关闭。'); + resolve("测试成功:连接成功并已关闭。"); + } + }); + } + else { + // 如果回调从未被正确触发(理论上不太可能,除非超时太短) + reject("测试失败:超时后未检测到成功或失败状态。"); + } + }, 1000); // 等待 1 秒让回调执行 + }); +} +// --- 执行测试 --- +console.log('--- 开始数据库连接测试 ---'); +testConnection(dbPath, flags) + .then(function (result) { + console.log('--- 测试结果 ---'); + console.log(result); + process.exit(0); // 成功退出 +}) + .catch(function (error) { + console.error('--- 测试结果 ---'); + console.error(error); + process.exit(1); // 失败退出 +}); diff --git a/packages/backend/package-lock.json b/packages/backend/package-lock.json index 30a2d39..ea484ab 100644 --- a/packages/backend/package-lock.json +++ b/packages/backend/package-lock.json @@ -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", diff --git a/packages/backend/package.json b/packages/backend/package.json index 7caca35..e996262 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -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", diff --git a/packages/backend/sessions/-fE-hxxfwPXFiUPuUpQOxqLjPOzWpmQX.json b/packages/backend/sessions/-fE-hxxfwPXFiUPuUpQOxqLjPOzWpmQX.json new file mode 100644 index 0000000..7beb3ec --- /dev/null +++ b/packages/backend/sessions/-fE-hxxfwPXFiUPuUpQOxqLjPOzWpmQX.json @@ -0,0 +1 @@ +{"cookie":{"secure":false,"httpOnly":true,"path":"/"},"userId":1,"username":"admin","requiresTwoFactor":false,"__lastAccess":1745133821105} \ No newline at end of file diff --git a/packages/backend/src/auth/auth.controller.ts b/packages/backend/src/auth/auth.controller.ts index 2115825..3298dad 100644 --- a/packages/backend/src/auth/auth.controller.ts +++ b/packages/backend/src/auth/auth.controller.ts @@ -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 => { } try { + const db = await getDbInstance(); // Get DB instance inside the function + // Use the promisified getDb helper + const user = await getDb(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((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 => { resolve(row); }); }); + */ if (!user) { console.log(`登录尝试失败: 用户未找到 - ${username}`); @@ -144,7 +152,11 @@ export const getAuthStatus = async (req: Request, res: Response): Promise } 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 resolve(row); }); }); + */ // 如果找不到用户(理论上不应发生),也视为未认证 if (!user) { @@ -194,7 +207,11 @@ export const verifyLogin2FA = async (req: Request, res: Response): Promise } try { - // 获取用户的 2FA 密钥 + const db = await getDbInstance(); // Get DB instance + // 获取用户的 2FA 密钥 using promisified getDb + const user = await getDb(db, 'SELECT id, username, two_factor_secret FROM users WHERE id = ?', [userId]); + + /* Original callback logic replaced by await getDb const user = await new Promise((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 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 } try { + const db = await getDbInstance(); // Get DB instance + // Use promisified getDb + const user = await getDb(db, 'SELECT id, hashed_password FROM users WHERE id = ?', [userId]); + + /* Original callback logic replaced by await getDb const user = await new Promise((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 resolve(row); }); }); + */ if (!user) { console.error(`修改密码错误: 未找到 ID 为 ${userId} 的用户。`); @@ -317,27 +341,21 @@ export const changePassword = async (req: Request, res: Response): Promise const newHashedPassword = await bcrypt.hash(newPassword, saltRounds); const now = Math.floor(Date.now() / 1000); - await new Promise((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 => { } 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((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((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 => } try { - // 1. 验证当前密码 + const db = await getDbInstance(); // Get DB instance + // 1. 验证当前密码 using promisified getDb + const user = await getDb(db, 'SELECT id, hashed_password FROM users WHERE id = ?', [userId]); + + /* Original callback logic replaced by await getDb const user = await new Promise((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 => res.status(400).json({ message: '当前密码不正确。' }); return; } - // 2. 清除数据库中的 2FA 密钥 + // 2. 清除数据库中的 2FA 密钥 using promisified runDb const now = Math.floor(Date.now() / 1000); - await new Promise((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 => */ export const needsSetup = async (req: Request, res: Response): Promise => { 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((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 => 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 => 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((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 => resolve(row ? row.count : 0); }); }); + */ if (userCount > 0) { console.warn('尝试在已有用户的情况下执行初始设置。'); @@ -692,33 +721,20 @@ export const setupAdmin = async (req: Request, res: Response): Promise => 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}) 已成功创建。`); diff --git a/packages/backend/src/connections/connections.controller.ts b/packages/backend/src/connections/connections.controller.ts index 2a26d8b..9d3c2ab 100644 --- a/packages/backend/src/connections/connections.controller.ts +++ b/packages/backend/src/connections/connections.controller.ts @@ -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(); // --- 清理结束 --- diff --git a/packages/backend/src/database.ts b/packages/backend/src/database.ts deleted file mode 100644 index 1b97653..0000000 --- a/packages/backend/src/database.ts +++ /dev/null @@ -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 函数。 diff --git a/packages/backend/src/database/connection.ts b/packages/backend/src/database/connection.ts new file mode 100644 index 0000000..6067eb5 --- /dev/null +++ b/packages/backend/src/database/connection.ts @@ -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 | 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 => { + 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 = (db: sqlite3.Database, sql: string, params: any[] = []): Promise => { + 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 = (db: sqlite3.Database, sql: string, params: any[] = []): Promise => { + 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 => { + 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 => { + 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. diff --git a/packages/backend/src/database/migrations.ts b/packages/backend/src/database/migrations.ts new file mode 100644 index 0000000..a2fd074 --- /dev/null +++ b/packages/backend/src/database/migrations.ts @@ -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 => { + return new Promise((resolve) => { + console.log('[Migrations] 检查数据库迁移(当前无操作)。'); + // 在这里添加未来的迁移逻辑,例如: + // db.serialize(() => { + // db.run("ALTER TABLE users ADD COLUMN last_login INTEGER;", (err) => { ... }); + // // 更多迁移步骤... + // }); + resolve(); // 立即解决,因为没有迁移要运行 + }); +}; + +// 可以保留一个默认导出或根据需要移除 +// export default runMigrations; diff --git a/packages/backend/src/database/schema.registry.ts b/packages/backend/src/database/schema.registry.ts new file mode 100644 index 0000000..2da886d --- /dev/null +++ b/packages/backend/src/database/schema.registry.ts @@ -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; // Optional initialization function +} + +// --- Initialization Functions --- + +/** + * Initializes default settings in the settings table. + */ +const initSettingsTable = async (db: Database): Promise => { + 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 => { + // 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 => { + // 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 +]; \ No newline at end of file diff --git a/packages/backend/src/database/schema.ts b/packages/backend/src/database/schema.ts new file mode 100644 index 0000000..ac91931 --- /dev/null +++ b/packages/backend/src/database/schema.ts @@ -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')) +); +`; \ No newline at end of file diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index d32eb23..85b4c3d 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -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((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 + }); }; // 先执行数据库初始化,成功后再启动服务器 diff --git a/packages/backend/src/migrations.ts b/packages/backend/src/migrations.ts deleted file mode 100644 index 4d2b061..0000000 --- a/packages/backend/src/migrations.ts +++ /dev/null @@ -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 => { - // 使用 Promise 包装 serialize,以便在所有操作完成后解析 - return new Promise((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 -}; diff --git a/packages/backend/src/repositories/appearance.repository.ts b/packages/backend/src/repositories/appearance.repository.ts index a223c6f..015cebf 100644 --- a/packages/backend/src/repositories/appearance.repository.ts +++ b/packages/backend/src/repositories/appearance.repository.ts @@ -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 = {}; + 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 => { 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 => { 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, 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 => { 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 */ export const getAppearanceSettings = async (): Promise => { - 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(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 是否成功更新 */ export const updateAppearanceSettings = async (settingsDto: UpdateAppearanceDto): Promise => { - 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) { - 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 - Success status + */ +// Internal function to update settings in the key-value table +const updateAppearanceSettingsInternal = async (db: sqlite3.Database, settingsDto: UpdateAppearanceDto): Promise => { + 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) { + 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('更新外观设置失败'); + } +}; diff --git a/packages/backend/src/repositories/audit.repository.ts b/packages/backend/src/repositories/audit.repository.ts index 7d19c52..804a7cd 100644 --- a/packages/backend/src/repositories/audit.repository.ts +++ b/packages/backend/src/repositories/audit.repository.ts @@ -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(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 }; diff --git a/packages/backend/src/repositories/command-history.repository.ts b/packages/backend/src/repositories/command-history.repository.ts index 46709fa..f59b048 100644 --- a/packages/backend/src/repositories/command-history.repository.ts +++ b/packages/backend/src/repositories/command-history.repository.ts @@ -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 => { - 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 => { 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 => { - const db = getDb(); +export const getAllCommands = async (): Promise => { 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(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 => { - const db = getDb(); +export const deleteCommandById = async (id: number): Promise => { 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 => { - const db = getDb(); +export const clearAllCommands = async (): Promise => { 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('无法清空命令历史记录'); + } }; diff --git a/packages/backend/src/repositories/connection.repository.ts b/packages/backend/src/repositories/connection.repository.ts index b409d5a..a113807 100644 --- a/packages/backend/src/repositories/connection.repository.ts +++ b/packages/backend/src/repositories/connection.repository.ts @@ -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 => { - 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(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 => { - 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(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 => { - 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 => { + 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(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 & { name: string | null }): Promise => { - 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): Promise => { + 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 & { name?: string | null }>): Promise => { +export const updateConnection = async (id: number, data: Partial>): Promise => { 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 => { - 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 => { - 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[]): 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 & { 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((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 }; diff --git a/packages/backend/src/repositories/notification.repository.ts b/packages/backend/src/repositories/notification.repository.ts index 39aa1fc..2e9dd7b 100644 --- a/packages/backend/src/repositories/notification.repository.ts +++ b/packages/backend/src/repositories/notification.repository.ts @@ -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 { - 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(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 { - 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(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 { - 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(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): Promise { 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>): Promise { @@ -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 { 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 }; diff --git a/packages/backend/src/repositories/passkey.repository.ts b/packages/backend/src/repositories/passkey.repository.ts index 5c961a0..4dc413c 100644 --- a/packages/backend/src/repositories/passkey.repository.ts +++ b/packages/backend/src/repositories/passkey.repository.ts @@ -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 新插入记录的 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 找到的记录或 null */ async getPasskeyByCredentialId(credentialId: string): Promise { 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(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 所有记录的数组 + * 获取所有已注册的 Passkey 记录 (仅选择必要字段) + * @returns Promise[]> 所有记录的部分信息的数组 */ - async getAllPasskeys(): Promise { - 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>> { + 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>(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 */ async updatePasskeyCounter(credentialId: string, newCounter: number): Promise { 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 + * @returns Promise 是否成功删除 */ - async deletePasskeyById(id: number): Promise { + async deletePasskeyById(id: number): Promise { 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 + * @returns Promise 是否成功删除 */ - async deletePasskeyByCredentialId(credentialId: string): Promise { + async deletePasskeyByCredentialId(credentialId: string): Promise { 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 匹配的记录数组 */ - async searchPasskeyByPrefix(prefix: string): Promise { + // Adjust return type based on selected columns if not selecting all (*) + async searchPasskeyByPrefix(prefix: string): Promise { 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(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' diff --git a/packages/backend/src/repositories/proxy.repository.ts b/packages/backend/src/repositories/proxy.repository.ts index f17ae7c..7e840c8 100644 --- a/packages/backend/src/repositories/proxy.repository.ts +++ b/packages/backend/src/repositories/proxy.repository.ts @@ -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): Promise => { - 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 => { - 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(db, sql); + return rows; + } catch (err: any) { + console.error('Repository: 查询代理列表时出错:', err.message); + throw new Error('获取代理列表失败'); + } }; /** * 根据 ID 获取单个代理 */ export const findProxyById = async (id: number): Promise => { - 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(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 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 => { - 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('删除代理记录失败'); + } }; diff --git a/packages/backend/src/repositories/quick-commands.repository.ts b/packages/backend/src/repositories/quick-commands.repository.ts index 6db08b2..fb1db1f 100644 --- a/packages/backend/src/repositories/quick-commands.repository.ts +++ b/packages/backend/src/repositories/quick-commands.repository.ts @@ -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 => { - const db = getDb(); +export const addQuickCommand = async (name: string | null, command: string): Promise => { 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 => { - const db = getDb(); +export const updateQuickCommand = async (id: number, name: string | null, command: string): Promise => { 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 => { - const db = getDb(); +export const deleteQuickCommand = async (id: number): Promise => { 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 => { - const db = getDb(); +export const getAllQuickCommands = async (sortBy: 'name' | 'usage_count' = 'name'): Promise => { 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(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 => { - const db = getDb(); +export const incrementUsageCount = async (id: number): Promise => { 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 => { - const db = getDb(); +export const findQuickCommandById = async (id: number): Promise => { 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(db, sql, [id]); + return row; // Returns undefined if not found + } catch (err: any) { + console.error('查找快捷指令时出错:', err.message); + throw new Error('无法查找快捷指令'); + } +}; // End of findQuickCommandById diff --git a/packages/backend/src/repositories/settings.repository.ts b/packages/backend/src/repositories/settings.repository.ts index b3ef660..27d41fd 100644 --- a/packages/backend/src/repositories/settings.repository.ts +++ b/packages/backend/src/repositories/settings.repository.ts @@ -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 { - 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(db, 'SELECT key, value FROM settings'); + return rows; + } catch (err: any) { + console.error('[Repository] 获取所有设置时出错:', err.message); + throw new Error('获取设置失败'); + } }, async getSetting(key: string): Promise { - 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 { - 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 { - 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 { // 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): Promise { - 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('批量设置失败'); + } }, }; diff --git a/packages/backend/src/repositories/tag.repository.ts b/packages/backend/src/repositories/tag.repository.ts index bcd2bca..34c286d 100644 --- a/packages/backend/src/repositories/tag.repository.ts +++ b/packages/backend/src/repositories/tag.repository.ts @@ -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 => { - 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(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 => { - 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(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 => { * 创建新标签 */ export const createTag = async (name: string): Promise => { - 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 => { - 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 => { - 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('删除标签失败'); + } }; diff --git a/packages/backend/src/repositories/terminal-theme.repository.ts b/packages/backend/src/repositories/terminal-theme.repository.ts index 125ae52..14e608c 100644 --- a/packages/backend/src/repositories/terminal-theme.repository.ts +++ b/packages/backend/src/repositories/terminal-theme.repository.ts @@ -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 */ export const findAllThemes = async (): Promise => { - 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(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 */ export const findThemeById = async (id: number): Promise => { - 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(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 = */ export const createTheme = async (themeDto: CreateTerminalThemeDto): Promise => { 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 => { 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 是否成功删除 */ export const deleteTheme = async (id: number): Promise => { - // 只允许删除非预设主题 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 & { preset_key?: string }>) => { +export const initializePresetThemes = async (db: Database, presets: Array & { 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((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 中进行 diff --git a/packages/backend/src/services/command-history.service.ts b/packages/backend/src/services/command-history.service.ts index 81c0aa9..62ef805 100644 --- a/packages/backend/src/services/command-history.service.ts +++ b/packages/backend/src/services/command-history.service.ts @@ -33,8 +33,9 @@ export const getAllCommandHistory = async (): Promise => * @returns 返回是否成功删除 (删除行数 > 0) */ export const deleteCommandHistoryById = async (id: number): Promise => { - const changes = await CommandHistoryRepository.deleteCommandById(id); - return changes > 0; + // deleteCommandById now directly returns boolean indicating success + const success = await CommandHistoryRepository.deleteCommandById(id); + return success; }; /** diff --git a/packages/backend/src/services/import-export.service.ts b/packages/backend/src/services/import-export.service.ts index 1ce3039..e3003bc 100644 --- a/packages/backend/src/services/import-export.service.ts +++ b/packages/backend/src/services/import-export.service.ts @@ -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 { - // Exclude fields not needed for export like id, created_at etc. -} - -// Define structure for import results +interface ExportedConnectionData extends Omit {} export interface ImportResult { successCount: number; failureCount: number; errors: { connectionName?: string; message: string }[]; } +// --- End Interface definitions --- + /** * 导出所有连接配置 */ export const exportConnections = async (): Promise => { - // 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((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(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 => 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[] = []; + const db = await getDbInstance(); // Get DB instance once for the transaction - // Use a transaction for atomicity - return new Promise((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 & { 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 = { + 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 }; + } }; diff --git a/packages/backend/src/services/ip-blacklist.service.ts b/packages/backend/src/services/ip-blacklist.service.ts index 39968cd..2cbf98d 100644 --- a/packages/backend/src/services/ip-blacklist.service.ts +++ b/packages/backend/src/services/ip-blacklist.service.ts @@ -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 { - 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(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 { - // 如果是本地 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((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((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 { try { - await new Promise((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((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((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(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 是否成功删除 */ - async removeFromBlacklist(ip: string): Promise { + async removeFromBlacklist(ip: string): Promise { try { - await new Promise((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 } } } diff --git a/packages/backend/src/services/quick-commands.service.ts b/packages/backend/src/services/quick-commands.service.ts index 4c4812c..5c4df47 100644 --- a/packages/backend/src/services/quick-commands.service.ts +++ b/packages/backend/src/services/quick-commands.service.ts @@ -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 => { 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 => { const changes = await QuickCommandsRepository.incrementUsageCount(id); - return changes > 0; + return changes; }; /** diff --git a/packages/backend/src/services/ssh.service.ts b/packages/backend/src/services/ssh.service.ts index 565fceb..3bf9014 100644 --- a/packages/backend/src/services/ssh.service.ts +++ b/packages/backend/src/services/ssh.service.ts @@ -49,11 +49,12 @@ export const getConnectionDetails = async (connectionId: number): Promise { 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 { 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 // 可以根据需要解密代理的其他凭证 }; } diff --git a/packages/backend/src/sftp/sftp.controller.ts b/packages/backend/src/sftp/sftp.controller.ts index 43d8f12..83558c7 100644 --- a/packages/backend/src/sftp/sftp.controller.ts +++ b/packages/backend/src/sftp/sftp.controller.ts @@ -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 = // 查找与当前用户会话关联的活动 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 会话 diff --git a/packages/backend/src/websocket.ts b/packages/backend/src/websocket.ts index 6d57d13..68b9759 100644 --- a/packages/backend/src/websocket.ts +++ b/packages/backend/src/websocket.ts @@ -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(); +export const clientStates = new Map(); // 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 => { // Make async const wss = new WebSocketServer({ noServer: true }); - const db = getDb(); // 获取数据库实例 + const db = await getDbInstance(); // 获取数据库实例 (use await and getDbInstance) // --- 心跳检测 --- const heartbeatInterval = setInterval(() => {