@lvxianchao/dsh-web-guard 2.1.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +45 -0
- package/README.md +64 -23
- package/README.zh.md +63 -31
- package/lib/auth.js +244 -232
- package/lib/client.js +1071 -0
- package/lib/i18n.js +49 -247
- package/lib/index.js +571 -465
- package/lib/inject.js +20 -69
- package/lib/page.js +52 -62
- package/lib/polyfill.js +9 -8
- package/lib/preferences.js +144 -0
- package/lib/proxy.js +161 -156
- package/lib/shared/api.js +19 -0
- package/lib/shared/theme.js +109 -0
- package/package.json +36 -4
- package/lib/admin.js +0 -190
package/lib/i18n.js
CHANGED
|
@@ -3,271 +3,73 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The harness stores the operator's language choice in the profile patch file
|
|
5
5
|
* (`- id: locale` → `config.preference`), which is exactly what the settings UI
|
|
6
|
-
* writes when someone switches language. Reading it means the login and
|
|
7
|
-
* pages follow the harness instead of guessing from the browser.
|
|
6
|
+
* writes when someone switches language. Reading it means the login and
|
|
7
|
+
* sign-out pages follow the harness instead of guessing from the browser.
|
|
8
8
|
*
|
|
9
9
|
* When no explicit preference exists, the harness falls back to the browser
|
|
10
10
|
* language — and so do we, via Accept-Language.
|
|
11
11
|
*/
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const SUPPORTED = ['zh', 'en']
|
|
16
|
-
|
|
12
|
+
import { createPreferenceReader, readPreference } from './preferences.js';
|
|
13
|
+
const SUPPORTED = ['zh', 'en'];
|
|
17
14
|
/**
|
|
18
15
|
* Read the explicit locale preference from a profile patch file.
|
|
19
|
-
*
|
|
20
|
-
* @
|
|
16
|
+
* Returns a supported locale id, or '' when unset/unreadable.
|
|
17
|
+
* @param patchPath - profile patch file path.
|
|
21
18
|
*/
|
|
22
|
-
export function
|
|
23
|
-
|
|
24
|
-
let text
|
|
25
|
-
try {
|
|
26
|
-
text = readFileSync(patchPath, 'utf8')
|
|
27
|
-
} catch {
|
|
28
|
-
return ''
|
|
29
|
-
}
|
|
30
|
-
const lines = text.split('\n')
|
|
31
|
-
let inLocaleRow = false
|
|
32
|
-
let rowIndent = 0
|
|
33
|
-
for (const line of lines) {
|
|
34
|
-
const trimmed = line.trim()
|
|
35
|
-
if (!trimmed || trimmed.startsWith('#')) continue
|
|
36
|
-
const indent = line.length - line.trimStart().length
|
|
37
|
-
|
|
38
|
-
// A new top-level-ish row ends the section we were scanning.
|
|
39
|
-
if (inLocaleRow && /^- /.test(trimmed) && indent <= rowIndent) break
|
|
40
|
-
|
|
41
|
-
if (/^-\s+id:\s*['"]?locale['"]?\s*$/.test(trimmed)) {
|
|
42
|
-
inLocaleRow = true
|
|
43
|
-
rowIndent = indent
|
|
44
|
-
continue
|
|
45
|
-
}
|
|
46
|
-
if (inLocaleRow) {
|
|
47
|
-
const m = trimmed.match(/^preference:\s*['"]?([A-Za-z-]+)['"]?/)
|
|
48
|
-
if (m) return normalize(m[1])
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
return ''
|
|
19
|
+
export function readLocalePreference(patchPath) {
|
|
20
|
+
return normalize(readPreference(patchPath, 'locale'));
|
|
52
21
|
}
|
|
53
|
-
|
|
54
22
|
/** Map an arbitrary tag (`zh-CN`, `en-US`) onto a supported id. */
|
|
55
23
|
export function normalize(tag) {
|
|
56
|
-
|
|
57
|
-
|
|
24
|
+
const base = String(tag ?? '').toLowerCase().split(/[-_]/)[0];
|
|
25
|
+
return SUPPORTED.includes(base) ? base : '';
|
|
58
26
|
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Resolve the page locale: explicit preference first, then Accept-Language.
|
|
62
|
-
* @param {string} patchPath - profile patch file
|
|
63
|
-
* @param {import('node:http').IncomingMessage} [req] - request carrying Accept-Language
|
|
64
|
-
*/
|
|
65
|
-
export function detectLocale(patchPath, req) {
|
|
66
|
-
const explicit = readPreference(patchPath)
|
|
67
|
-
if (explicit) return explicit
|
|
68
|
-
const header = String(req?.headers?.['accept-language'] ?? '')
|
|
69
|
-
const first = header.split(',')[0]?.trim()
|
|
70
|
-
return normalize(first) || 'en'
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/** Relative time in the active language. */
|
|
74
|
-
export function ago(ms, locale) {
|
|
75
|
-
if (!ms) return '—'
|
|
76
|
-
const s = Math.max(0, Math.round((Date.now() - ms) / 1000))
|
|
77
|
-
if (locale === 'zh') {
|
|
78
|
-
if (s < 60) return `${s} 秒前`
|
|
79
|
-
if (s < 3600) return `${Math.round(s / 60)} 分钟前`
|
|
80
|
-
if (s < 86400) return `${Math.round(s / 3600)} 小时前`
|
|
81
|
-
return `${Math.round(s / 86400)} 天前`
|
|
82
|
-
}
|
|
83
|
-
if (s < 60) return `${s}s ago`
|
|
84
|
-
if (s < 3600) return `${Math.round(s / 60)} min ago`
|
|
85
|
-
if (s < 86400) return `${Math.round(s / 3600)} h ago`
|
|
86
|
-
return `${Math.round(s / 86400)} d ago`
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const ZH = {
|
|
90
|
-
locale: 'zh',
|
|
91
|
-
adminTitle: 'DSH Web Guard',
|
|
92
|
-
pageTitleAdmin: 'DSH Web Guard',
|
|
93
|
-
backToDsh: '返回 DSH',
|
|
94
|
-
close: '关闭',
|
|
95
|
-
signOut: '退出登录',
|
|
96
|
-
secPassword: '登录密码',
|
|
97
|
-
hintPassword: '留空则不修改。修改后其他设备上的会话仍然有效,需要的话用下面的「退出所有设备」。',
|
|
98
|
-
currentPassword: '当前密码',
|
|
99
|
-
currentPasswordPh: '修改密码时必填',
|
|
100
|
-
newPassword: '新密码',
|
|
101
|
-
newPasswordPh: '至少 6 位',
|
|
102
|
-
confirmPassword: '确认新密码',
|
|
103
|
-
confirmPasswordPh: '再输一次',
|
|
104
|
-
secSession: '会话与安全',
|
|
105
|
-
hintSession: '会话有效期从最后一次活动开始计算;改为 0 表示关掉浏览器即失效。',
|
|
106
|
-
sessionDays: '会话有效期(天)',
|
|
107
|
-
lockMinutes: '锁定时间(分钟)',
|
|
108
|
-
maxAttempts: '连续失败次数上限',
|
|
109
|
-
secListen: '监听',
|
|
110
|
-
hintListen: '修改后需要重启 DSH 才生效;填错会导致管理页和外网入口都连不上,确认前先记下原值。',
|
|
111
|
-
listenHost: '监听地址',
|
|
112
|
-
listenPort: '监听端口',
|
|
113
|
-
save: '保存',
|
|
114
|
-
signOutAll: '退出所有设备',
|
|
115
|
-
confirmSignOutAll: '将退出所有设备上的登录,包括当前这个。继续?',
|
|
116
|
-
secSessions: '活跃会话',
|
|
117
|
-
hintSessions: '每个浏览器一条记录。会话存在内存里,重启 DSH 会全部失效。',
|
|
118
|
-
colSource: '来源',
|
|
119
|
-
colBrowser: '浏览器',
|
|
120
|
-
colLastSeen: '最后活动',
|
|
121
|
-
current: '当前',
|
|
122
|
-
kick: '踢出',
|
|
123
|
-
emptySessions: '当前没有其他活跃会话',
|
|
124
|
-
saved: '已保存',
|
|
125
|
-
savedPassword: '已保存,密码已更新',
|
|
126
|
-
restartNote: '监听地址/端口已记录,重启 DSH 后才会生效。',
|
|
127
|
-
errCurrent: '当前密码不正确',
|
|
128
|
-
errShort: '新密码至少 6 位',
|
|
129
|
-
errMismatch: '两次输入的新密码不一致',
|
|
130
|
-
errOrigin: '请求来源不合法',
|
|
131
|
-
kicked: '已踢出该会话',
|
|
132
|
-
kickMissing: '该会话已不存在',
|
|
133
|
-
// login page
|
|
134
|
-
loginSubtitle: '此服务需要密码才能访问',
|
|
135
|
-
passwordLabel: '访问密码',
|
|
136
|
-
passwordPh: '输入密码',
|
|
137
|
-
submit: '进入',
|
|
138
|
-
errWrong: '密码不正确',
|
|
139
|
-
errLocked: '尝试次数过多,请稍后再试',
|
|
140
|
-
loggedOut: '已退出登录',
|
|
141
|
-
signInAgain: '重新登录',
|
|
142
|
-
}
|
|
143
|
-
|
|
144
27
|
const EN = {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
colLastSeen: 'Last seen',
|
|
176
|
-
current: 'this device',
|
|
177
|
-
kick: 'Kick',
|
|
178
|
-
emptySessions: 'No other active sessions',
|
|
179
|
-
saved: 'Saved',
|
|
180
|
-
savedPassword: 'Saved — password updated',
|
|
181
|
-
restartNote: 'Listener change recorded; it takes effect after a DSH restart.',
|
|
182
|
-
errCurrent: 'Current password is incorrect',
|
|
183
|
-
errShort: 'New password must be at least 6 characters',
|
|
184
|
-
errMismatch: 'The new passwords do not match',
|
|
185
|
-
errOrigin: 'Request origin rejected',
|
|
186
|
-
kicked: 'Session kicked',
|
|
187
|
-
kickMissing: 'That session no longer exists',
|
|
188
|
-
// login page
|
|
189
|
-
loginSubtitle: 'This service requires a password',
|
|
190
|
-
passwordLabel: 'Password',
|
|
191
|
-
passwordPh: 'Enter password',
|
|
192
|
-
submit: 'Continue',
|
|
193
|
-
errWrong: 'Incorrect password',
|
|
194
|
-
errLocked: 'Too many attempts — try again later',
|
|
195
|
-
loggedOut: 'Signed out',
|
|
196
|
-
signInAgain: 'Sign in again',
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const CATALOG = { zh: ZH, en: EN }
|
|
200
|
-
|
|
28
|
+
locale: 'en',
|
|
29
|
+
// login page
|
|
30
|
+
loginSubtitle: 'This service requires a password',
|
|
31
|
+
passwordLabel: 'Password',
|
|
32
|
+
passwordPh: 'Enter password',
|
|
33
|
+
submit: 'Continue',
|
|
34
|
+
errWrong: 'Incorrect password',
|
|
35
|
+
errLocked: 'Too many attempts — try again later',
|
|
36
|
+
// sign-out page
|
|
37
|
+
loggedOut: 'Signed out',
|
|
38
|
+
signInAgain: 'Sign in again',
|
|
39
|
+
// shared
|
|
40
|
+
errOrigin: 'Request origin rejected',
|
|
41
|
+
};
|
|
42
|
+
const ZH = {
|
|
43
|
+
locale: 'zh',
|
|
44
|
+
// login page
|
|
45
|
+
loginSubtitle: '此服务需要密码才能访问',
|
|
46
|
+
passwordLabel: '访问密码',
|
|
47
|
+
passwordPh: '输入密码',
|
|
48
|
+
submit: '进入',
|
|
49
|
+
errWrong: '密码不正确',
|
|
50
|
+
errLocked: '尝试次数过多,请稍后再试',
|
|
51
|
+
// sign-out page
|
|
52
|
+
loggedOut: '已退出登录',
|
|
53
|
+
signInAgain: '重新登录',
|
|
54
|
+
// shared
|
|
55
|
+
errOrigin: '请求来源不合法',
|
|
56
|
+
};
|
|
57
|
+
const CATALOG = { zh: ZH, en: EN };
|
|
201
58
|
/** Copy bundle for a locale, falling back to English. */
|
|
202
59
|
export function t(locale) {
|
|
203
|
-
|
|
60
|
+
return CATALOG[locale] ?? EN;
|
|
204
61
|
}
|
|
205
|
-
|
|
206
62
|
/**
|
|
207
63
|
* Keep the patch file's locale preference in memory.
|
|
208
64
|
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
* change — the harness writes atomically via rename, so watching the directory
|
|
213
|
-
* is what actually catches the replacement. If the platform has no usable
|
|
214
|
-
* watcher, fall back to a slow mtime check on a timer.
|
|
215
|
-
*
|
|
216
|
-
* @param {string} patchPath - absolute path to cordis.patch.yml
|
|
217
|
-
* @returns {{get: () => string, dispose: () => void}}
|
|
65
|
+
* The watch/poll mechanics live in `preferences.ts`; this wrapper only maps the
|
|
66
|
+
* raw value onto a supported locale id.
|
|
67
|
+
* @param patchPath - profile patch file path.
|
|
218
68
|
*/
|
|
219
69
|
export function createLocaleReader(patchPath) {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const stamp = () => {
|
|
227
|
-
try {
|
|
228
|
-
return statSync(patchPath).mtimeMs
|
|
229
|
-
} catch {
|
|
230
|
-
return 0
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
mtime = stamp()
|
|
234
|
-
|
|
235
|
-
const refresh = () => {
|
|
236
|
-
const next = stamp()
|
|
237
|
-
if (next === mtime) return
|
|
238
|
-
mtime = next
|
|
239
|
-
value = readPreference(patchPath)
|
|
240
|
-
}
|
|
241
|
-
const schedule = () => {
|
|
242
|
-
if (debounce) clearTimeout(debounce)
|
|
243
|
-
debounce = setTimeout(refresh, 40)
|
|
244
|
-
debounce.unref?.()
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
try {
|
|
248
|
-
watcher = watch(dirname(patchPath), { persistent: false }, (_event, filename) => {
|
|
249
|
-
if (!filename || String(filename) === basename(patchPath)) schedule()
|
|
250
|
-
})
|
|
251
|
-
watcher.on('error', () => {
|
|
252
|
-
watcher?.close()
|
|
253
|
-
watcher = undefined
|
|
254
|
-
})
|
|
255
|
-
} catch {
|
|
256
|
-
watcher = undefined
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
if (!watcher) {
|
|
260
|
-
// No usable watcher: an inexpensive mtime check every 30s is the safety net.
|
|
261
|
-
poller = setInterval(refresh, 30_000)
|
|
262
|
-
poller.unref?.()
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
return {
|
|
266
|
-
get: () => value,
|
|
267
|
-
dispose: () => {
|
|
268
|
-
if (debounce) clearTimeout(debounce)
|
|
269
|
-
if (poller) clearInterval(poller)
|
|
270
|
-
watcher?.close()
|
|
271
|
-
},
|
|
272
|
-
}
|
|
70
|
+
const reader = createPreferenceReader(patchPath, 'locale');
|
|
71
|
+
return {
|
|
72
|
+
get: () => normalize(reader.get()),
|
|
73
|
+
dispose: () => reader.dispose(),
|
|
74
|
+
};
|
|
273
75
|
}
|