@dsh-plus/access-gate 0.1.1 → 0.1.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/lib/client.js +553 -449
- package/package.json +4 -2
- package/src/client/card.tsx +147 -221
- package/src/client/client.ts +5 -84
- package/src/client/i18n.ts +7 -24
- package/src/client/styles.ts +13 -53
- package/src/client/scope.ts +0 -53
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dsh-plus/access-gate",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "dsh-plus service+ui plugin: dsh web 全量访问围栏——本机直连永久放行,远程按白名单 IP(XFF 还原)或 token(登录页 + cookie)放行",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -31,7 +31,9 @@
|
|
|
31
31
|
"platform": "web"
|
|
32
32
|
}
|
|
33
33
|
},
|
|
34
|
-
"dependencies": {
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@dsh-plus/shared": "0.1.5"
|
|
36
|
+
},
|
|
35
37
|
"peerDependencies": {
|
|
36
38
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
37
39
|
"@deepseek-ai/dsh-home-paths": "^0.1.0-rc.8",
|
package/src/client/card.tsx
CHANGED
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 「访问控制」配置卡片:注册进 settings.plugin.item 插槽(官方插件配置页)。
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* 外壳与基础控件走 @dsh-plus/shared/client 套件(CardChrome/TextField/CheckRow),
|
|
4
|
+
* 本文件保留业务字段(token/白名单 textarea/诊断面板)与保存逻辑。
|
|
5
|
+
* 配置读写走官方 settingsScope 传输:value 为 schema 解析后的脱敏视图
|
|
6
|
+
* (token 不出现,tokenConfigured 经 describe 的 secrets 探测),
|
|
6
7
|
* 保存经 settings.update 深合并(空 token 剔除 = 保持不变)。
|
|
7
8
|
* 卡片顶部「当前页面诊断」读 /dsh-plus/gate/status(本页放行原因/客户端 IP)。
|
|
8
9
|
* @module access-gate/client/card
|
|
9
10
|
*/
|
|
10
|
-
import { type ReactElement, useEffect, useMemo, useState, useSyncExternalStore } from 'react'
|
|
11
11
|
|
|
12
|
+
import {
|
|
13
|
+
CardChrome,
|
|
14
|
+
type CardStatusState,
|
|
15
|
+
CheckRow,
|
|
16
|
+
IDLE_STATUS,
|
|
17
|
+
type Scope,
|
|
18
|
+
type SettingsApi,
|
|
19
|
+
TextField,
|
|
20
|
+
} from '@dsh-plus/shared/client'
|
|
21
|
+
import { type ReactElement, useEffect, useMemo, useState, useSyncExternalStore } from 'react'
|
|
12
22
|
import { SETTINGS_NS } from '../ns.ts'
|
|
13
|
-
import type { Scope, SettingsApi } from './scope.ts'
|
|
14
23
|
|
|
15
24
|
export interface CardProps {
|
|
16
25
|
t(key: string): string
|
|
@@ -38,11 +47,6 @@ interface Draft {
|
|
|
38
47
|
loginCooldownMs: string
|
|
39
48
|
}
|
|
40
49
|
|
|
41
|
-
interface Status {
|
|
42
|
-
kind: 'idle' | 'ok' | 'error'
|
|
43
|
-
text: string
|
|
44
|
-
}
|
|
45
|
-
|
|
46
50
|
interface Diag {
|
|
47
51
|
enabled: boolean
|
|
48
52
|
verdict: string
|
|
@@ -53,8 +57,6 @@ interface Diag {
|
|
|
53
57
|
allowedCount: number
|
|
54
58
|
}
|
|
55
59
|
|
|
56
|
-
const IDLE_STATUS: Status = { kind: 'idle', text: '' }
|
|
57
|
-
|
|
58
60
|
function draftFromValue(value: ConfigValue): Draft {
|
|
59
61
|
return {
|
|
60
62
|
enabled: value.enabled,
|
|
@@ -111,7 +113,7 @@ export function AccessGateCard(props: CardProps): ReactElement | null {
|
|
|
111
113
|
const [tokenConfigured, setTokenConfigured] = useState(false)
|
|
112
114
|
const [diag, setDiag] = useState<Diag | null>(null)
|
|
113
115
|
const [saving, setSaving] = useState(false)
|
|
114
|
-
const [status, setStatus] = useState<
|
|
116
|
+
const [status, setStatus] = useState<CardStatusState>(IDLE_STATUS)
|
|
115
117
|
|
|
116
118
|
// 首次拿到解析值后播种草稿;后续 Host 更新不覆盖在途编辑(与官方 staged 表单一致)。
|
|
117
119
|
useEffect(() => {
|
|
@@ -213,220 +215,144 @@ export function AccessGateCard(props: CardProps): ReactElement | null {
|
|
|
213
215
|
|
|
214
216
|
const disabled = !snapshot.writable
|
|
215
217
|
return (
|
|
216
|
-
<
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
{
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
218
|
+
<CardChrome
|
|
219
|
+
prefix="dag"
|
|
220
|
+
title={t('title')}
|
|
221
|
+
description={t('description')}
|
|
222
|
+
open={open}
|
|
223
|
+
onToggle={setOpen}
|
|
224
|
+
statusBadge={{ text: t(draft.enabled ? 'enabledOn' : 'enabledOff'), on: draft.enabled }}
|
|
225
|
+
dirty={dirty}
|
|
226
|
+
dirtyLabel={t('unsaved')}
|
|
227
|
+
readOnlyNotice={disabled ? t('readOnly') : undefined}
|
|
228
|
+
status={status}
|
|
229
|
+
actions={[
|
|
230
|
+
{
|
|
231
|
+
key: 'discard',
|
|
232
|
+
label: t('discard'),
|
|
233
|
+
disabled: !dirty || saving,
|
|
234
|
+
onClick: () => {
|
|
235
|
+
setDraft(draftFromValue(value))
|
|
236
|
+
setStatus(IDLE_STATUS)
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
key: 'save',
|
|
241
|
+
label: t(saving ? 'saving' : 'save'),
|
|
242
|
+
variant: 'primary',
|
|
243
|
+
disabled: !dirty || invalid || saving || disabled,
|
|
244
|
+
onClick: onSave,
|
|
245
|
+
},
|
|
246
|
+
]}
|
|
247
|
+
>
|
|
248
|
+
{failClosed ? <p className="dag-warn">{t('warnFailClosed')}</p> : null}
|
|
249
|
+
<CheckRow
|
|
250
|
+
prefix="dag"
|
|
251
|
+
id="dag-enabled"
|
|
252
|
+
label={t('enabledHint')}
|
|
253
|
+
checked={draft.enabled}
|
|
254
|
+
disabled={disabled}
|
|
255
|
+
onEdit={(v) => edit('enabled', v)}
|
|
256
|
+
/>
|
|
257
|
+
<TextField
|
|
258
|
+
prefix="dag"
|
|
259
|
+
id="dag-token"
|
|
260
|
+
label={t('token')}
|
|
261
|
+
hint={t('tokenHint')}
|
|
262
|
+
value={draft.token}
|
|
263
|
+
password
|
|
264
|
+
disabled={disabled}
|
|
265
|
+
badge={{ text: t(tokenConfigured ? 'tokenSet' : 'tokenUnset'), set: tokenConfigured }}
|
|
266
|
+
onEdit={(v) => edit('token', v)}
|
|
267
|
+
/>
|
|
268
|
+
<div className="dag-field">
|
|
269
|
+
<div className="dag-head">
|
|
270
|
+
<label className="dag-label" htmlFor="dag-allowed">
|
|
271
|
+
{t('allowedIps')}
|
|
272
|
+
</label>
|
|
273
|
+
</div>
|
|
274
|
+
<textarea
|
|
275
|
+
id="dag-allowed"
|
|
276
|
+
className="dag-textarea"
|
|
277
|
+
rows={Math.min(8, Math.max(3, draft.allowedIpsText.split('\n').length))}
|
|
278
|
+
value={draft.allowedIpsText}
|
|
279
|
+
disabled={disabled}
|
|
280
|
+
onChange={(event) => edit('allowedIpsText', event.target.value)}
|
|
281
|
+
/>
|
|
282
|
+
<p className="dag-hint">{t('allowedIpsHint')}</p>
|
|
283
|
+
</div>
|
|
284
|
+
<CheckRow
|
|
285
|
+
prefix="dag"
|
|
286
|
+
id="dag-xff"
|
|
287
|
+
label={t('trustForwardedForHint')}
|
|
288
|
+
checked={draft.trustForwardedFor}
|
|
289
|
+
disabled={disabled}
|
|
290
|
+
onEdit={(v) => edit('trustForwardedFor', v)}
|
|
291
|
+
/>
|
|
292
|
+
<TextField
|
|
293
|
+
prefix="dag"
|
|
294
|
+
id="dag-cookie"
|
|
295
|
+
label={t('cookieMaxAgeHours')}
|
|
296
|
+
hint={t('cookieMaxAgeHoursHint')}
|
|
297
|
+
value={draft.cookieMaxAgeHours}
|
|
298
|
+
numeric
|
|
299
|
+
disabled={disabled}
|
|
300
|
+
invalid={!isPositiveInt(draft.cookieMaxAgeHours)}
|
|
301
|
+
invalidLabel={t('invalidNumber')}
|
|
302
|
+
onEdit={(v) => edit('cookieMaxAgeHours', v)}
|
|
303
|
+
/>
|
|
304
|
+
<TextField
|
|
305
|
+
prefix="dag"
|
|
306
|
+
id="dag-faillimit"
|
|
307
|
+
label={t('loginFailLimit')}
|
|
308
|
+
hint={t('loginFailLimitHint')}
|
|
309
|
+
value={draft.loginFailLimit}
|
|
310
|
+
numeric
|
|
311
|
+
disabled={disabled}
|
|
312
|
+
invalid={!isPositiveInt(draft.loginFailLimit)}
|
|
313
|
+
invalidLabel={t('invalidNumber')}
|
|
314
|
+
onEdit={(v) => edit('loginFailLimit', v)}
|
|
315
|
+
/>
|
|
316
|
+
<TextField
|
|
317
|
+
prefix="dag"
|
|
318
|
+
id="dag-cooldown"
|
|
319
|
+
label={t('loginCooldownMs')}
|
|
320
|
+
hint={t('loginCooldownMsHint')}
|
|
321
|
+
value={draft.loginCooldownMs}
|
|
322
|
+
numeric
|
|
323
|
+
disabled={disabled}
|
|
324
|
+
invalid={!isPositiveInt(draft.loginCooldownMs)}
|
|
325
|
+
invalidLabel={t('invalidNumber')}
|
|
326
|
+
onEdit={(v) => edit('loginCooldownMs', v)}
|
|
327
|
+
/>
|
|
328
|
+
{diag !== null ? (
|
|
329
|
+
<div className={`dag-diag${diag.invalidEntries.length > 0 ? ' dag-diagWarn' : ''}`}>
|
|
330
|
+
<p className="dag-diagTitle">{t('diagTitle')}</p>
|
|
331
|
+
<div className="dag-diagRow">
|
|
332
|
+
<span className="dag-diagKey">{t('diagVerdict')}</span>
|
|
333
|
+
<span className="dag-diagVal">{verdictLabel(t, diag)}</span>
|
|
330
334
|
</div>
|
|
331
|
-
<div className="dag-
|
|
332
|
-
<
|
|
333
|
-
|
|
334
|
-
{t('loginFailLimit')}
|
|
335
|
-
</label>
|
|
336
|
-
</div>
|
|
337
|
-
<input
|
|
338
|
-
id="dag-faillimit"
|
|
339
|
-
className={`dag-input${isPositiveInt(draft.loginFailLimit) ? '' : ' dag-inputInvalid'}`}
|
|
340
|
-
type="text"
|
|
341
|
-
inputMode="numeric"
|
|
342
|
-
value={draft.loginFailLimit}
|
|
343
|
-
disabled={disabled}
|
|
344
|
-
aria-invalid={!isPositiveInt(draft.loginFailLimit) || undefined}
|
|
345
|
-
onChange={(event) => edit('loginFailLimit', event.target.value)}
|
|
346
|
-
/>
|
|
347
|
-
<p className={isPositiveInt(draft.loginFailLimit) ? 'dag-hint' : 'dag-invalid'}>
|
|
348
|
-
{isPositiveInt(draft.loginFailLimit) ? t('loginFailLimitHint') : t('invalidNumber')}
|
|
349
|
-
</p>
|
|
335
|
+
<div className="dag-diagRow">
|
|
336
|
+
<span className="dag-diagKey">{t('diagClientIp')}</span>
|
|
337
|
+
<span className="dag-diagVal">{diag.clientIp ?? '—'}</span>
|
|
350
338
|
</div>
|
|
351
|
-
<div className="dag-
|
|
352
|
-
<
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
</div>
|
|
357
|
-
<input
|
|
358
|
-
id="dag-cooldown"
|
|
359
|
-
className={`dag-input${isPositiveInt(draft.loginCooldownMs) ? '' : ' dag-inputInvalid'}`}
|
|
360
|
-
type="text"
|
|
361
|
-
inputMode="numeric"
|
|
362
|
-
value={draft.loginCooldownMs}
|
|
363
|
-
disabled={disabled}
|
|
364
|
-
aria-invalid={!isPositiveInt(draft.loginCooldownMs) || undefined}
|
|
365
|
-
onChange={(event) => edit('loginCooldownMs', event.target.value)}
|
|
366
|
-
/>
|
|
367
|
-
<p className={isPositiveInt(draft.loginCooldownMs) ? 'dag-hint' : 'dag-invalid'}>
|
|
368
|
-
{isPositiveInt(draft.loginCooldownMs) ? t('loginCooldownMsHint') : t('invalidNumber')}
|
|
369
|
-
</p>
|
|
339
|
+
<div className="dag-diagRow">
|
|
340
|
+
<span className="dag-diagKey">{t('diagReason')}</span>
|
|
341
|
+
<span className="dag-diagVal">
|
|
342
|
+
{diag.reason !== null ? t(REASON_KEYS[diag.reason] ?? diag.reason) : '—'}
|
|
343
|
+
</span>
|
|
370
344
|
</div>
|
|
371
|
-
{diag
|
|
372
|
-
<div className=
|
|
373
|
-
<
|
|
374
|
-
<
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
</
|
|
378
|
-
<div className="dag-diagRow">
|
|
379
|
-
<span className="dag-diagKey">{t('diagClientIp')}</span>
|
|
380
|
-
<span className="dag-diagVal">{diag.clientIp ?? '—'}</span>
|
|
381
|
-
</div>
|
|
382
|
-
<div className="dag-diagRow">
|
|
383
|
-
<span className="dag-diagKey">{t('diagReason')}</span>
|
|
384
|
-
<span className="dag-diagVal">
|
|
385
|
-
{diag.reason !== null ? t(REASON_KEYS[diag.reason] ?? diag.reason) : '—'}
|
|
386
|
-
</span>
|
|
387
|
-
</div>
|
|
388
|
-
{diag.invalidEntries.length > 0 ? (
|
|
389
|
-
<div className="dag-diagRow">
|
|
390
|
-
<span className="dag-diagKey">!</span>
|
|
391
|
-
<span className="dag-diagVal">
|
|
392
|
-
{t('diagInvalid')}
|
|
393
|
-
{diag.invalidEntries.join('、')}
|
|
394
|
-
</span>
|
|
395
|
-
</div>
|
|
396
|
-
) : null}
|
|
345
|
+
{diag.invalidEntries.length > 0 ? (
|
|
346
|
+
<div className="dag-diagRow">
|
|
347
|
+
<span className="dag-diagKey">!</span>
|
|
348
|
+
<span className="dag-diagVal">
|
|
349
|
+
{t('diagInvalid')}
|
|
350
|
+
{diag.invalidEntries.join('、')}
|
|
351
|
+
</span>
|
|
397
352
|
</div>
|
|
398
353
|
) : null}
|
|
399
|
-
<div className="dag-footer">
|
|
400
|
-
{status.kind !== 'idle' ? (
|
|
401
|
-
<p
|
|
402
|
-
className={`dag-status${status.kind === 'error' ? ' dag-statusError' : ''}`}
|
|
403
|
-
role="status"
|
|
404
|
-
>
|
|
405
|
-
{status.text}
|
|
406
|
-
</p>
|
|
407
|
-
) : null}
|
|
408
|
-
<button
|
|
409
|
-
type="button"
|
|
410
|
-
className="dag-btn dag-btnGhost"
|
|
411
|
-
disabled={!dirty || saving}
|
|
412
|
-
onClick={() => {
|
|
413
|
-
setDraft(draftFromValue(value))
|
|
414
|
-
setStatus(IDLE_STATUS)
|
|
415
|
-
}}
|
|
416
|
-
>
|
|
417
|
-
{t('discard')}
|
|
418
|
-
</button>
|
|
419
|
-
<button
|
|
420
|
-
type="button"
|
|
421
|
-
className="dag-btn dag-btnPrimary"
|
|
422
|
-
disabled={!dirty || invalid || saving || disabled}
|
|
423
|
-
onClick={onSave}
|
|
424
|
-
>
|
|
425
|
-
{t(saving ? 'saving' : 'save')}
|
|
426
|
-
</button>
|
|
427
|
-
</div>
|
|
428
354
|
</div>
|
|
429
355
|
) : null}
|
|
430
|
-
</
|
|
356
|
+
</CardChrome>
|
|
431
357
|
)
|
|
432
358
|
}
|
package/src/client/client.ts
CHANGED
|
@@ -4,22 +4,19 @@
|
|
|
4
4
|
* (包装见 tsdown.config.ts);样式沿用官方 data-plugin-css 约定,HMR 据此卸载。
|
|
5
5
|
*
|
|
6
6
|
* 类型说明:浏览器半只用到 slots/locale/connection/remote 的很窄一面,
|
|
7
|
-
*
|
|
8
|
-
* client 依赖树;运行时契约以官方 dsh-client-ui-settings-plugins 的
|
|
9
|
-
* settings.plugin.item 插槽与 dsh-client-connection 的 settings RPC 面为准。
|
|
7
|
+
* scope 与基础控件走 @dsh-plus/shared/client 套件(收编自本插件原实现)。
|
|
10
8
|
* rc7 起该插槽为 keyed 槽位:key 必须是卡片编辑的 settings 命名空间
|
|
11
9
|
* (即服务端 settingsNamespace() 注册的同一字面量,见 ../ns.ts),
|
|
12
10
|
* 官方配置页按此 key 与 Host 已注册命名空间配对分发。
|
|
13
11
|
* 配置读写经官方 settings RPC 直连(rc7 起第三方命名空间全量开放;
|
|
14
|
-
* 不复用 settingsScope 服务——非 loopback
|
|
12
|
+
* 不复用 settingsScope 服务——非 loopback 页面下它无数据)。
|
|
15
13
|
* @module @dsh-plus/access-gate/client
|
|
16
14
|
*/
|
|
17
15
|
import type { Context } from '@deepseek-ai/cordis'
|
|
18
|
-
|
|
16
|
+
import { createApiScope } from '@dsh-plus/shared/client'
|
|
19
17
|
import { SETTINGS_NS } from '../ns.ts'
|
|
20
18
|
import { AccessGateCard } from './card.tsx'
|
|
21
19
|
import { en, NS, zh } from './i18n.ts'
|
|
22
|
-
import type { Scope, ScopeSnapshot, SettingsApi } from './scope.ts'
|
|
23
20
|
import { injectStyle } from './styles.ts'
|
|
24
21
|
|
|
25
22
|
export const name = 'dsh-plus-access-gate'
|
|
@@ -42,7 +39,7 @@ interface RemoteLike {
|
|
|
42
39
|
}
|
|
43
40
|
|
|
44
41
|
interface ConnectionLike {
|
|
45
|
-
api: { settings:
|
|
42
|
+
api: { settings: Parameters<typeof createApiScope>[0] }
|
|
46
43
|
}
|
|
47
44
|
|
|
48
45
|
interface ClientContext {
|
|
@@ -54,82 +51,6 @@ interface ClientContext {
|
|
|
54
51
|
effect(execute: () => () => void, label?: string): unknown
|
|
55
52
|
}
|
|
56
53
|
|
|
57
|
-
/**
|
|
58
|
-
* api 直连实现的命名空间 scope(官方配置页 tab 同款模式)。
|
|
59
|
-
* 读:describe 取命名空间视图(脱敏 value/revision + 顶层 writable);
|
|
60
|
-
* 刷新:remote `settings/document-updated`(按命名空间过滤)与
|
|
61
|
-
* `connection/reset`;generation 防旧读覆盖新发布。任何页面 origin
|
|
62
|
-
* (loopback 或 tailnet 信任域名)下行为一致。
|
|
63
|
-
*/
|
|
64
|
-
function createApiScope(api: SettingsApi, ns: string, c: ClientContext): Scope {
|
|
65
|
-
let state: ScopeSnapshot = {
|
|
66
|
-
status: 'loading',
|
|
67
|
-
value: undefined,
|
|
68
|
-
revision: undefined,
|
|
69
|
-
writable: false,
|
|
70
|
-
}
|
|
71
|
-
const listeners = new Set<() => void>()
|
|
72
|
-
let generation = 0
|
|
73
|
-
const publish = (next: ScopeSnapshot): void => {
|
|
74
|
-
state = next
|
|
75
|
-
for (const listener of [...listeners]) listener()
|
|
76
|
-
}
|
|
77
|
-
const load = async (): Promise<void> => {
|
|
78
|
-
const gen = ++generation
|
|
79
|
-
let response: Awaited<ReturnType<SettingsApi['describe']>>
|
|
80
|
-
try {
|
|
81
|
-
response = await api.describe({})
|
|
82
|
-
} catch {
|
|
83
|
-
return
|
|
84
|
-
}
|
|
85
|
-
if (gen !== generation || !response.result.ok) return
|
|
86
|
-
const writable = response.result.value?.writable ?? false
|
|
87
|
-
const view = response.result.value?.namespaces.find((candidate) => candidate.ns === ns)
|
|
88
|
-
if (view === undefined) {
|
|
89
|
-
publish({
|
|
90
|
-
status: 'unavailable',
|
|
91
|
-
value: undefined,
|
|
92
|
-
revision: undefined,
|
|
93
|
-
writable,
|
|
94
|
-
})
|
|
95
|
-
return
|
|
96
|
-
}
|
|
97
|
-
publish({
|
|
98
|
-
status: 'ready',
|
|
99
|
-
value: view.value,
|
|
100
|
-
revision: view.revision,
|
|
101
|
-
writable,
|
|
102
|
-
})
|
|
103
|
-
}
|
|
104
|
-
const refresh = (namespace?: unknown): void => {
|
|
105
|
-
if (namespace !== undefined && namespace !== ns) return
|
|
106
|
-
void load()
|
|
107
|
-
}
|
|
108
|
-
const disposers = [
|
|
109
|
-
c.get('remote').$on('settings/document-updated', refresh),
|
|
110
|
-
c.on('connection/reset', () => {
|
|
111
|
-
void load()
|
|
112
|
-
}),
|
|
113
|
-
]
|
|
114
|
-
c.effect(
|
|
115
|
-
() => () => {
|
|
116
|
-
for (const dispose of disposers) dispose()
|
|
117
|
-
},
|
|
118
|
-
'access-gate: settings scope',
|
|
119
|
-
)
|
|
120
|
-
void load()
|
|
121
|
-
return {
|
|
122
|
-
getSnapshot: () => state,
|
|
123
|
-
subscribe: (listener: () => void) => {
|
|
124
|
-
listeners.add(listener)
|
|
125
|
-
return () => {
|
|
126
|
-
listeners.delete(listener)
|
|
127
|
-
}
|
|
128
|
-
},
|
|
129
|
-
load,
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
54
|
export function apply(ctx: Context): void {
|
|
134
55
|
const c = ctx as unknown as ClientContext
|
|
135
56
|
const tag = injectStyle()
|
|
@@ -141,7 +62,7 @@ export function apply(ctx: Context): void {
|
|
|
141
62
|
)
|
|
142
63
|
c.effect(() => c.locale.register(NS, { zh, en }), 'access-gate: locale')
|
|
143
64
|
const api = c.get('connection').api.settings
|
|
144
|
-
const scope = createApiScope(api, SETTINGS_NS, c)
|
|
65
|
+
const scope = createApiScope(api, SETTINGS_NS, c, 'access-gate: settings scope')
|
|
145
66
|
c.slots.inject('settings.plugin.item', () =>
|
|
146
67
|
c.slots.register(
|
|
147
68
|
{
|
package/src/client/i18n.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 配置卡片文案(zh/en)。经 ctx.locale.register 注册、bind 取用,与官方卡片同机制。
|
|
3
|
+
* 公共键(save/discard/unsaved 等)来自 shared 的 common 字典,本文件只维护业务键。
|
|
3
4
|
* @module access-gate/client/i18n
|
|
4
5
|
*/
|
|
6
|
+
import { commonEn, commonZh, mergeDict } from '@dsh-plus/shared/client'
|
|
5
7
|
|
|
6
8
|
export const NS = 'dsh-plus-access-gate'
|
|
7
9
|
|
|
8
|
-
|
|
10
|
+
const ownZh = {
|
|
9
11
|
title: '访问控制',
|
|
10
12
|
description: 'Web 访问围栏:本机直连放行;远程按白名单 IP 或访问令牌放行,其余拦截到登录页。',
|
|
11
13
|
enabled: '启用访问围栏',
|
|
@@ -39,20 +41,9 @@ export const zh: Record<string, string> = {
|
|
|
39
41
|
diagOff: '围栏未启用',
|
|
40
42
|
diagInvalid: '白名单存在无法解析的条目:',
|
|
41
43
|
warnFailClosed: '警告:已启用但令牌与白名单均为空——所有远程访问将被拒绝(含本页之外的设备)。',
|
|
42
|
-
save: '保存',
|
|
43
|
-
saving: '保存中…',
|
|
44
|
-
discard: '放弃',
|
|
45
|
-
unsaved: '未保存',
|
|
46
|
-
saveFailed: '保存失败,请检查填写内容。',
|
|
47
|
-
loading: '加载中…',
|
|
48
|
-
unavailable: '配置服务不可用。',
|
|
49
|
-
readOnly: '当前部署无 settings provider,配置为只读;请编辑 settings.yaml。',
|
|
50
|
-
expand: '展开',
|
|
51
|
-
collapse: '收起',
|
|
52
|
-
invalidNumber: '请输入有效数字',
|
|
53
44
|
}
|
|
54
45
|
|
|
55
|
-
|
|
46
|
+
const ownEn = {
|
|
56
47
|
title: 'Access Control',
|
|
57
48
|
description:
|
|
58
49
|
'Web access gate: loopback passes; remote traffic needs an allowlisted IP or the token, others hit the login page.',
|
|
@@ -89,15 +80,7 @@ export const en: Record<string, string> = {
|
|
|
89
80
|
diagInvalid: 'Allowlist entries that failed to parse:',
|
|
90
81
|
warnFailClosed:
|
|
91
82
|
'Warning: enabled with no token and an empty allowlist — all remote access will be refused.',
|
|
92
|
-
save: 'Save',
|
|
93
|
-
saving: 'Saving…',
|
|
94
|
-
discard: 'Discard',
|
|
95
|
-
unsaved: 'Unsaved',
|
|
96
|
-
saveFailed: 'Save failed; please check the inputs.',
|
|
97
|
-
loading: 'Loading…',
|
|
98
|
-
unavailable: 'Settings service unavailable.',
|
|
99
|
-
readOnly: 'No settings provider in this deployment; read-only. Edit settings.yaml instead.',
|
|
100
|
-
expand: 'Expand',
|
|
101
|
-
collapse: 'Collapse',
|
|
102
|
-
invalidNumber: 'Enter a valid number',
|
|
103
83
|
}
|
|
84
|
+
|
|
85
|
+
export const zh: Record<string, string> = mergeDict(commonZh, ownZh)
|
|
86
|
+
export const en: Record<string, string> = mergeDict(commonEn, ownEn)
|