dsh-db-tool 0.1.6 → 0.1.7

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 CHANGED
@@ -38,6 +38,7 @@ window.__ModuleLoader__.load({
38
38
  fieldsPort: "端口",
39
39
  fieldsUser: "用户名",
40
40
  fieldsPassword: "密码",
41
+ passwordSaved: "已保存(留空保持不变)",
41
42
  fieldsDatabase: "数据库名",
42
43
  ssl: "启用 SSL",
43
44
  test: "测试",
@@ -126,6 +127,7 @@ window.__ModuleLoader__.load({
126
127
  fieldsPort: "Port",
127
128
  fieldsUser: "User",
128
129
  fieldsPassword: "Password",
130
+ passwordSaved: "Saved (leave blank to keep)",
129
131
  fieldsDatabase: "Database",
130
132
  ssl: "Enable SSL",
131
133
  test: "Test",
@@ -486,6 +488,8 @@ window.__ModuleLoader__.load({
486
488
  }
487
489
  function draftBody() {
488
490
  const body = { kind: form.kind, ssl: !!form.ssl };
491
+ // 编辑已有连接:透传 connId,服务端测试草稿时拼回已存机密(密码留空/url 未改动语义)
492
+ if (props.initial) body.connId = form.id;
489
493
  // 编辑保存语义:url 模式未改动(仍等于回填的脱敏 url)→ 不发 url,保留 secrets 原值
490
494
  const origUrl = props.initial && props.initial.mode === "url" ? props.initial.url : undefined;
491
495
  if (form.mode === "url") {
@@ -547,7 +551,7 @@ window.__ModuleLoader__.load({
547
551
  React.createElement("input", { placeholder: t("fieldsDatabase"), value: form.database, onChange: (e) => patch({ database: e.target.value }) })),
548
552
  React.createElement("div", { className: "dbt-row" },
549
553
  React.createElement("input", { placeholder: t("fieldsUser"), value: form.user, onChange: (e) => patch({ user: e.target.value }) }),
550
- React.createElement("input", { type: "password", placeholder: t("fieldsPassword"), value: form.password, onChange: (e) => patch({ password: e.target.value }) })),
554
+ React.createElement("input", { type: "password", placeholder: props.initial && props.initial.hasPassword ? t("passwordSaved") : t("fieldsPassword"), value: form.password, onChange: (e) => patch({ password: e.target.value }) })),
551
555
  ),
552
556
  // 分组 3:操作
553
557
  React.createElement(
@@ -160,6 +160,8 @@ async function route(req, res, service, path, q, opts) {
160
160
  ...(b['url'] !== undefined ? { url: str(b['url']) } : {}),
161
161
  ...(b['fields'] !== undefined ? { fields: b['fields'] } : {}),
162
162
  ...(ssl !== undefined ? { ssl } : {}),
163
+ // 编辑已有连接时透传:服务端拼回已存机密(留空密码/url 未改动的测试语义)
164
+ ...(b['connId'] !== undefined ? { connId: optStr(b['connId']) } : {}),
163
165
  }));
164
166
  }
165
167
  const connMatch = /^\/api\/connections\/([^/]+)(\/test)?$/.exec(path);
package/dist/manager.js CHANGED
@@ -75,15 +75,28 @@ export class DbToolService {
75
75
  /**
76
76
  * 测试未保存的连接草稿(侧边栏「保存前测试」)。不落库、不写审计,
77
77
  * 一次性适配器用完即关。url/fields 校验交给适配器层(与保存后测试同口径)。
78
+ * 编辑已有连接时传 connId:密码留空/url 未改动时自动拼回已存机密
79
+ * (仅注入本次测试,绝不回传客户端)。
78
80
  */
79
81
  async testDraft(input) {
80
82
  const rc = {
81
- meta: { id: '(draft)', kind: input.kind, name: '(draft)' },
83
+ meta: { id: input.connId ?? '(draft)', kind: input.kind, name: '(draft)' },
82
84
  };
83
- if (input.url !== undefined)
84
- rc.url = input.url;
85
- if (input.fields !== undefined)
86
- rc.fields = { ...input.fields };
85
+ let url = input.url;
86
+ let fields = input.fields !== undefined ? { ...input.fields } : undefined;
87
+ if (input.connId) {
88
+ // 已存连接:编辑时客户端不发旧机密(留空语义),这里从 secrets 拼回
89
+ const sec = this.store.secrets.get(input.connId);
90
+ if (url === undefined && sec?.url !== undefined)
91
+ url = sec.url;
92
+ if (fields !== undefined && sec?.password !== undefined && fields.password === undefined) {
93
+ fields.password = sec.password;
94
+ }
95
+ }
96
+ if (url !== undefined)
97
+ rc.url = url;
98
+ if (fields !== undefined)
99
+ rc.fields = fields;
87
100
  if (input.ssl !== undefined)
88
101
  rc.ssl = input.ssl;
89
102
  try {
@@ -35,8 +35,9 @@ function splitPassword(fields) {
35
35
  const { [PASSWORD_KEY]: password, ...clean } = fields;
36
36
  return { clean, password: password };
37
37
  }
38
- /** ConnRecord → 用户可见的 ConnectionMeta(契约见 lib/adapters/types.ts) */
39
- function toMeta(rec) {
38
+ /** ConnRecord → 用户可见的 ConnectionMeta(契约见 lib/adapters/types.ts)。
39
+ * hasPassword 只回布尔指示,明文绝不出库。 */
40
+ function toMeta(rec, hasPassword = false) {
40
41
  const meta = { id: rec.id, kind: rec.kind };
41
42
  if (rec.name !== undefined)
42
43
  meta.name = rec.name;
@@ -47,6 +48,8 @@ function toMeta(rec) {
47
48
  else {
48
49
  meta.mode = 'fields';
49
50
  }
51
+ if (hasPassword)
52
+ meta.hasPassword = true;
50
53
  if (rec.fields) {
51
54
  const { host, port, user, database } = rec.fields;
52
55
  if (typeof host === 'string')
@@ -78,12 +81,16 @@ export class ConnectionStore {
78
81
  findRec(data, id) {
79
82
  return data.connections.find((c) => c.id === id);
80
83
  }
84
+ /** 该连接是否已存密码(布尔指示,不读明文) */
85
+ hasPassword(id) {
86
+ return this.secrets.get(id)?.password !== undefined;
87
+ }
81
88
  list() {
82
- return this.load().connections.map(toMeta);
89
+ return this.load().connections.map((r) => toMeta(r, this.hasPassword(r.id)));
83
90
  }
84
91
  get(id) {
85
92
  const rec = this.findRec(this.load(), id);
86
- return rec ? toMeta(rec) : undefined;
93
+ return rec ? toMeta(rec, this.hasPassword(id)) : undefined;
87
94
  }
88
95
  /** 新建连接;id 已存在时抛错 */
89
96
  create(input) {
@@ -110,7 +117,7 @@ export class ConnectionStore {
110
117
  this.secrets.set(input.id, secretPatch);
111
118
  data.connections.push(rec);
112
119
  this.save(data);
113
- return toMeta(rec);
120
+ return toMeta(rec, this.hasPassword(input.id));
114
121
  }
115
122
  /** 部分更新;不存在返回 undefined。url/fields 变更时同步拆分 secrets */
116
123
  update(id, patch) {
@@ -134,11 +141,24 @@ export class ConnectionStore {
134
141
  }
135
142
  if (patch.clearUrl && rec.urlSafe !== undefined) {
136
143
  delete rec.urlSafe;
144
+ // 密码迁移:原 url 内嵌密码 → fields 密码(切方式后保持可连,编辑留空即保留)
145
+ const secUrl = this.secrets.get(id)?.url;
146
+ if (secUrl !== undefined) {
147
+ try {
148
+ const u = new URL(secUrl);
149
+ if (u.password && this.secrets.get(id)?.password === undefined) {
150
+ this.secrets.set(id, { password: decodeURIComponent(u.password) });
151
+ }
152
+ }
153
+ catch {
154
+ /* 非 URL 形态,忽略迁移 */
155
+ }
156
+ }
137
157
  // set 为合并写;显式置 undefined 经 JSON 序列化后等效删除该键
138
158
  this.secrets.set(id, { url: undefined });
139
159
  }
140
160
  this.save(data);
141
- return toMeta(rec);
161
+ return toMeta(rec, this.hasPassword(id));
142
162
  }
143
163
  /** 删除连接,级联删除 secrets 与该连接的所有项目授权。
144
164
  * 顺序按"失败开放"原则:先删权限(grants),再删机密(secrets),最后改
@@ -162,7 +182,7 @@ export class ConnectionStore {
162
182
  const rec = this.findRec(this.load(), id);
163
183
  if (!rec)
164
184
  throw new Error(`连接不存在: ${id}`);
165
- const meta = toMeta(rec);
185
+ const meta = toMeta(rec, this.secrets.get(id)?.password !== undefined);
166
186
  const rc = { meta };
167
187
  const sec = this.secrets.get(id);
168
188
  if (sec?.url !== undefined)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-db-tool",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
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",