dsh-webui-oauth 0.7.9 → 0.8.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/lib/client.js CHANGED
@@ -68,6 +68,33 @@ window.__ModuleLoader__.load({
68
68
  if (styleEl.parentNode !== null) styleEl.parentNode.removeChild(styleEl);
69
69
  }, 'dsh-webui-oauth: styles');
70
70
 
71
+ // ---------------- DeepSeek 登录:打开回调中转子窗口 ----------------
72
+ // 官方登录会新开窗口去 IdP;远程部署回调落在 127.0.0.1,打不开。
73
+ // 在用户点击「登录」的同一手势里打开中转窗口(避免被弹窗拦截),
74
+ // 用户授权后把回调 URL 粘进去即可。命中失败时仍有内联粘贴框兜底。
75
+ (function hookDeepSeekLogin() {
76
+ if (typeof document === 'undefined' || typeof document.addEventListener !== 'function') return;
77
+ let last = 0;
78
+ const handler = (ev) => {
79
+ try {
80
+ const target = ev.target;
81
+ const btn = target && target.closest ? target.closest('button,[role="button"]') : null;
82
+ if (!btn) return;
83
+ const label = (btn.textContent || '').trim();
84
+ if (!/^(登录|Sign in|继续登录|重新登录)$/i.test(label)) return;
85
+ const scope = btn.closest('[role="dialog"],section,form,div');
86
+ const text = scope ? (scope.textContent || '') : '';
87
+ if (!/DeepSeek|账号|Account/i.test(text)) return;
88
+ const now = Date.now();
89
+ if (now - last < 1500) return;
90
+ last = now;
91
+ window.open('/dsh-webui-oauth/relay-window', 'dsh-wua-relay', 'width=480,height=620');
92
+ } catch (e) { /* best effort */ }
93
+ };
94
+ document.addEventListener('click', handler, true);
95
+ ctx.effect(() => () => document.removeEventListener('click', handler, true), 'dsh-webui-oauth: deepseek relay hook');
96
+ })();
97
+
71
98
  // ---------------- DeepSeek 登录回调:合并进官方「等待登录」对话框 ----------------
72
99
  //
73
100
  // 官方账号登录是 SignInDialog(另一个插件)渲染的,没有开放插槽;但它等待
@@ -105,8 +132,16 @@ window.__ModuleLoader__.load({
105
132
  submit.type = 'button';
106
133
  submit.className = 'wua-btn';
107
134
  submit.textContent = '完成登录';
135
+ const pop = document.createElement('button');
136
+ pop.type = 'button';
137
+ pop.className = 'wua-btn ghost';
138
+ pop.textContent = '在新窗口打开';
139
+ pop.addEventListener('click', () => {
140
+ window.open('/dsh-webui-oauth/relay-window', 'dsh-wua-relay', 'width=480,height=620');
141
+ });
108
142
  row.appendChild(input);
109
143
  row.appendChild(submit);
144
+ row.appendChild(pop);
110
145
 
111
146
  const msg = document.createElement('p');
112
147
  msg.style.display = 'none';
@@ -276,8 +311,8 @@ window.__ModuleLoader__.load({
276
311
  // (服务端只回传掩码,前端拿不到 sub 明文,也不应该拿)。
277
312
  const [oidcBound, setOidcBound] = react.useState(false);
278
313
  const [oidcBoundHint, setOidcBoundHint] = react.useState('');
279
- const [unbindPw, setUnbindPw] = react.useState('');
280
314
  const [bindMsg, setBindMsg] = react.useState(null);
315
+ const [bindWaiting, setBindWaiting] = react.useState(false);
281
316
  // 敏感操作复验(保存配置 / 禁用 / 解绑):密码或 SSO 一次性令牌。
282
317
  const [saveOpen, setSaveOpen] = react.useState(false);
283
318
  const [savePw, setSavePw] = react.useState('');
@@ -288,6 +323,13 @@ window.__ModuleLoader__.load({
288
323
  // 远程访问信任开关。
289
324
  const [remoteTrustBusy, setRemoteTrustBusy] = react.useState(false);
290
325
  const [remoteTrustMsg, setRemoteTrustMsg] = react.useState(null);
326
+ // 设备会话(App / CLI):独立列表,只有主动吊销才失效。
327
+ const [sessionList, setSessionList] = react.useState(null);
328
+ const [sessionBusy, setSessionBusy] = react.useState(false);
329
+ const [sessionMsg, setSessionMsg] = react.useState(null);
330
+ const [deviceList, setDeviceList] = react.useState(null);
331
+ const [deviceBusy, setDeviceBusy] = react.useState(false);
332
+ const [deviceMsg, setDeviceMsg] = react.useState(null);
291
333
  // 服务端算好的完整回调地址(含 scheme/host/port)。原生 OIDC 要求
292
334
  // redirect_uri 与 IdP 登记值精确匹配,少一个字符都会被拒,所以要显示全。
293
335
  const [oidcRedirectUri, setOidcRedirectUri] = react.useState('');
@@ -330,6 +372,14 @@ window.__ModuleLoader__.load({
330
372
  if (!alive) return;
331
373
  if (r && r.ok && Array.isArray(r.entries)) setAudit(r.entries);
332
374
  }).catch(() => { /* 审计不可用时静默 */ });
375
+ api('/dsh-webui-oauth/devices').then((r) => {
376
+ if (!alive) return;
377
+ if (r && r.ok && Array.isArray(r.devices)) setDeviceList(r.devices);
378
+ }).catch(() => { /* 设备列表不可用时静默 */ });
379
+ api('/dsh-webui-oauth/sessions').then((r) => {
380
+ if (!alive) return;
381
+ if (r && r.ok && Array.isArray(r.sessions)) setSessionList(r.sessions);
382
+ }).catch(() => { /* 会话列表不可用时静默 */ });
333
383
  return () => {
334
384
  alive = false;
335
385
  };
@@ -349,6 +399,18 @@ window.__ModuleLoader__.load({
349
399
  if (s && !s.error) setStatus(s);
350
400
  }).catch(() => {});
351
401
  }
402
+ if (e.data && e.data.type === 'dsh-wua-bound' && e.data.ok) {
403
+ setBindWaiting(false);
404
+ api('/dsh-webui-oauth/status').then((s) => {
405
+ if (s && !s.error) {
406
+ setStatus(s);
407
+ const o = s.oidc || {};
408
+ setOidcBound(!!o.bound);
409
+ setOidcBoundHint(o.boundSubHint || '');
410
+ }
411
+ }).catch(() => {});
412
+ setBindMsg({ kind: 'ok', text: 'OIDC 身份绑定成功。此后只有该身份可通过 SSO 登录。' });
413
+ }
352
414
  };
353
415
  window.addEventListener('message', onMessage);
354
416
  return () => window.removeEventListener('message', onMessage);
@@ -455,7 +517,10 @@ window.__ModuleLoader__.load({
455
517
  setBusy(false);
456
518
  if (r && r.ok) {
457
519
  if (enabled) {
458
- setMsg({ kind: 'ok', text: '已保存。其他已登录的设备/浏览器会话已全部吊销,当前会话保持有效。' });
520
+ const text = r.passwordChanged
521
+ ? '已保存。密码已更新,已吊销 ' + (r.revoked || 0) + ' 个其他浏览器会话(设备会话不受影响)。'
522
+ : '已保存。当前会话保持有效。';
523
+ setMsg({ kind: 'ok', text });
459
524
  } else {
460
525
  setMsg({ kind: 'ok', text: '已保存并启用认证,当前浏览器已登录。' });
461
526
  }
@@ -564,28 +629,39 @@ window.__ModuleLoader__.load({
564
629
  setBindMsg(null);
565
630
  // base 用当前页面的 origin:反代部署下浏览器看到的才是对外地址。
566
631
  const base = location.origin || '';
567
- location.href = '/dsh-webui-oauth/oidc/bind' + (base ? '?base=' + encodeURIComponent(base) : '');
632
+ const q = base ? '?base=' + encodeURIComponent(base) + '&popup=1' : '?popup=1';
633
+ const win = window.open('/dsh-webui-oauth/oidc/bind' + q, 'dsh-wua-bind', 'width=520,height=720');
634
+ if (!win) {
635
+ // 弹窗被拦截:回退整页跳转(回调会走 oidcbound=1 回执)
636
+ location.href = '/dsh-webui-oauth/oidc/bind' + (base ? '?base=' + encodeURIComponent(base) : '');
637
+ return;
638
+ }
639
+ setBindWaiting(true);
568
640
  };
569
641
 
570
- const onUnbind = () => {
642
+ const doUnbind = (auth) => {
571
643
  if (busy) return;
572
- if (!unbindPw) {
573
- setBindMsg({ kind: 'err', text: '请输入当前密码以解除绑定' });
644
+ const credentials = auth || {};
645
+ if (!credentials.current && !reauthValid) {
646
+ setSaveErr('解除绑定需要验证:请输入当前密码,或使用 SSO 验证。');
574
647
  return;
575
648
  }
576
649
  setBusy(true);
577
650
  setBindMsg(null);
578
- api('/dsh-webui-oauth/oidc/unbind', { current: unbindPw }).then((r) => {
651
+ api('/dsh-webui-oauth/oidc/unbind', { current: credentials.current || '' }).then((r) => {
579
652
  setBusy(false);
580
653
  if (r && r.ok) {
581
- setUnbindPw('');
582
654
  setOidcBound(false);
583
655
  setOidcBoundHint('');
656
+ setSaveOpen(false);
657
+ setSavePw('');
658
+ setSaveErr('');
584
659
  setBindMsg({ kind: 'ok', text: '已解除绑定。此后 OIDC 登录将被拒绝,直到重新绑定。' });
585
660
  api('/dsh-webui-oauth/status').then((s) => {
586
661
  if (s && !s.error) setStatus(s);
587
662
  }).catch(() => {});
588
663
  } else if (r && r.error === 'current-invalid') {
664
+ setSaveErr('当前密码不正确');
589
665
  setBindMsg({ kind: 'err', text: '当前密码不正确' });
590
666
  } else if (r && r.error === 'sso-only-active') {
591
667
  setBindMsg({ kind: 'err', text: (r && r.hint) || '请先关闭「仅允许 SSO」再解绑' });
@@ -598,6 +674,14 @@ window.__ModuleLoader__.load({
598
674
  });
599
675
  };
600
676
 
677
+ const onUnbindClick = () => {
678
+ if (busy) return;
679
+ if (reauthValid) { doUnbind({}); return; }
680
+ setVerifyAction('unbind');
681
+ setSaveErr('');
682
+ setSaveOpen(true);
683
+ };
684
+
601
685
  const onLogout = () => {
602
686
  if (busy) return;
603
687
  setBusy(true);
@@ -675,6 +759,54 @@ window.__ModuleLoader__.load({
675
759
  return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes());
676
760
  }
677
761
 
762
+ const onRevokeSession = (id) => {
763
+ if (sessionBusy) return;
764
+ setSessionBusy(true);
765
+ setSessionMsg(null);
766
+ api('/dsh-webui-oauth/sessions/revoke-one', { id }).then((r) => {
767
+ setSessionBusy(false);
768
+ if (r && r.ok) {
769
+ setSessionList((list) => (list || []).filter((s) => s.id !== id));
770
+ setSessionMsg({ kind: 'ok', text: '已吊销该浏览器会话。' });
771
+ } else {
772
+ setSessionMsg({ kind: 'err', text: (r && (r.reason || r.error)) || '吊销失败' });
773
+ }
774
+ }).catch(() => {
775
+ setSessionBusy(false);
776
+ setSessionMsg({ kind: 'err', text: '无法连接认证服务' });
777
+ });
778
+ };
779
+
780
+ const refreshSessions = () => {
781
+ api('/dsh-webui-oauth/sessions').then((r) => {
782
+ if (r && r.ok && Array.isArray(r.sessions)) setSessionList(r.sessions);
783
+ }).catch(() => {});
784
+ };
785
+
786
+ const onRevokeDevice = (id) => {
787
+ if (deviceBusy) return;
788
+ setDeviceBusy(true);
789
+ setDeviceMsg(null);
790
+ api('/dsh-webui-oauth/devices/revoke', { id }).then((r) => {
791
+ setDeviceBusy(false);
792
+ if (r && r.ok) {
793
+ setDeviceList((list) => (list || []).filter((d) => d.id !== id));
794
+ setDeviceMsg({ kind: 'ok', text: '已吊销该设备会话,对应 token 立即失效。' });
795
+ } else {
796
+ setDeviceMsg({ kind: 'err', text: (r && (r.reason || r.error)) || '吊销失败' });
797
+ }
798
+ }).catch(() => {
799
+ setDeviceBusy(false);
800
+ setDeviceMsg({ kind: 'err', text: '无法连接认证服务' });
801
+ });
802
+ };
803
+
804
+ const refreshDevices = () => {
805
+ api('/dsh-webui-oauth/devices').then((r) => {
806
+ if (r && r.ok && Array.isArray(r.devices)) setDeviceList(r.devices);
807
+ }).catch(() => {});
808
+ };
809
+
678
810
  const onToggleRemoteTrust = () => {
679
811
  if (remoteTrustBusy) return;
680
812
  const next = !remoteTrust;
@@ -825,20 +957,15 @@ window.__ModuleLoader__.load({
825
957
  '。只有该身份可通过 SSO 登录。')
826
958
  : react.createElement('p', { className: 'wua-warn' },
827
959
  '尚未绑定任何 OIDC 身份。此时 OIDC 登录一律被拒绝,登录页也不会显示 SSO 按钮。请先保存上方配置,再点击「绑定 OIDC 身份」并完成一次 IdP 登录。'),
828
- oidcBound
829
- ? react.createElement('div', { className: 'wua-row' },
830
- react.createElement('label', { className: 'wua-label' }, '当前密码(解除绑定需验证)'),
831
- react.createElement('input', { className: 'wua-input', type: 'password', value: unbindPw, autoComplete: 'current-password', onChange: (e) => setUnbindPw(e.target.value) }),
832
- )
833
- : null,
834
960
  react.createElement('div', { className: 'wua-actions' },
835
961
  oidcBound
836
- ? react.createElement('button', { className: 'wua-btn ghost', disabled: busy || status === null, onClick: onUnbind }, '解除绑定')
962
+ ? react.createElement('button', { className: 'wua-btn ghost', disabled: busy || status === null, onClick: onUnbindClick }, '解除绑定')
837
963
  : react.createElement('button', { className: 'wua-btn', disabled: busy || status === null, onClick: onBind }, '绑定 OIDC 身份'),
838
964
  oidcBound
839
965
  ? react.createElement('button', { className: 'wua-btn ghost', disabled: busy || status === null, onClick: onBind }, '重新绑定其他身份')
840
966
  : null,
841
967
  ),
968
+ bindWaiting ? react.createElement('p', { className: 'wua-msg' }, '等待绑定…请在打开的窗口中完成 IdP 授权。') : null,
842
969
  bindMsg ? react.createElement('p', { className: bindMsg.kind === 'ok' ? 'wua-msg' : 'wua-err' }, bindMsg.text) : null,
843
970
  ]) : null,
844
971
  card('远程访问', [
@@ -861,6 +988,68 @@ window.__ModuleLoader__.load({
861
988
  react.createElement('p', { className: 'wua-hint', style: { borderTop: 'none', paddingTop: 0 } },
862
989
  '在「设置 → 账号」点击 DeepSeek 登录后,浏览器会跳到 127.0.0.1 的回调地址(远程机器上通常打不开)。官方「等待登录」窗口里会直接出现一个粘贴框:把地址栏里的完整 URL 粘进去即可。'),
863
990
  ]),
991
+ card('浏览器会话', [
992
+ sessionList === null ? null : badge(String(sessionList.length) + ' 个', sessionList.length ? 'ok' : 'muted'),
993
+ ], [
994
+ react.createElement('p', { className: 'wua-intro' },
995
+ '当前已登录的浏览器。可单独吊销某个会话;设备会话在下一张卡片单独管理。'),
996
+ sessionList === null
997
+ ? react.createElement('p', { className: 'wua-intro' }, '正在读取…')
998
+ : (sessionList.length === 0
999
+ ? react.createElement('p', { className: 'wua-intro' }, '暂无活动的浏览器会话')
1000
+ : react.createElement('div', { className: 'wua-audit' },
1001
+ sessionList.map((s) => react.createElement('div', {
1002
+ key: s.id,
1003
+ className: 'wua-audit-row',
1004
+ style: { alignItems: 'center' },
1005
+ },
1006
+ react.createElement('span', { className: 'wua-audit-event' }, s.current ? '当前会话' : (s.device || '浏览器')),
1007
+ react.createElement('span', { className: 'wua-audit-user' }, [s.os, s.browser].filter(Boolean).join(' · ') || '未知设备'),
1008
+ react.createElement('span', { className: 'wua-audit-time' }, '指纹 ' + (s.fingerprint || '—')),
1009
+ react.createElement('span', { className: 'wua-audit-time' }, '最近 ' + fmtTime(s.lastSeenAt)),
1010
+ react.createElement('span', { style: { flex: 1 } }),
1011
+ react.createElement('button', {
1012
+ className: 'wua-btn ghost',
1013
+ disabled: sessionBusy || s.current,
1014
+ title: s.current ? '当前会话请用「退出登录」' : '吊销该会话',
1015
+ onClick: () => onRevokeSession(s.id),
1016
+ }, s.current ? '当前' : '吊销'),
1017
+ )))),
1018
+ react.createElement('div', { className: 'wua-actions' },
1019
+ react.createElement('button', { className: 'wua-btn ghost', onClick: refreshSessions }, '刷新列表'),
1020
+ ),
1021
+ sessionMsg ? react.createElement('p', { className: sessionMsg.kind === 'ok' ? 'wua-msg' : 'wua-err' }, sessionMsg.text) : null,
1022
+ ]),
1023
+ card('设备会话(App / CLI)', [
1024
+ deviceList === null ? null : badge(String(deviceList.length) + ' 个', deviceList.length ? 'ok' : 'muted'),
1025
+ ], [
1026
+ react.createElement('p', { className: 'wua-intro' },
1027
+ '设备通过 OAuth 2.1 设备授权(无回调)登录。它们与浏览器会话完全独立,不会过期,只有主动吊销才会失效。'),
1028
+ deviceList === null
1029
+ ? react.createElement('p', { className: 'wua-intro' }, '正在读取…')
1030
+ : (deviceList.length === 0
1031
+ ? react.createElement('p', { className: 'wua-intro' }, '暂无设备会话')
1032
+ : react.createElement('div', { className: 'wua-audit' },
1033
+ deviceList.map((d) => react.createElement('div', {
1034
+ key: d.id,
1035
+ className: 'wua-audit-row',
1036
+ style: { alignItems: 'center' },
1037
+ },
1038
+ react.createElement('span', { className: 'wua-audit-event' }, d.name || '设备'),
1039
+ react.createElement('span', { className: 'wua-audit-time' }, '创建 ' + fmtTime(d.createdAt)),
1040
+ d.lastUsedAt ? react.createElement('span', { className: 'wua-audit-time' }, '最近 ' + fmtTime(d.lastUsedAt)) : null,
1041
+ react.createElement('span', { style: { flex: 1 } }),
1042
+ react.createElement('button', {
1043
+ className: 'wua-btn ghost',
1044
+ disabled: deviceBusy,
1045
+ onClick: () => onRevokeDevice(d.id),
1046
+ }, '吊销'),
1047
+ )))),
1048
+ react.createElement('div', { className: 'wua-actions' },
1049
+ react.createElement('button', { className: 'wua-btn ghost', onClick: refreshDevices }, '刷新列表'),
1050
+ ),
1051
+ deviceMsg ? react.createElement('p', { className: deviceMsg.kind === 'ok' ? 'wua-msg' : 'wua-err' }, deviceMsg.text) : null,
1052
+ ]),
864
1053
  card('最近登录记录', [], [
865
1054
  react.createElement('p', { className: 'wua-hint', style: { borderTop: 'none', paddingTop: 0 } },
866
1055
  '完整审计日志可用 node index.js audit 查看。'),
@@ -880,7 +1069,10 @@ window.__ModuleLoader__.load({
880
1069
  saveOpen ? react.createElement('div', { className: 'wua-modal-mask' },
881
1070
  react.createElement('div', { className: 'wua-modal' },
882
1071
  react.createElement('h4', { className: 'wua-card-title' },
883
- verifyAction === 'disable' ? '验证后禁用' : (verifyAction === 'revoke' ? '验证后吊销会话' : '验证后保存')),
1072
+ verifyAction === 'disable' ? '验证后禁用'
1073
+ : verifyAction === 'revoke' ? '验证后吊销会话'
1074
+ : verifyAction === 'unbind' ? '验证后解绑'
1075
+ : '验证后保存'),
884
1076
  react.createElement('p', { className: 'wua-hint', style: { borderTop: 'none', paddingTop: 0 } },
885
1077
  reauthValid
886
1078
  ? '已通过 SSO 验证(2 分钟内有效),可直接继续。'
@@ -898,8 +1090,13 @@ window.__ModuleLoader__.load({
898
1090
  const auth = reauthValid ? {} : { current: savePw };
899
1091
  if (verifyAction === 'disable') doDisable(auth);
900
1092
  else if (verifyAction === 'revoke') doRevoke(auth);
1093
+ else if (verifyAction === 'unbind') doUnbind(auth);
901
1094
  else onSave(auth);
902
- } }, busy ? '处理中…' : (verifyAction === 'disable' ? '验证并禁用' : (verifyAction === 'revoke' ? '验证并吊销' : '验证并保存'))),
1095
+ } }, busy ? '处理中…' : (
1096
+ verifyAction === 'disable' ? '验证并禁用'
1097
+ : verifyAction === 'revoke' ? '验证并吊销'
1098
+ : verifyAction === 'unbind' ? '验证并解绑'
1099
+ : '验证并保存')),
903
1100
  react.createElement('button', { className: 'wua-btn ghost', disabled: busy, onClick: () => setSaveOpen(false) }, '取消'),
904
1101
  ),
905
1102
  (status && status.oidc && status.oidc.enabled && status.oidc.bound)
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "dsh-webui-oauth",
3
- "version": "0.7.9",
3
+ "version": "0.8.1",
4
4
  "type": "module",
5
5
  "main": "index.js",
6
6
  "scripts": {
7
- "test": "node test/redirect-scheme.test.mjs && node test/proxy-host.test.mjs && node test/trusted-origin.test.mjs && node test/oidc.test.mjs && node test/oidc-state-ttl.test.mjs && node test/set-cookie-multi.test.mjs && node test/oidc-integration.test.mjs && node test/oidc-config-store.test.mjs && node test/adopt-data-dir.test.mjs && node test/oidc-ec.test.mjs && node test/jwks-refresh.test.mjs && node test/oidc-bind.test.mjs && node test/client-bundle-id.test.mjs && node test/client-prefill.test.mjs && node test/displace-original.test.mjs && node test/oidc-redirect-matrix.test.mjs && node test/gate-default-deny.test.mjs && node test/fallback-shape.test.mjs && node test/desktop-marker.test.mjs && node test/audit-null.test.mjs && node test/remote-trust.test.mjs && node test/oauth-relay.test.mjs && node test/lifecycle.test.mjs"
7
+ "test": "node test/redirect-scheme.test.mjs && node test/proxy-host.test.mjs && node test/trusted-origin.test.mjs && node test/oidc.test.mjs && node test/oidc-state-ttl.test.mjs && node test/set-cookie-multi.test.mjs && node test/oidc-integration.test.mjs && node test/oidc-config-store.test.mjs && node test/adopt-data-dir.test.mjs && node test/oidc-ec.test.mjs && node test/jwks-refresh.test.mjs && node test/oidc-bind.test.mjs && node test/client-bundle-id.test.mjs && node test/client-prefill.test.mjs && node test/displace-original.test.mjs && node test/oidc-redirect-matrix.test.mjs && node test/gate-default-deny.test.mjs && node test/fallback-shape.test.mjs && node test/desktop-marker.test.mjs && node test/audit-null.test.mjs && node test/remote-trust.test.mjs && node test/oauth-relay.test.mjs && node test/lifecycle.test.mjs && node test/device-flow.test.mjs"
8
8
  },
9
9
  "exports": {
10
10
  ".": "./index.js",