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 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 根>/<部门各级>/<用户名>`,**一级部门一个目录**(`…\和平分局\刑侦大队\zhang`;可改成任意绝对路径);提交后目录按需创建、登记进 dsh 工作区列表,并把该路径写进这个账号的工作区白名单。部门不合法时**先报 400,再建目录**——不会留下半棵树 |
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: '新建用户勾选「同时创建该用户的工作区」时,默认路径为 <根路径>/<部门>/<用户名>。留空 = 用宿主提供的工作区根路径(dsh 默认是 harness 所在目录)。',
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>/<department>/<username>. Empty = use the host’s own workspace root (dsh defaults to the harness directory).',
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>/<一级>/<二级>/…/<username>`.
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>/<department>/<username>`) and left editable, so the
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: '新建用户勾选「同时创建该用户的工作区」时,默认路径为 <根路径>/<部门>/<用户名>。留空 = 用宿主提供的工作区根路径(dsh 默认是 harness 所在目录)。',
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>/<department>/<username>. Empty = use the host’s own workspace root (dsh defaults to the harness directory).',
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>/<一级>/<二级>/…/<username>`.
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>/<department>/<username>`) and left editable, so the
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.0",
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>/<department levels>/<username>, one folder per level). Admin-only:
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>/<department>/<username>. Empty (the default) asks the
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.
@@ -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>/<department levels>/<username>` (one folder
8
- * per department level); the admin may type any other absolute path instead. This module owns the pure half of that — the
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>/<一级>/<二级>/…/<username>`.
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