@oj-module/cli 0.2.50 → 0.2.53

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.
Files changed (49) hide show
  1. package/devkit/wecom-internal-app.md +111 -4
  2. package/package.json +2 -2
  3. package/shell-dist/assets/{index-FYt9CjFd.js → index-e_Mp2mb5.js} +5 -1
  4. package/shell-dist/assets/runtime.js +24 -3
  5. package/shell-dist/index.html +9 -12
  6. package/shell-dist/versions.json +3 -3
  7. package/src/build.ts +19 -2
  8. package/src/shared-deps.ts +27 -9
  9. package/src/site-base.ts +1 -1
  10. package/templates/init.include.yaml +4 -7
  11. package/templates/openapi.json +2520 -2412
  12. package/templates/src/_platform/api/_shared/service/domain/users.ts +40 -4
  13. package/templates/src/auth/api/_shared/wecom/admin.ts +4 -0
  14. package/templates/src/auth/api/_shared/wecom/client.ts +9 -2
  15. package/templates/src/auth/api/_shared/wecom/events.ts +3 -0
  16. package/templates/src/auth/api/_shared/wecom/tenant-admin.ts +194 -0
  17. package/templates/src/auth/api/wecom/admin/tenant-admin/api.ts +41 -0
  18. package/templates/src/auth/api/wecom/admin/tenant-admin/password/api.ts +66 -0
  19. package/templates/src/login/web/pages/login.tsx +54 -24
  20. package/templates/src/system/api/settings/api.ts +3 -3
  21. package/templates/src/system/web/entry.ts +4 -4
  22. package/templates/src/system/web/locales/en-US.json +14 -0
  23. package/templates/src/system/web/locales/zh-CN.json +14 -0
  24. package/templates/src/system/web/pages/users/index.tsx +7 -4
  25. package/templates/src/system/web/pages/users/roles.tsx +14 -3
  26. package/templates/src/system/web/pages/wecom/index.tsx +130 -1
  27. package/templates/tsconfig.paths.json +0 -1
  28. package/templates/web.config.ts +1 -2
  29. package/vendor/host-versions.json +3 -3
  30. package/shell-dist/assets/echarts-charts.js +0 -58571
  31. package/shell-dist/assets/echarts-features.js +0 -24057
  32. package/shell-dist/assets/echarts-for-react.js +0 -704
  33. package/shell-dist/assets/echarts.js +0 -82916
  34. package/shell-dist/assets/editor.js +0 -161457
  35. package/shell-dist/assets/vditor-index-css.js +0 -7
  36. package/shell-dist/assets/vditor.js +0 -15223
  37. package/templates/src/demo/api/manifest.yaml +0 -5
  38. package/templates/src/demo/api/migrations/0001__create_todos.sql +0 -9
  39. package/templates/src/demo/api/migrations/0002__tenant.mysql.sql +0 -2
  40. package/templates/src/demo/api/migrations/0002__tenant.sql +0 -2
  41. package/templates/src/demo/api/schema.yaml +0 -12
  42. package/templates/src/demo/api/seed.sql +0 -5
  43. package/templates/src/demo/api/todos/api.ts +0 -59
  44. package/templates/src/demo/web/__client/api.schemas.ts +0 -20
  45. package/templates/src/demo/web/__client/api.ts +0 -89
  46. package/templates/src/demo/web/entry.ts +0 -42
  47. package/templates/src/demo/web/locales/en-US.json +0 -10
  48. package/templates/src/demo/web/locales/zh-CN.json +0 -10
  49. package/templates/src/demo/web/pages/index.tsx +0 -45
@@ -228,6 +228,20 @@
228
228
  "connectConfirm": "接入会依次校验:应用 Secret 能否换 token、能否读通讯录、可见范围内能否读到人。改过企业ID / AgentId 或换过 Secret 都必须重新点一次。",
229
229
  "connectOk": "接入成功:可见范围内 {{depts}} 个部门,示例部门 {{members}} 人",
230
230
  "connectHint": "换过应用 Secret 后必须重新「验证并接入」,否则所有调用都会 40001;同步到多少人取决于「应用可见范围」,要全量请设为全公司。"
231
+ },
232
+ "tenantAdmin": {
233
+ "title": "租户管理员",
234
+ "hint": "每个企业租户(租户键=企业ID)在开通时会自动建一个 admin 账号,用于管理本租户的组织架构、成员与角色。它是**租户级**管理员,看不到别家企业。初始口令随机且无人知晓——需要接管时由你在这里重设一个已知口令,交给对方后对方首次登录会被强制改密。",
235
+ "username": "管理员账号",
236
+ "state": "状态",
237
+ "exists": "已开通",
238
+ "missing": "未开通",
239
+ "action": "操作",
240
+ "reset": "重设口令",
241
+ "resetTitle": "重设租户管理员口令",
242
+ "resetHint": "将重设企业 {{corpId}} 的租户管理员口令。新口令请自行设定并线下告知对方——系统只存 bcrypt,不会也无法回显任何已有口令。重设后对方首次登录必须改密,其旧会话立即失效。",
243
+ "resetPlaceholder": "新口令(至少 6 位)",
244
+ "resetOk": "租户管理员口令已重设"
231
245
  }
232
246
  },
233
247
  "err": {
@@ -19,7 +19,8 @@ type UserRow = SystemUsersGetData[number];
19
19
  /**
20
20
  * 用户管理(C1):GET /system/users 列表 + 建号 / 禁用切换 / 重置口令。
21
21
  * 本人行禁用一切自操作(后端 assertNotSelf 400 兜底,前端不给会失败的按钮,R3);
22
- * 成员资格入口仅 superadmin 可见(成员资格写是 superadmin-only,规格 §4)。
22
+ * 成员资格入口:superadmin 与 **本租户 admin** 均可见(授予 admin 已下放到租户内,
23
+ * 但授予 superadmin 仍只许 superadmin——见 domain/users.ts::assertMembershipAllowed)。
23
24
  * 部门列标出主部门,顶部可按部门筛选(含子部门)——筛选在前端做,名单本来就是一页全量。
24
25
  */
25
26
  export default function UsersPage() {
@@ -30,6 +31,8 @@ export default function UsersPage() {
30
31
  const me = useUserStore(s => s.id);
31
32
  const myRoles = useUserStore(s => s.roles);
32
33
  const isSuper = myRoles.includes("superadmin");
34
+ // 任命本租户管理员已下放到租户内的 admin(提权到 superadmin 仍只许 superadmin)
35
+ const canManageMembership = isSuper || myRoles.includes("admin");
33
36
 
34
37
  const [createOpen, setCreateOpen] = useState(false);
35
38
  const [createForm] = Form.useForm<{ username: string, password: string, displayName?: string, email?: string }>();
@@ -129,9 +132,9 @@ export default function UsersPage() {
129
132
  <Button size="small" disabled={String(row.id) === me} onClick={() => setPwdTarget(row)}>
130
133
  {t("system:users.resetPwd")}
131
134
  </Button>
132
- {isSuper
133
- ? (
134
- <Button size="small" type="link" onClick={() => navigate(`/system/users/${row.id}/roles`)}>
135
+ {canManageMembership
136
+ ? (
137
+ <Button size="small" type="link" onClick={() => navigate(`/system/users/${row.id}/roles`)}>
135
138
  {t("system:users.membership")}
136
139
  </Button>
137
140
  )
@@ -11,9 +11,13 @@ import { explainError, isForbidden } from "../errors";
11
11
  import { ForbiddenPanel } from "../forbidden";
12
12
 
13
13
  /**
14
- * 成员资格(AC1-7,superadmin-only):整集替换某用户角色。后端无 GET 单用户端点,
14
+ * 成员资格(AC1-7):整集替换某用户角色。后端无 GET 单用户端点,
15
15
  * 当前角色从 GET /system/users 列表按 uid 回显;写走 PUT /system/users/roles?uid=。
16
- * admin(非 superadmin)手输 URL 进入后提交会被 service 层 403 → 页面渲染无权限(§3.2)。
16
+ *
17
+ * 可见性已从「superadmin-only」放宽到「本租户 admin 也可以」(见 users/index.tsx 的入口):
18
+ * 授予 admin 已下放到租户内,否则企业租户永远配不出管理员(平台 superadmin 因租户
19
+ * 隔离查不到企业租户的用户)。但 **superadmin 仍只许 superadmin 授**——它握着跨租户
20
+ * 权限,故这里对非 superadmin 直接把该选项置灰,比让他提交后吃 403 清楚。
17
21
  */
18
22
  export default function UserRolesPage() {
19
23
  const { t } = useTranslation();
@@ -22,6 +26,8 @@ export default function UserRolesPage() {
22
26
  const { uid } = useParams();
23
27
  const queryClient = useQueryClient();
24
28
  const me = useUserStore(s => s.id);
29
+ const myRoles = useUserStore(s => s.roles);
30
+ const isSuper = myRoles.includes("superadmin");
25
31
 
26
32
  // null = 尚未改动,回显服务端值;一旦勾选即持本地整集
27
33
  const [selected, setSelected] = useState<string[] | null>(null);
@@ -51,7 +57,12 @@ export default function UserRolesPage() {
51
57
  <Card title={`${t("system:userRoles.title")}: ${user?.username ?? `#${uid}`}`}>
52
58
  <Space direction="vertical">
53
59
  <Checkbox.Group
54
- options={(rolesQuery.data ?? []).map(r => ({ label: `${r.name}${r.deprecated ? "(已废弃)" : ""}`, value: r.name }))}
60
+ options={(rolesQuery.data ?? []).map(r => ({
61
+ label: `${r.name}${r.deprecated ? "(已废弃)" : ""}`,
62
+ value: r.name,
63
+ // 非 superadmin 授不了平台级角色:置灰,提交后 403 太晚也太含糊
64
+ disabled: r.name === "superadmin" && !isSuper,
65
+ }))}
55
66
  value={value}
56
67
  onChange={v => setSelected(v)}
57
68
  />
@@ -1,6 +1,6 @@
1
1
  import { request, useUserStore } from "@oj-module/runtime";
2
2
  import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
3
- import { Alert, App, Button, Card, Descriptions, Popconfirm, QRCode, Space, Table, Tag, Typography } from "antd";
3
+ import { Alert, App, Button, Card, Descriptions, Input, Modal, Popconfirm, QRCode, Space, Table, Tag, Typography } from "antd";
4
4
  import { useEffect, useState } from "react";
5
5
  import { useTranslation } from "react-i18next";
6
6
 
@@ -47,6 +47,16 @@ interface ConnectResult {
47
47
  members: number
48
48
  }
49
49
 
50
+ /** GET /auth/wecom/admin/tenant-admin:某企业的租户管理员状态 */
51
+ interface TenantAdminState {
52
+ corpId: string
53
+ username: string
54
+ /** 该租户是否已有一个握着 admin 角色的账号 */
55
+ exists: boolean
56
+ /** 是否须改密(开通后恒 true,直到本人改过一次) */
57
+ mustChange: boolean
58
+ }
59
+
50
60
  /**
51
61
  * 端点不在本模块的生成 client 里(`__client` 按模块切分),故直接用 runtime 的 ky 出口。
52
62
  * 路径不带前导 `/`:与 request 的 baseURL 拼接口径一致(fetchLogin 同款)。
@@ -80,6 +90,24 @@ async function connectCorp(): Promise<ConnectResult> {
80
90
  return env.data;
81
91
  }
82
92
 
93
+ async function fetchTenantAdmin(corpId: string): Promise<TenantAdminState> {
94
+ const env = await request.get("auth/wecom/admin/tenant-admin", { searchParams: { corpId } }).json<{ code: number, data?: TenantAdminState }>();
95
+ return env.data ?? { corpId, username: "admin", exists: false, mustChange: false };
96
+ }
97
+
98
+ /**
99
+ * 重设租户管理员口令。
100
+ *
101
+ * 口令**不由系统生成**:让 superadmin 自己输入,他知道这个值 → 直接告知对方即可。
102
+ * 后端只存 bcrypt,没有任何可逆副本,所以「系统生成后回显」这条路本来也走不通。
103
+ */
104
+ async function resetTenantAdminPassword(input: { corpId: string, password: string }): Promise<TenantAdminState> {
105
+ const env = await request.post("auth/wecom/admin/tenant-admin/password", { json: input }).json<{ code: number, data?: TenantAdminState }>();
106
+ if (!env.data)
107
+ throw new Error("企业微信未返回重设结果");
108
+ return env.data;
109
+ }
110
+
83
111
  /**
84
112
  * 企微后台(superadmin 独占),两块卡片按「本部署配了哪套凭据」决定显不显示:
85
113
  * · 代开发:已授权企业 × 应用清单、启停、开通入口(点「生成开通链接」→ 后端调
@@ -155,6 +183,74 @@ export default function WecomAdminPage() {
155
183
  onError: e => message.error(explainError(e, t)),
156
184
  });
157
185
 
186
+ // 企业清单按 corp_id 去重:租户键就是 corp_id,同一企业的多个应用共用一个租户管理员
187
+ const corps = [...new Map((appsQuery.data ?? []).map(r => [r.corp_id, { corpId: r.corp_id, name: r.corp_name }])).values()];
188
+
189
+ // 租户管理员状态:一个企业一条查询并并发取(企业数量级很小,不值得为它加批量端点)
190
+ const tenantAdminQuery = useQuery({
191
+ queryKey: ["system", "wecom-tenant-admin", corps.map(c => c.corpId).join(",")],
192
+ queryFn: async () => {
193
+ const out: Record<string, TenantAdminState> = {};
194
+ await Promise.all(corps.map(async c => {
195
+ out[c.corpId] = await fetchTenantAdmin(c.corpId);
196
+ }));
197
+ return out;
198
+ },
199
+ enabled: isSuper && corps.length > 0,
200
+ retry: false,
201
+ });
202
+
203
+ const [resetTarget, setResetTarget] = useState<{ corpId: string, name: string | null } | null>(null);
204
+ const [resetPwd, setResetPwd] = useState("");
205
+
206
+ const resetMut = useMutation({
207
+ mutationFn: resetTenantAdminPassword,
208
+ onSuccess: () => {
209
+ message.success(t("system:wecom.tenantAdmin.resetOk"));
210
+ setResetTarget(null);
211
+ setResetPwd("");
212
+ queryClient.invalidateQueries({ queryKey: ["system", "wecom-tenant-admin"] });
213
+ },
214
+ onError: e => message.error(explainError(e, t)),
215
+ });
216
+
217
+ const tenantAdminColumns = [
218
+ {
219
+ title: t("system:wecom.col.corp"),
220
+ dataIndex: "corpId",
221
+ render: (_: string, row: { corpId: string, name: string | null }) => (
222
+ <div>
223
+ <div>{row.corpId}</div>
224
+ {row.name && <Typography.Text type="secondary">{row.name}</Typography.Text>}
225
+ </div>
226
+ ),
227
+ },
228
+ {
229
+ title: t("system:wecom.tenantAdmin.username"),
230
+ dataIndex: "username",
231
+ render: (_: unknown, row: { corpId: string }) => tenantAdminQuery.data?.[row.corpId]?.username ?? "-",
232
+ },
233
+ {
234
+ title: t("system:wecom.tenantAdmin.state"),
235
+ dataIndex: "exists",
236
+ render: (_: unknown, row: { corpId: string }) => {
237
+ const s = tenantAdminQuery.data?.[row.corpId];
238
+ return s?.exists
239
+ ? <Tag color="green">{t("system:wecom.tenantAdmin.exists")}</Tag>
240
+ : <Tag color="orange">{t("system:wecom.tenantAdmin.missing")}</Tag>;
241
+ },
242
+ },
243
+ {
244
+ title: t("system:wecom.tenantAdmin.action"),
245
+ key: "action",
246
+ render: (_: unknown, row: { corpId: string, name: string | null }) => (
247
+ <Button size="small" onClick={() => { setResetTarget(row); setResetPwd(""); }}>
248
+ {t("system:wecom.tenantAdmin.reset")}
249
+ </Button>
250
+ ),
251
+ },
252
+ ];
253
+
158
254
  if (!isSuper)
159
255
  return <ForbiddenPanel t={t} />;
160
256
 
@@ -319,6 +415,39 @@ export default function WecomAdminPage() {
319
415
  </Typography.Paragraph>
320
416
  </Card>
321
417
  )}
418
+ {corps.length > 0 && (
419
+ <Card title={t("system:wecom.tenantAdmin.title")}>
420
+ <Typography.Paragraph type="secondary">
421
+ {t("system:wecom.tenantAdmin.hint")}
422
+ </Typography.Paragraph>
423
+ <Table
424
+ rowKey={row => row.corpId}
425
+ size="small"
426
+ loading={tenantAdminQuery.isLoading}
427
+ dataSource={corps}
428
+ columns={tenantAdminColumns}
429
+ pagination={false}
430
+ />
431
+ </Card>
432
+ )}
433
+ <Modal
434
+ open={resetTarget != null}
435
+ title={t("system:wecom.tenantAdmin.resetTitle")}
436
+ onCancel={() => { setResetTarget(null); setResetPwd(""); }}
437
+ onOk={() => resetMut.mutate({ corpId: resetTarget!.corpId, password: resetPwd })}
438
+ confirmLoading={resetMut.isPending}
439
+ okButtonProps={{ disabled: resetPwd.length < 6 }}
440
+ destroyOnHidden
441
+ >
442
+ <Typography.Paragraph type="secondary">
443
+ {t("system:wecom.tenantAdmin.resetHint", { corpId: resetTarget?.corpId ?? "" })}
444
+ </Typography.Paragraph>
445
+ <Input.Password
446
+ value={resetPwd}
447
+ onChange={e => setResetPwd(e.target.value)}
448
+ placeholder={t("system:wecom.tenantAdmin.resetPlaceholder")}
449
+ />
450
+ </Modal>
322
451
  <Card title={t("system:wecom.appsTitle")}>
323
452
  <Typography.Paragraph type="secondary" style={{ marginBottom: 12 }}>
324
453
  {t("system:wecom.authModeHint")}
@@ -8,7 +8,6 @@
8
8
  "paths": {
9
9
  "#/_platform/*": ["./src/_platform/api/*"],
10
10
  "#/auth/*": ["./src/auth/api/*"],
11
- "#/demo/*": ["./src/demo/api/*"],
12
11
  "#/home/*": ["./src/home/api/*"],
13
12
  "#/notification/*": ["./src/notification/api/*"],
14
13
  "#/personal-center/*": ["./src/personal-center/api/*"],
@@ -30,14 +30,13 @@ const allModules = [
30
30
  // R29:第三方方案不得仿照此调用面),默认随脚手架下发
31
31
  { name: "oj-market", entry: "src/oj-market/web/entry.ts" },
32
32
  // #region demo-only
33
- // 以下为模板内置的演示模块(demo/about/access/exception/outside/route-nest),
33
+ // 以下为模板内置的演示模块(about/access/exception/outside/route-nest),
34
34
  // 用于把 templates 当可运行 demo 直接跑;其文件不在 `ojm init` 的
35
35
  // init.include.yaml 白名单里,不拷入新工程,故本区间连同这些条目会被
36
36
  // init 一并剥离,脚手架产出的配置不会残留指向缺失文件的死条目。
37
37
  //
38
38
  // 注意:新布局的清单是「扫描 + 覆盖」并集——注释掉不会让它们消失,
39
39
  // 扫描仍会捡到 `src/<name>/web/entry.ts`。要「不构建」必须写 `enabled: false`。
40
- { name: "demo", entry: "src/demo/web/entry.ts", enabled: false },
41
40
  { name: "about", entry: "src/about/web/entry.ts", enabled: false },
42
41
  { name: "access", entry: "src/access/web/entry.ts", enabled: false },
43
42
  { name: "exception", entry: "src/exception/web/entry.ts", enabled: false },
@@ -8,9 +8,9 @@
8
8
  "react-router": "7.18.2",
9
9
  "react-router/dom": "7.18.2",
10
10
  "@tanstack/react-query": "5.102.8",
11
- "@oj-module/runtime": "0.2.50",
12
- "@oj-module/runtime/contract": "0.2.50",
13
- "@oj-module/runtime/contract/errors": "0.2.50",
11
+ "@oj-module/runtime": "0.2.53",
12
+ "@oj-module/runtime/contract": "0.2.53",
13
+ "@oj-module/runtime/contract/errors": "0.2.53",
14
14
  "@plane/kit": "0.1.20",
15
15
  "@plane/editor": "0.1.20",
16
16
  "antd": "6.6.1",