kczx-user-management 1.6.0 → 1.6.2
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/README.md +5 -3
- package/client/bundle.js +13 -5
- package/client/index.js +13 -5
- package/package.json +1 -1
- package/src/index.js +4 -2
- package/src/workspace-create.js +22 -5
package/README.md
CHANGED
|
@@ -88,7 +88,7 @@ node scripts/ensure-deps.mjs # 仅当上述解析不到 sche
|
|
|
88
88
|
|---|---|
|
|
89
89
|
| 用户增删改查 | 用户表每行一个「编辑」**全屏页**:基本资料(部门 / 角色)+ 账号操作(重置密码 / 重置两步验证 / 禁用启用 / 删除)+ 权限与配额,集中一屏;改动类走「保存」,破坏性操作仍是各自带确认的按钮 |
|
|
90
90
|
| 所属部门(1.6.0 起支持多层级) | 账号资料字段,支持**多层级路径**:用 `/` 分层,例如 `和平分局/刑侦大队/一中队`(`\` 也认,存的时候统一成 `/`)。新增用户时可填,编辑页可随时改或清空;编辑时框下会把层级摊开显示,并列出**已在用的层级**供一键接续;变更写进操作日志。规则(服务端为准,界面同款提示在前面):最多 **5 级**、每一级 ≤ **24 字**、整串 ≤ **64 字**(含分隔符);层级里不能有 `\ / : * ? " < > |`,也不能是 `.` / `..`——因为这些层级要变成目录名。写不进去的值**报错而不是悄悄改写**(理由同长度上限) |
|
|
91
|
-
| 新建账号即建工作区 | 新增用户时勾选「同时创建该用户的工作区」:路径预填 `<harness
|
|
91
|
+
| 新建账号即建工作区 | 新增用户时勾选「同时创建该用户的工作区」:路径预填 `<harness 根>/workspace/<部门各级>/<用户名>`,**一级部门一个目录**(`…\workspace\和平分局\刑侦大队\zhang`;可改成任意绝对路径);提交后目录按需创建、登记进 dsh 工作区列表,并把该路径写进这个账号的工作区白名单。部门不合法时**先报 400,再建目录**——不会留下半棵树 |
|
|
92
92
|
| 工作区白名单 | **留空 = 不允许任何工作区**(白名单默认拒绝);填写后只允许这些目录下的工作区;条目 `*` = 允许全部。Windows 路径折叠大小写,末尾斜杠自动忽略 |
|
|
93
93
|
| 工作区归属跟踪 | 谁创建的工作区归谁:别人看不到、也不能删改;删除后释放,账号删除后清理 |
|
|
94
94
|
| 会话可见性(1.6.0 起) | 会话按**会话自己的工作目录**判定:目录不在本账号白名单内、或落在别人认领的工作区里,这个会话就不下发。**为什么必须有**:前端把「没有被任何可见工作区认领」的会话统统塞进「未分组」那一组——只藏工作区行的话,里面的会话会带着标题、点开即读的历史原地复活。**三个入口一起堵**:① `session.list`(0.1.1)/ `session/list`(0.1.2+)的一元响应;② 0.1.1 的 `host/session-added` 实时推送(`GET /api/events.host` 的 SSE——每个新建会话都会被推给所有在线浏览器,只改一元响应的话,别人新建一条就会把行实时推回来);③ 0.1.2+ 的 `api-session/added`(`$events` mux 流上的 emit 帧)。`session.search` 沿用列表刚藏掉的 id 清单摘命中片段。判定只看 `cwd`:`blank` **不**豁免(别人目录里的「新建会话」行也还是别人的目录),只有子代理行(`origin: subagent`,前端本来就不列)和**读不到 cwd** 的行保留——「判断不了」不等于「不该看」 |
|
|
@@ -231,7 +231,8 @@ user-management:
|
|
|
231
231
|
|
|
232
232
|
### 部门层级(1.6.0 起)
|
|
233
233
|
|
|
234
|
-
部门的「多层级」不是一张独立的部门表,而是**一个字符串里的路径**:`和平分局/刑侦大队/一中队`。这么定有两个好处——老数据不用迁移(原来的 `平台组`
|
|
234
|
+
部门的「多层级」不是一张独立的部门表,而是**一个字符串里的路径**:`和平分局/刑侦大队/一中队`。这么定有两个好处——老数据不用迁移(原来的 `平台组` 就是单级),而且它正好就是工作区目录的形状:勾了「同时创建该用户的工作区」的账号,目录会一级一个文件夹地铺开
|
|
235
|
+
(都挂在 `<harness 根>/workspace/` 这一层下面)。
|
|
235
236
|
|
|
236
237
|
| 规则 | 值 |
|
|
237
238
|
|---|---|
|
|
@@ -248,7 +249,8 @@ user-management:
|
|
|
248
249
|
### 新建用户时一并创建工作区(1.5.0 起)
|
|
249
250
|
|
|
250
251
|
「用户 → 新增用户」多了一个复选框「同时创建该用户的工作区」。勾上后出现路径框,**预填**默认路径
|
|
251
|
-
`<harness
|
|
252
|
+
`<harness 根>/workspace/<部门各级>/<用户名>`(部门留空就少一层,只到用户名;所有账号都在同一个 `workspace` 目录下,
|
|
253
|
+
根目录因此不会被一堆部门目录铺满),可以直接改成任意绝对路径;下方一行小字写着默认值,
|
|
252
254
|
改过之后还有「恢复默认」。不勾选则一切照旧。
|
|
253
255
|
|
|
254
256
|
提交时服务端按这个顺序做事,**能失败的都在写账号之前失败**:
|
package/client/bundle.js
CHANGED
|
@@ -309,7 +309,7 @@ window.__ModuleLoader__.load({
|
|
|
309
309
|
cfgFieldOtpFailLimit: '两步验证失败上限',
|
|
310
310
|
cfgFieldOtpLockoutSeconds: '两步验证锁定时长(秒)',
|
|
311
311
|
cfgFieldWorkspaceRoot: '新账号工作区根路径',
|
|
312
|
-
cfgHintWorkspaceRoot: '新建用户勾选「同时创建该用户的工作区」时,默认路径为
|
|
312
|
+
cfgHintWorkspaceRoot: '新建用户勾选「同时创建该用户的工作区」时,默认路径为 <根路径>/workspace/<部门各级>/<用户名>。留空 = 用宿主提供的工作区根路径(dsh 默认是 harness 所在目录)。',
|
|
313
313
|
}
|
|
314
314
|
|
|
315
315
|
const EN = {
|
|
@@ -576,7 +576,7 @@ window.__ModuleLoader__.load({
|
|
|
576
576
|
cfgFieldOtpFailLimit: '2FA failure limit',
|
|
577
577
|
cfgFieldOtpLockoutSeconds: '2FA lockout (seconds)',
|
|
578
578
|
cfgFieldWorkspaceRoot: 'Workspace root for new accounts',
|
|
579
|
-
cfgHintWorkspaceRoot: 'When a new account is created with “also create its workspace”, the default path is <root
|
|
579
|
+
cfgHintWorkspaceRoot: 'When a new account is created with “also create its workspace”, the default path is <root>/workspace/<department levels>/<username>. Empty = use the host’s own workspace root (dsh defaults to the harness directory).',
|
|
580
580
|
}
|
|
581
581
|
|
|
582
582
|
/** Ledger event type → locale key (also the login-log type filter). */
|
|
@@ -634,6 +634,13 @@ window.__ModuleLoader__.load({
|
|
|
634
634
|
* one that decides.
|
|
635
635
|
*/
|
|
636
636
|
const DEPARTMENT_MAX_DEPTH = 5
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* The directory every default workspace lives under (mirror of
|
|
640
|
+
* WORKSPACE_SUBDIR in src/workspace-create.js): the preview must name the same
|
|
641
|
+
* folder the server will build, level for level.
|
|
642
|
+
*/
|
|
643
|
+
const WORKSPACE_SUBDIR = 'workspace'
|
|
637
644
|
const DEPARTMENT_MAX_LEVEL_LEN = 24
|
|
638
645
|
const DEPARTMENT_MAX_LEN = 64
|
|
639
646
|
|
|
@@ -676,7 +683,7 @@ window.__ModuleLoader__.load({
|
|
|
676
683
|
|
|
677
684
|
/**
|
|
678
685
|
* The default workspace path, as the create-user dialog shows it:
|
|
679
|
-
* `<root
|
|
686
|
+
* `<root>/workspace/<一级>/<二级>/…/<username>`.
|
|
680
687
|
*
|
|
681
688
|
* This is the DISPLAY twin of the host half's rule (src/workspace-create.js):
|
|
682
689
|
* one folder PER department level, each folded into a directory name, joined
|
|
@@ -689,7 +696,7 @@ window.__ModuleLoader__.load({
|
|
|
689
696
|
// No root = no preview: the dialog then asks for an absolute path instead of
|
|
690
697
|
// showing a path relative to nothing.
|
|
691
698
|
if (base === '') return ''
|
|
692
|
-
const parts = [base]
|
|
699
|
+
const parts = [base, WORKSPACE_SUBDIR]
|
|
693
700
|
// The username is folded by the same rule as a level: it is one segment too.
|
|
694
701
|
for (const raw of [...departmentLevels(department), username]) {
|
|
695
702
|
const segment = String(raw === undefined || raw === null ? '' : raw)
|
|
@@ -1576,7 +1583,8 @@ window.__ModuleLoader__.load({
|
|
|
1576
1583
|
*
|
|
1577
1584
|
* Split out from the tab that opens it because the workspace half is the part
|
|
1578
1585
|
* worth pinning down: the checkbox reveals a path box pre-filled with the
|
|
1579
|
-
* default (`<harness root
|
|
1586
|
+
* default (`<harness root>/workspace/<department levels>/<username>`) and left
|
|
1587
|
+
* editable, so the
|
|
1580
1588
|
* admin either accepts it or types another absolute path. As a component of its
|
|
1581
1589
|
* own it can be rendered on demand — the tab's dialog otherwise only exists
|
|
1582
1590
|
* after a click, which a contract test cannot perform.
|
package/client/index.js
CHANGED
|
@@ -299,7 +299,7 @@ const ZH = {
|
|
|
299
299
|
cfgFieldOtpFailLimit: '两步验证失败上限',
|
|
300
300
|
cfgFieldOtpLockoutSeconds: '两步验证锁定时长(秒)',
|
|
301
301
|
cfgFieldWorkspaceRoot: '新账号工作区根路径',
|
|
302
|
-
cfgHintWorkspaceRoot: '新建用户勾选「同时创建该用户的工作区」时,默认路径为
|
|
302
|
+
cfgHintWorkspaceRoot: '新建用户勾选「同时创建该用户的工作区」时,默认路径为 <根路径>/workspace/<部门各级>/<用户名>。留空 = 用宿主提供的工作区根路径(dsh 默认是 harness 所在目录)。',
|
|
303
303
|
}
|
|
304
304
|
|
|
305
305
|
const EN = {
|
|
@@ -566,7 +566,7 @@ const EN = {
|
|
|
566
566
|
cfgFieldOtpFailLimit: '2FA failure limit',
|
|
567
567
|
cfgFieldOtpLockoutSeconds: '2FA lockout (seconds)',
|
|
568
568
|
cfgFieldWorkspaceRoot: 'Workspace root for new accounts',
|
|
569
|
-
cfgHintWorkspaceRoot: 'When a new account is created with “also create its workspace”, the default path is <root
|
|
569
|
+
cfgHintWorkspaceRoot: 'When a new account is created with “also create its workspace”, the default path is <root>/workspace/<department levels>/<username>. Empty = use the host’s own workspace root (dsh defaults to the harness directory).',
|
|
570
570
|
}
|
|
571
571
|
|
|
572
572
|
/** Ledger event type → locale key (also the login-log type filter). */
|
|
@@ -624,6 +624,13 @@ function interpolate(template, vars) {
|
|
|
624
624
|
* one that decides.
|
|
625
625
|
*/
|
|
626
626
|
const DEPARTMENT_MAX_DEPTH = 5
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* The directory every default workspace lives under (mirror of
|
|
630
|
+
* WORKSPACE_SUBDIR in src/workspace-create.js): the preview must name the same
|
|
631
|
+
* folder the server will build, level for level.
|
|
632
|
+
*/
|
|
633
|
+
const WORKSPACE_SUBDIR = 'workspace'
|
|
627
634
|
const DEPARTMENT_MAX_LEVEL_LEN = 24
|
|
628
635
|
const DEPARTMENT_MAX_LEN = 64
|
|
629
636
|
|
|
@@ -666,7 +673,7 @@ function departmentProblem(levels, t) {
|
|
|
666
673
|
|
|
667
674
|
/**
|
|
668
675
|
* The default workspace path, as the create-user dialog shows it:
|
|
669
|
-
* `<root
|
|
676
|
+
* `<root>/workspace/<一级>/<二级>/…/<username>`.
|
|
670
677
|
*
|
|
671
678
|
* This is the DISPLAY twin of the host half's rule (src/workspace-create.js):
|
|
672
679
|
* one folder PER department level, each folded into a directory name, joined
|
|
@@ -679,7 +686,7 @@ function workspaceDefaultPath(root, sep, department, username) {
|
|
|
679
686
|
// No root = no preview: the dialog then asks for an absolute path instead of
|
|
680
687
|
// showing a path relative to nothing.
|
|
681
688
|
if (base === '') return ''
|
|
682
|
-
const parts = [base]
|
|
689
|
+
const parts = [base, WORKSPACE_SUBDIR]
|
|
683
690
|
// The username is folded by the same rule as a level: it is one segment too.
|
|
684
691
|
for (const raw of [...departmentLevels(department), username]) {
|
|
685
692
|
const segment = String(raw === undefined || raw === null ? '' : raw)
|
|
@@ -1566,7 +1573,8 @@ function EditUserPage({ user, me, departments, onClose, onDone, onTempPassword,
|
|
|
1566
1573
|
*
|
|
1567
1574
|
* Split out from the tab that opens it because the workspace half is the part
|
|
1568
1575
|
* worth pinning down: the checkbox reveals a path box pre-filled with the
|
|
1569
|
-
* default (`<harness root
|
|
1576
|
+
* default (`<harness root>/workspace/<department levels>/<username>`) and left
|
|
1577
|
+
* editable, so the
|
|
1570
1578
|
* admin either accepts it or types another absolute path. As a component of its
|
|
1571
1579
|
* own it can be rendered on demand — the tab's dialog otherwise only exists
|
|
1572
1580
|
* after a click, which a contract test cannot perform.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kczx-user-management",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "dsh 插件 · 登录门 + 账号 / 权限 / 配额 / 审计台账(dsh-passwords 的功能已并入):HTTPS 登录网关与自签证书、用户增删改查(可一并创建工作区)、工作区白名单与归属跟踪、每小时 token 与每日时长配额、沙盒档位下限、上传/git 开关、三本审计台账(登录/访问/操作)、IP 封禁、TOTP 两步验证。",
|
|
6
6
|
"license": "MIT",
|
package/src/index.js
CHANGED
|
@@ -718,7 +718,8 @@ async function handleApi(req, res, deps) {
|
|
|
718
718
|
* The root the console builds per-account workspaces under.
|
|
719
719
|
*
|
|
720
720
|
* Read by the create-user form so it can prefill the default path
|
|
721
|
-
* (<root
|
|
721
|
+
* (<root>/workspace/<department levels>/<username>, one folder per level).
|
|
722
|
+
* Admin-only:
|
|
722
723
|
* the value is a path on the
|
|
723
724
|
* host, which is exactly what a plain account must not be handed.
|
|
724
725
|
*/
|
|
@@ -1112,7 +1113,8 @@ const Config = z.object({
|
|
|
1112
1113
|
lockoutSeconds: z.natural().min(1).default(60),
|
|
1113
1114
|
// ── per-account workspaces (the "create user" flow) ──────────────────────
|
|
1114
1115
|
// Root the console builds a new account's workspace under:
|
|
1115
|
-
// <workspaceRoot
|
|
1116
|
+
// <workspaceRoot>/workspace/<department levels>/<username>. Empty (the
|
|
1117
|
+
// default) asks the
|
|
1116
1118
|
// running host for it — the sandbox policy's own workspaceRoot, which dsh
|
|
1117
1119
|
// defaults to the harness's process cwd — and falls back to process.cwd().
|
|
1118
1120
|
// Set it explicitly to park every account folder on another volume.
|
package/src/workspace-create.js
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* Workspace provisioning for the console's "create user" flow.
|
|
5
5
|
*
|
|
6
6
|
* An account created by an administrator may come with its own workspace. The
|
|
7
|
-
* DEFAULT home is `<harness root
|
|
8
|
-
* per department level
|
|
7
|
+
* DEFAULT home is `<harness root>/workspace/<department levels>/<username>`
|
|
8
|
+
* (one folder per department level, all of them under a single `workspace`
|
|
9
|
+
* directory); the admin may type any other absolute path instead. This module owns the pure half of that — the
|
|
9
10
|
* path arithmetic and its validation — plus the directory step, so both are
|
|
10
11
|
* verifiable without a store, a request or a running dsh.
|
|
11
12
|
*
|
|
@@ -26,6 +27,16 @@ import { departmentLevels, StoreError } from './store.js'
|
|
|
26
27
|
|
|
27
28
|
/** Longest path this module will hand to the filesystem. */
|
|
28
29
|
const MAX_PATH_LEN = 1024
|
|
30
|
+
/**
|
|
31
|
+
* The single directory every default workspace lives under:
|
|
32
|
+
* `<root>/workspace/<department levels>/<username>`.
|
|
33
|
+
*
|
|
34
|
+
* One level between the harness root and the accounts, so the root keeps its
|
|
35
|
+
* own files (the checkout, its config, whatever else lives there) beside a
|
|
36
|
+
* single `workspace` folder instead of being mixed with one folder per
|
|
37
|
+
* department.
|
|
38
|
+
*/
|
|
39
|
+
const WORKSPACE_SUBDIR = 'workspace'
|
|
29
40
|
/** Longest directory name built out of free text (a department is capped at 64
|
|
30
41
|
* characters by the store, and a username at 32). */
|
|
31
42
|
const MAX_SEGMENT_LEN = 64
|
|
@@ -55,10 +66,10 @@ export function sanitizeSegment(value) {
|
|
|
55
66
|
|
|
56
67
|
/**
|
|
57
68
|
* The default workspace for one account:
|
|
58
|
-
* `<root
|
|
69
|
+
* `<root>/workspace/<一级>/<二级>/…/<username>`.
|
|
59
70
|
*
|
|
60
71
|
* A multi-level department becomes NESTED folders, one per level, so the tree on
|
|
61
|
-
* disk mirrors the organisation: `E:\work\和平分局\刑侦大队\zhang`. A blank
|
|
72
|
+
* disk mirrors the organisation: `E:\work\workspace\和平分局\刑侦大队\zhang`. A blank
|
|
62
73
|
* department drops its segments instead of inventing a placeholder — the username
|
|
63
74
|
* alone is already unique, and a folder named after nothing is worse than one
|
|
64
75
|
* level less.
|
|
@@ -75,7 +86,7 @@ export function sanitizeSegment(value) {
|
|
|
75
86
|
* @returns an absolute path built with the host separator.
|
|
76
87
|
*/
|
|
77
88
|
export function defaultWorkspacePath(root, department, username) {
|
|
78
|
-
const parts = [String(root === undefined || root === null ? '' : root).trim()]
|
|
89
|
+
const parts = [String(root === undefined || root === null ? '' : root).trim(), WORKSPACE_SUBDIR]
|
|
79
90
|
for (const level of departmentLevels(department)) {
|
|
80
91
|
const segment = sanitizeSegment(level)
|
|
81
92
|
if (segment !== '') parts.push(segment)
|
|
@@ -127,6 +138,12 @@ export function resolveWorkspaceTarget({ root, department, username, custom } =
|
|
|
127
138
|
if (sameWorkspacePath(path, base)) {
|
|
128
139
|
throw new StoreError('bad_workspace_path', '工作区不能就是 harness 根目录本身(该目录是所有账号工作区的父目录)')
|
|
129
140
|
}
|
|
141
|
+
// The container is off limits for the same reason: a username that sanitizes
|
|
142
|
+
// away would otherwise turn the folder every account lives under into one
|
|
143
|
+
// account's workspace.
|
|
144
|
+
if (sameWorkspacePath(path, join(base, WORKSPACE_SUBDIR))) {
|
|
145
|
+
throw new StoreError('bad_workspace_path', `工作区不能就是 ${WORKSPACE_SUBDIR} 目录本身(该目录是所有账号工作区的父目录)`)
|
|
146
|
+
}
|
|
130
147
|
return { path, root: base, source }
|
|
131
148
|
}
|
|
132
149
|
|