dsh-webui-oauth 0.7.8 → 0.7.9
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 +21 -0
- package/index.js +48 -0
- package/lib/client.js +48 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
本文件记录每个版本的**用户可见**变更。破坏性变更与升级动作单独标注。
|
|
4
4
|
|
|
5
|
+
## 0.7.9
|
|
6
|
+
|
|
7
|
+
「概览」卡片新增**「吊销所有浏览器会话」**按钮。
|
|
8
|
+
|
|
9
|
+
### 新增:吊销所有其他浏览器会话
|
|
10
|
+
|
|
11
|
+
- 位置:设置 → 身份认证 → 概览(与保存/退出/禁用并列);
|
|
12
|
+
- 与保存/禁用共用验证弹窗:当前密码,或 2 分钟内的 SSO 复验;
|
|
13
|
+
- 服务端端点 `POST /dsh-webui-oauth/sessions/revoke`(要求会话):
|
|
14
|
+
- 校验通过后调用 `deleteAllExcept(当前会话)`;
|
|
15
|
+
- **保留发起请求的当前会话**,避免点一下把自己踢回登录页;
|
|
16
|
+
- 返回被吊销的会话数,审计记录 `sessions_revoked`;
|
|
17
|
+
- 成功后清除会话复验时间戳(一次性);
|
|
18
|
+
- 想连当前会话一起退出,用「退出登录」。
|
|
19
|
+
|
|
20
|
+
### 测试
|
|
21
|
+
|
|
22
|
+
- `test/lifecycle.test.mjs` 新增 6 项:错误密码拒绝、吊销成功且
|
|
23
|
+
`revoked >= 1`、其他会话 401、当前会话仍 200;
|
|
24
|
+
- 全量 23 个测试文件、629 项断言通过。
|
|
25
|
+
|
|
5
26
|
## 0.7.8
|
|
6
27
|
|
|
7
28
|
SSO 复验改为**新窗口 + 等待状态**;设置页做了一轮视觉打磨。
|
package/index.js
CHANGED
|
@@ -1129,6 +1129,7 @@ const SELF_GATED_EXACT_PATHS = new Set([
|
|
|
1129
1129
|
'/dsh-webui-oauth/oidc/reauth',
|
|
1130
1130
|
'/dsh-webui-oauth/oauth-relay',
|
|
1131
1131
|
'/dsh-webui-oauth/remote-trust',
|
|
1132
|
+
'/dsh-webui-oauth/sessions/revoke',
|
|
1132
1133
|
])
|
|
1133
1134
|
|
|
1134
1135
|
/**
|
|
@@ -3129,6 +3130,53 @@ export async function apply(ctx) {
|
|
|
3129
3130
|
},
|
|
3130
3131
|
}), 'dsh-webui-oauth: logout')
|
|
3131
3132
|
|
|
3133
|
+
// 吊销所有其他浏览器会话:POST /dsh-webui-oauth/sessions/revoke
|
|
3134
|
+
//
|
|
3135
|
+
// 与「保存配置」同一套验证(当前密码或 2 分钟内的 SSO 复验)。
|
|
3136
|
+
// 保留发起请求的当前会话,避免用户点一下就把自己踢回登录页;
|
|
3137
|
+
// 想连当前会话一起退出,用「退出登录」。
|
|
3138
|
+
ctx.effect(() => ctx.webServer.register({
|
|
3139
|
+
kind: 'exact',
|
|
3140
|
+
path: '/dsh-webui-oauth/sessions/revoke',
|
|
3141
|
+
handler: async (req, res) => {
|
|
3142
|
+
try {
|
|
3143
|
+
if (req.method !== 'POST') {
|
|
3144
|
+
sendJson(res, 405, { error: '仅支持 POST' })
|
|
3145
|
+
return
|
|
3146
|
+
}
|
|
3147
|
+
if (!checkRequest(req)) {
|
|
3148
|
+
sendJson(res, 401, { error: 'unauthorized' })
|
|
3149
|
+
return
|
|
3150
|
+
}
|
|
3151
|
+
const body = await readJsonBody(req, res)
|
|
3152
|
+
if (body === null) return
|
|
3153
|
+
const creds = await readCredentials(ctx)
|
|
3154
|
+
const current = String(body.current || '')
|
|
3155
|
+
const reauthOk = sessionReauthOk(req)
|
|
3156
|
+
const curValid = reauthOk || (current && creds && typeof creds.hash === 'string' && await verifyPassword(current, creds.hash))
|
|
3157
|
+
if (!curValid) {
|
|
3158
|
+
const meta = requestMeta(req)
|
|
3159
|
+
await auditLog(ctx, 'sessions_revoke_failure', { username: creds && creds.username ? creds.username : null, ip: meta.ip, ua: meta.ua, detail: '当前密码不正确' })
|
|
3160
|
+
sendJson(res, 200, { ok: false, error: 'current-invalid' })
|
|
3161
|
+
return
|
|
3162
|
+
}
|
|
3163
|
+
let before = 0
|
|
3164
|
+
for (const _ of sessions.live) before += 1
|
|
3165
|
+
const keep = cookieOf(req, COOKIE_NAME)
|
|
3166
|
+
destroyAllSessionsExcept(keep)
|
|
3167
|
+
let after = 0
|
|
3168
|
+
for (const _ of sessions.live) after += 1
|
|
3169
|
+
const revoked = Math.max(0, before - after)
|
|
3170
|
+
clearSessionReauth(req)
|
|
3171
|
+
const meta = requestMeta(req)
|
|
3172
|
+
await auditLog(ctx, 'sessions_revoked', { username: creds.username, ip: meta.ip, ua: meta.ua, detail: '吊销 ' + revoked + ' 个其他会话' })
|
|
3173
|
+
sendJson(res, 200, { ok: true, revoked })
|
|
3174
|
+
} catch (e) {
|
|
3175
|
+
sendJson(res, 500, { error: e && e.message ? e.message : String(e) })
|
|
3176
|
+
}
|
|
3177
|
+
},
|
|
3178
|
+
}), 'dsh-webui-oauth: revoke sessions')
|
|
3179
|
+
|
|
3132
3180
|
ctx.effect(() => ctx.webServer.register({
|
|
3133
3181
|
kind: 'exact',
|
|
3134
3182
|
path: '/dsh-webui-oauth/status',
|
package/lib/client.js
CHANGED
|
@@ -525,6 +525,38 @@ window.__ModuleLoader__.load({
|
|
|
525
525
|
});
|
|
526
526
|
};
|
|
527
527
|
|
|
528
|
+
const doRevoke = (auth) => {
|
|
529
|
+
if (busy) return;
|
|
530
|
+
const credentials = auth || {};
|
|
531
|
+
if (!credentials.current && !reauthValid) {
|
|
532
|
+
setSaveErr('吊销会话需要验证:请输入当前密码,或使用 SSO 验证。');
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
setBusy(true);
|
|
536
|
+
setMsg(null);
|
|
537
|
+
api('/dsh-webui-oauth/sessions/revoke', { current: credentials.current || '' }).then((r) => {
|
|
538
|
+
setBusy(false);
|
|
539
|
+
if (r && r.ok) {
|
|
540
|
+
setMsg({
|
|
541
|
+
kind: 'ok',
|
|
542
|
+
text: '已吊销 ' + (typeof r.revoked === 'number' ? r.revoked : 0) + ' 个其他浏览器会话;当前会话保持有效。',
|
|
543
|
+
});
|
|
544
|
+
setSaveOpen(false);
|
|
545
|
+
setSavePw('');
|
|
546
|
+
setSaveErr('');
|
|
547
|
+
api('/dsh-webui-oauth/status').then((s) => {
|
|
548
|
+
if (s && !s.error) setStatus(s);
|
|
549
|
+
}).catch(() => {});
|
|
550
|
+
} else {
|
|
551
|
+
setSaveErr('当前密码不正确');
|
|
552
|
+
setMsg({ kind: 'err', text: '当前密码不正确' });
|
|
553
|
+
}
|
|
554
|
+
}).catch(() => {
|
|
555
|
+
setBusy(false);
|
|
556
|
+
setMsg({ kind: 'err', text: '操作失败:无法连接认证服务' });
|
|
557
|
+
});
|
|
558
|
+
};
|
|
559
|
+
|
|
528
560
|
// 绑定:先发起服务端授权往返,IdP 校验通过后由回调把 sub 落库。
|
|
529
561
|
// 这里不直接调 API 拿 sub——那样等于让浏览器自报身份,毫无安全意义。
|
|
530
562
|
const onBind = () => {
|
|
@@ -595,6 +627,13 @@ window.__ModuleLoader__.load({
|
|
|
595
627
|
setSaveErr('');
|
|
596
628
|
setSaveOpen(true);
|
|
597
629
|
};
|
|
630
|
+
const onRevokeClick = () => {
|
|
631
|
+
if (!enabled) return;
|
|
632
|
+
if (reauthValid) { doRevoke({}); return; }
|
|
633
|
+
setVerifyAction('revoke');
|
|
634
|
+
setSaveErr('');
|
|
635
|
+
setSaveOpen(true);
|
|
636
|
+
};
|
|
598
637
|
const intro = status === null
|
|
599
638
|
? '正在读取认证状态…'
|
|
600
639
|
: (enabled
|
|
@@ -619,6 +658,8 @@ window.__ModuleLoader__.load({
|
|
|
619
658
|
configure_success: '修改凭据',
|
|
620
659
|
configure_failure: '修改凭据失败',
|
|
621
660
|
disable_success: '禁用认证',
|
|
661
|
+
sessions_revoked: '吊销其他会话',
|
|
662
|
+
sessions_revoke_failure: '吊销会话失败',
|
|
622
663
|
disable_failure: '禁用认证失败',
|
|
623
664
|
logout: '退出登录',
|
|
624
665
|
oidc_login_success: 'SSO 登录成功',
|
|
@@ -681,6 +722,7 @@ window.__ModuleLoader__.load({
|
|
|
681
722
|
react.createElement('button', { className: 'wua-btn', disabled: busy || status === null, onClick: onSaveClick }, busy ? '保存中…' : '保存设置'),
|
|
682
723
|
enabled ? react.createElement('button', { className: 'wua-btn ghost', disabled: busy || status === null, onClick: onLogout }, '退出登录') : null,
|
|
683
724
|
enabled ? react.createElement('button', { className: 'wua-btn ghost', disabled: busy || status === null, onClick: onDisableClick }, '禁用认证') : null,
|
|
725
|
+
enabled ? react.createElement('button', { className: 'wua-btn ghost', disabled: busy || status === null, onClick: onRevokeClick }, '吊销所有浏览器会话') : null,
|
|
684
726
|
),
|
|
685
727
|
]),
|
|
686
728
|
card('账号与密码', [], [
|
|
@@ -837,7 +879,8 @@ window.__ModuleLoader__.load({
|
|
|
837
879
|
]),
|
|
838
880
|
saveOpen ? react.createElement('div', { className: 'wua-modal-mask' },
|
|
839
881
|
react.createElement('div', { className: 'wua-modal' },
|
|
840
|
-
react.createElement('h4', { className: 'wua-card-title' },
|
|
882
|
+
react.createElement('h4', { className: 'wua-card-title' },
|
|
883
|
+
verifyAction === 'disable' ? '验证后禁用' : (verifyAction === 'revoke' ? '验证后吊销会话' : '验证后保存')),
|
|
841
884
|
react.createElement('p', { className: 'wua-hint', style: { borderTop: 'none', paddingTop: 0 } },
|
|
842
885
|
reauthValid
|
|
843
886
|
? '已通过 SSO 验证(2 分钟内有效),可直接继续。'
|
|
@@ -853,8 +896,10 @@ window.__ModuleLoader__.load({
|
|
|
853
896
|
react.createElement('button', { className: 'wua-btn', disabled: busy || reauthWaiting, onClick: () => {
|
|
854
897
|
if (!reauthValid && !savePw) { setSaveErr('请输入当前密码'); return; }
|
|
855
898
|
const auth = reauthValid ? {} : { current: savePw };
|
|
856
|
-
if (verifyAction === 'disable') doDisable(auth);
|
|
857
|
-
|
|
899
|
+
if (verifyAction === 'disable') doDisable(auth);
|
|
900
|
+
else if (verifyAction === 'revoke') doRevoke(auth);
|
|
901
|
+
else onSave(auth);
|
|
902
|
+
} }, busy ? '处理中…' : (verifyAction === 'disable' ? '验证并禁用' : (verifyAction === 'revoke' ? '验证并吊销' : '验证并保存'))),
|
|
858
903
|
react.createElement('button', { className: 'wua-btn ghost', disabled: busy, onClick: () => setSaveOpen(false) }, '取消'),
|
|
859
904
|
),
|
|
860
905
|
(status && status.oidc && status.oidc.enabled && status.oidc.bound)
|