dsh-webui-oauth 0.7.6 → 0.7.8
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/CHANGELOG.md +67 -0
- package/index.js +86 -26
- package/lib/client.js +124 -94
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,73 @@
|
|
|
2
2
|
|
|
3
3
|
本文件记录每个版本的**用户可见**变更。破坏性变更与升级动作单独标注。
|
|
4
4
|
|
|
5
|
+
## 0.7.8
|
|
6
|
+
|
|
7
|
+
SSO 复验改为**新窗口 + 等待状态**;设置页做了一轮视觉打磨。
|
|
8
|
+
|
|
9
|
+
### 调整:SSO 验证另开窗口,设置页显示「等待验证」
|
|
10
|
+
|
|
11
|
+
- 弹窗里的「使用 SSO 验证」不再整页跳转,改为 `window.open` 打开独立窗口
|
|
12
|
+
(`/dsh-webui-oauth/oidc/reauth?popup=1`);设置页保持打开并显示
|
|
13
|
+
「等待验证…请在打开的窗口中完成 SSO 登录」;
|
|
14
|
+
- 复验回调(仍走统一的 `/oidc/callback`)在弹窗模式下返回一个
|
|
15
|
+
**自我关闭的 HTML 页**:`postMessage({type:'dsh-wua-reauth'})` 通知设置页,
|
|
16
|
+
随后 `window.close()`,不把整个应用加载进小窗口;
|
|
17
|
+
- 设置页同时监听 message 并每 2 秒轮询 `status.reauthActive` 兜底;
|
|
18
|
+
验证成功后弹窗内即可直接保存;
|
|
19
|
+
- 弹窗被浏览器拦截时自动回退为整页跳转(原行为)。
|
|
20
|
+
|
|
21
|
+
### 视觉打磨
|
|
22
|
+
|
|
23
|
+
- 卡片:更大圆角、轻投影、hover 边框高亮、标题前品牌色竖条;
|
|
24
|
+
- 输入框/下拉:统一 36px 高度、聚焦光环、hover 边框;
|
|
25
|
+
- 按钮:主按钮填充色 + 阴影、ghost 描边、active 微下沉、disabled 降透明度;
|
|
26
|
+
- 提示条:成功/错误/警告统一为左侧色条 + 浅色底;
|
|
27
|
+
- 审计列表:行内 hover、圆角、等宽时间;
|
|
28
|
+
- 弹窗:背景模糊、入场动画、更大圆角;
|
|
29
|
+
- 窄屏(≤640px)单列布局。
|
|
30
|
+
|
|
31
|
+
### 测试
|
|
32
|
+
|
|
33
|
+
- `oidc-bind` 新增 5 项:弹窗复验回调返回 HTML、通知 opener、自我关闭、
|
|
34
|
+
会话标记生效;
|
|
35
|
+
- **新增 `test/lifecycle.test.mjs`(19 项)**:从零凭据首次启动 → setup token
|
|
36
|
+
初始化 → 密码登录 → 改配置/TTL → 禁用 → 重新启用 → 退出,完整生命周期;
|
|
37
|
+
- 全量 23 个测试文件、623 项断言通过。
|
|
38
|
+
|
|
39
|
+
## 0.7.7
|
|
40
|
+
|
|
41
|
+
设置页再收敛:移除冗余密码框、保存按钮移到顶部、SSO 复验改为会话级。
|
|
42
|
+
|
|
43
|
+
### 调整:账号卡片不再有「当前密码」输入框
|
|
44
|
+
|
|
45
|
+
保存/禁用已经统一走验证弹窗,账号卡片里的「当前密码」输入框是冗余的,已移除。
|
|
46
|
+
禁用认证改为与保存一致的流程:顶部按钮 → 弹窗验证(密码或 SSO)。
|
|
47
|
+
|
|
48
|
+
### 调整:「保存设置」从右下角悬浮改为顶部卡片按钮
|
|
49
|
+
|
|
50
|
+
- 移除右下角悬浮的「保存设置」按钮;
|
|
51
|
+
- 保存按钮放到「概览」卡片的操作区(与退出登录、禁用认证并列);
|
|
52
|
+
- 首次初始化(未启用认证)直接保存,无需验证;
|
|
53
|
+
- 已启用时点击弹窗验证:当前密码,或 SSO 验证。
|
|
54
|
+
|
|
55
|
+
### 调整:SSO 复验改为**会话级**,不再签发独立令牌
|
|
56
|
+
|
|
57
|
+
- 复验回调仍走统一的 `/dsh-webui-oauth/oidc/callback`;
|
|
58
|
+
- 发起复验时把当前会话 token 记进 OIDC state(服务端);
|
|
59
|
+
- 回调校验 sub 通过后,直接给该会话写 `reauthAt = now`(2 分钟有效),
|
|
60
|
+
**不再生成令牌、也不再往 URL 里放令牌**;
|
|
61
|
+
- `configure` / `disable` / `oidc/unbind` 通过 `sessionReauthOk(req)` 判断;
|
|
62
|
+
成功后清除 `reauthAt`(一次性);
|
|
63
|
+
- `status` 回显 `reauthActive`,前端据此显示「已通过 SSO 验证」;
|
|
64
|
+
- 回调回执只保留 `oidcreauth=1` 状态标记。
|
|
65
|
+
|
|
66
|
+
### 测试
|
|
67
|
+
|
|
68
|
+
- `test/oidc-bind.test.mjs` 的复验用例改为会话级断言(回调只带状态标记、
|
|
69
|
+
同会话无密码保存成功、复验不可重放);
|
|
70
|
+
- 全量 22 个测试文件、599 项断言通过。
|
|
71
|
+
|
|
5
72
|
## 0.7.6
|
|
6
73
|
|
|
7
74
|
把 DeepSeek 登录回调输入**合并进官方「等待登录」对话框**,不再单独占一个悬浮入口。
|
package/index.js
CHANGED
|
@@ -2155,6 +2155,20 @@ const OIDC_CALLBACK_PATH = '/dsh-webui-oauth/oidc/callback'
|
|
|
2155
2155
|
// 登录入口。ssoOnly 开启时改走 SSO 入口——两者都是公开白名单路径。
|
|
2156
2156
|
const LOGIN_PATH = '/dsh-webui-oauth/login'
|
|
2157
2157
|
const SSO_LOGIN_PATH = '/dsh-webui-oauth/oidc/login'
|
|
2158
|
+
|
|
2159
|
+
// SSO 复验弹窗的回调页:只负责通知 opener 并自我关闭,不加载整个应用。
|
|
2160
|
+
const REAUTH_POPUP_HTML = `<!doctype html>
|
|
2161
|
+
<html lang="zh-CN"><head><meta charset="utf-8"><title>验证成功</title>
|
|
2162
|
+
<style>
|
|
2163
|
+
html,body{height:100%;margin:0}
|
|
2164
|
+
body{display:flex;align-items:center;justify-content:center;font:14px/1.6 system-ui,-apple-system,"Segoe UI",sans-serif;background:#0b0b0c;color:#e8e8e8}
|
|
2165
|
+
.box{text-align:center;opacity:.92}
|
|
2166
|
+
</style></head>
|
|
2167
|
+
<body><div class="box"><p>验证成功,正在返回设置页…</p><p style="font-size:12px;opacity:.6">如果窗口没有自动关闭,请手动关闭。</p></div>
|
|
2168
|
+
<script>
|
|
2169
|
+
try { if (window.opener) window.opener.postMessage({ type: 'dsh-wua-reauth', ok: true }, location.origin); } catch (e) {}
|
|
2170
|
+
setTimeout(function () { try { window.close(); } catch (e) {} }, 400);
|
|
2171
|
+
</script></body></html>`
|
|
2158
2172
|
// 授权请求状态(state / nonce / PKCE verifier)的存活时间。三次握手应在分钟内
|
|
2159
2173
|
// 完成,10 分钟足够覆盖用户在 IdP 侧慢慢登录的情形,又限制了被滥用的窗口。
|
|
2160
2174
|
const OIDC_STATE_TTL_MS = 10 * 60 * 1000
|
|
@@ -2691,6 +2705,31 @@ export async function apply(ctx) {
|
|
|
2691
2705
|
|
|
2692
2706
|
const SESSION_TTL_MS = SESSION_BROWSER_TTL_MS
|
|
2693
2707
|
|
|
2708
|
+
/** 取当前请求对应的会话记录(不做续期),无效/过期返回 null。 */
|
|
2709
|
+
function sessionOf(req) {
|
|
2710
|
+
const token = cookieOf(req, COOKIE_NAME)
|
|
2711
|
+
if (!token) return null
|
|
2712
|
+
const s = sessions.get(token)
|
|
2713
|
+
if (!s) return null
|
|
2714
|
+
if (s.expiresAt <= Date.now()) {
|
|
2715
|
+
sessions.delete(token)
|
|
2716
|
+
return null
|
|
2717
|
+
}
|
|
2718
|
+
return s
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2721
|
+
/** 该会话是否在 2 分钟内通过过 SSO 复验。 */
|
|
2722
|
+
function sessionReauthOk(req) {
|
|
2723
|
+
const s = sessionOf(req)
|
|
2724
|
+
return !!(s && typeof s.reauthAt === 'number' && Date.now() - s.reauthAt < REAUTH_WINDOW_MS)
|
|
2725
|
+
}
|
|
2726
|
+
|
|
2727
|
+
/** 复验是一次性的:敏感操作成功后清掉时间戳。 */
|
|
2728
|
+
function clearSessionReauth(req) {
|
|
2729
|
+
const s = sessionOf(req)
|
|
2730
|
+
if (s) delete s.reauthAt
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2694
2733
|
function createSession(username, ttl) {
|
|
2695
2734
|
const token = randomBytes(24).toString('hex')
|
|
2696
2735
|
const ttlMs = ttl > 0 ? ttl * 3600 * 1000 : SESSION_TTL_MS
|
|
@@ -2832,30 +2871,20 @@ export async function apply(ctx) {
|
|
|
2832
2871
|
// 必须在后台定时器使用 purge 之前定义。
|
|
2833
2872
|
const oidcStateStore = makeOidcStateStore()
|
|
2834
2873
|
|
|
2835
|
-
//
|
|
2836
|
-
//
|
|
2837
|
-
const
|
|
2838
|
-
const REAUTH_TTL_MS = 2 * 60 * 1000
|
|
2839
|
-
const mintReauthToken = (username) => {
|
|
2840
|
-
const token = randomBytes(16).toString('hex')
|
|
2841
|
-
reauthTokens.set(token, { username, expiresAt: Date.now() + REAUTH_TTL_MS })
|
|
2842
|
-
return token
|
|
2843
|
-
}
|
|
2844
|
-
const consumeReauthToken = (token) => {
|
|
2845
|
-
if (typeof token !== 'string' || !token) return null
|
|
2846
|
-
const rec = reauthTokens.get(token)
|
|
2847
|
-
if (!rec) return null
|
|
2848
|
-
reauthTokens.delete(token)
|
|
2849
|
-
if (rec.expiresAt <= Date.now()) return null
|
|
2850
|
-
return rec
|
|
2851
|
-
}
|
|
2874
|
+
// SSO 复验通过后,直接在**当前会话**上记一个时间戳(2 分钟有效),
|
|
2875
|
+
// 不再签发独立令牌、也不经过 URL 传递。敏感操作直接检查会话。
|
|
2876
|
+
const REAUTH_WINDOW_MS = 2 * 60 * 1000
|
|
2852
2877
|
const purgeExpiredOidcStates = oidcStateStore.purge
|
|
2853
2878
|
|
|
2854
2879
|
// 后台任务:过期会话清理 + enabled 状态刷新
|
|
2855
2880
|
const bgTimer = setInterval(async () => {
|
|
2856
2881
|
const now = Date.now()
|
|
2857
|
-
for (const [k, s] of sessions.live)
|
|
2858
|
-
|
|
2882
|
+
for (const [k, s] of sessions.live) {
|
|
2883
|
+
if (s.expiresAt <= now || (typeof s.reauthAt === 'number' && now - s.reauthAt >= REAUTH_WINDOW_MS)) {
|
|
2884
|
+
if (s.expiresAt <= now) sessions.delete(k)
|
|
2885
|
+
else delete s.reauthAt
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2859
2888
|
purgeExpiredOidcStates()
|
|
2860
2889
|
try {
|
|
2861
2890
|
const creds = await readCredentials(ctx)
|
|
@@ -3124,6 +3153,8 @@ export async function apply(ctx) {
|
|
|
3124
3153
|
sessionsPersisted: sessions.ok,
|
|
3125
3154
|
// 远程访问信任(可选):是否已注入 ownsHost。
|
|
3126
3155
|
remoteTrust: remoteTrustFlag,
|
|
3156
|
+
// 当前会话是否在 2 分钟内通过过 SSO 复验(保存/禁用/解绑可直接进行)。
|
|
3157
|
+
reauthActive: sessionReauthOk(req),
|
|
3127
3158
|
// 核心兜底席位形态(handler | resource | absent | unknown):核心升级后
|
|
3128
3159
|
// 如果这里变成 unknown,文档请求会 404 —— 排查时先看这个字段。
|
|
3129
3160
|
fallback: fallbackShape(ctx.webServer),
|
|
@@ -3243,8 +3274,8 @@ export async function apply(ctx) {
|
|
|
3243
3274
|
}
|
|
3244
3275
|
}
|
|
3245
3276
|
if (wasEnabled) {
|
|
3246
|
-
// 支持 SSO
|
|
3247
|
-
const reauthOk =
|
|
3277
|
+
// 支持 SSO 复验(「仅允许 SSO」时密码登录入口不可用):复验结果记在会话上
|
|
3278
|
+
const reauthOk = sessionReauthOk(req)
|
|
3248
3279
|
const curValid = reauthOk || (current && typeof creds.hash === 'string' && await verifyPassword(current, creds.hash))
|
|
3249
3280
|
if (!curValid) {
|
|
3250
3281
|
await auditLog(ctx, 'configure_failure', { username, ip: meta.ip, ua: meta.ua, detail: '当前密码不正确' })
|
|
@@ -3383,6 +3414,7 @@ export async function apply(ctx) {
|
|
|
3383
3414
|
if (!keepPassword) detailParts.push('密码已修改')
|
|
3384
3415
|
if (body.ttl !== undefined) detailParts.push('有效期已修改')
|
|
3385
3416
|
await auditLog(ctx, 'configure_success', { username, ip: meta.ip, ua: meta.ua, detail: detailParts.length ? detailParts.join(',') : null })
|
|
3417
|
+
clearSessionReauth(req)
|
|
3386
3418
|
sendJson(res, 200, { ok: true })
|
|
3387
3419
|
} catch (e) {
|
|
3388
3420
|
sendJson(res, 500, { error: e && e.message ? e.message : String(e) })
|
|
@@ -3412,7 +3444,7 @@ export async function apply(ctx) {
|
|
|
3412
3444
|
sendJson(res, 200, { ok: true })
|
|
3413
3445
|
return
|
|
3414
3446
|
}
|
|
3415
|
-
const reauthOk =
|
|
3447
|
+
const reauthOk = sessionReauthOk(req)
|
|
3416
3448
|
const curValid = reauthOk || (current && typeof creds.hash === 'string' && await verifyPassword(current, creds.hash))
|
|
3417
3449
|
if (!curValid) {
|
|
3418
3450
|
await auditLog(ctx, 'disable_failure', { username: creds.username, ip: meta.ip, ua: meta.ua, detail: '当前密码不正确' })
|
|
@@ -3498,7 +3530,16 @@ export async function apply(ctx) {
|
|
|
3498
3530
|
const state = oidcRandom(24)
|
|
3499
3531
|
const nonce = oidcRandom(24)
|
|
3500
3532
|
const verifier = oidcRandom(32)
|
|
3501
|
-
|
|
3533
|
+
// 复验要绑定到发起它的浏览器的**会话**:回调时直接把 reauthAt 记在该会话上,
|
|
3534
|
+
// 不再签发独立令牌、也不把令牌放进 URL。
|
|
3535
|
+
const sessionToken = purpose === 'reauth' ? cookieOf(req, COOKIE_NAME) : null
|
|
3536
|
+
// 复验可以由设置页的弹窗发起(popup=1):回调时回一个"验证成功,请关闭窗口"
|
|
3537
|
+
// 的小页面,而不是把整个应用加载进弹窗。
|
|
3538
|
+
let popup = false
|
|
3539
|
+
try {
|
|
3540
|
+
popup = purpose === 'reauth' && new URL(req.url || '/', 'http://x').searchParams.get('popup') === '1'
|
|
3541
|
+
} catch (e) { popup = false }
|
|
3542
|
+
oidcStateStore.put(state, { nonce, verifier, redirectUri, purpose, sessionToken, popup })
|
|
3502
3543
|
const authUrl = new URL(disc.metadata.authorization_endpoint)
|
|
3503
3544
|
authUrl.searchParams.set('response_type', 'code')
|
|
3504
3545
|
authUrl.searchParams.set('client_id', o.clientId)
|
|
@@ -3629,7 +3670,7 @@ export async function apply(ctx) {
|
|
|
3629
3670
|
const creds = await readCredentials(ctx)
|
|
3630
3671
|
const meta = requestMeta(req)
|
|
3631
3672
|
const current = String(body.current || '')
|
|
3632
|
-
const reauthOk =
|
|
3673
|
+
const reauthOk = sessionReauthOk(req)
|
|
3633
3674
|
const curValid = reauthOk || (current && creds && typeof creds.hash === 'string' && await verifyPassword(current, creds.hash))
|
|
3634
3675
|
if (!curValid) {
|
|
3635
3676
|
await auditLog(ctx, 'oidc_unbind_failure', { username: creds && creds.username ? creds.username : null, ip: meta.ip, ua: meta.ua, detail: '当前密码不正确' })
|
|
@@ -3662,6 +3703,7 @@ export async function apply(ctx) {
|
|
|
3662
3703
|
await writeCredentials(ctx, next)
|
|
3663
3704
|
await refreshSsoOnly(next)
|
|
3664
3705
|
await auditLog(ctx, 'oidc_unbind_success', { username: creds.username, ip: meta.ip, ua: meta.ua })
|
|
3706
|
+
clearSessionReauth(req)
|
|
3665
3707
|
sendJson(res, 200, { ok: true, bound: false })
|
|
3666
3708
|
} catch (e) {
|
|
3667
3709
|
sendJson(res, 500, { error: e && e.message ? e.message : String(e) })
|
|
@@ -3793,9 +3835,27 @@ export async function apply(ctx) {
|
|
|
3793
3835
|
redirectBackToApp(ctx, req, res, 'oidcerr=sub-mismatch', clearStateCookie)
|
|
3794
3836
|
return
|
|
3795
3837
|
}
|
|
3796
|
-
|
|
3838
|
+
// 把复验结果记到发起登录的那个会话上(2 分钟内有效)。
|
|
3839
|
+
const target = st.sessionToken ? sessions.get(st.sessionToken) : null
|
|
3840
|
+
if (!target || target.expiresAt <= Date.now()) {
|
|
3841
|
+
await auditLog(ctx, 'oidc_login_failure', { username: auditName, ip: meta.ip, ua: meta.ua, detail: '复验时会话已失效' })
|
|
3842
|
+
redirectBackToApp(ctx, req, res, 'oidcerr=session-expired', clearStateCookie)
|
|
3843
|
+
return
|
|
3844
|
+
}
|
|
3845
|
+
target.reauthAt = Date.now()
|
|
3797
3846
|
await auditLog(ctx, 'oidc_reauth_success', { username: auditName, ip: meta.ip, ua: meta.ua })
|
|
3798
|
-
|
|
3847
|
+
// 弹窗发起:回一个自我关闭的页面,避免把整个应用加载进小窗口。
|
|
3848
|
+
if (st.popup) {
|
|
3849
|
+
res.writeHead(200, {
|
|
3850
|
+
'content-type': 'text/html; charset=utf-8',
|
|
3851
|
+
'cache-control': 'no-store',
|
|
3852
|
+
'x-content-type-options': 'nosniff',
|
|
3853
|
+
'set-cookie': clearStateCookie['Set-Cookie'],
|
|
3854
|
+
})
|
|
3855
|
+
res.end(REAUTH_POPUP_HTML)
|
|
3856
|
+
return
|
|
3857
|
+
}
|
|
3858
|
+
redirectBackToApp(ctx, req, res, 'oidcreauth=1', clearStateCookie)
|
|
3799
3859
|
return
|
|
3800
3860
|
}
|
|
3801
3861
|
|
package/lib/client.js
CHANGED
|
@@ -9,51 +9,54 @@ window.__ModuleLoader__.load({
|
|
|
9
9
|
const inject = ['slots'];
|
|
10
10
|
|
|
11
11
|
const CSS = [
|
|
12
|
-
'.wua-section { display: flex; flex-direction: column; gap:
|
|
13
|
-
'.wua-card { display: flex; flex-direction: column; gap:
|
|
14
|
-
'.wua-card
|
|
15
|
-
'.wua-card-
|
|
12
|
+
'.wua-section { display: flex; flex-direction: column; gap: 18px; max-width: 820px; padding: 4px 2px 24px; color: var(--dsw-alias-label-primary, #222); }',
|
|
13
|
+
'.wua-card { display: flex; flex-direction: column; gap: 14px; padding: 18px 20px; border: 1px solid var(--dsw-alias-border-l1, #e5e5e5); border-radius: 14px; background: var(--dsw-alias-bg-layer-2, #fff); box-shadow: 0 1px 2px rgba(0,0,0,.04), 0 10px 28px rgba(0,0,0,.045); transition: border-color .18s ease, box-shadow .18s ease; }',
|
|
14
|
+
'.wua-card:hover { border-color: color-mix(in srgb, var(--dsw-alias-brand-primary, #4a7cf7) 28%, var(--dsw-alias-border-l1, #e5e5e5)); box-shadow: 0 1px 2px rgba(0,0,0,.04), 0 14px 34px rgba(0,0,0,.07); }',
|
|
15
|
+
'.wua-card-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }',
|
|
16
|
+
'.wua-card-title { position: relative; margin: 0; padding-left: 10px; font-size: 14.5px; font-weight: 650; letter-spacing: .01em; color: var(--dsw-alias-label-primary, #222); }',
|
|
17
|
+
'.wua-card-title::before { content: ""; position: absolute; left: 0; top: 50%; width: 3px; height: 14px; border-radius: 2px; transform: translateY(-50%); background: var(--dsw-alias-brand-primary, #4a7cf7); }',
|
|
16
18
|
'.wua-badges { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }',
|
|
17
|
-
'.wua-badge { font-size: 11px; line-height: 1.
|
|
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)
|
|
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)
|
|
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)
|
|
21
|
-
'.wua-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(
|
|
19
|
+
'.wua-badge { font-size: 11px; line-height: 1.7; padding: 1px 9px; border-radius: 999px; border: 1px solid transparent; white-space: nowrap; font-weight: 500; }',
|
|
20
|
+
'.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) 28%, transparent); }',
|
|
21
|
+
'.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) 28%, transparent); }',
|
|
22
|
+
'.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) 20%, transparent); }',
|
|
23
|
+
'.wua-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 14px; }',
|
|
24
|
+
'.wua-row { display: flex; flex-direction: column; gap: 6px; min-width: 0; }',
|
|
25
|
+
'.wua-label { font-size: 12.5px; font-weight: 500; color: var(--dsw-alias-label-secondary, #666); }',
|
|
26
|
+
'.wua-input, .wua-select { box-sizing: border-box; width: 100%; height: 36px; padding: 0 11px; font-size: 13px; color: var(--dsw-alias-label-primary, #222); background: var(--dsw-alias-bg-layer-1, #fff); border: 1px solid var(--dsw-alias-border-l1, #d9d9d9); border-radius: 9px; outline: none; transition: border-color .15s ease, box-shadow .15s ease, background .15s ease; }',
|
|
27
|
+
'.wua-input:hover, .wua-select:hover { border-color: var(--dsw-alias-border-l2, #b8b8b8); }',
|
|
28
|
+
'.wua-input:focus, .wua-select:focus { border-color: var(--dsw-alias-brand-primary, #4a7cf7); box-shadow: 0 0 0 3px color-mix(in srgb, var(--dsw-alias-brand-primary, #4a7cf7) 18%, transparent); }',
|
|
29
|
+
'.wua-input:-webkit-autofill, .wua-input:-webkit-autofill:hover, .wua-input:-webkit-autofill:focus { -webkit-text-fill-color: var(--dsw-alias-label-primary, #222); -webkit-box-shadow: 0 0 0 1000px var(--dsw-alias-bg-layer-1, #fff) inset; box-shadow: 0 0 0 1000px var(--dsw-alias-bg-layer-1, #fff) inset; caret-color: var(--dsw-alias-label-primary, #222); transition: background-color 999999s ease-in-out 0s; }',
|
|
30
|
+
'.wua-input::placeholder { color: var(--dsw-alias-label-tertiary, #999); }',
|
|
31
|
+
'.wua-btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px; padding: 8px 16px; font-size: 13px; font-weight: 500; line-height: 1.2; cursor: pointer; color: #fff; background: var(--dsw-alias-brand-primary, #4a7cf7); border: 1px solid transparent; border-radius: 9px; transition: filter .15s ease, transform .06s ease, box-shadow .15s ease; box-shadow: 0 2px 8px color-mix(in srgb, var(--dsw-alias-brand-primary, #4a7cf7) 26%, transparent); }',
|
|
32
|
+
'.wua-btn:hover:not(:disabled) { filter: brightness(1.06); }',
|
|
33
|
+
'.wua-btn:active:not(:disabled) { transform: translateY(1px); }',
|
|
34
|
+
'.wua-btn:disabled { opacity: .5; cursor: default; box-shadow: none; }',
|
|
35
|
+
'.wua-btn.ghost { color: var(--dsw-alias-label-primary, #333); background: transparent; border-color: var(--dsw-alias-border-l1, #d0d0d0); box-shadow: none; }',
|
|
36
|
+
'.wua-btn.ghost:hover:not(:disabled) { border-color: var(--dsw-alias-border-l2, #999); background: color-mix(in srgb, var(--dsw-alias-label-primary, #333) 5%, transparent); }',
|
|
37
|
+
'.wua-btnrow { display: flex; flex-wrap: wrap; gap: 8px; }',
|
|
22
38
|
'.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; }',
|
|
39
|
+
'.wua-sep { height: 1px; background: linear-gradient(90deg, transparent, var(--dsw-alias-border-l1, #e5e5e5), transparent); margin: 2px 0; }',
|
|
24
40
|
'.wua-kv { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }',
|
|
25
|
-
'.wua-
|
|
26
|
-
'.wua-
|
|
27
|
-
'.wua-
|
|
28
|
-
'.wua-
|
|
29
|
-
'.wua-
|
|
30
|
-
'.wua-
|
|
31
|
-
'.wua-
|
|
32
|
-
'.wua-
|
|
33
|
-
'.wua-
|
|
34
|
-
'.wua-
|
|
35
|
-
'.wua-
|
|
36
|
-
'.wua-input { box-sizing: border-box; width: 100%; padding: 7px 9px; font-size: 13px; color: var(--dsw-alias-label-primary, #222); background: var(--dsw-alias-bg-layer-2, #fff); border: 1px solid var(--dsw-alias-border-l1, #ccc); border-radius: 4px; outline: none; }',
|
|
37
|
-
'.wua-input:focus { border-color: var(--dsw-alias-brand-primary, #4a7cf7); }',
|
|
38
|
-
'/* 深色模式下浏览器自动填充会把输入框刷成白色/黄色:inset 大阴影 + text-fill-color 回压为当前主题输入底色 */',
|
|
39
|
-
'.wua-input:-webkit-autofill, .wua-input:-webkit-autofill:hover, .wua-input:-webkit-autofill:focus { -webkit-text-fill-color: var(--dsw-alias-label-primary, #222); -webkit-box-shadow: 0 0 0 1000px var(--dsw-alias-bg-layer-2, #fff) inset; box-shadow: 0 0 0 1000px var(--dsw-alias-bg-layer-2, #fff) inset; caret-color: var(--dsw-alias-label-primary, #222); transition: background-color 999999s ease-in-out 0s; }',
|
|
40
|
-
'.wua-input::placeholder { color: var(--dsw-alias-label-secondary, #888); }',
|
|
41
|
-
'.wua-select { box-sizing: border-box; width: 100%; padding: 7px 9px; font-size: 13px; color: var(--dsw-alias-label-primary, #222); background: var(--dsw-alias-bg-layer-2, #fff); border: 1px solid var(--dsw-alias-border-l1, #ccc); border-radius: 4px; outline: none; }',
|
|
42
|
-
'.wua-btn { align-self: flex-start; padding: 7px 18px; font-size: 13px; cursor: pointer; color: var(--dsw-alias-label-primary, #222); background: var(--dsw-alias-bg-layer-1, #fff); border: 1px solid var(--dsw-alias-border-l2, #999); border-radius: 4px; }',
|
|
43
|
-
'.wua-btn:hover:not(:disabled) { border-color: var(--dsw-alias-label-secondary, #555); }',
|
|
44
|
-
'.wua-btn:disabled { opacity: 0.55; cursor: default; }',
|
|
45
|
-
'.wua-btn.ghost { background: transparent; border-color: var(--dsw-alias-border-l1, #ccc); }',
|
|
46
|
-
'.wua-btnrow { display: flex; flex-wrap: wrap; gap: 8px; }',
|
|
47
|
-
'.wua-msg { margin: 0; font-size: 12px; color: var(--dsw-alias-state-success-primary, #1a7f37); }',
|
|
48
|
-
'.wua-err { margin: 0; font-size: 12px; color: var(--dsw-alias-state-error-primary, #d1242f); }',
|
|
49
|
-
'.wua-warn { margin: 0; padding: 10px 12px; font-size: 12px; line-height: 1.6; color: var(--dsw-alias-state-warn-primary, #9a6700); background: color-mix(in srgb, var(--dsw-alias-state-warn-primary, #9a6700) 10%, transparent); border: 1px solid var(--dsw-alias-state-warn-primary, #9a6700); border-radius: 4px; }',
|
|
50
|
-
'.wua-hint { margin: 0; font-size: 12px; line-height: 1.6; color: var(--dsw-alias-label-secondary, #888); border-top: 1px solid var(--dsw-alias-border-l1, #e5e5e5); padding-top: 10px; }',
|
|
51
|
-
'.wua-audit { display: flex; flex-direction: column; gap: 4px; }',
|
|
52
|
-
'.wua-audit-row { display: flex; flex-wrap: wrap; align-items: baseline; gap: 3px 10px; font-size: 12px; line-height: 1.6; }',
|
|
53
|
-
'.wua-audit-time { font-family: ui-monospace, SFMono-Regular, Consolas, monospace; color: var(--dsw-alias-label-secondary, #888); }',
|
|
54
|
-
'.wua-audit-event { color: var(--dsw-alias-label-primary, #222); }',
|
|
41
|
+
'.wua-intro { margin: 0; font-size: 13px; line-height: 1.65; color: var(--dsw-alias-label-secondary, #666); }',
|
|
42
|
+
'.wua-msg, .wua-err, .wua-warn { margin: 0; padding: 9px 12px; font-size: 12.5px; line-height: 1.6; border-radius: 9px; border-left: 3px solid transparent; }',
|
|
43
|
+
'.wua-msg { color: var(--dsw-alias-state-success-primary, #1a7f37); background: color-mix(in srgb, var(--dsw-alias-state-success-primary, #1a7f37) 9%, transparent); border-left-color: var(--dsw-alias-state-success-primary, #1a7f37); }',
|
|
44
|
+
'.wua-err { color: var(--dsw-alias-state-error-primary, #d1242f); background: color-mix(in srgb, var(--dsw-alias-state-error-primary, #d1242f) 9%, transparent); border-left-color: var(--dsw-alias-state-error-primary, #d1242f); }',
|
|
45
|
+
'.wua-warn { color: var(--dsw-alias-state-warn-primary, #9a6700); background: color-mix(in srgb, var(--dsw-alias-state-warn-primary, #9a6700) 9%, transparent); border-left-color: var(--dsw-alias-state-warn-primary, #9a6700); }',
|
|
46
|
+
'.wua-hint { margin: 0; padding-top: 10px; font-size: 12px; line-height: 1.65; color: var(--dsw-alias-label-secondary, #777); border-top: 1px solid var(--dsw-alias-border-l1, #eee); }',
|
|
47
|
+
'.wua-audit { display: flex; flex-direction: column; gap: 2px; }',
|
|
48
|
+
'.wua-audit-row { display: flex; flex-wrap: wrap; align-items: baseline; gap: 3px 10px; padding: 6px 8px; font-size: 12px; line-height: 1.55; border-radius: 8px; transition: background .12s ease; }',
|
|
49
|
+
'.wua-audit-row:hover { background: color-mix(in srgb, var(--dsw-alias-label-primary, #222) 4%, transparent); }',
|
|
50
|
+
'.wua-audit-time { font-family: ui-monospace, SFMono-Regular, Consolas, monospace; color: var(--dsw-alias-label-tertiary, #999); }',
|
|
51
|
+
'.wua-audit-event { color: var(--dsw-alias-label-primary, #222); font-weight: 500; }',
|
|
55
52
|
'.wua-audit-user { color: var(--dsw-alias-state-business-primary, #4176e6); }',
|
|
56
53
|
'.wua-audit-ip { color: var(--dsw-alias-label-tertiary, #999); font-family: ui-monospace, SFMono-Regular, Consolas, monospace; }',
|
|
54
|
+
'.wua-inline-relay { display: flex; flex-direction: column; gap: 9px; margin-top: 14px; padding: 12px 14px; border: 1px dashed color-mix(in srgb, var(--dsw-alias-brand-primary, #4a7cf7) 35%, transparent); border-radius: 10px; background: color-mix(in srgb, var(--dsw-alias-brand-primary, #4a7cf7) 5%, transparent); }',
|
|
55
|
+
'.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); backdrop-filter: blur(2px); animation: wua-fade .16s ease; }',
|
|
56
|
+
'.wua-modal { width: min(440px, 100%); display: flex; flex-direction: column; gap: 13px; padding: 22px 24px; border: 1px solid var(--dsw-alias-border-l1, #e5e5e5); border-radius: 16px; background: var(--dsw-alias-bg-layer-1, #fff); box-shadow: 0 28px 70px rgba(0,0,0,.35); animation: wua-pop .18s cubic-bezier(.2,.9,.3,1.2); }',
|
|
57
|
+
'@keyframes wua-fade { from { opacity: 0 } to { opacity: 1 } }',
|
|
58
|
+
'@keyframes wua-pop { from { opacity: 0; transform: translateY(8px) scale(.98) } to { opacity: 1; transform: none } }',
|
|
59
|
+
'@media (max-width: 640px) { .wua-card { padding: 14px 15px; } .wua-grid { grid-template-columns: 1fr; } .wua-modal { padding: 18px; } }',
|
|
57
60
|
].join('\n');
|
|
58
61
|
|
|
59
62
|
function apply(ctx) {
|
|
@@ -250,7 +253,7 @@ window.__ModuleLoader__.load({
|
|
|
250
253
|
const [username, setUsername] = react.useState('');
|
|
251
254
|
const [password, setPassword] = react.useState('');
|
|
252
255
|
const [confirm, setConfirm] = react.useState('');
|
|
253
|
-
|
|
256
|
+
|
|
254
257
|
const [ttl, setTtl] = react.useState('12');
|
|
255
258
|
const [msg, setMsg] = react.useState(null);
|
|
256
259
|
const [busy, setBusy] = react.useState(false);
|
|
@@ -279,8 +282,9 @@ window.__ModuleLoader__.load({
|
|
|
279
282
|
const [saveOpen, setSaveOpen] = react.useState(false);
|
|
280
283
|
const [savePw, setSavePw] = react.useState('');
|
|
281
284
|
const [saveErr, setSaveErr] = react.useState('');
|
|
282
|
-
const [
|
|
283
|
-
|
|
285
|
+
const [verifyAction, setVerifyAction] = react.useState('save');
|
|
286
|
+
// SSO 复验走独立弹窗:设置页保持打开并显示「等待验证」。
|
|
287
|
+
const [reauthWaiting, setReauthWaiting] = react.useState(false);
|
|
284
288
|
// 远程访问信任开关。
|
|
285
289
|
const [remoteTrustBusy, setRemoteTrustBusy] = react.useState(false);
|
|
286
290
|
const [remoteTrustMsg, setRemoteTrustMsg] = react.useState(null);
|
|
@@ -334,19 +338,35 @@ window.__ModuleLoader__.load({
|
|
|
334
338
|
// 读取 OIDC 回调带回来的回执。绑定/登录结束后浏览器会被 302 回应用,
|
|
335
339
|
// 结果通过 query 传递(oidcbound=1 / oidcerr=...):因为是整页跳转,
|
|
336
340
|
// 组件重新挂载时才知道刚才发生了什么。
|
|
337
|
-
//
|
|
341
|
+
// 复验弹窗通过 postMessage 通知;同时轮询 status 兜底(弹窗被拦截/关不掉)。
|
|
338
342
|
react.useEffect(() => {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
if (
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
343
|
+
if (typeof window === 'undefined' || typeof window.addEventListener !== 'function') return;
|
|
344
|
+
const onMessage = (e) => {
|
|
345
|
+
if (e.origin !== location.origin) return;
|
|
346
|
+
if (e.data && e.data.type === 'dsh-wua-reauth' && e.data.ok) {
|
|
347
|
+
setReauthWaiting(false);
|
|
348
|
+
api('/dsh-webui-oauth/status').then((s) => {
|
|
349
|
+
if (s && !s.error) setStatus(s);
|
|
350
|
+
}).catch(() => {});
|
|
346
351
|
}
|
|
347
|
-
}
|
|
352
|
+
};
|
|
353
|
+
window.addEventListener('message', onMessage);
|
|
354
|
+
return () => window.removeEventListener('message', onMessage);
|
|
348
355
|
}, []);
|
|
349
356
|
|
|
357
|
+
react.useEffect(() => {
|
|
358
|
+
if (!reauthWaiting) return;
|
|
359
|
+
const timer = setInterval(() => {
|
|
360
|
+
api('/dsh-webui-oauth/status').then((s) => {
|
|
361
|
+
if (s && s.reauthActive) {
|
|
362
|
+
setReauthWaiting(false);
|
|
363
|
+
setStatus(s);
|
|
364
|
+
}
|
|
365
|
+
}).catch(() => {});
|
|
366
|
+
}, 2000);
|
|
367
|
+
return () => clearInterval(timer);
|
|
368
|
+
}, [reauthWaiting]);
|
|
369
|
+
|
|
350
370
|
react.useEffect(() => {
|
|
351
371
|
let qs = ''
|
|
352
372
|
try { qs = String((location && location.search) || '') } catch (e) { qs = '' }
|
|
@@ -357,10 +377,7 @@ window.__ModuleLoader__.load({
|
|
|
357
377
|
const reauth = p.get('oidcreauth')
|
|
358
378
|
if (bound === null && err === null && reauth === null) return
|
|
359
379
|
if (reauth !== null) {
|
|
360
|
-
|
|
361
|
-
setReauthToken(reauth)
|
|
362
|
-
setReauthExp(Date.now() + 120000)
|
|
363
|
-
setBindMsg({ kind: 'ok', text: 'SSO 验证成功(2 分钟内有效)。请回到设置页点击「保存设置」。' })
|
|
380
|
+
setBindMsg({ kind: 'ok', text: 'SSO 验证成功(2 分钟内有效)。请点击「保存设置」继续。' })
|
|
364
381
|
} else if (bound !== null) {
|
|
365
382
|
setBindMsg({ kind: 'ok', text: 'OIDC 身份绑定成功。此后只有该身份可通过 SSO 登录。' })
|
|
366
383
|
} else {
|
|
@@ -409,7 +426,7 @@ window.__ModuleLoader__.load({
|
|
|
409
426
|
setMsg({ kind: 'err', text: '两次输入的密码不一致' });
|
|
410
427
|
return;
|
|
411
428
|
}
|
|
412
|
-
if (enabled && !credentials.current && !
|
|
429
|
+
if (enabled && !credentials.current && !reauthValid) {
|
|
413
430
|
setSaveErr('修改认证信息需要验证:请输入当前密码,或使用 SSO 验证。');
|
|
414
431
|
return;
|
|
415
432
|
}
|
|
@@ -432,7 +449,6 @@ window.__ModuleLoader__.load({
|
|
|
432
449
|
username: name,
|
|
433
450
|
password,
|
|
434
451
|
current: credentials.current || '',
|
|
435
|
-
reauthToken: credentials.reauthToken || undefined,
|
|
436
452
|
ttl: Number(ttl),
|
|
437
453
|
oidc: oidcPayload,
|
|
438
454
|
}).then((r) => {
|
|
@@ -445,13 +461,9 @@ window.__ModuleLoader__.load({
|
|
|
445
461
|
}
|
|
446
462
|
setPassword('');
|
|
447
463
|
setConfirm('');
|
|
448
|
-
setCurrent('');
|
|
449
464
|
setSaveOpen(false);
|
|
450
465
|
setSavePw('');
|
|
451
466
|
setSaveErr('');
|
|
452
|
-
setReauthToken(null);
|
|
453
|
-
setReauthExp(0);
|
|
454
|
-
try { sessionStorage.removeItem('dsh_wua_reauth') } catch (e) { /* ignore */ }
|
|
455
467
|
api('/dsh-webui-oauth/status').then((s) => {
|
|
456
468
|
if (s && !s.error) setStatus(s);
|
|
457
469
|
});
|
|
@@ -484,23 +496,27 @@ window.__ModuleLoader__.load({
|
|
|
484
496
|
});
|
|
485
497
|
};
|
|
486
498
|
|
|
487
|
-
const
|
|
499
|
+
const doDisable = (auth) => {
|
|
488
500
|
if (busy) return;
|
|
489
|
-
|
|
490
|
-
|
|
501
|
+
const credentials = auth || {};
|
|
502
|
+
if (!credentials.current && !reauthValid) {
|
|
503
|
+
setSaveErr('禁用认证需要验证:请输入当前密码,或使用 SSO 验证。');
|
|
491
504
|
return;
|
|
492
505
|
}
|
|
493
506
|
setBusy(true);
|
|
494
507
|
setMsg(null);
|
|
495
|
-
api('/dsh-webui-oauth/disable', { current }).then((r) => {
|
|
508
|
+
api('/dsh-webui-oauth/disable', { current: credentials.current || '' }).then((r) => {
|
|
496
509
|
setBusy(false);
|
|
497
510
|
if (r && r.ok) {
|
|
498
511
|
setMsg({ kind: 'ok', text: '已禁用认证,所有会话已注销。刷新页面后不再要求登录。' });
|
|
499
|
-
|
|
512
|
+
setSaveOpen(false);
|
|
513
|
+
setSavePw('');
|
|
514
|
+
setSaveErr('');
|
|
500
515
|
api('/dsh-webui-oauth/status').then((s) => {
|
|
501
516
|
if (s && !s.error) setStatus(s);
|
|
502
517
|
}).catch(() => {});
|
|
503
518
|
} else {
|
|
519
|
+
setSaveErr('当前密码不正确');
|
|
504
520
|
setMsg({ kind: 'err', text: '当前密码不正确' });
|
|
505
521
|
}
|
|
506
522
|
}).catch(() => {
|
|
@@ -563,7 +579,22 @@ window.__ModuleLoader__.load({
|
|
|
563
579
|
|
|
564
580
|
const enabled = Boolean(status && status.enabled);
|
|
565
581
|
const remoteTrust = Boolean(status && status.remoteTrust);
|
|
566
|
-
const reauthValid = Boolean(
|
|
582
|
+
const reauthValid = Boolean(status && status.reauthActive);
|
|
583
|
+
|
|
584
|
+
// 顶部「保存设置」/「禁用认证」:需要验证时统一打开弹窗(密码或 SSO)。
|
|
585
|
+
const onSaveClick = () => {
|
|
586
|
+
if (!enabled || reauthValid) { onSave({}); return; }
|
|
587
|
+
setVerifyAction('save');
|
|
588
|
+
setSaveErr('');
|
|
589
|
+
setSaveOpen(true);
|
|
590
|
+
};
|
|
591
|
+
const onDisableClick = () => {
|
|
592
|
+
if (!enabled) return;
|
|
593
|
+
if (reauthValid) { doDisable({}); return; }
|
|
594
|
+
setVerifyAction('disable');
|
|
595
|
+
setSaveErr('');
|
|
596
|
+
setSaveOpen(true);
|
|
597
|
+
};
|
|
567
598
|
const intro = status === null
|
|
568
599
|
? '正在读取认证状态…'
|
|
569
600
|
: (enabled
|
|
@@ -647,8 +678,9 @@ window.__ModuleLoader__.load({
|
|
|
647
678
|
patchWarn ? react.createElement('p', { className: 'wua-warn' }, patchWarn) : null,
|
|
648
679
|
persistNote ? react.createElement('p', { className: 'wua-warn' }, persistNote) : null,
|
|
649
680
|
react.createElement('div', { className: 'wua-actions' },
|
|
681
|
+
react.createElement('button', { className: 'wua-btn', disabled: busy || status === null, onClick: onSaveClick }, busy ? '保存中…' : '保存设置'),
|
|
650
682
|
enabled ? react.createElement('button', { className: 'wua-btn ghost', disabled: busy || status === null, onClick: onLogout }, '退出登录') : null,
|
|
651
|
-
enabled ? react.createElement('button', { className: 'wua-btn ghost', disabled: busy || status === null, onClick:
|
|
683
|
+
enabled ? react.createElement('button', { className: 'wua-btn ghost', disabled: busy || status === null, onClick: onDisableClick }, '禁用认证') : null,
|
|
652
684
|
),
|
|
653
685
|
]),
|
|
654
686
|
card('账号与密码', [], [
|
|
@@ -678,10 +710,6 @@ window.__ModuleLoader__.load({
|
|
|
678
710
|
react.createElement('input', { className: 'wua-input', type: 'password', value: confirm, onChange: (e) => setConfirm(e.target.value) }),
|
|
679
711
|
),
|
|
680
712
|
),
|
|
681
|
-
enabled ? react.createElement('div', { className: 'wua-row' },
|
|
682
|
-
react.createElement('label', { className: 'wua-label' }, '当前密码(修改或禁用认证需验证)'),
|
|
683
|
-
react.createElement('input', { className: 'wua-input', type: 'password', value: current, onChange: (e) => setCurrent(e.target.value) }),
|
|
684
|
-
) : null,
|
|
685
713
|
msg ? react.createElement('div', { className: msg.kind === 'ok' ? 'wua-msg' : 'wua-err' }, msg.text) : null,
|
|
686
714
|
]),
|
|
687
715
|
card('OIDC 单点登录', [
|
|
@@ -807,40 +835,42 @@ window.__ModuleLoader__.load({
|
|
|
807
835
|
)),
|
|
808
836
|
)),
|
|
809
837
|
]),
|
|
810
|
-
react.createElement('button', {
|
|
811
|
-
className: 'wua-save-fab',
|
|
812
|
-
disabled: busy || status === null,
|
|
813
|
-
onClick: () => {
|
|
814
|
-
if (!enabled) { onSave({}); return; }
|
|
815
|
-
if (reauthValid) { onSave({ reauthToken }); return; }
|
|
816
|
-
setSaveErr('');
|
|
817
|
-
setSaveOpen(true);
|
|
818
|
-
},
|
|
819
|
-
}, busy ? '保存中…' : '保存设置'),
|
|
820
838
|
saveOpen ? react.createElement('div', { className: 'wua-modal-mask' },
|
|
821
839
|
react.createElement('div', { className: 'wua-modal' },
|
|
822
|
-
react.createElement('h4', { className: 'wua-card-title' }, '验证后保存'),
|
|
840
|
+
react.createElement('h4', { className: 'wua-card-title' }, verifyAction === 'disable' ? '验证后禁用' : '验证后保存'),
|
|
823
841
|
react.createElement('p', { className: 'wua-hint', style: { borderTop: 'none', paddingTop: 0 } },
|
|
824
842
|
reauthValid
|
|
825
|
-
? '已通过 SSO 验证(2
|
|
826
|
-
:
|
|
843
|
+
? '已通过 SSO 验证(2 分钟内有效),可直接继续。'
|
|
844
|
+
: reauthWaiting
|
|
845
|
+
? '等待验证…请在打开的窗口中完成 SSO 登录,本页会保持打开。'
|
|
846
|
+
: '该操作需要验证当前身份:输入当前密码,或使用 SSO 验证。'),
|
|
827
847
|
reauthValid ? null : react.createElement('div', { className: 'wua-row' },
|
|
828
848
|
react.createElement('label', { className: 'wua-label' }, '当前密码'),
|
|
829
849
|
react.createElement('input', { className: 'wua-input', type: 'password', value: savePw, autoComplete: 'current-password', onChange: (e) => setSavePw(e.target.value) }),
|
|
830
850
|
),
|
|
831
851
|
saveErr ? react.createElement('p', { className: 'wua-err' }, saveErr) : null,
|
|
832
852
|
react.createElement('div', { className: 'wua-actions' },
|
|
833
|
-
react.createElement('button', { className: 'wua-btn', disabled: busy, onClick: () => {
|
|
834
|
-
if (reauthValid) {
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
} }, busy ? '
|
|
853
|
+
react.createElement('button', { className: 'wua-btn', disabled: busy || reauthWaiting, onClick: () => {
|
|
854
|
+
if (!reauthValid && !savePw) { setSaveErr('请输入当前密码'); return; }
|
|
855
|
+
const auth = reauthValid ? {} : { current: savePw };
|
|
856
|
+
if (verifyAction === 'disable') doDisable(auth); else onSave(auth);
|
|
857
|
+
} }, busy ? '处理中…' : (verifyAction === 'disable' ? '验证并禁用' : '验证并保存')),
|
|
838
858
|
react.createElement('button', { className: 'wua-btn ghost', disabled: busy, onClick: () => setSaveOpen(false) }, '取消'),
|
|
839
859
|
),
|
|
840
860
|
(status && status.oidc && status.oidc.enabled && status.oidc.bound)
|
|
841
|
-
? react.createElement('button', {
|
|
842
|
-
|
|
843
|
-
|
|
861
|
+
? react.createElement('button', {
|
|
862
|
+
className: 'wua-btn ghost',
|
|
863
|
+
disabled: busy,
|
|
864
|
+
onClick: () => {
|
|
865
|
+
const url = '/dsh-webui-oauth/oidc/reauth?base=' + encodeURIComponent(location.origin || '') + '&popup=1';
|
|
866
|
+
const win = window.open(url, 'dsh-wua-reauth', 'width=520,height=720');
|
|
867
|
+
if (!win) { location.href = url; return; }
|
|
868
|
+
setReauthWaiting(true);
|
|
869
|
+
},
|
|
870
|
+
}, reauthWaiting ? '重新打开 SSO 验证窗口' : '使用 SSO 验证(新窗口)')
|
|
871
|
+
: null,
|
|
872
|
+
reauthWaiting
|
|
873
|
+
? react.createElement('p', { className: 'wua-msg' }, '等待验证…')
|
|
844
874
|
: null,
|
|
845
875
|
),
|
|
846
876
|
) : null,
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-webui-oauth",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.8",
|
|
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"
|
|
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"
|
|
8
8
|
},
|
|
9
9
|
"exports": {
|
|
10
10
|
".": "./index.js",
|