dsh-webui-oauth 0.4.3
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/LICENSE +21 -0
- package/README.en.md +197 -0
- package/README.md +214 -0
- package/cordis.patch.yml +6 -0
- package/index.js +2627 -0
- package/lib/client.js +402 -0
- package/package.json +49 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: 'dsh-webui-auth',
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
7
|
+
let react = require('react');
|
|
8
|
+
|
|
9
|
+
const inject = ['slots'];
|
|
10
|
+
|
|
11
|
+
const CSS = [
|
|
12
|
+
'.wua-section { display: flex; flex-direction: column; gap: 14px; max-width: 560px; padding: 4px 2px; }',
|
|
13
|
+
'.wua-intro { margin: 0; font-size: 13px; line-height: 1.6; color: var(--dsw-alias-label-secondary, #888); }',
|
|
14
|
+
'.wua-row { display: flex; flex-direction: column; gap: 5px; }',
|
|
15
|
+
'.wua-label { font-size: 12px; color: var(--dsw-alias-label-secondary, #888); }',
|
|
16
|
+
'.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; }',
|
|
17
|
+
'.wua-input:focus { border-color: var(--dsw-alias-brand-primary, #4a7cf7); }',
|
|
18
|
+
'/* 深色模式下浏览器自动填充会把输入框刷成白色/黄色:inset 大阴影 + text-fill-color 回压为当前主题输入底色 */',
|
|
19
|
+
'.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; }',
|
|
20
|
+
'.wua-input::placeholder { color: var(--dsw-alias-label-secondary, #888); }',
|
|
21
|
+
'.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; }',
|
|
22
|
+
'.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; }',
|
|
23
|
+
'.wua-btn:hover:not(:disabled) { border-color: var(--dsw-alias-label-secondary, #555); }',
|
|
24
|
+
'.wua-btn:disabled { opacity: 0.55; cursor: default; }',
|
|
25
|
+
'.wua-btn.ghost { background: transparent; border-color: var(--dsw-alias-border-l1, #ccc); }',
|
|
26
|
+
'.wua-btnrow { display: flex; flex-wrap: wrap; gap: 8px; }',
|
|
27
|
+
'.wua-msg { margin: 0; font-size: 12px; color: var(--dsw-alias-state-success-primary, #1a7f37); }',
|
|
28
|
+
'.wua-err { margin: 0; font-size: 12px; color: var(--dsw-alias-state-error-primary, #d1242f); }',
|
|
29
|
+
'.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; }',
|
|
30
|
+
'.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; }',
|
|
31
|
+
'.wua-audit { display: flex; flex-direction: column; gap: 4px; }',
|
|
32
|
+
'.wua-audit-row { display: flex; flex-wrap: wrap; align-items: baseline; gap: 3px 10px; font-size: 12px; line-height: 1.6; }',
|
|
33
|
+
'.wua-audit-time { font-family: ui-monospace, SFMono-Regular, Consolas, monospace; color: var(--dsw-alias-label-secondary, #888); }',
|
|
34
|
+
'.wua-audit-event { color: var(--dsw-alias-label-primary, #222); }',
|
|
35
|
+
'.wua-audit-user { color: var(--dsw-alias-state-business-primary, #4176e6); }',
|
|
36
|
+
'.wua-audit-ip { color: var(--dsw-alias-label-tertiary, #999); font-family: ui-monospace, SFMono-Regular, Consolas, monospace; }',
|
|
37
|
+
].join('\n');
|
|
38
|
+
|
|
39
|
+
function apply(ctx) {
|
|
40
|
+
const styleEl = document.createElement('style');
|
|
41
|
+
styleEl.setAttribute('data-plugin', 'dsh-webui-auth');
|
|
42
|
+
styleEl.textContent = CSS;
|
|
43
|
+
document.head.appendChild(styleEl);
|
|
44
|
+
ctx.effect(() => () => {
|
|
45
|
+
if (styleEl.parentNode !== null) styleEl.parentNode.removeChild(styleEl);
|
|
46
|
+
}, 'dsh-webui-auth: styles');
|
|
47
|
+
|
|
48
|
+
// ---------------- 同源 HTTP API(会话 cookie 由浏览器自动携带) ----------------
|
|
49
|
+
function api(path, body) {
|
|
50
|
+
const opts = body === undefined
|
|
51
|
+
? {}
|
|
52
|
+
: { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) };
|
|
53
|
+
return fetch(path, opts).then((r) => {
|
|
54
|
+
if (r.status === 401) {
|
|
55
|
+
// 会话过期或无效:跳回首页,由 HTTP 层重定向到登录页
|
|
56
|
+
location.href = '/';
|
|
57
|
+
return { ok: false, error: 'unauthorized' };
|
|
58
|
+
}
|
|
59
|
+
return r.json();
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ---------------- 设置页(settings.section) ----------------
|
|
64
|
+
let loadedOnce = false;
|
|
65
|
+
function AuthSettings() {
|
|
66
|
+
const [status, setStatus] = react.useState(null);
|
|
67
|
+
const [username, setUsername] = react.useState('');
|
|
68
|
+
const [password, setPassword] = react.useState('');
|
|
69
|
+
const [confirm, setConfirm] = react.useState('');
|
|
70
|
+
const [current, setCurrent] = react.useState('');
|
|
71
|
+
const [ttl, setTtl] = react.useState('12');
|
|
72
|
+
const [msg, setMsg] = react.useState(null);
|
|
73
|
+
const [busy, setBusy] = react.useState(false);
|
|
74
|
+
const [audit, setAudit] = react.useState(null);
|
|
75
|
+
// OIDC 相关状态
|
|
76
|
+
const [oidcEnabled, setOidcEnabled] = react.useState(false);
|
|
77
|
+
const [oidcIssuer, setOidcIssuer] = react.useState('');
|
|
78
|
+
const [oidcClientId, setOidcClientId] = react.useState('');
|
|
79
|
+
const [oidcClientSecret, setOidcClientSecret] = react.useState('');
|
|
80
|
+
const [oidcScope, setOidcScope] = react.useState('openid profile email');
|
|
81
|
+
const [oidcRedirectBase, setOidcRedirectBase] = react.useState('');
|
|
82
|
+
const [oidcTrustBrowser, setOidcTrustBrowser] = react.useState(true);
|
|
83
|
+
|
|
84
|
+
react.useEffect(() => {
|
|
85
|
+
let alive = true;
|
|
86
|
+
const load = () => api('/dsh-webui-oauth/status').then((s) => {
|
|
87
|
+
if (!alive) return;
|
|
88
|
+
if (!s || s.error) return;
|
|
89
|
+
setStatus(s);
|
|
90
|
+
setTtl(String(typeof s.ttl === 'number' ? s.ttl : 12));
|
|
91
|
+
if (!loadedOnce) {
|
|
92
|
+
loadedOnce = true;
|
|
93
|
+
setUsername(s.username || '');
|
|
94
|
+
const o = (s && s.oidc) || null;
|
|
95
|
+
setOidcEnabled(!!(o && o.enabled));
|
|
96
|
+
setOidcIssuer((o && o.issuer) || '');
|
|
97
|
+
setOidcTrustBrowser(!o || o.trustBrowserOrigin !== false);
|
|
98
|
+
// clientSecret 不回传(敏感),clientId/scope/redirectBase 尽量回填
|
|
99
|
+
setOidcClientId((o && o.clientId) || '');
|
|
100
|
+
if (o && o.scope) setOidcScope(o.scope);
|
|
101
|
+
if (o && o.redirectBase) setOidcRedirectBase(o.redirectBase);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
load();
|
|
105
|
+
api('/dsh-webui-oauth/audit?limit=8').then((r) => {
|
|
106
|
+
if (!alive) return;
|
|
107
|
+
if (r && r.ok && Array.isArray(r.entries)) setAudit(r.entries);
|
|
108
|
+
}).catch(() => { /* 审计不可用时静默 */ });
|
|
109
|
+
return () => {
|
|
110
|
+
alive = false;
|
|
111
|
+
};
|
|
112
|
+
}, []);
|
|
113
|
+
|
|
114
|
+
// 与服务端 passwordStrength 一致的同步校验
|
|
115
|
+
const SPECIAL_CHARS = /[!@#$%^&*()_+\-=\[\]{}|;:,.<>?/~]/;
|
|
116
|
+
function checkPassword(p) {
|
|
117
|
+
if (!p || p.length < 8) return { ok: false, text: '密码至少需要 8 位' };
|
|
118
|
+
if (!/[a-z]/.test(p)) return { ok: false, text: '密码必须包含小写字母' };
|
|
119
|
+
if (!/[A-Z]/.test(p)) return { ok: false, text: '密码必须包含大写字母' };
|
|
120
|
+
if (!/[0-9]/.test(p)) return { ok: false, text: '密码必须包含数字' };
|
|
121
|
+
if (!SPECIAL_CHARS.test(p)) return { ok: false, text: '密码必须包含特殊符号(如 !@#$%^&*)' };
|
|
122
|
+
return { ok: true, text: '' };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const onSave = () => {
|
|
126
|
+
if (busy) return;
|
|
127
|
+
const name = username.trim();
|
|
128
|
+
// 与服务端 usernameError 一致的用户名约束
|
|
129
|
+
if (!/^[A-Za-z0-9_-]{3,32}$/.test(name)) {
|
|
130
|
+
setMsg({ kind: 'err', text: '用户名需为 3-32 位字母、数字、下划线或连字符' });
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (!(enabled && !password)) {
|
|
134
|
+
const st = checkPassword(password);
|
|
135
|
+
if (!st.ok) {
|
|
136
|
+
setMsg({ kind: 'err', text: st.text });
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (password !== confirm) {
|
|
141
|
+
setMsg({ kind: 'err', text: '两次输入的密码不一致' });
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (enabled && !current) {
|
|
145
|
+
setMsg({ kind: 'err', text: '修改认证信息需要输入当前密码' });
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
setBusy(true);
|
|
149
|
+
setMsg(null);
|
|
150
|
+
const oidcPayload = {
|
|
151
|
+
enabled: oidcEnabled,
|
|
152
|
+
issuer: oidcIssuer.trim(),
|
|
153
|
+
clientId: oidcClientId.trim(),
|
|
154
|
+
clientSecret: oidcClientSecret,
|
|
155
|
+
scope: oidcScope.trim(),
|
|
156
|
+
redirectBase: oidcRedirectBase.trim() || null,
|
|
157
|
+
trustBrowserOrigin: oidcTrustBrowser,
|
|
158
|
+
};
|
|
159
|
+
api('/dsh-webui-oauth/configure', { username: name, password, current, ttl: Number(ttl), oidc: oidcPayload }).then((r) => {
|
|
160
|
+
setBusy(false);
|
|
161
|
+
if (r && r.ok) {
|
|
162
|
+
if (enabled) {
|
|
163
|
+
setMsg({ kind: 'ok', text: '已保存。其他已登录的设备/浏览器会话已全部吊销,当前会话保持有效。' });
|
|
164
|
+
} else {
|
|
165
|
+
setMsg({ kind: 'ok', text: '已保存并启用认证,当前浏览器已登录。' });
|
|
166
|
+
}
|
|
167
|
+
setPassword('');
|
|
168
|
+
setConfirm('');
|
|
169
|
+
setCurrent('');
|
|
170
|
+
api('/dsh-webui-oauth/status').then((s) => {
|
|
171
|
+
if (s && !s.error) setStatus(s);
|
|
172
|
+
});
|
|
173
|
+
} else if (r && r.error === 'current-invalid') {
|
|
174
|
+
setMsg({ kind: 'err', text: '当前密码不正确' });
|
|
175
|
+
} else if (r && r.error === 'weak-password') {
|
|
176
|
+
setMsg({ kind: 'err', text: '密码强度不足:至少 8 位,需包含大小写字母、数字和特殊符号' });
|
|
177
|
+
} else if (r && r.error === 'username-invalid') {
|
|
178
|
+
setMsg({ kind: 'err', text: '用户名需为 3-32 位字母、数字、下划线或连字符' });
|
|
179
|
+
} else if (r && r.error === 'legacy-hash') {
|
|
180
|
+
setMsg({ kind: 'err', text: '当前凭据为旧版哈希(0.2.0 起仅支持 scrypt),必须填写新密码才能保存' });
|
|
181
|
+
} else if (r && typeof r.error === 'string') {
|
|
182
|
+
// 服务端 500/未知错误:展示真实信息便于排查
|
|
183
|
+
setMsg({ kind: 'err', text: '保存失败:' + r.error });
|
|
184
|
+
} else {
|
|
185
|
+
setMsg({ kind: 'err', text: '保存失败,请检查输入' });
|
|
186
|
+
}
|
|
187
|
+
}).catch(() => {
|
|
188
|
+
setBusy(false);
|
|
189
|
+
setMsg({ kind: 'err', text: '保存失败:无法连接认证服务' });
|
|
190
|
+
});
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const onDisable = () => {
|
|
194
|
+
if (busy) return;
|
|
195
|
+
if (!current) {
|
|
196
|
+
setMsg({ kind: 'err', text: '请输入当前密码以禁用认证' });
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
setBusy(true);
|
|
200
|
+
setMsg(null);
|
|
201
|
+
api('/dsh-webui-oauth/disable', { current }).then((r) => {
|
|
202
|
+
setBusy(false);
|
|
203
|
+
if (r && r.ok) {
|
|
204
|
+
setMsg({ kind: 'ok', text: '已禁用认证,所有会话已注销。刷新页面后不再要求登录。' });
|
|
205
|
+
setCurrent('');
|
|
206
|
+
api('/dsh-webui-oauth/status').then((s) => {
|
|
207
|
+
if (s && !s.error) setStatus(s);
|
|
208
|
+
}).catch(() => {});
|
|
209
|
+
} else {
|
|
210
|
+
setMsg({ kind: 'err', text: '当前密码不正确' });
|
|
211
|
+
}
|
|
212
|
+
}).catch(() => {
|
|
213
|
+
setBusy(false);
|
|
214
|
+
setMsg({ kind: 'err', text: '操作失败:无法连接认证服务' });
|
|
215
|
+
});
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const onLogout = () => {
|
|
219
|
+
if (busy) return;
|
|
220
|
+
setBusy(true);
|
|
221
|
+
api('/dsh-webui-oauth/logout', {}).then(() => {
|
|
222
|
+
location.href = '/';
|
|
223
|
+
}).catch(() => {
|
|
224
|
+
setBusy(false);
|
|
225
|
+
setMsg({ kind: 'err', text: '退出失败:无法连接认证服务' });
|
|
226
|
+
});
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const enabled = Boolean(status && status.enabled);
|
|
230
|
+
const intro = status === null
|
|
231
|
+
? '正在读取认证状态…'
|
|
232
|
+
: (enabled
|
|
233
|
+
? '认证已在 HTTP 层强制执行,当前账号:' + status.username + '。未登录的浏览器无法加载 WebUI 资源、调用接口或建立实时连接。'
|
|
234
|
+
: '当前未启用认证。创建账号密码后,访问 WebUI 将强制要求登录。');
|
|
235
|
+
|
|
236
|
+
const patchWarn = (status && status.gate && !status.gate.ok)
|
|
237
|
+
? ('路由闸门不完整:' + (status.gate.problems && status.gate.problems.join('; ') || '未知原因')
|
|
238
|
+
+ '。认证无法启用,直到 /api 与 WebSocket 路由可被保护。')
|
|
239
|
+
: null;
|
|
240
|
+
const persistNote = (status && status.sessionsPersisted === false)
|
|
241
|
+
? '会话持久化写入失败(磁盘权限?):dsh 重启后已登录设备需要重新登录。'
|
|
242
|
+
: null;
|
|
243
|
+
|
|
244
|
+
// 审计事件显示名
|
|
245
|
+
const EVENT_LABELS = {
|
|
246
|
+
login_success: '登录成功',
|
|
247
|
+
login_failure: '登录失败',
|
|
248
|
+
login_rate_limited: '登录被限流',
|
|
249
|
+
setup_success: '初始化成功',
|
|
250
|
+
setup_failure: '初始化失败',
|
|
251
|
+
configure_success: '修改凭据',
|
|
252
|
+
configure_failure: '修改凭据失败',
|
|
253
|
+
disable_success: '禁用认证',
|
|
254
|
+
disable_failure: '禁用认证失败',
|
|
255
|
+
logout: '退出登录',
|
|
256
|
+
oidc_login_success: 'SSO 登录成功',
|
|
257
|
+
oidc_login_failure: 'SSO 登录失败',
|
|
258
|
+
oidc_logout: 'SSO 退出',
|
|
259
|
+
oidc_discovery_failure: 'OIDC 发现失败',
|
|
260
|
+
};
|
|
261
|
+
function fmtTime(ts) {
|
|
262
|
+
if (!ts) return '?';
|
|
263
|
+
const d = new Date(ts);
|
|
264
|
+
if (Number.isNaN(d.getTime())) return String(ts).slice(0, 19).replace('T', ' ');
|
|
265
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
266
|
+
return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes());
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return react.createElement('div', { className: 'wua-section' },
|
|
270
|
+
react.createElement('p', { className: 'wua-intro' }, intro),
|
|
271
|
+
patchWarn ? react.createElement('p', { className: 'wua-warn' }, patchWarn) : null,
|
|
272
|
+
persistNote ? react.createElement('p', { className: 'wua-warn' }, persistNote) : null,
|
|
273
|
+
react.createElement('div', { className: 'wua-row' },
|
|
274
|
+
react.createElement('label', { className: 'wua-label' }, '用户名(3-32 位字母、数字、下划线或连字符)'),
|
|
275
|
+
react.createElement('input', {
|
|
276
|
+
className: 'wua-input',
|
|
277
|
+
type: 'text',
|
|
278
|
+
value: username,
|
|
279
|
+
spellCheck: false,
|
|
280
|
+
onChange: (e) => setUsername(e.target.value),
|
|
281
|
+
}),
|
|
282
|
+
),
|
|
283
|
+
react.createElement('div', { className: 'wua-row' },
|
|
284
|
+
react.createElement('label', { className: 'wua-label' }, enabled ? '新密码(留空表示不修改)' : '密码(至少 8 位,含大小写、数字、特殊符号)'),
|
|
285
|
+
react.createElement('input', {
|
|
286
|
+
className: 'wua-input',
|
|
287
|
+
type: 'password',
|
|
288
|
+
value: password,
|
|
289
|
+
onChange: (e) => setPassword(e.target.value),
|
|
290
|
+
}),
|
|
291
|
+
),
|
|
292
|
+
react.createElement('div', { className: 'wua-row' },
|
|
293
|
+
react.createElement('label', { className: 'wua-label' }, '确认密码'),
|
|
294
|
+
react.createElement('input', {
|
|
295
|
+
className: 'wua-input',
|
|
296
|
+
type: 'password',
|
|
297
|
+
value: confirm,
|
|
298
|
+
onChange: (e) => setConfirm(e.target.value),
|
|
299
|
+
}),
|
|
300
|
+
),
|
|
301
|
+
enabled ? react.createElement('div', { className: 'wua-row' },
|
|
302
|
+
react.createElement('label', { className: 'wua-label' }, '当前密码(修改或禁用认证需验证)'),
|
|
303
|
+
react.createElement('input', {
|
|
304
|
+
className: 'wua-input',
|
|
305
|
+
type: 'password',
|
|
306
|
+
value: current,
|
|
307
|
+
onChange: (e) => setCurrent(e.target.value),
|
|
308
|
+
}),
|
|
309
|
+
) : null,
|
|
310
|
+
react.createElement('div', { className: 'wua-row' },
|
|
311
|
+
react.createElement('label', { className: 'wua-label' }, '会话有效期(登录后免登录时长)'),
|
|
312
|
+
react.createElement('select', {
|
|
313
|
+
className: 'wua-select',
|
|
314
|
+
value: ttl,
|
|
315
|
+
disabled: busy || status === null,
|
|
316
|
+
onChange: (e) => setTtl(e.target.value),
|
|
317
|
+
},
|
|
318
|
+
react.createElement('option', { key: '0', value: '0' }, '浏览器会话:关闭浏览器后失效'),
|
|
319
|
+
react.createElement('option', { key: '1', value: '1' }, '1 小时'),
|
|
320
|
+
react.createElement('option', { key: '12', value: '12' }, '12 小时(默认)'),
|
|
321
|
+
react.createElement('option', { key: '24', value: '24' }, '1 天'),
|
|
322
|
+
react.createElement('option', { key: '72', value: '72' }, '3 天'),
|
|
323
|
+
),
|
|
324
|
+
),
|
|
325
|
+
msg ? react.createElement('div', { className: msg.kind === 'ok' ? 'wua-msg' : 'wua-err' }, msg.text) : null,
|
|
326
|
+
react.createElement('div', { className: 'wua-row' },
|
|
327
|
+
react.createElement('label', { className: 'wua-label' }, 'OIDC 单点登录'),
|
|
328
|
+
react.createElement('label', { className: 'wua-label' },
|
|
329
|
+
react.createElement('input', {
|
|
330
|
+
type: 'checkbox',
|
|
331
|
+
checked: oidcEnabled,
|
|
332
|
+
onChange: (e) => setOidcEnabled(e.target.checked),
|
|
333
|
+
}),
|
|
334
|
+
' 启用(登录页显示「SSO 单点登录」按钮)'),
|
|
335
|
+
),
|
|
336
|
+
oidcEnabled ? react.createElement('div', { className: 'wua-section' },
|
|
337
|
+
react.createElement('div', { className: 'wua-row' },
|
|
338
|
+
react.createElement('label', { className: 'wua-label' }, 'Issuer(IdP 地址,须为 https)'),
|
|
339
|
+
react.createElement('input', { className: 'wua-input', type: 'text', value: oidcIssuer, spellCheck: false, onChange: (e) => setOidcIssuer(e.target.value) }),
|
|
340
|
+
),
|
|
341
|
+
react.createElement('div', { className: 'wua-row' },
|
|
342
|
+
react.createElement('label', { className: 'wua-label' }, 'Client ID'),
|
|
343
|
+
react.createElement('input', { className: 'wua-input', type: 'text', value: oidcClientId, spellCheck: false, onChange: (e) => setOidcClientId(e.target.value) }),
|
|
344
|
+
),
|
|
345
|
+
react.createElement('div', { className: 'wua-row' },
|
|
346
|
+
react.createElement('label', { className: 'wua-label' }, 'Client Secret(留空保留原值;机密客户端必填)'),
|
|
347
|
+
react.createElement('input', { className: 'wua-input', type: 'password', value: oidcClientSecret, autoComplete: 'new-password', onChange: (e) => setOidcClientSecret(e.target.value) }),
|
|
348
|
+
),
|
|
349
|
+
react.createElement('div', { className: 'wua-row' },
|
|
350
|
+
react.createElement('label', { className: 'wua-label' }, 'Scope(空格分隔)'),
|
|
351
|
+
react.createElement('input', { className: 'wua-input', type: 'text', value: oidcScope, spellCheck: false, onChange: (e) => setOidcScope(e.target.value) }),
|
|
352
|
+
),
|
|
353
|
+
react.createElement('div', { className: 'wua-row' },
|
|
354
|
+
react.createElement('label', { className: 'wua-label' }, 'Redirect Base(可选;反代重写 Host 时必填,如 https://dsh.example.com)'),
|
|
355
|
+
react.createElement('input', { className: 'wua-input', type: 'text', value: oidcRedirectBase, spellCheck: false, onChange: (e) => setOidcRedirectBase(e.target.value) }),
|
|
356
|
+
),
|
|
357
|
+
react.createElement('div', { className: 'wua-row' },
|
|
358
|
+
react.createElement('label', { className: 'wua-label' },
|
|
359
|
+
react.createElement('input', { type: 'checkbox', checked: oidcTrustBrowser, onChange: (e) => setOidcTrustBrowser(e.target.checked) }),
|
|
360
|
+
' 信任浏览器 origin(默认开启;关闭时仅用上方 Redirect Base)'),
|
|
361
|
+
),
|
|
362
|
+
) : null,
|
|
363
|
+
react.createElement('div', { className: 'wua-btnrow' },
|
|
364
|
+
react.createElement('button', { className: 'wua-btn', disabled: busy || status === null, onClick: onSave },
|
|
365
|
+
busy ? '保存中…' : '保存账号密码'),
|
|
366
|
+
enabled ? react.createElement('button', { className: 'wua-btn ghost', disabled: busy || status === null, onClick: onDisable }, '禁用认证') : null,
|
|
367
|
+
enabled ? react.createElement('button', { className: 'wua-btn ghost', disabled: busy || status === null, onClick: onLogout }, '退出登录') : null,
|
|
368
|
+
),
|
|
369
|
+
react.createElement('div', { className: 'wua-row' },
|
|
370
|
+
react.createElement('label', { className: 'wua-label' }, '最近登录记录(完整审计日志可用 node index.js audit 查看)'),
|
|
371
|
+
audit === null
|
|
372
|
+
? react.createElement('p', { className: 'wua-intro' }, '正在读取…')
|
|
373
|
+
: (audit.length === 0
|
|
374
|
+
? react.createElement('p', { className: 'wua-intro' }, '暂无审计记录')
|
|
375
|
+
: react.createElement('div', { className: 'wua-audit' },
|
|
376
|
+
audit.map((entry, i) => react.createElement('div', { key: i, className: 'wua-audit-row' },
|
|
377
|
+
react.createElement('span', { className: 'wua-audit-time' }, fmtTime(entry.ts)),
|
|
378
|
+
react.createElement('span', { className: 'wua-audit-event' }, EVENT_LABELS[entry.event] || entry.event),
|
|
379
|
+
entry.username ? react.createElement('span', { className: 'wua-audit-user' }, entry.username) : null,
|
|
380
|
+
entry.ip ? react.createElement('span', { className: 'wua-audit-ip' }, entry.ip) : null,
|
|
381
|
+
)),
|
|
382
|
+
)),
|
|
383
|
+
),
|
|
384
|
+
react.createElement('p', { className: 'wua-hint' },
|
|
385
|
+
'密码规则:至少 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 作为本地会话用户名,并沿用相同的免登录时长机制。'),
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// ---------------- 注册 ----------------
|
|
390
|
+
ctx.slots.inject('settings.section', () => ctx.slots.register({
|
|
391
|
+
name: 'settings.section',
|
|
392
|
+
id: 'webui-auth',
|
|
393
|
+
order: 30,
|
|
394
|
+
label: '身份认证',
|
|
395
|
+
}, AuthSettings));
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
exports.apply = apply;
|
|
399
|
+
exports.inject = inject;
|
|
400
|
+
return module.exports;
|
|
401
|
+
}
|
|
402
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-webui-oauth",
|
|
3
|
+
"version": "0.4.3",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "index.js",
|
|
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/displace-original.test.mjs"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./index.js",
|
|
11
|
+
"./client": {
|
|
12
|
+
"default": "./lib/client.js"
|
|
13
|
+
},
|
|
14
|
+
"./package.json": "./package.json"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"index.js",
|
|
18
|
+
"lib/client.js",
|
|
19
|
+
"cordis.patch.yml",
|
|
20
|
+
"README.md",
|
|
21
|
+
"README.en.md"
|
|
22
|
+
],
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"keywords": [
|
|
25
|
+
"deepseek-harness",
|
|
26
|
+
"dsh",
|
|
27
|
+
"auth",
|
|
28
|
+
"login",
|
|
29
|
+
"gate"
|
|
30
|
+
],
|
|
31
|
+
"dsh": {
|
|
32
|
+
"bundle": {
|
|
33
|
+
"patch": "./cordis.patch.yml"
|
|
34
|
+
},
|
|
35
|
+
"client": {
|
|
36
|
+
"platform": "web",
|
|
37
|
+
"inject": [
|
|
38
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
39
|
+
"@deepseek-ai/dsh-client-ui-settings",
|
|
40
|
+
"@deepseek-ai/dsh-client-connection"
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"description": "WebUI authentication plugin for DeepSeek Harness: password or OIDC/SSO login enforced at the HTTP/transport layer, with server-side sessions, HttpOnly cookies and audit logging. Zero dependencies.",
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "git+https://github.com/WU-HINS/dsh-webui-oauth.git"
|
|
48
|
+
}
|
|
49
|
+
}
|