dsh-webui-oauth 0.7.3 → 0.7.5

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/lib/client.js CHANGED
@@ -9,7 +9,28 @@ window.__ModuleLoader__.load({
9
9
  const inject = ['slots'];
10
10
 
11
11
  const CSS = [
12
- '.wua-section { display: flex; flex-direction: column; gap: 14px; max-width: 560px; padding: 4px 2px; }',
12
+ '.wua-section { display: flex; flex-direction: column; gap: 16px; max-width: 760px; padding: 4px 2px; }',
13
+ '.wua-card { display: flex; flex-direction: column; gap: 12px; padding: 16px 18px; border: 1px solid var(--dsw-alias-border-l1, #e5e5e5); border-radius: 10px; background: var(--dsw-alias-bg-layer-2, #fff); }',
14
+ '.wua-card-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; }',
15
+ '.wua-card-title { margin: 0; font-size: 14px; font-weight: 600; color: var(--dsw-alias-label-primary, #222); }',
16
+ '.wua-badges { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }',
17
+ '.wua-badge { font-size: 11px; line-height: 1.6; padding: 1px 8px; border-radius: 999px; border: 1px solid transparent; white-space: nowrap; }',
18
+ '.wua-badge.ok { color: var(--dsw-alias-state-success-primary, #1a7f37); background: color-mix(in srgb, var(--dsw-alias-state-success-primary, #1a7f37) 10%, transparent); border-color: color-mix(in srgb, var(--dsw-alias-state-success-primary, #1a7f37) 32%, transparent); }',
19
+ '.wua-badge.warn { color: var(--dsw-alias-state-warn-primary, #9a6700); background: color-mix(in srgb, var(--dsw-alias-state-warn-primary, #9a6700) 10%, transparent); border-color: color-mix(in srgb, var(--dsw-alias-state-warn-primary, #9a6700) 32%, transparent); }',
20
+ '.wua-badge.muted { color: var(--dsw-alias-label-tertiary, #777); background: color-mix(in srgb, var(--dsw-alias-label-tertiary, #777) 8%, transparent); border-color: color-mix(in srgb, var(--dsw-alias-label-tertiary, #777) 22%, transparent); }',
21
+ '.wua-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 12px; }',
22
+ '.wua-actions { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }',
23
+ '.wua-sep { height: 1px; background: var(--dsw-alias-border-l1, #e5e5e5); margin: 2px 0; }',
24
+ '.wua-kv { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }',
25
+ '.wua-float { position: fixed; right: 24px; bottom: 24px; z-index: 9999; width: min(440px, calc(100vw - 48px)); display: flex; flex-direction: column; gap: 10px; padding: 16px 18px; border: 1px solid var(--dsw-alias-border-l2, #999); border-radius: 12px; background: var(--dsw-alias-bg-layer-1, #fff); box-shadow: 0 18px 48px rgba(0, 0, 0, .22); }',
26
+ '.wua-float-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }',
27
+ '.wua-float-title { font-size: 13px; font-weight: 600; color: var(--dsw-alias-label-primary, #222); }',
28
+ '.wua-fab { position: fixed; right: 24px; bottom: 96px; z-index: 9998; padding: 9px 16px; font-size: 13px; cursor: pointer; color: #fff; background: var(--dsw-alias-brand-primary, #4a7cf7); border: none; border-radius: 999px; box-shadow: 0 10px 26px rgba(74, 124, 247, .35); }',
29
+ '.wua-fab:hover { filter: brightness(1.05); }',
30
+ '.wua-save-fab { position: fixed; right: 24px; bottom: 24px; z-index: 9997; padding: 10px 20px; font-size: 13px; font-weight: 600; cursor: pointer; color: #fff; background: var(--dsw-alias-brand-primary, #4a7cf7); border: none; border-radius: 999px; box-shadow: 0 12px 30px rgba(74, 124, 247, .38); }',
31
+ '.wua-save-fab:disabled { opacity: .55; cursor: default; }',
32
+ '.wua-modal-mask { position: fixed; inset: 0; z-index: 10000; display: flex; align-items: center; justify-content: center; padding: 24px; background: rgba(0, 0, 0, .45); }',
33
+ '.wua-modal { width: min(420px, 100%); display: flex; flex-direction: column; gap: 12px; padding: 20px 22px; border-radius: 12px; background: var(--dsw-alias-bg-layer-1, #fff); box-shadow: 0 24px 60px rgba(0, 0, 0, .35); }',
13
34
  '.wua-intro { margin: 0; font-size: 13px; line-height: 1.6; color: var(--dsw-alias-label-secondary, #888); }',
14
35
  '.wua-row { display: flex; flex-direction: column; gap: 5px; }',
15
36
  '.wua-label { font-size: 12px; color: var(--dsw-alias-label-secondary, #888); }',
@@ -45,6 +66,110 @@ window.__ModuleLoader__.load({
45
66
  if (styleEl.parentNode !== null) styleEl.parentNode.removeChild(styleEl);
46
67
  }, 'dsh-webui-oauth: styles');
47
68
 
69
+ // ---------------- 全局悬浮:DeepSeek 登录回调中转 ----------------
70
+ // 不放在设置页里:官方登录是在「设置 → 账号」发起的,回调地址可能在任何
71
+ // 页面/标签页拿到。这里挂一个全局悬浮按钮,任何页面都能打开粘贴窗口。
72
+ (function mountRelayWidget() {
73
+ if (typeof document === 'undefined' || !document.body) return;
74
+ if (document.getElementById('dsh-wua-relay')) return;
75
+
76
+ const wrap = document.createElement('div');
77
+ wrap.id = 'dsh-wua-relay';
78
+
79
+ const fab = document.createElement('button');
80
+ fab.type = 'button';
81
+ fab.className = 'wua-fab';
82
+ fab.textContent = 'DeepSeek 登录回调';
83
+ fab.title = '官方 DeepSeek 登录的回调中转';
84
+
85
+ const panel = document.createElement('div');
86
+ panel.className = 'wua-float';
87
+ panel.style.display = 'none';
88
+
89
+ const head = document.createElement('div');
90
+ head.className = 'wua-float-head';
91
+ const title = document.createElement('span');
92
+ title.className = 'wua-float-title';
93
+ title.textContent = 'DeepSeek 登录回调中转';
94
+ const close = document.createElement('button');
95
+ close.type = 'button';
96
+ close.className = 'wua-btn ghost';
97
+ close.textContent = '关闭';
98
+ head.appendChild(title);
99
+ head.appendChild(close);
100
+
101
+ const hint = document.createElement('p');
102
+ hint.className = 'wua-hint';
103
+ hint.style.borderTop = 'none';
104
+ hint.style.paddingTop = '0';
105
+ hint.textContent = '把浏览器地址栏里的完整回调 URL 粘贴到这里。只接受 http://127.0.0.1 或 localhost 的本机端口地址。';
106
+
107
+ const input = document.createElement('input');
108
+ input.className = 'wua-input';
109
+ input.type = 'text';
110
+ input.placeholder = 'http://127.0.0.1:3080/oauth/callback?code=…&state=…';
111
+
112
+ const actions = document.createElement('div');
113
+ actions.className = 'wua-actions';
114
+ const submit = document.createElement('button');
115
+ submit.type = 'button';
116
+ submit.className = 'wua-btn';
117
+ submit.textContent = '完成登录';
118
+ actions.appendChild(submit);
119
+
120
+ const msg = document.createElement('p');
121
+ msg.style.display = 'none';
122
+
123
+ panel.appendChild(head);
124
+ panel.appendChild(hint);
125
+ panel.appendChild(input);
126
+ panel.appendChild(actions);
127
+ panel.appendChild(msg);
128
+ wrap.appendChild(fab);
129
+ wrap.appendChild(panel);
130
+ document.body.appendChild(wrap);
131
+
132
+ const show = (text, ok) => {
133
+ msg.textContent = text;
134
+ msg.className = ok ? 'wua-msg' : 'wua-err';
135
+ msg.style.display = 'block';
136
+ };
137
+ const setOpen = (open) => {
138
+ panel.style.display = open ? 'flex' : 'none';
139
+ fab.style.display = open ? 'none' : 'inline-flex';
140
+ };
141
+
142
+ fab.addEventListener('click', () => setOpen(true));
143
+ close.addEventListener('click', () => setOpen(false));
144
+ submit.addEventListener('click', () => {
145
+ const url = input.value.trim();
146
+ if (!url) { show('请粘贴完整回调地址', false); return; }
147
+ submit.disabled = true;
148
+ fetch('/dsh-webui-oauth/oauth-relay', {
149
+ method: 'POST',
150
+ headers: { 'content-type': 'application/json' },
151
+ body: JSON.stringify({ url }),
152
+ }).then((r) => {
153
+ if (r.status === 401) { location.href = '/dsh-webui-oauth/login'; return null; }
154
+ return r.json();
155
+ }).then((res) => {
156
+ submit.disabled = false;
157
+ if (!res) return;
158
+ if (res.ok) {
159
+ show('已收到回调,正在完成登录。请回到「设置 → 账号」查看状态。', true);
160
+ input.value = '';
161
+ } else {
162
+ show(res.hint || res.reason || res.error || '中转失败', false);
163
+ }
164
+ }).catch(() => {
165
+ submit.disabled = false;
166
+ show('无法连接认证服务', false);
167
+ });
168
+ });
169
+
170
+ ctx.effect(() => () => wrap.remove(), 'dsh-webui-oauth: relay widget');
171
+ })();
172
+
48
173
  // ---------------- 会话失效:统一回登录页 ----------------
49
174
  const LOGIN_PATH = '/dsh-webui-oauth/login';
50
175
 
@@ -141,6 +266,15 @@ window.__ModuleLoader__.load({
141
266
  const [oidcBoundHint, setOidcBoundHint] = react.useState('');
142
267
  const [unbindPw, setUnbindPw] = react.useState('');
143
268
  const [bindMsg, setBindMsg] = react.useState(null);
269
+ // 敏感操作复验(保存配置 / 禁用 / 解绑):密码或 SSO 一次性令牌。
270
+ const [saveOpen, setSaveOpen] = react.useState(false);
271
+ const [savePw, setSavePw] = react.useState('');
272
+ const [saveErr, setSaveErr] = react.useState('');
273
+ const [reauthToken, setReauthToken] = react.useState(null);
274
+ const [reauthExp, setReauthExp] = react.useState(0);
275
+ // 远程访问信任开关。
276
+ const [remoteTrustBusy, setRemoteTrustBusy] = react.useState(false);
277
+ const [remoteTrustMsg, setRemoteTrustMsg] = react.useState(null);
144
278
  // 服务端算好的完整回调地址(含 scheme/host/port)。原生 OIDC 要求
145
279
  // redirect_uri 与 IdP 登记值精确匹配,少一个字符都会被拒,所以要显示全。
146
280
  const [oidcRedirectUri, setOidcRedirectUri] = react.useState('');
@@ -191,6 +325,19 @@ window.__ModuleLoader__.load({
191
325
  // 读取 OIDC 回调带回来的回执。绑定/登录结束后浏览器会被 302 回应用,
192
326
  // 结果通过 query 传递(oidcbound=1 / oidcerr=...):因为是整页跳转,
193
327
  // 组件重新挂载时才知道刚才发生了什么。
328
+ // 从 sessionStorage 恢复 SSO 复验令牌(2 分钟内有效)。
329
+ react.useEffect(() => {
330
+ try {
331
+ const raw = sessionStorage.getItem('dsh_wua_reauth')
332
+ if (!raw) return
333
+ const v = JSON.parse(raw)
334
+ if (v && typeof v.token === 'string' && v.exp > Date.now()) {
335
+ setReauthToken(v.token)
336
+ setReauthExp(v.exp)
337
+ }
338
+ } catch (e) { /* ignore */ }
339
+ }, []);
340
+
194
341
  react.useEffect(() => {
195
342
  let qs = ''
196
343
  try { qs = String((location && location.search) || '') } catch (e) { qs = '' }
@@ -198,8 +345,14 @@ window.__ModuleLoader__.load({
198
345
  const p = new URLSearchParams(qs)
199
346
  const bound = p.get('oidcbound')
200
347
  const err = p.get('oidcerr')
201
- if (bound === null && err === null) return
202
- if (bound !== null) {
348
+ const reauth = p.get('oidcreauth')
349
+ if (bound === null && err === null && reauth === null) return
350
+ if (reauth !== null) {
351
+ try { sessionStorage.setItem('dsh_wua_reauth', JSON.stringify({ token: reauth, exp: Date.now() + 120000 })) } catch (e) { /* ignore */ }
352
+ setReauthToken(reauth)
353
+ setReauthExp(Date.now() + 120000)
354
+ setBindMsg({ kind: 'ok', text: 'SSO 验证成功(2 分钟内有效)。请回到设置页点击「保存设置」。' })
355
+ } else if (bound !== null) {
203
356
  setBindMsg({ kind: 'ok', text: 'OIDC 身份绑定成功。此后只有该身份可通过 SSO 登录。' })
204
357
  } else {
205
358
  const LABELS = {
@@ -227,8 +380,9 @@ window.__ModuleLoader__.load({
227
380
  return { ok: true, text: '' };
228
381
  }
229
382
 
230
- const onSave = () => {
383
+ const onSave = (auth) => {
231
384
  if (busy) return;
385
+ const credentials = auth || {};
232
386
  const name = username.trim();
233
387
  // 与服务端 usernameError 一致的用户名约束
234
388
  if (!/^[A-Za-z0-9_-]{3,32}$/.test(name)) {
@@ -246,8 +400,8 @@ window.__ModuleLoader__.load({
246
400
  setMsg({ kind: 'err', text: '两次输入的密码不一致' });
247
401
  return;
248
402
  }
249
- if (enabled && !current) {
250
- setMsg({ kind: 'err', text: '修改认证信息需要输入当前密码' });
403
+ if (enabled && !credentials.current && !credentials.reauthToken) {
404
+ setSaveErr('修改认证信息需要验证:请输入当前密码,或使用 SSO 验证。');
251
405
  return;
252
406
  }
253
407
  setBusy(true);
@@ -265,7 +419,14 @@ window.__ModuleLoader__.load({
265
419
  // OIDC 关闭时不能「仅允许 SSO」,否则等于关掉所有登录入口
266
420
  ssoOnly: oidcEnabled ? oidcSsoOnly : false,
267
421
  };
268
- api('/dsh-webui-oauth/configure', { username: name, password, current, ttl: Number(ttl), oidc: oidcPayload }).then((r) => {
422
+ api('/dsh-webui-oauth/configure', {
423
+ username: name,
424
+ password,
425
+ current: credentials.current || '',
426
+ reauthToken: credentials.reauthToken || undefined,
427
+ ttl: Number(ttl),
428
+ oidc: oidcPayload,
429
+ }).then((r) => {
269
430
  setBusy(false);
270
431
  if (r && r.ok) {
271
432
  if (enabled) {
@@ -276,24 +437,37 @@ window.__ModuleLoader__.load({
276
437
  setPassword('');
277
438
  setConfirm('');
278
439
  setCurrent('');
440
+ setSaveOpen(false);
441
+ setSavePw('');
442
+ setSaveErr('');
443
+ setReauthToken(null);
444
+ setReauthExp(0);
445
+ try { sessionStorage.removeItem('dsh_wua_reauth') } catch (e) { /* ignore */ }
279
446
  api('/dsh-webui-oauth/status').then((s) => {
280
447
  if (s && !s.error) setStatus(s);
281
448
  });
282
449
  } else if (r && r.error === 'current-invalid') {
450
+ setSaveErr('当前密码不正确');
283
451
  setMsg({ kind: 'err', text: '当前密码不正确' });
284
452
  } else if (r && r.error === 'weak-password') {
285
- setMsg({ kind: 'err', text: '密码强度不足:至少 8 位,需包含大小写字母、数字和特殊符号' });
453
+ const text = '密码强度不足:至少 8 位,需包含大小写字母、数字和特殊符号';
454
+ setMsg({ kind: 'err', text }); setSaveErr(text);
286
455
  } else if (r && r.error === 'username-invalid') {
287
- setMsg({ kind: 'err', text: '用户名需为 3-32 位字母、数字、下划线或连字符' });
456
+ const text = '用户名需为 3-32 位字母、数字、下划线或连字符';
457
+ setMsg({ kind: 'err', text }); setSaveErr(text);
288
458
  } else if (r && r.error === 'legacy-hash') {
289
- setMsg({ kind: 'err', text: '当前凭据为旧版哈希(0.2.0 起仅支持 scrypt),必须填写新密码才能保存' });
459
+ const text = '当前凭据为旧版哈希(0.2.0 起仅支持 scrypt),必须填写新密码才能保存';
460
+ setMsg({ kind: 'err', text }); setSaveErr(text);
290
461
  } else if (r && (r.error === 'oidc-sso-only-unbound' || r.error === 'oidc-sso-only-invalid')) {
291
- setMsg({ kind: 'err', text: (r && r.reason) || '无法启用「仅允许 SSO」' });
462
+ const text = (r && r.reason) || '无法启用「仅允许 SSO」';
463
+ setMsg({ kind: 'err', text }); setSaveErr(text);
292
464
  } else if (r && typeof r.error === 'string') {
293
465
  // 服务端 500/未知错误:展示真实信息便于排查
294
- setMsg({ kind: 'err', text: '保存失败:' + r.error });
466
+ const text = '保存失败:' + r.error;
467
+ setMsg({ kind: 'err', text }); setSaveErr(text);
295
468
  } else {
296
- setMsg({ kind: 'err', text: '保存失败,请检查输入' });
469
+ const text = '保存失败,请检查输入';
470
+ setMsg({ kind: 'err', text }); setSaveErr(text);
297
471
  }
298
472
  }).catch(() => {
299
473
  setBusy(false);
@@ -379,6 +553,8 @@ window.__ModuleLoader__.load({
379
553
  };
380
554
 
381
555
  const enabled = Boolean(status && status.enabled);
556
+ const remoteTrust = Boolean(status && status.remoteTrust);
557
+ const reauthValid = Boolean(reauthToken && Date.now() < reauthExp);
382
558
  const intro = status === null
383
559
  ? '正在读取认证状态…'
384
560
  : (enabled
@@ -418,135 +594,152 @@ window.__ModuleLoader__.load({
418
594
  return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes());
419
595
  }
420
596
 
597
+ const onToggleRemoteTrust = () => {
598
+ if (remoteTrustBusy) return;
599
+ const next = !remoteTrust;
600
+ setRemoteTrustBusy(true);
601
+ setRemoteTrustMsg(null);
602
+ api('/dsh-webui-oauth/remote-trust', { enabled: next }).then((r) => {
603
+ setRemoteTrustBusy(false);
604
+ if (r && r.ok) {
605
+ setStatus((s) => (s ? { ...s, remoteTrust: !!r.enabled } : s));
606
+ setRemoteTrustMsg({
607
+ kind: 'ok',
608
+ text: r.enabled
609
+ ? '已开启远程访问信任。刷新页面后生效(设置 → 模型应可正常加载)。'
610
+ : '已关闭远程访问信任。刷新页面后生效。',
611
+ });
612
+ } else {
613
+ setRemoteTrustMsg({ kind: 'err', text: (r && (r.reason || r.error)) || '操作失败' });
614
+ }
615
+ }).catch(() => {
616
+ setRemoteTrustBusy(false);
617
+ setRemoteTrustMsg({ kind: 'err', text: '无法连接认证服务' });
618
+ });
619
+ };
620
+
621
+ // ---- 渲染辅助:卡片 + 徽章,避免重复嵌套 ----
622
+ const badge = (text, kind) => react.createElement('span', { className: 'wua-badge ' + (kind || 'muted') }, text);
623
+ const card = (title, badges, children) => react.createElement('section', { className: 'wua-card' },
624
+ react.createElement('div', { className: 'wua-card-head' },
625
+ react.createElement('h3', { className: 'wua-card-title' }, title),
626
+ react.createElement('div', { className: 'wua-badges' }, ...(badges || []).filter(Boolean))),
627
+ ...(children || []));
628
+
421
629
  return react.createElement('div', { className: 'wua-section' },
422
- react.createElement('p', { className: 'wua-intro' }, intro),
423
- patchWarn ? react.createElement('p', { className: 'wua-warn' }, patchWarn) : null,
424
- persistNote ? react.createElement('p', { className: 'wua-warn' }, persistNote) : null,
425
- react.createElement('div', { className: 'wua-row' },
426
- react.createElement('label', { className: 'wua-label' }, '用户名(3-32 位字母、数字、下划线或连字符)'),
427
- react.createElement('input', {
428
- className: 'wua-input',
429
- type: 'text',
430
- value: username,
431
- spellCheck: false,
432
- onChange: (e) => setUsername(e.target.value),
433
- }),
434
- ),
435
- react.createElement('div', { className: 'wua-row' },
436
- react.createElement('label', { className: 'wua-label' }, enabled ? '新密码(留空表示不修改)' : '密码(至少 8 位,含大小写、数字、特殊符号)'),
437
- react.createElement('input', {
438
- className: 'wua-input',
439
- type: 'password',
440
- value: password,
441
- onChange: (e) => setPassword(e.target.value),
442
- }),
443
- ),
444
- react.createElement('div', { className: 'wua-row' },
445
- react.createElement('label', { className: 'wua-label' }, '确认密码'),
446
- react.createElement('input', {
447
- className: 'wua-input',
448
- type: 'password',
449
- value: confirm,
450
- onChange: (e) => setConfirm(e.target.value),
451
- }),
452
- ),
453
- enabled ? react.createElement('div', { className: 'wua-row' },
454
- react.createElement('label', { className: 'wua-label' }, '当前密码(修改或禁用认证需验证)'),
455
- react.createElement('input', {
456
- className: 'wua-input',
457
- type: 'password',
458
- value: current,
459
- onChange: (e) => setCurrent(e.target.value),
460
- }),
461
- ) : null,
462
- react.createElement('div', { className: 'wua-row' },
463
- react.createElement('label', { className: 'wua-label' }, '会话有效期(登录后免登录时长)'),
464
- react.createElement('select', {
465
- className: 'wua-select',
466
- value: ttl,
467
- disabled: busy || status === null,
468
- onChange: (e) => setTtl(e.target.value),
469
- },
470
- react.createElement('option', { key: '0', value: '0' }, '浏览器会话:关闭浏览器后失效'),
471
- react.createElement('option', { key: '1', value: '1' }, '1 小时'),
472
- react.createElement('option', { key: '12', value: '12' }, '12 小时(默认)'),
473
- react.createElement('option', { key: '24', value: '24' }, '1 天'),
474
- react.createElement('option', { key: '72', value: '72' }, '3 天'),
475
- ),
476
- ),
477
- msg ? react.createElement('div', { className: msg.kind === 'ok' ? 'wua-msg' : 'wua-err' }, msg.text) : null,
478
- react.createElement('div', { className: 'wua-row' },
479
- react.createElement('label', { className: 'wua-label' }, 'OIDC 单点登录'),
480
- react.createElement('label', { className: 'wua-label' },
481
- react.createElement('input', {
482
- type: 'checkbox',
483
- checked: oidcEnabled,
484
- onChange: (e) => setOidcEnabled(e.target.checked),
485
- }),
486
- ' 启用(登录页显示「SSO 单点登录」按钮)'),
487
- ),
488
- oidcEnabled ? react.createElement('div', { className: 'wua-section' },
489
- react.createElement('div', { className: 'wua-row' },
490
- react.createElement('label', { className: 'wua-label' },
491
- react.createElement('input', {
492
- type: 'checkbox',
493
- checked: oidcSsoOnly,
494
- onChange: (e) => setOidcSsoOnly(e.target.checked),
495
- }),
496
- ' 仅允许 SSO(未登录时自动跳转到 SSO,并禁用密码登录)'),
497
- react.createElement('p', { className: 'wua-hint', style: { borderTop: 'none', paddingTop: 0 } },
498
- oidcBound
499
- ? '启用后密码登录接口会被拒绝,未登录访问一律跳转 SSO。请确认 IdP 可用,否则将无法登录;如被锁在门外,可手工把 dsh-webui-oauth.json 里的 ssoOnly 改回 false。'
500
- : '尚未绑定 OIDC 身份,无法启用:请先在下方完成「绑定 OIDC 身份」。'),
501
- ),
502
- react.createElement('div', { className: 'wua-row' },
503
- react.createElement('label', { className: 'wua-label' }, 'Issuer(IdP 地址,须为 https)'),
504
- react.createElement('input', { className: 'wua-input', type: 'text', value: oidcIssuer, spellCheck: false, onChange: (e) => setOidcIssuer(e.target.value) }),
505
- ),
506
- react.createElement('div', { className: 'wua-row' },
507
- react.createElement('label', { className: 'wua-label' }, 'Client ID'),
508
- react.createElement('input', { className: 'wua-input', type: 'text', value: oidcClientId, spellCheck: false, onChange: (e) => setOidcClientId(e.target.value) }),
509
- ),
510
- react.createElement('div', { className: 'wua-row' },
511
- react.createElement('label', { className: 'wua-label' }, 'Client Secret(留空保留原值;机密客户端必填)'),
512
- react.createElement('input', { className: 'wua-input', type: 'password', value: oidcClientSecret, autoComplete: 'new-password', onChange: (e) => setOidcClientSecret(e.target.value) }),
630
+ card('概览', [
631
+ enabled ? badge('认证已启用', 'ok') : badge('认证未启用', 'muted'),
632
+ oidcEnabled ? badge('OIDC 已启用', 'ok') : badge('OIDC 未启用', 'muted'),
633
+ oidcEnabled ? (oidcBound ? badge('身份已绑定', 'ok') : badge('身份未绑定', 'warn')) : null,
634
+ (status && status.oidc && status.oidc.ssoOnlyActive) ? badge('仅允许 SSO', 'ok') : null,
635
+ remoteTrust ? badge('远程信任已开启', 'ok') : null,
636
+ ], [
637
+ react.createElement('p', { className: 'wua-intro' }, intro),
638
+ patchWarn ? react.createElement('p', { className: 'wua-warn' }, patchWarn) : null,
639
+ persistNote ? react.createElement('p', { className: 'wua-warn' }, persistNote) : null,
640
+ react.createElement('div', { className: 'wua-actions' },
641
+ enabled ? react.createElement('button', { className: 'wua-btn ghost', disabled: busy || status === null, onClick: onLogout }, '退出登录') : null,
642
+ enabled ? react.createElement('button', { className: 'wua-btn ghost', disabled: busy || status === null, onClick: onDisable }, '禁用认证') : null,
513
643
  ),
514
- react.createElement('div', { className: 'wua-row' },
515
- react.createElement('label', { className: 'wua-label' }, 'Scope(空格分隔)'),
516
- react.createElement('input', { className: 'wua-input', type: 'text', value: oidcScope, spellCheck: false, onChange: (e) => setOidcScope(e.target.value) }),
644
+ ]),
645
+ card('账号与密码', [], [
646
+ react.createElement('div', { className: 'wua-grid' },
647
+ react.createElement('div', { className: 'wua-row' },
648
+ react.createElement('label', { className: 'wua-label' }, '用户名(3-32 位字母、数字、下划线或连字符)'),
649
+ react.createElement('input', { className: 'wua-input', type: 'text', value: username, spellCheck: false, onChange: (e) => setUsername(e.target.value) }),
650
+ ),
651
+ react.createElement('div', { className: 'wua-row' },
652
+ react.createElement('label', { className: 'wua-label' }, '会话有效期(登录后免登录时长)'),
653
+ react.createElement('select', { className: 'wua-select', value: ttl, disabled: busy || status === null, onChange: (e) => setTtl(e.target.value) },
654
+ react.createElement('option', { key: '0', value: '0' }, '浏览器会话:关闭浏览器后失效'),
655
+ react.createElement('option', { key: '1', value: '1' }, '1 小时'),
656
+ react.createElement('option', { key: '12', value: '12' }, '12 小时(默认)'),
657
+ react.createElement('option', { key: '24', value: '24' }, '1 天'),
658
+ react.createElement('option', { key: '72', value: '72' }, '3 天'),
659
+ ),
660
+ ),
517
661
  ),
518
- react.createElement('div', { className: 'wua-row' },
519
- react.createElement('label', { className: 'wua-label' }, 'Redirect Base(可选;反代重写 Host 时必填,如 https://dsh.example.com)'),
520
- react.createElement('input', { className: 'wua-input', type: 'text', value: oidcRedirectBase, spellCheck: false, onChange: (e) => setOidcRedirectBase(e.target.value) }),
662
+ react.createElement('div', { className: 'wua-grid' },
663
+ react.createElement('div', { className: 'wua-row' },
664
+ react.createElement('label', { className: 'wua-label' }, enabled ? '新密码(留空表示不修改)' : '密码(至少 8 位,含大小写、数字、特殊符号)'),
665
+ react.createElement('input', { className: 'wua-input', type: 'password', value: password, onChange: (e) => setPassword(e.target.value) }),
666
+ ),
667
+ react.createElement('div', { className: 'wua-row' },
668
+ react.createElement('label', { className: 'wua-label' }, '确认密码'),
669
+ react.createElement('input', { className: 'wua-input', type: 'password', value: confirm, onChange: (e) => setConfirm(e.target.value) }),
670
+ ),
521
671
  ),
522
- // 回调地址:给部署者抄进 IdP 的允许列表用。
523
- // 原生 OIDC 要求精确匹配,必须显示完整地址(含协议与端口)。
524
- react.createElement('div', { className: 'wua-row' },
525
- react.createElement('label', { className: 'wua-label' }, '回调地址(填入 IdP 的 Redirect URI 允许列表,须精确匹配)'),
526
- oidcRedirectUri
527
- ? react.createElement('p', { className: 'wua-hint', style: { borderTop: 'none', paddingTop: 0, wordBreak: 'break-all' } },
528
- react.createElement('code', null, oidcRedirectUri))
529
- : react.createElement('p', { className: 'wua-warn' },
530
- '无法确定回调地址:请先填写上方 Redirect Base(或在本插件的 remote-web-ui 设置里配置 publicBaseUrl)。'),
531
- ),
532
- react.createElement('div', { className: 'wua-row' },
533
- react.createElement('label', { className: 'wua-label' },
534
- react.createElement('input', { type: 'checkbox', checked: oidcTrustBrowser, onChange: (e) => setOidcTrustBrowser(e.target.checked) }),
535
- ' 信任浏览器 origin(默认开启;关闭时仅用上方 Redirect Base)'),
536
- ),
537
- react.createElement('div', { className: 'wua-row' },
538
- react.createElement('label', { className: 'wua-label' }, 'Prompt(可选,建议留空)'),
539
- react.createElement('input', { className: 'wua-input', type: 'text', value: oidcPrompt, spellCheck: false, placeholder: '留空则不发送该参数', onChange: (e) => setOidcPrompt(e.target.value) }),
540
- react.createElement('p', { className: 'wua-hint', style: { borderTop: 'none', paddingTop: 0 } },
541
- '多数 IdP 无需该参数。部分 IdP 会对不认识的值直接拒绝授权(提示 unsupported prompt value requested),此时请保持留空。'),
542
- ),
543
- ) : null,
544
- // ---- OIDC 身份绑定 ----
545
- // 绑定是访问控制前提:未绑定时任何能通过该 IdP 的人都会被拒,
546
- // 登录页也不会显示 SSO 按钮。因此这块独立于"是否勾选启用",
547
- // 只要 OIDC 已配置就展示当前绑定状态。
548
- oidcEnabled ? react.createElement('div', { className: 'wua-section' },
549
- react.createElement('label', { className: 'wua-label' }, 'OIDC 身份绑定'),
672
+ enabled ? react.createElement('div', { className: 'wua-row' },
673
+ react.createElement('label', { className: 'wua-label' }, '当前密码(修改或禁用认证需验证)'),
674
+ react.createElement('input', { className: 'wua-input', type: 'password', value: current, onChange: (e) => setCurrent(e.target.value) }),
675
+ ) : null,
676
+ msg ? react.createElement('div', { className: msg.kind === 'ok' ? 'wua-msg' : 'wua-err' }, msg.text) : null,
677
+ ]),
678
+ card('OIDC 单点登录', [
679
+ oidcEnabled ? badge('已启用', 'ok') : badge('未启用', 'muted'),
680
+ (oidcEnabled && oidcBound) ? badge('身份已绑定', 'ok') : null,
681
+ ], [
682
+ react.createElement('label', { className: 'wua-label' },
683
+ react.createElement('input', { type: 'checkbox', checked: oidcEnabled, onChange: (e) => setOidcEnabled(e.target.checked) }),
684
+ ' 启用(登录页显示「SSO 单点登录」按钮)'),
685
+ oidcEnabled ? react.createElement('div', { className: 'wua-section', style: { gap: '12px' } },
686
+ react.createElement('div', { className: 'wua-row' },
687
+ react.createElement('label', { className: 'wua-label' },
688
+ react.createElement('input', { type: 'checkbox', checked: oidcSsoOnly, onChange: (e) => setOidcSsoOnly(e.target.checked) }),
689
+ ' 仅允许 SSO(未登录时自动跳转到 SSO,并禁用密码登录)'),
690
+ react.createElement('p', { className: 'wua-hint', style: { borderTop: 'none', paddingTop: 0 } },
691
+ oidcBound
692
+ ? '启用后密码登录接口会被拒绝,未登录访问一律跳转 SSO。请确认 IdP 可用,否则将无法登录;如被锁在门外,可手工把 dsh-webui-oauth.json 里的 ssoOnly 改回 false。'
693
+ : '尚未绑定 OIDC 身份,无法启用:请先在下方完成「绑定 OIDC 身份」。'),
694
+ ),
695
+ react.createElement('div', { className: 'wua-grid' },
696
+ react.createElement('div', { className: 'wua-row' },
697
+ react.createElement('label', { className: 'wua-label' }, 'Issuer(IdP 地址,须为 https)'),
698
+ react.createElement('input', { className: 'wua-input', type: 'text', value: oidcIssuer, spellCheck: false, onChange: (e) => setOidcIssuer(e.target.value) }),
699
+ ),
700
+ react.createElement('div', { className: 'wua-row' },
701
+ react.createElement('label', { className: 'wua-label' }, 'Client ID'),
702
+ react.createElement('input', { className: 'wua-input', type: 'text', value: oidcClientId, spellCheck: false, onChange: (e) => setOidcClientId(e.target.value) }),
703
+ ),
704
+ ),
705
+ react.createElement('div', { className: 'wua-grid' },
706
+ react.createElement('div', { className: 'wua-row' },
707
+ react.createElement('label', { className: 'wua-label' }, 'Client Secret(留空保留原值;机密客户端必填)'),
708
+ react.createElement('input', { className: 'wua-input', type: 'password', value: oidcClientSecret, autoComplete: 'new-password', onChange: (e) => setOidcClientSecret(e.target.value) }),
709
+ ),
710
+ react.createElement('div', { className: 'wua-row' },
711
+ react.createElement('label', { className: 'wua-label' }, 'Scope(空格分隔)'),
712
+ react.createElement('input', { className: 'wua-input', type: 'text', value: oidcScope, spellCheck: false, onChange: (e) => setOidcScope(e.target.value) }),
713
+ ),
714
+ ),
715
+ react.createElement('div', { className: 'wua-row' },
716
+ react.createElement('label', { className: 'wua-label' }, 'Redirect Base(可选;反代重写 Host 时必填,如 https://dsh.example.com)'),
717
+ react.createElement('input', { className: 'wua-input', type: 'text', value: oidcRedirectBase, spellCheck: false, onChange: (e) => setOidcRedirectBase(e.target.value) }),
718
+ ),
719
+ react.createElement('div', { className: 'wua-row' },
720
+ react.createElement('label', { className: 'wua-label' }, '回调地址(填入 IdP 的 Redirect URI 允许列表,须精确匹配)'),
721
+ oidcRedirectUri
722
+ ? react.createElement('p', { className: 'wua-hint', style: { borderTop: 'none', paddingTop: 0, wordBreak: 'break-all' } },
723
+ react.createElement('code', null, oidcRedirectUri))
724
+ : react.createElement('p', { className: 'wua-warn' },
725
+ '无法确定回调地址:请先填写上方 Redirect Base(或在本插件的 remote-web-ui 设置里配置 publicBaseUrl)。'),
726
+ ),
727
+ react.createElement('div', { className: 'wua-row' },
728
+ react.createElement('label', { className: 'wua-label' },
729
+ react.createElement('input', { type: 'checkbox', checked: oidcTrustBrowser, onChange: (e) => setOidcTrustBrowser(e.target.checked) }),
730
+ ' 信任浏览器 origin(默认开启;关闭时仅用上方 Redirect Base)'),
731
+ ),
732
+ react.createElement('div', { className: 'wua-row' },
733
+ react.createElement('label', { className: 'wua-label' }, 'Prompt(可选,建议留空)'),
734
+ react.createElement('input', { className: 'wua-input', type: 'text', value: oidcPrompt, spellCheck: false, placeholder: '留空则不发送该参数', onChange: (e) => setOidcPrompt(e.target.value) }),
735
+ react.createElement('p', { className: 'wua-hint', style: { borderTop: 'none', paddingTop: 0 } },
736
+ '多数 IdP 无需该参数。部分 IdP 会对不认识的值直接拒绝授权(提示 unsupported prompt value requested),此时请保持留空。'),
737
+ ),
738
+ ) : null,
739
+ ]),
740
+ oidcEnabled ? card('OIDC 身份绑定', [
741
+ oidcBound ? badge('已绑定', 'ok') : badge('未绑定', 'warn'),
742
+ ], [
550
743
  oidcBound
551
744
  ? react.createElement('p', { className: 'wua-intro' },
552
745
  '当前已绑定身份:', react.createElement('code', null, oidcBoundHint || '(已绑定)'),
@@ -559,7 +752,7 @@ window.__ModuleLoader__.load({
559
752
  react.createElement('input', { className: 'wua-input', type: 'password', value: unbindPw, autoComplete: 'current-password', onChange: (e) => setUnbindPw(e.target.value) }),
560
753
  )
561
754
  : null,
562
- react.createElement('div', { className: 'wua-btnrow' },
755
+ react.createElement('div', { className: 'wua-actions' },
563
756
  oidcBound
564
757
  ? react.createElement('button', { className: 'wua-btn ghost', disabled: busy || status === null, onClick: onUnbind }, '解除绑定')
565
758
  : react.createElement('button', { className: 'wua-btn', disabled: busy || status === null, onClick: onBind }, '绑定 OIDC 身份'),
@@ -568,15 +761,30 @@ window.__ModuleLoader__.load({
568
761
  : null,
569
762
  ),
570
763
  bindMsg ? react.createElement('p', { className: bindMsg.kind === 'ok' ? 'wua-msg' : 'wua-err' }, bindMsg.text) : null,
571
- ) : null,
572
- react.createElement('div', { className: 'wua-btnrow' },
573
- react.createElement('button', { className: 'wua-btn', disabled: busy || status === null, onClick: onSave },
574
- busy ? '保存中…' : '保存账号密码'),
575
- enabled ? react.createElement('button', { className: 'wua-btn ghost', disabled: busy || status === null, onClick: onDisable }, '禁用认证') : null,
576
- enabled ? react.createElement('button', { className: 'wua-btn ghost', disabled: busy || status === null, onClick: onLogout }, '退出登录') : null,
577
- ),
578
- react.createElement('div', { className: 'wua-row' },
579
- react.createElement('label', { className: 'wua-label' }, '最近登录记录(完整审计日志可用 node index.js audit 查看)'),
764
+ ]) : null,
765
+ card('远程访问', [
766
+ remoteTrust ? badge('远程信任已开启', 'ok') : badge('远程信任未开启', 'muted'),
767
+ ], [
768
+ react.createElement('p', { className: 'wua-intro' },
769
+ remoteTrust
770
+ ? '已通过 index.html 注入 __DSH_TRANSPORT__.ownsHost = true,反代域名下「设置 → 模型」可正常读取。刷新页面后生效。'
771
+ : '若经域名反代访问时「设置 → 模型」报 settings are unavailable in this browser,可开启远程访问信任。'),
772
+ react.createElement('div', { className: 'wua-actions' },
773
+ react.createElement('button', {
774
+ className: 'wua-btn',
775
+ disabled: remoteTrustBusy || status === null,
776
+ onClick: onToggleRemoteTrust,
777
+ }, remoteTrustBusy ? '处理中…' : (remoteTrust ? '关闭远程访问信任' : '开启远程访问信任')),
778
+ ),
779
+ remoteTrustMsg ? react.createElement('p', { className: remoteTrustMsg.kind === 'ok' ? 'wua-msg' : 'wua-err' }, remoteTrustMsg.text) : null,
780
+ react.createElement('div', { className: 'wua-sep' }),
781
+ react.createElement('h4', { className: 'wua-card-title' }, 'DeepSeek 官方账号登录(远程回调)'),
782
+ react.createElement('p', { className: 'wua-hint', style: { borderTop: 'none', paddingTop: 0 } },
783
+ '在「设置 → 账号」点击 DeepSeek 登录后,浏览器会跳到 127.0.0.1 的回调地址(远程机器上通常打不开)。点右下角的「DeepSeek 登录回调」悬浮按钮,把地址栏里的完整 URL 粘贴进去即可。'),
784
+ ]),
785
+ card('最近登录记录', [], [
786
+ react.createElement('p', { className: 'wua-hint', style: { borderTop: 'none', paddingTop: 0 } },
787
+ '完整审计日志可用 node index.js audit 查看。'),
580
788
  audit === null
581
789
  ? react.createElement('p', { className: 'wua-intro' }, '正在读取…')
582
790
  : (audit.length === 0
@@ -589,7 +797,44 @@ window.__ModuleLoader__.load({
589
797
  entry.ip ? react.createElement('span', { className: 'wua-audit-ip' }, entry.ip) : null,
590
798
  )),
591
799
  )),
592
- ),
800
+ ]),
801
+ react.createElement('button', {
802
+ className: 'wua-save-fab',
803
+ disabled: busy || status === null,
804
+ onClick: () => {
805
+ if (!enabled) { onSave({}); return; }
806
+ if (reauthValid) { onSave({ reauthToken }); return; }
807
+ setSaveErr('');
808
+ setSaveOpen(true);
809
+ },
810
+ }, busy ? '保存中…' : '保存设置'),
811
+ saveOpen ? react.createElement('div', { className: 'wua-modal-mask' },
812
+ react.createElement('div', { className: 'wua-modal' },
813
+ react.createElement('h4', { className: 'wua-card-title' }, '验证后保存'),
814
+ react.createElement('p', { className: 'wua-hint', style: { borderTop: 'none', paddingTop: 0 } },
815
+ reauthValid
816
+ ? '已通过 SSO 验证(2 分钟内有效),可直接保存。'
817
+ : '修改认证配置需要验证当前身份:输入当前密码,或使用 SSO 验证。'),
818
+ reauthValid ? null : react.createElement('div', { className: 'wua-row' },
819
+ react.createElement('label', { className: 'wua-label' }, '当前密码'),
820
+ react.createElement('input', { className: 'wua-input', type: 'password', value: savePw, autoComplete: 'current-password', onChange: (e) => setSavePw(e.target.value) }),
821
+ ),
822
+ saveErr ? react.createElement('p', { className: 'wua-err' }, saveErr) : null,
823
+ react.createElement('div', { className: 'wua-actions' },
824
+ react.createElement('button', { className: 'wua-btn', disabled: busy, onClick: () => {
825
+ if (reauthValid) { onSave({ reauthToken }); return; }
826
+ if (!savePw) { setSaveErr('请输入当前密码'); return; }
827
+ onSave({ current: savePw });
828
+ } }, busy ? '保存中…' : '验证并保存'),
829
+ react.createElement('button', { className: 'wua-btn ghost', disabled: busy, onClick: () => setSaveOpen(false) }, '取消'),
830
+ ),
831
+ (status && status.oidc && status.oidc.enabled && status.oidc.bound)
832
+ ? react.createElement('button', { className: 'wua-btn ghost', disabled: busy, onClick: () => {
833
+ location.href = '/dsh-webui-oauth/oidc/reauth?base=' + encodeURIComponent(location.origin || '');
834
+ } }, '使用 SSO 验证')
835
+ : null,
836
+ ),
837
+ ) : null,
593
838
  react.createElement('p', { className: 'wua-hint' },
594
839
  '密码规则:至少 8 位,必须包含大写字母、小写字母、数字和特殊符号(如 !@#$%^&*)。认证在 HTTP/传输层强制执行(WebUI 资源、/api 接口、WebSocket 全部要求有效会话),会话保存在服务端并由 HttpOnly Cookie 携带,修改密码会吊销其他所有会话。凭据以 scrypt 哈希保存在插件目录的 dsh-webui-auth.json(0.2.0 起仅接受 scrypt,旧版 SHA-256 凭据需删除该文件重新创建);忘记密码时删除该文件并重启 DSH 即可重置。可选用 OIDC 单点登录:配置 issuer/clientId/clientSecret 后登录页出现 SSO 按钮,认证通过后以 IdP 的 sub 作为本地会话用户名,并沿用相同的免登录时长机制。'),
595
840
  );