@oj-module/cli 0.2.53 → 0.2.54

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 (96) hide show
  1. package/devkit/SKILL.md +2 -2
  2. package/devkit/manual.md +32 -25
  3. package/package.json +2 -2
  4. package/shell-dist/assets/runtime.js +7 -4
  5. package/shell-dist/index.html +3 -3
  6. package/shell-dist/versions.json +3 -3
  7. package/src/__tests__/templates-tenant-scope.test.ts +3 -2
  8. package/src/config.ts +9 -0
  9. package/src/contract/__tests__/client-path.test.ts +30 -0
  10. package/src/contract/__tests__/emit-client.test.ts +18 -0
  11. package/src/contract/__tests__/handler-scan.test.ts +23 -0
  12. package/src/contract/check.ts +5 -5
  13. package/src/contract/client-path.ts +30 -0
  14. package/src/contract/emit-client.ts +18 -4
  15. package/src/contract/handler-scan.ts +40 -3
  16. package/src/contract/run.ts +5 -3
  17. package/src/init.ts +19 -1
  18. package/templates/README.md +40 -44
  19. package/templates/init.include.yaml +12 -8
  20. package/templates/openapi.json +1393 -34
  21. package/templates/src/_platform/api/contacts/api.ts +29 -1
  22. package/templates/src/_platform/api/contacts/detail/api.ts +25 -1
  23. package/templates/src/_platform/api/solutions/api.ts +60 -1
  24. package/templates/src/_platform/web/__client/api.schemas.ts +99 -35
  25. package/templates/src/_platform/web/__client/api.ts +103 -59
  26. package/templates/src/app/web/entry.ts +77 -0
  27. package/templates/src/{home/web → app/web/home}/__client/api.ts +5 -2
  28. package/templates/src/app/web/home/routes.ts +41 -0
  29. package/templates/src/app/web/login/routes.ts +71 -0
  30. package/templates/src/{notification/web → app/web/notification}/__client/api.schemas.ts +14 -0
  31. package/templates/src/{notification/web → app/web/notification}/__client/api.ts +67 -20
  32. package/templates/src/app/web/notification/routes.ts +70 -0
  33. package/templates/src/{personal-center/web → app/web/personal-center}/__client/api.ts +5 -2
  34. package/templates/src/app/web/personal-center/routes.ts +49 -0
  35. package/templates/src/{system/web → app/web/system}/__client/api.schemas.ts +28 -22
  36. package/templates/src/{system/web → app/web/system}/__client/api.ts +103 -58
  37. package/templates/src/{system/web → app/web/system}/pages/users/index.tsx +3 -3
  38. package/templates/src/{system/web → app/web/system}/pages/wecom/index.tsx +12 -3
  39. package/templates/src/{system/web/entry.ts → app/web/system/routes.ts} +24 -35
  40. package/templates/src/auth/api/api.ts +59 -0
  41. package/templates/src/auth/api/ldap-login/api.ts +37 -1
  42. package/templates/src/auth/api/wecom/api.ts +23 -0
  43. package/templates/src/auth/web/__client/api.schemas.ts +133 -13
  44. package/templates/src/auth/web/__client/api.ts +129 -31
  45. package/templates/src/notification/api/api.ts +17 -0
  46. package/templates/src/notification/api/del/api.ts +2 -0
  47. package/templates/src/system/api/roles/api.ts +3 -0
  48. package/templates/src/system/api/roles/bindings/api.ts +3 -0
  49. package/templates/src/system/api/settings/api.ts +3 -0
  50. package/templates/src/system/api/users/forbidden/api.ts +3 -0
  51. package/templates/src/system/api/users/password/api.ts +3 -0
  52. package/templates/src/system/api/users/roles/api.ts +3 -0
  53. package/templates/src/web/api/get-async-routes/api.ts +16 -1
  54. package/templates/src/web/api/hello/api.ts +13 -1
  55. package/templates/src/web/api/user-info/api.ts +21 -1
  56. package/templates/src/web/web/__client/api.schemas.ts +28 -0
  57. package/templates/src/web/web/__client/api.ts +33 -9
  58. package/templates/web.config.ts +21 -15
  59. package/vendor/host-versions.json +3 -3
  60. package/templates/src/home/web/entry.ts +0 -49
  61. package/templates/src/login/web/entry.ts +0 -79
  62. package/templates/src/notification/web/entry.ts +0 -84
  63. package/templates/src/personal-center/web/entry.ts +0 -60
  64. /package/templates/src/{home/web → app/web/home}/__client/api.schemas.ts +0 -0
  65. /package/templates/src/{home/web → app/web/home}/locales/en-US.json +0 -0
  66. /package/templates/src/{home/web → app/web/home}/locales/zh-CN.json +0 -0
  67. /package/templates/src/{home/web → app/web/home}/pages/components/bar-chart.tsx +0 -0
  68. /package/templates/src/{home/web → app/web/home}/pages/components/card-list.tsx +0 -0
  69. /package/templates/src/{home/web → app/web/home}/pages/components/line-chart.tsx +0 -0
  70. /package/templates/src/{home/web → app/web/home}/pages/components/pie-chart.tsx +0 -0
  71. /package/templates/src/{home/web → app/web/home}/pages/index.tsx +0 -0
  72. /package/templates/src/{login/web → app/web/login}/auth-provider.ts +0 -0
  73. /package/templates/src/{login/web → app/web/login}/locales/en-US.json +0 -0
  74. /package/templates/src/{login/web → app/web/login}/locales/zh-CN.json +0 -0
  75. /package/templates/src/{login/web → app/web/login}/pages/change-password.tsx +0 -0
  76. /package/templates/src/{login/web → app/web/login}/pages/login.tsx +0 -0
  77. /package/templates/src/{login/web → app/web/login}/wecom.ts +0 -0
  78. /package/templates/src/{notification/web → app/web/notification}/locales/en-US.json +0 -0
  79. /package/templates/src/{notification/web → app/web/notification}/locales/zh-CN.json +0 -0
  80. /package/templates/src/{notification/web → app/web/notification}/pages/center/index.tsx +0 -0
  81. /package/templates/src/{personal-center/web → app/web/personal-center}/__client/api.schemas.ts +0 -0
  82. /package/templates/src/{personal-center/web → app/web/personal-center}/locales/en-US.json +0 -0
  83. /package/templates/src/{personal-center/web → app/web/personal-center}/locales/zh-CN.json +0 -0
  84. /package/templates/src/{personal-center/web → app/web/personal-center}/pages/my-profile/index.tsx +0 -0
  85. /package/templates/src/{system/web → app/web/system}/locales/en-US.json +0 -0
  86. /package/templates/src/{system/web → app/web/system}/locales/zh-CN.json +0 -0
  87. /package/templates/src/{system/web → app/web/system}/pages/departments/index.tsx +0 -0
  88. /package/templates/src/{system/web → app/web/system}/pages/departments/sync-bar.tsx +0 -0
  89. /package/templates/src/{system/web → app/web/system}/pages/departments/tree.ts +0 -0
  90. /package/templates/src/{system/web → app/web/system}/pages/errors.ts +0 -0
  91. /package/templates/src/{system/web → app/web/system}/pages/forbidden.tsx +0 -0
  92. /package/templates/src/{system/web → app/web/system}/pages/login-methods/index.tsx +0 -0
  93. /package/templates/src/{system/web → app/web/system}/pages/oplog/index.tsx +0 -0
  94. /package/templates/src/{system/web → app/web/system}/pages/roles/index.tsx +0 -0
  95. /package/templates/src/{system/web → app/web/system}/pages/settings/index.tsx +0 -0
  96. /package/templates/src/{system/web → app/web/system}/pages/users/roles.tsx +0 -0
@@ -48,17 +48,6 @@ export const schemas = {
48
48
  }),
49
49
  data: z.object({
50
50
  "ok": z.boolean()
51
- }),
52
- },
53
- "systemDepartmentsMovePut": {
54
- query: z.object({
55
- "id": z.number()
56
- }),
57
- body: z.object({
58
- "parentId": z.number().nullable()
59
- }),
60
- data: z.object({
61
- "ok": z.boolean()
62
51
  }),
63
52
  },
64
53
  "systemDepartmentsDetachPost": {
@@ -118,6 +107,17 @@ export const schemas = {
118
107
  }),
119
108
  data: z.object({
120
109
  "ok": z.boolean()
110
+ }),
111
+ },
112
+ "systemDepartmentsMovePut": {
113
+ query: z.object({
114
+ "id": z.number()
115
+ }),
116
+ body: z.object({
117
+ "parentId": z.number().nullable()
118
+ }),
119
+ data: z.object({
120
+ "ok": z.boolean()
121
121
  }),
122
122
  },
123
123
  "systemLoginMethodsGet": {
@@ -197,6 +197,7 @@ export const schemas = {
197
197
  query: z.object({
198
198
  "name": z.string().min(1)
199
199
  }),
200
+ data: z.null(),
200
201
  },
201
202
  "systemRolesBindingsPut": {
202
203
  query: z.object({
@@ -205,6 +206,7 @@ export const schemas = {
205
206
  body: z.object({
206
207
  "permissions": z.array(z.string())
207
208
  }),
209
+ data: z.null(),
208
210
  },
209
211
  "systemSettingsGet": {
210
212
  data: z.array(z.object({
@@ -228,6 +230,7 @@ export const schemas = {
228
230
  "key": z.string().min(1),
229
231
  "value": z.string()
230
232
  }),
233
+ data: z.null(),
231
234
  },
232
235
  "systemUsersGet": {
233
236
  data: z.array(z.object({
@@ -265,33 +268,35 @@ export const schemas = {
265
268
  "ok": z.boolean()
266
269
  }),
267
270
  },
268
- "systemUsersProfilePut": {
271
+ "systemUsersForbiddenPut": {
269
272
  query: z.object({
270
273
  "uid": z.number()
271
274
  }),
272
275
  body: z.object({
273
- "displayName": z.string().nullable().optional(),
274
- "email": z.string().nullable().optional(),
275
- "position": z.string().nullable().optional()
276
- }),
277
- data: z.object({
278
- "ok": z.boolean()
276
+ "forbidden": z.boolean()
279
277
  }),
278
+ data: z.null(),
280
279
  },
281
- "systemUsersForbiddenPut": {
280
+ "systemUsersPasswordPut": {
282
281
  query: z.object({
283
282
  "uid": z.number()
284
283
  }),
285
284
  body: z.object({
286
- "forbidden": z.boolean()
285
+ "password": z.string().min(6)
287
286
  }),
287
+ data: z.null(),
288
288
  },
289
- "systemUsersPasswordPut": {
289
+ "systemUsersProfilePut": {
290
290
  query: z.object({
291
291
  "uid": z.number()
292
292
  }),
293
293
  body: z.object({
294
- "password": z.string().min(6)
294
+ "displayName": z.string().nullable().optional(),
295
+ "email": z.string().nullable().optional(),
296
+ "position": z.string().nullable().optional()
297
+ }),
298
+ data: z.object({
299
+ "ok": z.boolean()
295
300
  }),
296
301
  },
297
302
  "systemUsersRolesPut": {
@@ -301,5 +306,6 @@ export const schemas = {
301
306
  body: z.object({
302
307
  "roles": z.array(z.string())
303
308
  }),
309
+ data: z.null(),
304
310
  },
305
311
  };
@@ -17,6 +17,9 @@ export const API_PREFIX = "/system";
17
17
  /** 认领身份前缀(契约 apiPrefix,字面等于模块目录名):跨模块认领护栏对比基准 */
18
18
  export const MODULE_PREFIX = "/system";
19
19
 
20
+ /** 认领方模块(web.config clientOwners 指定):跨模块认领护栏对比基准 */
21
+ export const MODULE_OWNER = "/app";
22
+
20
23
  /**
21
24
  * 模块 entry 的 onInit 里一行创建(构造即 install):完成 apiPrefix 登记 +
22
25
  * scoped request 绑定,返回绑好前缀的端点函数集合。一模块一 client;
@@ -28,12 +31,12 @@ export function createSystemClient(ctx: ModuleContext) {
28
31
  // 单槽、卸载互相耦合;跨模块需求应由本模块创建后经 provider 暴露。
29
32
  // 注意对比基准是 MODULE_PREFIX(目录名)而非 API_PREFIX——D-M8 映射时线上前缀
30
33
  // 属于别的命名空间,认领身份仍按目录名。
31
- if (import.meta.env.DEV && ctx.module.name !== MODULE_PREFIX.slice(1)) {
32
- console.warn(`[ojm-api] 模块 "${ctx.module.name}" 正在认领 ${MODULE_PREFIX} 的 client——跨模块认领会共享 request 单槽、卸载互相耦合;应由 "${MODULE_PREFIX.slice(1)}" 模块在自身 onInit 创建,跨模块需求经 provider 暴露。`);
34
+ if (import.meta.env.DEV && ctx.module.name !== MODULE_OWNER.slice(1)) {
35
+ console.warn(`[ojm-api] 模块 "${ctx.module.name}" 正在认领 ${MODULE_PREFIX} 的 client——跨模块认领会共享 request 单槽、卸载互相耦合;应由 "${MODULE_OWNER.slice(1)}" 模块在自身 onInit 创建,跨模块需求经 provider 暴露。`);
33
36
  }
34
37
  ctx.register.apiPrefix(API_PREFIX);
35
38
  bindRequest(ctx.utils.request);
36
- return { systemDepartmentsGet, systemDepartmentsPost, systemDepartmentsPut, systemDepartmentsDelete, systemDepartmentsMovePut, systemDepartmentsDetachPost, systemDepartmentsMembersGet, systemDepartmentsMembersPost, systemDepartmentsMembersPut, systemDepartmentsMembersDelete, systemLoginMethodsGet, systemOplogGet, systemOplogPost, systemPermissionsGet, systemRolesGet, systemRolesPost, systemRolesDelete, systemRolesBindingsPut, systemSettingsGet, systemSettingsPut, systemUsersGet, systemUsersPost, systemUsersDelete, systemUsersProfilePut, systemUsersForbiddenPut, systemUsersPasswordPut, systemUsersRolesPut };
39
+ return { systemDepartmentsGet, systemDepartmentsPost, systemDepartmentsPut, systemDepartmentsDelete, systemDepartmentsDetachPost, systemDepartmentsMembersGet, systemDepartmentsMembersPost, systemDepartmentsMembersPut, systemDepartmentsMembersDelete, systemDepartmentsMovePut, systemLoginMethodsGet, systemOplogGet, systemOplogPost, systemPermissionsGet, systemRolesGet, systemRolesPost, systemRolesDelete, systemRolesBindingsPut, systemSettingsGet, systemSettingsPut, systemUsersGet, systemUsersPost, systemUsersDelete, systemUsersForbiddenPut, systemUsersPasswordPut, systemUsersProfilePut, systemUsersRolesPut };
37
40
  }
38
41
 
39
42
  /**
@@ -160,31 +163,6 @@ export async function systemDepartmentsDelete(query: SystemDepartmentsDeleteQuer
160
163
  }
161
164
  }
162
165
 
163
- export type SystemDepartmentsMovePutQuery = z.input<(typeof schemas)["systemDepartmentsMovePut"]["query"]>;
164
- export type SystemDepartmentsMovePutBody = z.input<(typeof schemas)["systemDepartmentsMovePut"]["body"]>;
165
- export type SystemDepartmentsMovePutData = z.infer<(typeof schemas)["systemDepartmentsMovePut"]["data"]>;
166
-
167
- export async function systemDepartmentsMovePut(input: { query: SystemDepartmentsMovePutQuery, body: SystemDepartmentsMovePutBody }): Promise<SystemDepartmentsMovePutData> {
168
- const client = ensureReq();
169
- try {
170
- const env = await client.put(`system/departments/move`, { searchParams: input.query as Record<string, string | number | boolean>, json: input.body }).json<OjEnvelope<SystemDepartmentsMovePutData>>();
171
- // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
172
- if (typeof env.code === "number" && env.code !== 0)
173
- throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
174
- const data = env.data as SystemDepartmentsMovePutData;
175
- if (import.meta.env.DEV) {
176
- const { schemas } = await import("./api.schemas");
177
- const r = schemas["systemDepartmentsMovePut"].data.safeParse(data);
178
- if (!r.success)
179
- throw new ContractApiError(-1, `[契约违例] systemDepartmentsMovePut 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
180
- }
181
- return data;
182
- }
183
- catch (e) {
184
- throw await toApiError(e);
185
- }
186
- }
187
-
188
166
  export type SystemDepartmentsDetachPostQuery = z.input<(typeof schemas)["systemDepartmentsDetachPost"]["query"]>;
189
167
  export type SystemDepartmentsDetachPostData = z.infer<(typeof schemas)["systemDepartmentsDetachPost"]["data"]>;
190
168
 
@@ -307,6 +285,31 @@ export async function systemDepartmentsMembersDelete(query: SystemDepartmentsMem
307
285
  }
308
286
  }
309
287
 
288
+ export type SystemDepartmentsMovePutQuery = z.input<(typeof schemas)["systemDepartmentsMovePut"]["query"]>;
289
+ export type SystemDepartmentsMovePutBody = z.input<(typeof schemas)["systemDepartmentsMovePut"]["body"]>;
290
+ export type SystemDepartmentsMovePutData = z.infer<(typeof schemas)["systemDepartmentsMovePut"]["data"]>;
291
+
292
+ export async function systemDepartmentsMovePut(input: { query: SystemDepartmentsMovePutQuery, body: SystemDepartmentsMovePutBody }): Promise<SystemDepartmentsMovePutData> {
293
+ const client = ensureReq();
294
+ try {
295
+ const env = await client.put(`system/departments/move`, { searchParams: input.query as Record<string, string | number | boolean>, json: input.body }).json<OjEnvelope<SystemDepartmentsMovePutData>>();
296
+ // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
297
+ if (typeof env.code === "number" && env.code !== 0)
298
+ throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
299
+ const data = env.data as SystemDepartmentsMovePutData;
300
+ if (import.meta.env.DEV) {
301
+ const { schemas } = await import("./api.schemas");
302
+ const r = schemas["systemDepartmentsMovePut"].data.safeParse(data);
303
+ if (!r.success)
304
+ throw new ContractApiError(-1, `[契约违例] systemDepartmentsMovePut 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
305
+ }
306
+ return data;
307
+ }
308
+ catch (e) {
309
+ throw await toApiError(e);
310
+ }
311
+ }
312
+
310
313
  export type SystemLoginMethodsGetData = z.infer<(typeof schemas)["systemLoginMethodsGet"]["data"]>;
311
314
 
312
315
  export async function systemLoginMethodsGet(): Promise<SystemLoginMethodsGetData> {
@@ -449,15 +452,22 @@ export async function systemRolesPost(body: SystemRolesPostBody): Promise<System
449
452
  }
450
453
 
451
454
  export type SystemRolesDeleteQuery = z.input<(typeof schemas)["systemRolesDelete"]["query"]>;
455
+ export type SystemRolesDeleteData = z.infer<(typeof schemas)["systemRolesDelete"]["data"]>;
452
456
 
453
- export async function systemRolesDelete(query: SystemRolesDeleteQuery): Promise<unknown> {
457
+ export async function systemRolesDelete(query: SystemRolesDeleteQuery): Promise<SystemRolesDeleteData> {
454
458
  const client = ensureReq();
455
459
  try {
456
- const env = await client.delete(`system/roles`, { searchParams: query as Record<string, string | number | boolean> }).json<OjEnvelope<unknown>>();
460
+ const env = await client.delete(`system/roles`, { searchParams: query as Record<string, string | number | boolean> }).json<OjEnvelope<SystemRolesDeleteData>>();
457
461
  // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
458
462
  if (typeof env.code === "number" && env.code !== 0)
459
463
  throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
460
- const data = env.data as unknown;
464
+ const data = env.data as SystemRolesDeleteData;
465
+ if (import.meta.env.DEV) {
466
+ const { schemas } = await import("./api.schemas");
467
+ const r = schemas["systemRolesDelete"].data.safeParse(data);
468
+ if (!r.success)
469
+ throw new ContractApiError(-1, `[契约违例] systemRolesDelete 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
470
+ }
461
471
  return data;
462
472
  }
463
473
  catch (e) {
@@ -467,15 +477,22 @@ export async function systemRolesDelete(query: SystemRolesDeleteQuery): Promise<
467
477
 
468
478
  export type SystemRolesBindingsPutQuery = z.input<(typeof schemas)["systemRolesBindingsPut"]["query"]>;
469
479
  export type SystemRolesBindingsPutBody = z.input<(typeof schemas)["systemRolesBindingsPut"]["body"]>;
480
+ export type SystemRolesBindingsPutData = z.infer<(typeof schemas)["systemRolesBindingsPut"]["data"]>;
470
481
 
471
- export async function systemRolesBindingsPut(input: { query: SystemRolesBindingsPutQuery, body: SystemRolesBindingsPutBody }): Promise<unknown> {
482
+ export async function systemRolesBindingsPut(input: { query: SystemRolesBindingsPutQuery, body: SystemRolesBindingsPutBody }): Promise<SystemRolesBindingsPutData> {
472
483
  const client = ensureReq();
473
484
  try {
474
- const env = await client.put(`system/roles/bindings`, { searchParams: input.query as Record<string, string | number | boolean>, json: input.body }).json<OjEnvelope<unknown>>();
485
+ const env = await client.put(`system/roles/bindings`, { searchParams: input.query as Record<string, string | number | boolean>, json: input.body }).json<OjEnvelope<SystemRolesBindingsPutData>>();
475
486
  // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
476
487
  if (typeof env.code === "number" && env.code !== 0)
477
488
  throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
478
- const data = env.data as unknown;
489
+ const data = env.data as SystemRolesBindingsPutData;
490
+ if (import.meta.env.DEV) {
491
+ const { schemas } = await import("./api.schemas");
492
+ const r = schemas["systemRolesBindingsPut"].data.safeParse(data);
493
+ if (!r.success)
494
+ throw new ContractApiError(-1, `[契约违例] systemRolesBindingsPut 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
495
+ }
479
496
  return data;
480
497
  }
481
498
  catch (e) {
@@ -507,15 +524,22 @@ export async function systemSettingsGet(): Promise<SystemSettingsGetData> {
507
524
  }
508
525
 
509
526
  export type SystemSettingsPutBody = z.input<(typeof schemas)["systemSettingsPut"]["body"]>;
527
+ export type SystemSettingsPutData = z.infer<(typeof schemas)["systemSettingsPut"]["data"]>;
510
528
 
511
- export async function systemSettingsPut(body: SystemSettingsPutBody): Promise<unknown> {
529
+ export async function systemSettingsPut(body: SystemSettingsPutBody): Promise<SystemSettingsPutData> {
512
530
  const client = ensureReq();
513
531
  try {
514
- const env = await client.put(`system/settings`, { json: body }).json<OjEnvelope<unknown>>();
532
+ const env = await client.put(`system/settings`, { json: body }).json<OjEnvelope<SystemSettingsPutData>>();
515
533
  // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
516
534
  if (typeof env.code === "number" && env.code !== 0)
517
535
  throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
518
- const data = env.data as unknown;
536
+ const data = env.data as SystemSettingsPutData;
537
+ if (import.meta.env.DEV) {
538
+ const { schemas } = await import("./api.schemas");
539
+ const r = schemas["systemSettingsPut"].data.safeParse(data);
540
+ if (!r.success)
541
+ throw new ContractApiError(-1, `[契约违例] systemSettingsPut 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
542
+ }
519
543
  return data;
520
544
  }
521
545
  catch (e) {
@@ -594,23 +618,23 @@ export async function systemUsersDelete(query: SystemUsersDeleteQuery): Promise<
594
618
  }
595
619
  }
596
620
 
597
- export type SystemUsersProfilePutQuery = z.input<(typeof schemas)["systemUsersProfilePut"]["query"]>;
598
- export type SystemUsersProfilePutBody = z.input<(typeof schemas)["systemUsersProfilePut"]["body"]>;
599
- export type SystemUsersProfilePutData = z.infer<(typeof schemas)["systemUsersProfilePut"]["data"]>;
621
+ export type SystemUsersForbiddenPutQuery = z.input<(typeof schemas)["systemUsersForbiddenPut"]["query"]>;
622
+ export type SystemUsersForbiddenPutBody = z.input<(typeof schemas)["systemUsersForbiddenPut"]["body"]>;
623
+ export type SystemUsersForbiddenPutData = z.infer<(typeof schemas)["systemUsersForbiddenPut"]["data"]>;
600
624
 
601
- export async function systemUsersProfilePut(input: { query: SystemUsersProfilePutQuery, body: SystemUsersProfilePutBody }): Promise<SystemUsersProfilePutData> {
625
+ export async function systemUsersForbiddenPut(input: { query: SystemUsersForbiddenPutQuery, body: SystemUsersForbiddenPutBody }): Promise<SystemUsersForbiddenPutData> {
602
626
  const client = ensureReq();
603
627
  try {
604
- const env = await client.put(`system/users/profile`, { searchParams: input.query as Record<string, string | number | boolean>, json: input.body }).json<OjEnvelope<SystemUsersProfilePutData>>();
628
+ const env = await client.put(`system/users/forbidden`, { searchParams: input.query as Record<string, string | number | boolean>, json: input.body }).json<OjEnvelope<SystemUsersForbiddenPutData>>();
605
629
  // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
606
630
  if (typeof env.code === "number" && env.code !== 0)
607
631
  throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
608
- const data = env.data as SystemUsersProfilePutData;
632
+ const data = env.data as SystemUsersForbiddenPutData;
609
633
  if (import.meta.env.DEV) {
610
634
  const { schemas } = await import("./api.schemas");
611
- const r = schemas["systemUsersProfilePut"].data.safeParse(data);
635
+ const r = schemas["systemUsersForbiddenPut"].data.safeParse(data);
612
636
  if (!r.success)
613
- throw new ContractApiError(-1, `[契约违例] systemUsersProfilePut 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
637
+ throw new ContractApiError(-1, `[契约违例] systemUsersForbiddenPut 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
614
638
  }
615
639
  return data;
616
640
  }
@@ -619,17 +643,24 @@ export async function systemUsersProfilePut(input: { query: SystemUsersProfilePu
619
643
  }
620
644
  }
621
645
 
622
- export type SystemUsersForbiddenPutQuery = z.input<(typeof schemas)["systemUsersForbiddenPut"]["query"]>;
623
- export type SystemUsersForbiddenPutBody = z.input<(typeof schemas)["systemUsersForbiddenPut"]["body"]>;
646
+ export type SystemUsersPasswordPutQuery = z.input<(typeof schemas)["systemUsersPasswordPut"]["query"]>;
647
+ export type SystemUsersPasswordPutBody = z.input<(typeof schemas)["systemUsersPasswordPut"]["body"]>;
648
+ export type SystemUsersPasswordPutData = z.infer<(typeof schemas)["systemUsersPasswordPut"]["data"]>;
624
649
 
625
- export async function systemUsersForbiddenPut(input: { query: SystemUsersForbiddenPutQuery, body: SystemUsersForbiddenPutBody }): Promise<unknown> {
650
+ export async function systemUsersPasswordPut(input: { query: SystemUsersPasswordPutQuery, body: SystemUsersPasswordPutBody }): Promise<SystemUsersPasswordPutData> {
626
651
  const client = ensureReq();
627
652
  try {
628
- const env = await client.put(`system/users/forbidden`, { searchParams: input.query as Record<string, string | number | boolean>, json: input.body }).json<OjEnvelope<unknown>>();
653
+ const env = await client.put(`system/users/password`, { searchParams: input.query as Record<string, string | number | boolean>, json: input.body }).json<OjEnvelope<SystemUsersPasswordPutData>>();
629
654
  // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
630
655
  if (typeof env.code === "number" && env.code !== 0)
631
656
  throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
632
- const data = env.data as unknown;
657
+ const data = env.data as SystemUsersPasswordPutData;
658
+ if (import.meta.env.DEV) {
659
+ const { schemas } = await import("./api.schemas");
660
+ const r = schemas["systemUsersPasswordPut"].data.safeParse(data);
661
+ if (!r.success)
662
+ throw new ContractApiError(-1, `[契约违例] systemUsersPasswordPut 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
663
+ }
633
664
  return data;
634
665
  }
635
666
  catch (e) {
@@ -637,17 +668,24 @@ export async function systemUsersForbiddenPut(input: { query: SystemUsersForbidd
637
668
  }
638
669
  }
639
670
 
640
- export type SystemUsersPasswordPutQuery = z.input<(typeof schemas)["systemUsersPasswordPut"]["query"]>;
641
- export type SystemUsersPasswordPutBody = z.input<(typeof schemas)["systemUsersPasswordPut"]["body"]>;
671
+ export type SystemUsersProfilePutQuery = z.input<(typeof schemas)["systemUsersProfilePut"]["query"]>;
672
+ export type SystemUsersProfilePutBody = z.input<(typeof schemas)["systemUsersProfilePut"]["body"]>;
673
+ export type SystemUsersProfilePutData = z.infer<(typeof schemas)["systemUsersProfilePut"]["data"]>;
642
674
 
643
- export async function systemUsersPasswordPut(input: { query: SystemUsersPasswordPutQuery, body: SystemUsersPasswordPutBody }): Promise<unknown> {
675
+ export async function systemUsersProfilePut(input: { query: SystemUsersProfilePutQuery, body: SystemUsersProfilePutBody }): Promise<SystemUsersProfilePutData> {
644
676
  const client = ensureReq();
645
677
  try {
646
- const env = await client.put(`system/users/password`, { searchParams: input.query as Record<string, string | number | boolean>, json: input.body }).json<OjEnvelope<unknown>>();
678
+ const env = await client.put(`system/users/profile`, { searchParams: input.query as Record<string, string | number | boolean>, json: input.body }).json<OjEnvelope<SystemUsersProfilePutData>>();
647
679
  // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
648
680
  if (typeof env.code === "number" && env.code !== 0)
649
681
  throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
650
- const data = env.data as unknown;
682
+ const data = env.data as SystemUsersProfilePutData;
683
+ if (import.meta.env.DEV) {
684
+ const { schemas } = await import("./api.schemas");
685
+ const r = schemas["systemUsersProfilePut"].data.safeParse(data);
686
+ if (!r.success)
687
+ throw new ContractApiError(-1, `[契约违例] systemUsersProfilePut 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
688
+ }
651
689
  return data;
652
690
  }
653
691
  catch (e) {
@@ -657,15 +695,22 @@ export async function systemUsersPasswordPut(input: { query: SystemUsersPassword
657
695
 
658
696
  export type SystemUsersRolesPutQuery = z.input<(typeof schemas)["systemUsersRolesPut"]["query"]>;
659
697
  export type SystemUsersRolesPutBody = z.input<(typeof schemas)["systemUsersRolesPut"]["body"]>;
698
+ export type SystemUsersRolesPutData = z.infer<(typeof schemas)["systemUsersRolesPut"]["data"]>;
660
699
 
661
- export async function systemUsersRolesPut(input: { query: SystemUsersRolesPutQuery, body: SystemUsersRolesPutBody }): Promise<unknown> {
700
+ export async function systemUsersRolesPut(input: { query: SystemUsersRolesPutQuery, body: SystemUsersRolesPutBody }): Promise<SystemUsersRolesPutData> {
662
701
  const client = ensureReq();
663
702
  try {
664
- const env = await client.put(`system/users/roles`, { searchParams: input.query as Record<string, string | number | boolean>, json: input.body }).json<OjEnvelope<unknown>>();
703
+ const env = await client.put(`system/users/roles`, { searchParams: input.query as Record<string, string | number | boolean>, json: input.body }).json<OjEnvelope<SystemUsersRolesPutData>>();
665
704
  // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
666
705
  if (typeof env.code === "number" && env.code !== 0)
667
706
  throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
668
- const data = env.data as unknown;
707
+ const data = env.data as SystemUsersRolesPutData;
708
+ if (import.meta.env.DEV) {
709
+ const { schemas } = await import("./api.schemas");
710
+ const r = schemas["systemUsersRolesPut"].data.safeParse(data);
711
+ if (!r.success)
712
+ throw new ContractApiError(-1, `[契约违例] systemUsersRolesPut 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
713
+ }
669
714
  return data;
670
715
  }
671
716
  catch (e) {
@@ -132,9 +132,9 @@ export default function UsersPage() {
132
132
  <Button size="small" disabled={String(row.id) === me} onClick={() => setPwdTarget(row)}>
133
133
  {t("system:users.resetPwd")}
134
134
  </Button>
135
- {canManageMembership
136
- ? (
137
- <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`)}>
138
138
  {t("system:users.membership")}
139
139
  </Button>
140
140
  )
@@ -191,7 +191,7 @@ export default function WecomAdminPage() {
191
191
  queryKey: ["system", "wecom-tenant-admin", corps.map(c => c.corpId).join(",")],
192
192
  queryFn: async () => {
193
193
  const out: Record<string, TenantAdminState> = {};
194
- await Promise.all(corps.map(async c => {
194
+ await Promise.all(corps.map(async (c) => {
195
195
  out[c.corpId] = await fetchTenantAdmin(c.corpId);
196
196
  }));
197
197
  return out;
@@ -244,7 +244,13 @@ export default function WecomAdminPage() {
244
244
  title: t("system:wecom.tenantAdmin.action"),
245
245
  key: "action",
246
246
  render: (_: unknown, row: { corpId: string, name: string | null }) => (
247
- <Button size="small" onClick={() => { setResetTarget(row); setResetPwd(""); }}>
247
+ <Button
248
+ size="small"
249
+ onClick={() => {
250
+ setResetTarget(row);
251
+ setResetPwd("");
252
+ }}
253
+ >
248
254
  {t("system:wecom.tenantAdmin.reset")}
249
255
  </Button>
250
256
  ),
@@ -433,7 +439,10 @@ export default function WecomAdminPage() {
433
439
  <Modal
434
440
  open={resetTarget != null}
435
441
  title={t("system:wecom.tenantAdmin.resetTitle")}
436
- onCancel={() => { setResetTarget(null); setResetPwd(""); }}
442
+ onCancel={() => {
443
+ setResetTarget(null);
444
+ setResetPwd("");
445
+ }}
437
446
  onOk={() => resetMut.mutate({ corpId: resetTarget!.corpId, password: resetPwd })}
438
447
  confirmLoading={resetMut.isPending}
439
448
  okButtonProps={{ disabled: resetPwd.length < 6 }}
@@ -1,7 +1,6 @@
1
- import type { AppRouteRecordRaw } from "@oj-module/runtime";
1
+ import type { AppRouteRecordRaw, ModuleContext } from "@oj-module/runtime";
2
2
 
3
3
  import { SettingOutlined } from "@ant-design/icons";
4
- import { defineModule } from "@oj-module/runtime";
5
4
 
6
5
  import { createElement, lazy } from "react";
7
6
 
@@ -16,7 +15,24 @@ const Settings = lazy(() => import("./pages/settings"));
16
15
  const LoginMethods = lazy(() => import("./pages/login-methods"));
17
16
  const WecomAdmin = lazy(() => import("./pages/wecom"));
18
17
 
19
- const routes: AppRouteRecordRaw[] = [
18
+ /**
19
+ * `/system` 根的角色门禁(对齐后端 assertSystemAdmin,access.ts:87)——
20
+ * superadmin **或** admin 均可,两者必须同时保留:D12(2026-10-22)起账号名=角色名,
21
+ * admin 与 superadmin 是两个独立账号(seed.sql),只留 ["admin"] 会把 superadmin
22
+ * 挡在门外(注册期过滤 → 路由消失 → 登录后 404 兜底)。改这两个值前先跑
23
+ * tests/system-guard.regression.spec.ts。
24
+ */
25
+ const SYSTEM_ROLES = ["admin", "superadmin"];
26
+
27
+ /**
28
+ * 系统管理根 `/system`:框架管理面(用户/成员资格/角色权限/操作日志/站点设置/登录方式),
29
+ * 数据源为 oj 后端 /api/system/*(模板 api 模块)。__client 由 `ojm api` 生成。
30
+ *
31
+ * 路由级门禁(B16)挂在根上:非系统管理员整棵子树不注入、菜单不可见(等价于原先的
32
+ * 模块级 requiredRoles——聚合后模块级会把 /home 一起挡掉,故必须下移到根路由);
33
+ * API 级 403 由 assertSystemAdmin + service 层硬 enforcement 兜底。
34
+ */
35
+ export const routes: AppRouteRecordRaw[] = [
20
36
  {
21
37
  path: "/system",
22
38
  handle: {
@@ -25,6 +41,7 @@ const routes: AppRouteRecordRaw[] = [
25
41
  // 负 order = 底部固定区:系统管理 pin 在侧边栏底部(2026-10-20 序轴约定)
26
42
  order: -100,
27
43
  layout: "container",
44
+ roles: SYSTEM_ROLES,
28
45
  },
29
46
  children: [
30
47
  {
@@ -74,35 +91,7 @@ const routes: AppRouteRecordRaw[] = [
74
91
  },
75
92
  ];
76
93
 
77
- /**
78
- * system 模块:框架管理面(用户/成员资格/角色权限/操作日志/站点设置/登录方式),
79
- * 数据源为 oj 后端 /api/system/*(模板 api 模块)。__client 不随模板分发——
80
- * init 后 `oj openapi -o openapi.json` + `ojm api` 生成(生成前本文件 ts 报红属预期,
81
- * devkit manual §2.2)。
82
- */
83
- export default defineModule({
84
- name: "system",
85
- description: "系统管理模块(框架管理面)",
86
- version: "0.1.0",
87
- peerRuntime: ">=0.0.0",
88
- // 路由级门禁(B16):非系统管理员不注入 /system 路由、菜单不可见;
89
- // API 级 403 由 assertSystemAdmin + service 层硬 enforcement 兜底。
90
- // 角色口径对齐后端 assertSystemAdmin(access.ts:87)——superadmin **或**
91
- // admin 均可,两者必须同时保留:D12(2026-10-22)起账号名=角色名,admin 与
92
- // superadmin 是两个独立账号(seed.sql),只留 ["admin"] 会把 superadmin 挡在
93
- // 门外(注册期过滤 → 路由消失 → 登录后 404 兜底)。改这两个值前先跑
94
- // tests/system-guard.regression.spec.ts(只留 ["admin"] 会立刻变红)。
95
- config: { requiredRoles: ["admin", "superadmin"] },
96
- routes,
97
- i18n: {
98
- "zh-CN": () => import("./locales/zh-CN.json"),
99
- "en-US": () => import("./locales/en-US.json"),
100
- },
101
- lifecycle: {
102
- async onInit(ctx) {
103
- // 构造即接线:登记 /system 前缀 + 绑定 scoped request(D11 前缀收敛),
104
- // 页面直接 import 生成的端点函数(模块级单槽 req 已由本调用绑定)。
105
- createSystemClient(ctx);
106
- },
107
- },
108
- });
94
+ /** 构造即接线:登记 /system 前缀 + 绑定 scoped request(D11 前缀收敛)。 */
95
+ export async function onInit(ctx: ModuleContext) {
96
+ createSystemClient(ctx);
97
+ }
@@ -9,6 +9,30 @@ import { createSession } from "./_shared/session";
9
9
  // 模块级禁止 createSession():oj build 的 introspect 在无 jwt 的空沙箱里评估模块(§9.4),
10
10
  // runtimeDeps() 读 g.jwt 会直接炸构建——session 在 handler 内按需构造(无状态,纯闭包)。
11
11
 
12
+ /** 会话票据(TokenBundle):登录 / 刷新 / 改密三处同形(D14 + AC1-27) */
13
+ const tokenBundle = {
14
+ type: "object",
15
+ required: ["access_token", "refresh_token", "expires_in", "restricted", "user"],
16
+ properties: {
17
+ access_token: { type: "string" },
18
+ refresh_token: { type: "string" },
19
+ expires_in: { type: "number" },
20
+ restricted: { type: "boolean" },
21
+ user: {
22
+ type: "object",
23
+ required: ["id", "username", "display_name", "avatar_base64", "must_change", "roles"],
24
+ properties: {
25
+ id: { type: "number" },
26
+ username: { type: "string" },
27
+ display_name: { type: ["string", "null"] },
28
+ avatar_base64: { type: ["string", "null"] },
29
+ must_change: { type: "boolean" },
30
+ roles: { type: "array", items: { type: "string" } },
31
+ },
32
+ },
33
+ },
34
+ };
35
+
12
36
  const login = {
13
37
  // POST /api/auth/login —— 本地登录(D14:先验口令后查禁用,三种失败统一 401)。
14
38
  // B5/AC1-26:(IP, username) 连败阈值 → 429 + 指数 backoff;粗粒度 per-IP 窗口防撞库扫描。
@@ -73,4 +97,39 @@ password.put.schema = {
73
97
  },
74
98
  };
75
99
 
100
+ // 入参:本地账号口令(两种失败统一 401,不泄露账号状态——D14)
101
+ login.post.schema = {
102
+ body: {
103
+ type: "object",
104
+ required: ["username", "password"],
105
+ properties: {
106
+ username: { type: "string", minLength: 1 },
107
+ password: { type: "string", minLength: 1 },
108
+ },
109
+ },
110
+ };
111
+ login.post.response = tokenBundle;
112
+
113
+ // 登出:吊销给定 refresh 串(无业务返回体)
114
+ logout.post.schema = {
115
+ body: {
116
+ type: "object",
117
+ required: ["refresh_token"],
118
+ properties: { refresh_token: { type: "string", minLength: 1 } },
119
+ },
120
+ };
121
+ logout.post.response = { type: "null" };
122
+
123
+ // 刷新:旧串轮换(reuse 检测在 service 内)
124
+ refresh.post.schema = {
125
+ body: {
126
+ type: "object",
127
+ required: ["refresh_token"],
128
+ properties: { refresh_token: { type: "string", minLength: 1 } },
129
+ },
130
+ };
131
+ refresh.post.response = tokenBundle;
132
+
133
+ password.put.response = tokenBundle;
134
+
76
135
  export default { login, logout, refresh, password };
@@ -7,7 +7,7 @@ import { createSession } from "../_shared/session";
7
7
 
8
8
  // 模块级禁止 createSession()/runtimeLdapGateway():oj build introspect 的空沙箱无 jwt/axis(§9.4)。
9
9
 
10
- export default {
10
+ const handler = {
11
11
  async post() {
12
12
  const body = http.body || {};
13
13
  const username = String(body.username ?? "");
@@ -27,3 +27,39 @@ export default {
27
27
  });
28
28
  },
29
29
  };
30
+
31
+ // 入参与本地登录同形(username/password);出参同 TokenBundle
32
+ handler.post.schema = {
33
+ body: {
34
+ type: "object",
35
+ required: ["username", "password"],
36
+ properties: {
37
+ username: { type: "string", minLength: 1 },
38
+ password: { type: "string", minLength: 1 },
39
+ },
40
+ },
41
+ };
42
+ handler.post.response = {
43
+ type: "object",
44
+ required: ["access_token", "refresh_token", "expires_in", "restricted", "user"],
45
+ properties: {
46
+ access_token: { type: "string" },
47
+ refresh_token: { type: "string" },
48
+ expires_in: { type: "number" },
49
+ restricted: { type: "boolean" },
50
+ user: {
51
+ type: "object",
52
+ required: ["id", "username", "display_name", "avatar_base64", "must_change", "roles"],
53
+ properties: {
54
+ id: { type: "number" },
55
+ username: { type: "string" },
56
+ display_name: { type: ["string", "null"] },
57
+ avatar_base64: { type: ["string", "null"] },
58
+ must_change: { type: "boolean" },
59
+ roles: { type: "array", items: { type: "string" } },
60
+ },
61
+ },
62
+ },
63
+ };
64
+
65
+ export default handler;