@wenbin_wb/dsh-bridge 2.9.0 → 2.10.0

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.
@@ -0,0 +1,142 @@
1
+ // 统一管理员解锁管理器(client/unlock-manager.js)
2
+ //
3
+ // 背景:多个 RPC 封装(BridgePanel 的 authRpcCall、目录选择器的 authRpc)都会遇到
4
+ // "需要管理员权限" 的拦截,此前各自处理、目录选择器甚至没有解锁入口导致流程卡死。
5
+ // 本模块把"token 管理 + 权限拦截检测 + 解锁动作 + 被拦截操作自动重放"统一收敛。
6
+ //
7
+ // 设计要点:
8
+ // - token 与 sessionStorage 同步,是本插件唯一的 adminToken 缓存来源
9
+ // - 权限失败 -> 解锁成功 -> 自动重放被拦截的 RPC(pending 队列)
10
+ // - 与具体 UI 框架解耦,纯 JS 单例
11
+
12
+ const SESSION_KEY = 'dsh_admin_token';
13
+
14
+ let _token = '';
15
+ let _pendingOps = []; // 被拦截待重放的操作队列(解锁后按序重放)
16
+ let _onUnlocked = null; // 解锁成功后的全局回调(可多个)
17
+
18
+ // 读写 sessionStorage
19
+ function _readStorage() {
20
+ try { return typeof window !== 'undefined' ? window.sessionStorage.getItem(SESSION_KEY) : ''; } catch { return ''; }
21
+ }
22
+ function _writeStorage(t) {
23
+ try {
24
+ if (typeof window === 'undefined') return;
25
+ if (t) window.sessionStorage.setItem(SESSION_KEY, t);
26
+ else window.sessionStorage.removeItem(SESSION_KEY);
27
+ } catch {}
28
+ }
29
+
30
+ export function getAdminToken() {
31
+ if (_token) return _token;
32
+ const saved = _readStorage();
33
+ if (saved) { _token = saved; return saved; }
34
+ return '';
35
+ }
36
+
37
+ export function setAdminToken(t) {
38
+ _token = t || '';
39
+ _writeStorage(_token);
40
+ }
41
+
42
+ export function clearAdminToken() {
43
+ setAdminToken('');
44
+ }
45
+
46
+ /**
47
+ * 本机(回环)场景:从多候选 URL 获取 loopback adminToken(免输密码直通管理)。
48
+ * 支持 3080 原生端口与 3082 代理端口;不信任 sessionStorage 里的旧 token
49
+ * ——DSH 重启后 adminSessions 清空,旧 token 失效,因此强制每次获取新的。
50
+ * 返回 null 表示获取失败(非本机 / 端点不可达)。
51
+ * @param {boolean} [force] true 时忽略已缓存 token 强制重取(默认 true)
52
+ */
53
+ export async function fetchLoopbackTokenOnce(force = true) {
54
+ if (typeof window === 'undefined') return null;
55
+ if (!force) {
56
+ const existing = getAdminToken();
57
+ if (existing) return existing;
58
+ }
59
+ const candidates = [
60
+ '/__dsh_bridge__/loopback-token',
61
+ 'http://127.0.0.1:3082/__dsh_bridge__/loopback-token',
62
+ 'http://localhost:3082/__dsh_bridge__/loopback-token',
63
+ ];
64
+ for (const url of [...new Set(candidates)]) {
65
+ try {
66
+ const res = await fetch(url, { method: 'POST' });
67
+ if (res.ok) {
68
+ const data = await res.json();
69
+ if (data?.ok && data.adminToken) {
70
+ setAdminToken(data.adminToken);
71
+ return data.adminToken;
72
+ }
73
+ }
74
+ } catch {}
75
+ }
76
+ return null;
77
+ }
78
+
79
+ /**
80
+ * 暂存被拦截的操作,解锁成功后自动重放(队列:多个被拦操作按序重放)。
81
+ * @param {function} retry 返回 Promise 的重试函数(带新 token 重新发起)
82
+ */
83
+ export function queuePendingOperation(retry) {
84
+ _pendingOps.push(retry);
85
+ }
86
+
87
+ /** 解锁成功后重放所有 pending 操作(若有) */
88
+ export async function replayPending() {
89
+ if (_pendingOps.length === 0) return;
90
+ const ops = _pendingOps;
91
+ _pendingOps = [];
92
+ for (const retry of ops) {
93
+ try { await retry(); } catch {}
94
+ }
95
+ }
96
+
97
+ /** 监听解锁成功(可多个);返回注销函数 */
98
+ export function onUnlocked(cb) {
99
+ if (!_onUnlocked) _onUnlocked = [];
100
+ _onUnlocked.push(cb);
101
+ return () => { _onUnlocked = _onUnlocked.filter((f) => f !== cb); };
102
+ }
103
+
104
+ /** 内部:解锁成功后触发 */
105
+ function _emitUnlocked() {
106
+ if (_onUnlocked) {
107
+ for (const cb of _onUnlocked) { try { cb(); } catch {} }
108
+ }
109
+ }
110
+
111
+ /**
112
+ * 统一解锁动作:调 authAdminUnlock,成功存 token 并触发回调/重放。
113
+ * @param {function} rpcCall 底层 rpcCall(用于调 authAdminUnlock)
114
+ * @param {string} password 管理密码
115
+ * @returns {Promise<{ok:boolean, error?:string}>}
116
+ */
117
+ export async function unlockAdmin(rpcCall, password) {
118
+ try {
119
+ const res = await rpcCall('authAdminUnlock', { password });
120
+ if (res?.ok) {
121
+ const token = res.value?.adminToken || '';
122
+ setAdminToken(token);
123
+ _emitUnlocked();
124
+ await replayPending();
125
+ return { ok: true };
126
+ }
127
+ return { ok: false, error: res?.error?.message || '管理员密码错误' };
128
+ } catch (err) {
129
+ // 访问会话失效(HTTP 401):authAdminUnlock 即使豁免,主面板后续请求也会 401,
130
+ // 解锁成功也进不去。此时应整页重载回绿色登录页重新获取访问会话。
131
+ const msg = String(err?.message || err || '');
132
+ if (msg.includes('401') || msg.includes('transport failure') || msg.includes('unauthorized')) {
133
+ if (typeof window !== 'undefined' && typeof window.location?.reload === 'function') {
134
+ try {
135
+ window.sessionStorage.setItem('dsh_access_expired_reloaded', '1');
136
+ } catch {}
137
+ window.location.reload();
138
+ }
139
+ }
140
+ return { ok: false, error: err?.message || '解锁请求失败' };
141
+ }
142
+ }