dsh-db-tool 1.4.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/client.js +19 -2
- package/dist/adapters/mongodb/index.js +22 -5
- package/dist/http/index.js +6 -0
- package/dist/manager.js +12 -3
- package/dist/store/connections.js +59 -6
- package/dist/store/index.js +1 -1
- package/package.json +1 -1
package/client/client.js
CHANGED
|
@@ -34,6 +34,8 @@ window.__ModuleLoader__.load({
|
|
|
34
34
|
urlMode: "URL 方式",
|
|
35
35
|
fieldsMode: "分字段方式",
|
|
36
36
|
url: "连接 URL(密码将自动拆入密钥存储)",
|
|
37
|
+
urlUser: "用户名(可选,自动注入 URL)",
|
|
38
|
+
urlPassword: "密码(可选,自动注入 URL;留空保持已存)",
|
|
37
39
|
fieldsHost: "主机地址",
|
|
38
40
|
fieldsPort: "端口",
|
|
39
41
|
fieldsUser: "用户名",
|
|
@@ -123,6 +125,8 @@ window.__ModuleLoader__.load({
|
|
|
123
125
|
urlMode: "URL",
|
|
124
126
|
fieldsMode: "Fields",
|
|
125
127
|
url: "Connection URL (password moved to secrets automatically)",
|
|
128
|
+
urlUser: "Username (optional, injected into URL)",
|
|
129
|
+
urlPassword: "Password (optional, injected into URL; blank keeps saved)",
|
|
126
130
|
fieldsHost: "Host",
|
|
127
131
|
fieldsPort: "Port",
|
|
128
132
|
fieldsUser: "User",
|
|
@@ -375,7 +379,7 @@ window.__ModuleLoader__.load({
|
|
|
375
379
|
}
|
|
376
380
|
|
|
377
381
|
/* ---------------- 连接管理 ---------------- */
|
|
378
|
-
const EMPTY_FORM = { id: "", kind: "mysql", name: "", mode: "url", url: "", host: "", port: "", user: "", password: "", database: "", ssl: false };
|
|
382
|
+
const EMPTY_FORM = { id: "", kind: "mysql", name: "", mode: "url", url: "", urlUser: "", urlPassword: "", host: "", port: "", user: "", password: "", database: "", ssl: false };
|
|
379
383
|
|
|
380
384
|
// 通用表格渲染:columns + rows(审计列表与查询结果共用)
|
|
381
385
|
function resultTable(columns, rows, cellTitles) {
|
|
@@ -457,6 +461,7 @@ window.__ModuleLoader__.load({
|
|
|
457
461
|
...EMPTY_FORM,
|
|
458
462
|
id: c.id, kind: c.kind, name: c.name || "", mode,
|
|
459
463
|
url: c.safeUrl || "",
|
|
464
|
+
urlUser: "", urlPassword: "",
|
|
460
465
|
host: c.host || "", port: c.port != null ? String(c.port) : "",
|
|
461
466
|
user: c.user || "", database: c.database || "",
|
|
462
467
|
ssl: !!c.ssl,
|
|
@@ -496,6 +501,11 @@ window.__ModuleLoader__.load({
|
|
|
496
501
|
if (form.url && form.url !== origUrl) body.url = form.url;
|
|
497
502
|
// 新建 url 连接必须发
|
|
498
503
|
if (!props.initial && form.url) body.url = form.url;
|
|
504
|
+
// URL 模式独立凭据:trim 后非空才发(服务端注入 userinfo;url 未发时以已存 URL 为基底)
|
|
505
|
+
const uu = (form.urlUser || "").trim();
|
|
506
|
+
const up = (form.urlPassword || "").trim();
|
|
507
|
+
if (uu) body.urlUser = uu;
|
|
508
|
+
if (up) body.urlPassword = up;
|
|
499
509
|
} else {
|
|
500
510
|
body.fields = { host: form.host, user: form.user, database: form.database || undefined };
|
|
501
511
|
if (form.port) body.fields.port = Number(form.port);
|
|
@@ -541,7 +551,14 @@ window.__ModuleLoader__.load({
|
|
|
541
551
|
React.createElement("button", { className: form.mode === "fields" ? "active" : "", onClick: reenterFields }, t("fieldsMode")),
|
|
542
552
|
),
|
|
543
553
|
form.mode === "url"
|
|
544
|
-
? React.createElement(
|
|
554
|
+
? React.createElement(
|
|
555
|
+
"div",
|
|
556
|
+
{ style: { display: "flex", flexDirection: "column", gap: 6 } },
|
|
557
|
+
React.createElement("input", { placeholder: t("url"), value: form.url, onChange: (e) => patch({ url: e.target.value }) }),
|
|
558
|
+
React.createElement("div", { className: "dbt-row" },
|
|
559
|
+
React.createElement("input", { placeholder: t("urlUser"), value: form.urlUser, onChange: (e) => patch({ urlUser: e.target.value }) }),
|
|
560
|
+
React.createElement("input", { type: "password", placeholder: t("urlPassword"), value: form.urlPassword, onChange: (e) => patch({ urlPassword: e.target.value }) })),
|
|
561
|
+
)
|
|
545
562
|
: React.createElement(
|
|
546
563
|
"div",
|
|
547
564
|
{ style: { display: "flex", flexDirection: "column", gap: 6 } },
|
|
@@ -166,7 +166,9 @@ export async function createMongoAdapter(conn, opts) {
|
|
|
166
166
|
}
|
|
167
167
|
const dbName = typeof conn.meta.database === 'string' && conn.meta.database !== ''
|
|
168
168
|
? conn.meta.database
|
|
169
|
-
:
|
|
169
|
+
: conn.url
|
|
170
|
+
? urlDbName(conn.url)
|
|
171
|
+
: defaultDbName(conn.fields);
|
|
170
172
|
const db = client.db(dbName);
|
|
171
173
|
function wrap(e) {
|
|
172
174
|
const M = vendorOrNull();
|
|
@@ -524,15 +526,30 @@ function inferBsonType(v) {
|
|
|
524
526
|
return 'object';
|
|
525
527
|
return typeof v;
|
|
526
528
|
}
|
|
527
|
-
function buildUri(fields, ssl) {
|
|
529
|
+
export function buildUri(fields, ssl) {
|
|
528
530
|
const host = typeof fields?.host === 'string' && fields.host !== '' ? fields.host : '127.0.0.1';
|
|
529
531
|
const port = typeof fields?.port === 'number' ? fields.port : 27017;
|
|
530
532
|
const user = typeof fields?.user === 'string' ? encodeURIComponent(fields.user) : '';
|
|
531
533
|
const pass = typeof fields?.password === 'string' ? `:${encodeURIComponent(fields.password)}` : '';
|
|
532
534
|
const auth = user !== '' ? `${user}${pass}@` : '';
|
|
533
|
-
|
|
534
|
-
const
|
|
535
|
-
|
|
535
|
+
// tls 用查询参数表达:mongodb+srv 是 DNS seedlist 形态(禁止显式端口),与 TLS 无关,不可混用
|
|
536
|
+
const params = [];
|
|
537
|
+
if (typeof fields?.authDatabase === 'string' && fields.authDatabase !== '') {
|
|
538
|
+
params.push(`authSource=${fields.authDatabase}`);
|
|
539
|
+
}
|
|
540
|
+
if (ssl || fields?.ssl === true || fields?.tls === true)
|
|
541
|
+
params.push('tls=true');
|
|
542
|
+
const qs = params.length > 0 ? `?${params.join('&')}` : '';
|
|
543
|
+
return `mongodb://${auth}${host}:${port}${qs}`;
|
|
544
|
+
}
|
|
545
|
+
/** URL 模式库名:取 URL path 首段(percent 解码),无则 'test' 兜底(与 pg-like mainDb 同口径) */
|
|
546
|
+
function urlDbName(url) {
|
|
547
|
+
try {
|
|
548
|
+
return decodeURIComponent(new URL(url).pathname.replace(/^\//, '')) || 'test';
|
|
549
|
+
}
|
|
550
|
+
catch {
|
|
551
|
+
return 'test';
|
|
552
|
+
}
|
|
536
553
|
}
|
|
537
554
|
function defaultDbName(fields) {
|
|
538
555
|
return typeof fields?.database === 'string' && fields.database !== '' ? fields.database : 'test';
|
package/dist/http/index.js
CHANGED
|
@@ -141,6 +141,8 @@ async function route(req, res, service, path, q, opts) {
|
|
|
141
141
|
id, kind: kind,
|
|
142
142
|
...(b['name'] !== undefined ? { name: str(b['name']) } : {}),
|
|
143
143
|
...(b['url'] !== undefined ? { url: str(b['url']) } : {}),
|
|
144
|
+
...(b['urlUser'] !== undefined ? { urlUser: str(b['urlUser']) } : {}),
|
|
145
|
+
...(b['urlPassword'] !== undefined ? { urlPassword: str(b['urlPassword']) } : {}),
|
|
144
146
|
...(b['fields'] !== undefined ? { fields: b['fields'] } : {}),
|
|
145
147
|
...(ssl !== undefined ? { ssl } : {}),
|
|
146
148
|
}));
|
|
@@ -158,6 +160,8 @@ async function route(req, res, service, path, q, opts) {
|
|
|
158
160
|
return sendMaybeConfirm(res, service.testDraft({
|
|
159
161
|
kind: kind,
|
|
160
162
|
...(b['url'] !== undefined ? { url: str(b['url']) } : {}),
|
|
163
|
+
...(b['urlUser'] !== undefined ? { urlUser: str(b['urlUser']) } : {}),
|
|
164
|
+
...(b['urlPassword'] !== undefined ? { urlPassword: str(b['urlPassword']) } : {}),
|
|
161
165
|
...(b['fields'] !== undefined ? { fields: b['fields'] } : {}),
|
|
162
166
|
...(ssl !== undefined ? { ssl } : {}),
|
|
163
167
|
// 编辑已有连接时透传:服务端拼回已存机密(留空密码/url 未改动的测试语义)
|
|
@@ -178,6 +182,8 @@ async function route(req, res, service, path, q, opts) {
|
|
|
178
182
|
return sendOk(res, service.updateConnection(id, {
|
|
179
183
|
...(b['name'] !== undefined ? { name: str(b['name']) } : {}),
|
|
180
184
|
...(b['url'] !== undefined ? { url: str(b['url']) } : {}),
|
|
185
|
+
...(b['urlUser'] !== undefined ? { urlUser: str(b['urlUser']) } : {}),
|
|
186
|
+
...(b['urlPassword'] !== undefined ? { urlPassword: str(b['urlPassword']) } : {}),
|
|
181
187
|
...(b['fields'] !== undefined ? { fields: b['fields'] } : {}),
|
|
182
188
|
...(b['clearUrl'] === true ? { clearUrl: true } : {}),
|
|
183
189
|
...(ssl !== undefined ? { ssl } : {}),
|
package/dist/manager.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getAdapter } from './adapters/index.js';
|
|
2
2
|
import { ChallengeStore, classifyStatement, } from './guard/index.js';
|
|
3
|
-
import { normalizeProjectKey, } from './store/index.js';
|
|
3
|
+
import { mergeUrlCredentials, normalizeProjectKey, } from './store/index.js';
|
|
4
4
|
import { runScriptInChild } from './script/runner.js';
|
|
5
5
|
/* ---------------- 错误与结果类型 ---------------- */
|
|
6
6
|
/** 携带机器可读 code 的业务错误(HTTP 层直接映射 {ok:false,error,code}) */
|
|
@@ -84,15 +84,24 @@ export class DbToolService {
|
|
|
84
84
|
};
|
|
85
85
|
let url = input.url;
|
|
86
86
|
let fields = input.fields !== undefined ? { ...input.fields } : undefined;
|
|
87
|
+
let secretsUrl;
|
|
87
88
|
if (input.connId) {
|
|
88
89
|
// 已存连接:编辑时客户端不发旧机密(留空语义),这里从 secrets 拼回
|
|
89
90
|
const sec = this.store.secrets.get(input.connId);
|
|
90
|
-
|
|
91
|
-
|
|
91
|
+
secretsUrl = sec?.url;
|
|
92
|
+
if (url === undefined && secretsUrl !== undefined)
|
|
93
|
+
url = secretsUrl;
|
|
92
94
|
if (fields !== undefined && sec?.password !== undefined && fields.password === undefined) {
|
|
93
95
|
fields.password = sec.password;
|
|
94
96
|
}
|
|
95
97
|
}
|
|
98
|
+
// URL 模式独立凭据注入(拼回已存 URL 之后;无凭据字段时 helper 原样返回,?? 保持不变)
|
|
99
|
+
url = mergeUrlCredentials({
|
|
100
|
+
url,
|
|
101
|
+
urlUser: input.urlUser,
|
|
102
|
+
urlPassword: input.urlPassword,
|
|
103
|
+
secretsUrl,
|
|
104
|
+
}) ?? url;
|
|
96
105
|
if (url !== undefined)
|
|
97
106
|
rc.url = url;
|
|
98
107
|
if (fields !== undefined)
|
|
@@ -28,6 +28,45 @@ export function redactUrl(url) {
|
|
|
28
28
|
const redacted = m[1] + userinfo.slice(0, colon + 1) + '***@' + authority.slice(at + 1);
|
|
29
29
|
return redacted + url.slice(m[0].length);
|
|
30
30
|
}
|
|
31
|
+
/** URL 模式独立凭据注入:把 urlUser/urlPassword 合并进 URL userinfo(8 库 scheme 均为标准 URL 形态)。
|
|
32
|
+
* - 两者均空 → 原样返回(url 可为 undefined,现状语义不变);
|
|
33
|
+
* - url 为 undefined → 有 secretsUrl(编辑场景已存真实 URL)以其为基底注入,否则凭据忽略返回 undefined;
|
|
34
|
+
* - url 含脱敏标记 ':***@'(编辑回填值)→ 有 secretsUrl 以其为基底整体替换,否则报错;
|
|
35
|
+
* - 注入用 WHATWG URL 的 username/password setter(自动 percent-encode '@:/?# ' 等特殊字符),
|
|
36
|
+
* 与适配器侧 decodeURIComponent / 驱动 RFC 3986 解码严格互逆;只填一项只覆盖一项
|
|
37
|
+
* (redis 'redis://:pass@host' 无用户名形态、oracle 必填用户名校验均兼容)。 */
|
|
38
|
+
export function mergeUrlCredentials(opts) {
|
|
39
|
+
const { url, urlUser, urlPassword, secretsUrl } = opts;
|
|
40
|
+
if (urlUser === undefined && urlPassword === undefined)
|
|
41
|
+
return url;
|
|
42
|
+
let base;
|
|
43
|
+
if (url === undefined) {
|
|
44
|
+
if (secretsUrl === undefined)
|
|
45
|
+
return undefined; // 无 URL 可注入,凭据忽略
|
|
46
|
+
base = secretsUrl;
|
|
47
|
+
}
|
|
48
|
+
else if (url.includes(':***@')) {
|
|
49
|
+
if (secretsUrl === undefined) {
|
|
50
|
+
throw new Error('URL 中含脱敏占位(***)且无已存真实 URL 可回退,请重新输入完整 URL 或清空用户名/密码框');
|
|
51
|
+
}
|
|
52
|
+
base = secretsUrl;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
base = url;
|
|
56
|
+
}
|
|
57
|
+
let u;
|
|
58
|
+
try {
|
|
59
|
+
u = new URL(base);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
throw new Error('连接 URL 无效,无法注入用户名/密码');
|
|
63
|
+
}
|
|
64
|
+
if (urlUser !== undefined && urlUser !== '')
|
|
65
|
+
u.username = urlUser;
|
|
66
|
+
if (urlPassword !== undefined && urlPassword !== '')
|
|
67
|
+
u.password = urlPassword;
|
|
68
|
+
return u.toString();
|
|
69
|
+
}
|
|
31
70
|
function splitPassword(fields) {
|
|
32
71
|
if (!fields || typeof fields[PASSWORD_KEY] !== 'string') {
|
|
33
72
|
return { clean: fields, password: undefined };
|
|
@@ -107,9 +146,15 @@ export class ConnectionStore {
|
|
|
107
146
|
if (clean !== undefined)
|
|
108
147
|
rec.fields = clean;
|
|
109
148
|
const secretPatch = {};
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
149
|
+
// URL 模式独立凭据注入(无凭据字段时 mergeUrlCredentials 原样返回,现状语义不变)
|
|
150
|
+
const finalUrl = mergeUrlCredentials({
|
|
151
|
+
url: input.url,
|
|
152
|
+
urlUser: input.urlUser,
|
|
153
|
+
urlPassword: input.urlPassword,
|
|
154
|
+
});
|
|
155
|
+
if (finalUrl !== undefined) {
|
|
156
|
+
rec.urlSafe = redactUrl(finalUrl);
|
|
157
|
+
secretPatch.url = finalUrl;
|
|
113
158
|
}
|
|
114
159
|
if (password !== undefined)
|
|
115
160
|
secretPatch.password = password;
|
|
@@ -135,9 +180,17 @@ export class ConnectionStore {
|
|
|
135
180
|
if (password !== undefined)
|
|
136
181
|
this.secrets.set(id, { password });
|
|
137
182
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
183
|
+
// URL 模式独立凭据注入:url 未发/发脱敏回填值时以已存 secrets.url 为基底(update 场景);
|
|
184
|
+
// 无 url 且无凭据字段时 mergeUrlCredentials 返回 undefined,自然跳过(无需外层守卫)
|
|
185
|
+
const finalUrl = mergeUrlCredentials({
|
|
186
|
+
url: patch.url,
|
|
187
|
+
urlUser: patch.urlUser,
|
|
188
|
+
urlPassword: patch.urlPassword,
|
|
189
|
+
secretsUrl: this.secrets.get(id)?.url,
|
|
190
|
+
});
|
|
191
|
+
if (finalUrl !== undefined) {
|
|
192
|
+
rec.urlSafe = redactUrl(finalUrl);
|
|
193
|
+
this.secrets.set(id, { url: finalUrl });
|
|
141
194
|
}
|
|
142
195
|
if (patch.clearUrl && rec.urlSafe !== undefined) {
|
|
143
196
|
delete rec.urlSafe;
|
package/dist/store/index.js
CHANGED
|
@@ -18,7 +18,7 @@ import { GrantStore } from './grants.js';
|
|
|
18
18
|
import { chmodBestEffort } from './io.js';
|
|
19
19
|
import { SecretsBox } from './secrets.js';
|
|
20
20
|
export { normalizeProjectKey } from './normalize.js';
|
|
21
|
-
export { redactUrl } from './connections.js';
|
|
21
|
+
export { mergeUrlCredentials, redactUrl } from './connections.js';
|
|
22
22
|
export { AuditLog, ConnectionStore, GrantStore, SecretsBox };
|
|
23
23
|
export class DbToolStore {
|
|
24
24
|
/** 数据目录:$DSH_HOME/db-tool */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-db-tool",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.1",
|
|
4
4
|
"description": "DSH community plugin: chat-operated multi-database admin tool with sidebar management, project-scoped grants, ro/rw modes and dangerous-operation confirmation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|