@oj-module/cli 0.2.53 → 0.2.56

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 (330) hide show
  1. package/devkit/SKILL.md +4 -4
  2. package/devkit/manual.md +79 -65
  3. package/package.json +2 -2
  4. package/shell-dist/assets/{index-e_Mp2mb5.js → index-CImwskAz.js} +24 -11
  5. package/shell-dist/assets/runtime.js +7 -4
  6. package/shell-dist/index.html +3 -3
  7. package/shell-dist/versions.json +3 -3
  8. package/src/__tests__/layout.test.ts +30 -1
  9. package/src/__tests__/templates-schema-columns.test.ts +99 -0
  10. package/src/__tests__/templates-tenant-scope.test.ts +3 -2
  11. package/src/alias-paths.ts +11 -12
  12. package/src/builtin.ts +10 -3
  13. package/src/config.ts +9 -0
  14. package/src/contract/__tests__/client-path.test.ts +30 -0
  15. package/src/contract/__tests__/emit-client.test.ts +20 -2
  16. package/src/contract/__tests__/handler-scan.test.ts +23 -0
  17. package/src/contract/__tests__/market-client.integration.test.ts +2 -2
  18. package/src/contract/__tests__/run-api.test.ts +2 -2
  19. package/src/contract/check.ts +11 -7
  20. package/src/contract/client-path.ts +30 -0
  21. package/src/contract/emit-client.ts +27 -6
  22. package/src/contract/handler-scan.ts +40 -3
  23. package/src/contract/run.ts +15 -5
  24. package/src/init.ts +29 -3
  25. package/src/layout.ts +72 -16
  26. package/src/lifecycle/__tests__/manifest-contract.test.ts +2 -2
  27. package/src/market/__tests__/bootstrap.test.ts +1 -1
  28. package/src/site.ts +1 -1
  29. package/templates/README.md +49 -43
  30. package/templates/init.include.yaml +31 -19
  31. package/templates/openapi.json +1488 -120
  32. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/domain/users.ts +27 -0
  33. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/infra/oplog.ts +4 -2
  34. package/templates/src/{_platform/api → app/api/_platform}/contacts/api.ts +29 -1
  35. package/templates/src/{_platform/api → app/api/_platform}/contacts/detail/api.ts +25 -1
  36. package/templates/src/app/api/_platform/migrations/0017__users_employee_no.mysql.sql +7 -0
  37. package/templates/src/app/api/_platform/migrations/0017__users_employee_no.pg.sql +4 -0
  38. package/templates/src/app/api/_platform/migrations/0017__users_employee_no.sql +8 -0
  39. package/templates/src/{_platform/api → app/api/_platform}/schema.yaml +5 -0
  40. package/templates/src/{_platform/api → app/api/_platform}/solutions/api.ts +60 -1
  41. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/replay.ts +9 -3
  42. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/tenant-admin.ts +18 -27
  43. package/templates/src/{auth/api → app/api/auth}/api.ts +59 -0
  44. package/templates/src/{auth/api → app/api/auth}/ldap-login/api.ts +37 -1
  45. package/templates/src/{auth/api → app/api/auth}/wecom/api.ts +23 -0
  46. package/templates/src/{notification/api → app/api/notification}/api.ts +17 -0
  47. package/templates/src/{notification/api → app/api/notification}/del/api.ts +2 -0
  48. package/templates/src/{system/api → app/api/system}/oplog/api.ts +5 -3
  49. package/templates/src/{system/api → app/api/system}/roles/api.ts +3 -0
  50. package/templates/src/{system/api → app/api/system}/roles/bindings/api.ts +3 -0
  51. package/templates/src/{system/api → app/api/system}/settings/api.ts +3 -0
  52. package/templates/src/{system/api → app/api/system}/users/forbidden/api.ts +3 -0
  53. package/templates/src/{system/api → app/api/system}/users/password/api.ts +3 -0
  54. package/templates/src/{system/api → app/api/system}/users/roles/api.ts +3 -0
  55. package/templates/src/app/api/web/get-async-routes/api.ts +23 -0
  56. package/templates/src/app/api/web/hello/api.ts +22 -0
  57. package/templates/src/{web/api → app/api/web}/user-info/api.ts +21 -1
  58. package/templates/src/{_platform/web → app/web/_platform}/__client/api.schemas.ts +99 -35
  59. package/templates/src/{_platform/web/__client/api.ts → app/web/_platform/__client/index.ts} +108 -61
  60. package/templates/src/{auth/web → app/web/auth}/__client/api.schemas.ts +133 -13
  61. package/templates/src/{auth/web/__client/api.ts → app/web/auth/__client/index.ts} +134 -33
  62. package/templates/src/app/web/entry.ts +77 -0
  63. package/templates/src/{home/web/__client/api.ts → app/web/home/__client/index.ts} +5 -2
  64. package/templates/src/app/web/home/routes.ts +41 -0
  65. package/templates/src/app/web/login/routes.ts +71 -0
  66. package/templates/src/{notification/web → app/web/notification}/__client/api.schemas.ts +14 -0
  67. package/templates/src/{notification/web/__client/api.ts → app/web/notification/__client/index.ts} +67 -20
  68. package/templates/src/{notification/web → app/web/notification}/pages/center/index.tsx +1 -1
  69. package/templates/src/app/web/notification/routes.ts +70 -0
  70. package/templates/src/{personal-center/web/__client/api.ts → app/web/personal-center/__client/index.ts} +5 -2
  71. package/templates/src/{personal-center/web → app/web/personal-center}/pages/my-profile/index.tsx +1 -1
  72. package/templates/src/app/web/personal-center/routes.ts +49 -0
  73. package/templates/src/{system/web → app/web/system}/__client/api.schemas.ts +29 -22
  74. package/templates/src/{system/web/__client/api.ts → app/web/system/__client/index.ts} +103 -58
  75. package/templates/src/{system/web → app/web/system}/locales/en-US.json +2 -3
  76. package/templates/src/{system/web → app/web/system}/locales/zh-CN.json +2 -3
  77. package/templates/src/{system/web → app/web/system}/pages/departments/index.tsx +2 -2
  78. package/templates/src/{system/web → app/web/system}/pages/login-methods/index.tsx +1 -1
  79. package/templates/src/{system/web → app/web/system}/pages/oplog/index.tsx +44 -19
  80. package/templates/src/{system/web → app/web/system}/pages/roles/index.tsx +5 -3
  81. package/templates/src/app/web/system/pages/roles/visible-roles.ts +14 -0
  82. package/templates/src/{system/web → app/web/system}/pages/settings/index.tsx +2 -2
  83. package/templates/src/app/web/system/pages/users/can-manage.ts +21 -0
  84. package/templates/src/{system/web → app/web/system}/pages/users/index.tsx +23 -15
  85. package/templates/src/{system/web → app/web/system}/pages/users/roles.tsx +1 -1
  86. package/templates/src/{system/web → app/web/system}/pages/wecom/index.tsx +12 -3
  87. package/templates/src/{system/web/entry.ts → app/web/system/routes.ts} +25 -36
  88. package/templates/src/app/web/web/__client/api.schemas.ts +36 -0
  89. package/templates/src/{web/web/__client/api.ts → app/web/web/__client/index.ts} +38 -11
  90. package/templates/src/oj-market/web/api/defaultClient.ts +1 -1
  91. package/templates/tests/auth-ldap.spec.ts +2 -2
  92. package/templates/tests/auth-session.spec.ts +3 -3
  93. package/templates/tests/auth-throttle.spec.ts +1 -1
  94. package/templates/tests/fixture.ts +6 -4
  95. package/templates/tests/guard.spec.ts +1 -1
  96. package/templates/tests/lifecycle/atomic-state.spec.ts +3 -3
  97. package/templates/tests/lifecycle/bootstrap.spec.ts +3 -3
  98. package/templates/tests/lifecycle/deploy-smoke.spec.ts +8 -8
  99. package/templates/tests/lifecycle/installer.spec.ts +10 -10
  100. package/templates/tests/lifecycle/market-api.spec.ts +14 -14
  101. package/templates/tests/lifecycle/migrate.spec.ts +6 -6
  102. package/templates/tests/lifecycle/preflight.spec.ts +3 -3
  103. package/templates/tests/lifecycle/register.spec.ts +1 -1
  104. package/templates/tests/lifecycle/semver.spec.ts +1 -1
  105. package/templates/tests/lifecycle/sha384.spec.ts +1 -1
  106. package/templates/tests/lifecycle/solutions-api.spec.ts +1 -1
  107. package/templates/tests/lifecycle/source-seq.spec.ts +1 -1
  108. package/templates/tests/lifecycle/steps-fs.spec.ts +9 -9
  109. package/templates/tests/lifecycle/text-edit.spec.ts +1 -1
  110. package/templates/tests/lifecycle/tgz-guard.spec.ts +2 -2
  111. package/templates/tests/lifecycle/trust.spec.ts +5 -5
  112. package/templates/tests/lifecycle/uninstall.spec.ts +7 -7
  113. package/templates/tests/lifecycle/verify-checks.spec.ts +5 -5
  114. package/templates/tests/lifecycle/verify-report.spec.ts +4 -4
  115. package/templates/tests/org-departments.spec.ts +10 -6
  116. package/templates/tests/org-read.spec.ts +7 -7
  117. package/templates/tests/query-utils.spec.ts +1 -1
  118. package/templates/tests/rbac-audit.spec.ts +1 -1
  119. package/templates/tests/rbac-contacts.spec.ts +2 -2
  120. package/templates/tests/rbac-guard.spec.ts +1 -1
  121. package/templates/tests/rbac-roles.spec.ts +10 -2
  122. package/templates/tests/rbac-seed.spec.ts +1 -1
  123. package/templates/tests/rbac-settings.spec.ts +77 -7
  124. package/templates/tests/rbac-solution-perms.spec.ts +1 -1
  125. package/templates/tests/rbac-tables.spec.ts +29 -0
  126. package/templates/tests/rbac-users.spec.ts +55 -3
  127. package/templates/tests/roles-page.spec.ts +40 -0
  128. package/templates/tests/seed-role-accounts.spec.ts +1 -1
  129. package/templates/tests/tenant-access-cache.spec.ts +1 -1
  130. package/templates/tests/tenant-forgery.spec.ts +5 -5
  131. package/templates/tests/tenant-isolation.spec.ts +5 -4
  132. package/templates/tests/username-namespace.spec.ts +3 -3
  133. package/templates/tests/users-page.spec.ts +39 -0
  134. package/templates/tests/wecom-admin.spec.ts +6 -6
  135. package/templates/tests/wecom-client-timeout.spec.ts +1 -1
  136. package/templates/tests/wecom-connector.spec.ts +9 -9
  137. package/templates/tests/wecom-corp.spec.ts +9 -9
  138. package/templates/tests/wecom-crypt.spec.ts +1 -1
  139. package/templates/tests/wecom-endpoints.spec.ts +2 -2
  140. package/templates/tests/wecom-landing.spec.ts +1 -1
  141. package/templates/tests/wecom-org-sync.spec.ts +7 -7
  142. package/templates/tests/wecom-sync.spec.ts +3 -3
  143. package/templates/tsconfig.paths.json +11 -11
  144. package/templates/vitest.config.ts +2 -2
  145. package/templates/web.config.ts +24 -15
  146. package/vendor/host-versions.json +3 -3
  147. package/templates/src/home/web/entry.ts +0 -49
  148. package/templates/src/login/web/entry.ts +0 -79
  149. package/templates/src/notification/web/entry.ts +0 -84
  150. package/templates/src/personal-center/web/entry.ts +0 -60
  151. package/templates/src/web/api/get-async-routes/api.ts +0 -8
  152. package/templates/src/web/api/hello/api.ts +0 -10
  153. package/templates/src/web/web/__client/api.schemas.ts +0 -8
  154. package/templates/src/{_platform/api → app/api/_platform}/README.md +0 -0
  155. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/atomic.ts +0 -0
  156. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/history.ts +0 -0
  157. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/installer.ts +0 -0
  158. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/limits.ts +0 -0
  159. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/lock.ts +0 -0
  160. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/semver.ts +0 -0
  161. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/sha384.ts +0 -0
  162. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/source-seq.ts +0 -0
  163. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/state.ts +0 -0
  164. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/deploy-web.ts +0 -0
  165. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/download.ts +0 -0
  166. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/extract.ts +0 -0
  167. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/migrate.ts +0 -0
  168. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/preflight.ts +0 -0
  169. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/register.ts +0 -0
  170. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/reload.ts +0 -0
  171. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/smoke.ts +0 -0
  172. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/verify.ts +0 -0
  173. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/tasks.ts +0 -0
  174. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/text-edit.ts +0 -0
  175. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/tgz-guard.ts +0 -0
  176. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/trust.ts +0 -0
  177. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/types.ts +0 -0
  178. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/uninstall.ts +0 -0
  179. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/verify-checks.ts +0 -0
  180. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/verify-report.ts +0 -0
  181. package/templates/src/{_platform/api → app/api/_platform}/_shared/respond.ts +0 -0
  182. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/domain/contacts.ts +0 -0
  183. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/domain/org-read.ts +0 -0
  184. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/domain/org.ts +0 -0
  185. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/domain/roles.ts +0 -0
  186. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/domain/settings.ts +0 -0
  187. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/domain/solution-perms.ts +0 -0
  188. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/infra/access.ts +0 -0
  189. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/infra/audit.ts +0 -0
  190. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/infra/guard.ts +0 -0
  191. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/infra/query.ts +0 -0
  192. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/infra/rate.ts +0 -0
  193. package/templates/src/{_platform/api → app/api/_platform}/_shared/site-facade.ts +0 -0
  194. package/templates/src/{_platform/api → app/api/_platform}/fixtures/l1-test-user.sql +0 -0
  195. package/templates/src/{_platform/api → app/api/_platform}/manifest.yaml +0 -0
  196. package/templates/src/{_platform/api → app/api/_platform}/market/api.ts +0 -0
  197. package/templates/src/{_platform/api → app/api/_platform}/market/bootstrap.ts +0 -0
  198. package/templates/src/{_platform/api → app/api/_platform}/market/index-source.ts +0 -0
  199. package/templates/src/{_platform/api → app/api/_platform}/migrations/0001__create_users.mysql.sql +0 -0
  200. package/templates/src/{_platform/api → app/api/_platform}/migrations/0001__create_users.pg.sql +0 -0
  201. package/templates/src/{_platform/api → app/api/_platform}/migrations/0001__create_users.sql +0 -0
  202. package/templates/src/{_platform/api → app/api/_platform}/migrations/0002__add_users_avatar_base64.mysql.sql +0 -0
  203. package/templates/src/{_platform/api → app/api/_platform}/migrations/0002__add_users_avatar_base64.sql +0 -0
  204. package/templates/src/{_platform/api → app/api/_platform}/migrations/0003__users_source_display_name.mysql.sql +0 -0
  205. package/templates/src/{_platform/api → app/api/_platform}/migrations/0003__users_source_display_name.sql +0 -0
  206. package/templates/src/{_platform/api → app/api/_platform}/migrations/0004__sys_rbac.mysql.sql +0 -0
  207. package/templates/src/{_platform/api → app/api/_platform}/migrations/0004__sys_rbac.pg.sql +0 -0
  208. package/templates/src/{_platform/api → app/api/_platform}/migrations/0004__sys_rbac.sql +0 -0
  209. package/templates/src/{_platform/api → app/api/_platform}/migrations/0005__users_must_change.mysql.sql +0 -0
  210. package/templates/src/{_platform/api → app/api/_platform}/migrations/0005__users_must_change.sql +0 -0
  211. package/templates/src/{_platform/api → app/api/_platform}/migrations/0006__users_is_forbidden.mysql.sql +0 -0
  212. package/templates/src/{_platform/api → app/api/_platform}/migrations/0006__users_is_forbidden.sql +0 -0
  213. package/templates/src/{_platform/api → app/api/_platform}/migrations/0007__users_email.sql +0 -0
  214. package/templates/src/{_platform/api → app/api/_platform}/migrations/0008__sys_refresh_token.mysql.sql +0 -0
  215. package/templates/src/{_platform/api → app/api/_platform}/migrations/0008__sys_refresh_token.pg.sql +0 -0
  216. package/templates/src/{_platform/api → app/api/_platform}/migrations/0008__sys_refresh_token.sql +0 -0
  217. package/templates/src/{_platform/api → app/api/_platform}/migrations/0009__sys_throttle.mysql.sql +0 -0
  218. package/templates/src/{_platform/api → app/api/_platform}/migrations/0009__sys_throttle.pg.sql +0 -0
  219. package/templates/src/{_platform/api → app/api/_platform}/migrations/0009__sys_throttle.sql +0 -0
  220. package/templates/src/{_platform/api → app/api/_platform}/migrations/0010__sys_solution_route.mysql.sql +0 -0
  221. package/templates/src/{_platform/api → app/api/_platform}/migrations/0010__sys_solution_route.pg.sql +0 -0
  222. package/templates/src/{_platform/api → app/api/_platform}/migrations/0010__sys_solution_route.sql +0 -0
  223. package/templates/src/{_platform/api → app/api/_platform}/migrations/0011__sys_setting_group_label.mysql.sql +0 -0
  224. package/templates/src/{_platform/api → app/api/_platform}/migrations/0011__sys_setting_group_label.sql +0 -0
  225. package/templates/src/{_platform/api → app/api/_platform}/migrations/0012__market_history.mysql.sql +0 -0
  226. package/templates/src/{_platform/api → app/api/_platform}/migrations/0012__market_history.pg.sql +0 -0
  227. package/templates/src/{_platform/api → app/api/_platform}/migrations/0012__market_history.sql +0 -0
  228. package/templates/src/{_platform/api → app/api/_platform}/migrations/0013__users_profile.sql +0 -0
  229. package/templates/src/{_platform/api → app/api/_platform}/migrations/0014__reg_source_seq.mysql.sql +0 -0
  230. package/templates/src/{_platform/api → app/api/_platform}/migrations/0014__reg_source_seq.pg.sql +0 -0
  231. package/templates/src/{_platform/api → app/api/_platform}/migrations/0014__reg_source_seq.sql +0 -0
  232. package/templates/src/{_platform/api → app/api/_platform}/migrations/0015__tenant.mysql.sql +0 -0
  233. package/templates/src/{_platform/api → app/api/_platform}/migrations/0015__tenant.pg.sql +0 -0
  234. package/templates/src/{_platform/api → app/api/_platform}/migrations/0015__tenant.sql +0 -0
  235. package/templates/src/{_platform/api → app/api/_platform}/migrations/0016__org.mysql.sql +0 -0
  236. package/templates/src/{_platform/api → app/api/_platform}/migrations/0016__org.pg.sql +0 -0
  237. package/templates/src/{_platform/api → app/api/_platform}/migrations/0016__org.sql +0 -0
  238. package/templates/src/{_platform/api → app/api/_platform}/org/departments/api.ts +0 -0
  239. package/templates/src/{_platform/api → app/api/_platform}/org/is-member/api.ts +0 -0
  240. package/templates/src/{_platform/api → app/api/_platform}/org/members/api.ts +0 -0
  241. package/templates/src/{_platform/api → app/api/_platform}/org/picker/api.ts +0 -0
  242. package/templates/src/{_platform/api → app/api/_platform}/org/resolve/api.ts +0 -0
  243. package/templates/src/{_platform/api → app/api/_platform}/org/tree/api.ts +0 -0
  244. package/templates/src/{_platform/api → app/api/_platform}/org/user/api.ts +0 -0
  245. package/templates/src/{_platform/api → app/api/_platform}/seed.mysql.sql +0 -0
  246. package/templates/src/{_platform/api → app/api/_platform}/seed.pg.sql +0 -0
  247. package/templates/src/{_platform/api → app/api/_platform}/seed.sql +0 -0
  248. package/templates/src/{auth/api → app/api/auth}/_shared/ldap-login.ts +0 -0
  249. package/templates/src/{auth/api → app/api/auth}/_shared/ldap.ts +0 -0
  250. package/templates/src/{auth/api → app/api/auth}/_shared/login-throttle.ts +0 -0
  251. package/templates/src/{auth/api → app/api/auth}/_shared/session.ts +0 -0
  252. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/admin.ts +1 -1
  253. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/client.ts +0 -0
  254. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/config.ts +0 -0
  255. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/contacts.ts +0 -0
  256. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/crypt.ts +0 -0
  257. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/events.ts +1 -1
  258. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/login.ts +0 -0
  259. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/payload.ts +0 -0
  260. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/store.ts +0 -0
  261. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/sync.ts +0 -0
  262. package/templates/src/{auth/api → app/api/auth}/manifest.yaml +0 -0
  263. package/templates/src/{auth/api → app/api/auth}/migrations/0001__wecom.mysql.sql +0 -0
  264. package/templates/src/{auth/api → app/api/auth}/migrations/0001__wecom.pg.sql +0 -0
  265. package/templates/src/{auth/api → app/api/auth}/migrations/0001__wecom.sql +0 -0
  266. package/templates/src/{auth/api → app/api/auth}/migrations/0002__org_sync.mysql.sql +0 -0
  267. package/templates/src/{auth/api → app/api/auth}/migrations/0002__org_sync.pg.sql +0 -0
  268. package/templates/src/{auth/api → app/api/auth}/migrations/0002__org_sync.sql +0 -0
  269. package/templates/src/{auth/api → app/api/auth}/migrations/0003__credential_kind.mysql.sql +0 -0
  270. package/templates/src/{auth/api → app/api/auth}/migrations/0003__credential_kind.pg.sql +0 -0
  271. package/templates/src/{auth/api → app/api/auth}/migrations/0003__credential_kind.sql +0 -0
  272. package/templates/src/{auth/api → app/api/auth}/schema.yaml +0 -0
  273. package/templates/src/{auth/api → app/api/auth}/wecom/admin/api.ts +0 -0
  274. package/templates/src/{auth/api → app/api/auth}/wecom/admin/tenant-admin/api.ts +1 -1
  275. package/templates/src/{auth/api → app/api/auth}/wecom/admin/tenant-admin/password/api.ts +1 -1
  276. /package/templates/src/{auth/api → app/api/auth}/wecom/sync/api.ts +0 -0
  277. /package/templates/src/{home/api → app/api/home}/line/api.ts +0 -0
  278. /package/templates/src/{home/api → app/api/home}/manifest.yaml +0 -0
  279. /package/templates/src/{home/api → app/api/home}/migrations/0001__create_home_tables.sql +0 -0
  280. /package/templates/src/{home/api → app/api/home}/migrations/0002__tenant.mysql.sql +0 -0
  281. /package/templates/src/{home/api → app/api/home}/migrations/0002__tenant.sql +0 -0
  282. /package/templates/src/{home/api → app/api/home}/pie/api.ts +0 -0
  283. /package/templates/src/{home/api → app/api/home}/schema.yaml +0 -0
  284. /package/templates/src/{home/api → app/api/home}/seed.sql +0 -0
  285. /package/templates/src/{notification/api → app/api/notification}/_shared/notify.ts +0 -0
  286. /package/templates/src/{notification/api → app/api/notification}/manifest.yaml +0 -0
  287. /package/templates/src/{notification/api → app/api/notification}/migrations/0001__create_notifications.sql +0 -0
  288. /package/templates/src/{notification/api → app/api/notification}/migrations/0002__notifications_user.sql +0 -0
  289. /package/templates/src/{notification/api → app/api/notification}/migrations/0003__notifications_broadcast.sql +0 -0
  290. /package/templates/src/{notification/api → app/api/notification}/migrations/0004__notifications_broadcast_hidden.sql +0 -0
  291. /package/templates/src/{notification/api → app/api/notification}/migrations/0005__tenant.mysql.sql +0 -0
  292. /package/templates/src/{notification/api → app/api/notification}/migrations/0005__tenant.sql +0 -0
  293. /package/templates/src/{notification/api → app/api/notification}/schema.yaml +0 -0
  294. /package/templates/src/{notification/api → app/api/notification}/seed.sql +0 -0
  295. /package/templates/src/{personal-center/api → app/api/personal-center}/manifest.yaml +0 -0
  296. /package/templates/src/{personal-center/api → app/api/personal-center}/profile/api.ts +0 -0
  297. /package/templates/src/{personal-center/api → app/api/personal-center}/upload/api.ts +0 -0
  298. /package/templates/src/{system/api → app/api/system}/departments/api.ts +0 -0
  299. /package/templates/src/{system/api → app/api/system}/departments/detach/api.ts +0 -0
  300. /package/templates/src/{system/api → app/api/system}/departments/members/api.ts +0 -0
  301. /package/templates/src/{system/api → app/api/system}/departments/move/api.ts +0 -0
  302. /package/templates/src/{system/api → app/api/system}/login-methods/api.ts +0 -0
  303. /package/templates/src/{system/api → app/api/system}/manifest.yaml +0 -0
  304. /package/templates/src/{system/api → app/api/system}/permissions/api.ts +0 -0
  305. /package/templates/src/{system/api → app/api/system}/users/api.ts +0 -0
  306. /package/templates/src/{system/api → app/api/system}/users/profile/api.ts +0 -0
  307. /package/templates/src/{web/api → app/api/web}/manifest.yaml +0 -0
  308. /package/templates/src/{home/web → app/web/home}/__client/api.schemas.ts +0 -0
  309. /package/templates/src/{home/web → app/web/home}/locales/en-US.json +0 -0
  310. /package/templates/src/{home/web → app/web/home}/locales/zh-CN.json +0 -0
  311. /package/templates/src/{home/web → app/web/home}/pages/components/bar-chart.tsx +0 -0
  312. /package/templates/src/{home/web → app/web/home}/pages/components/card-list.tsx +0 -0
  313. /package/templates/src/{home/web → app/web/home}/pages/components/line-chart.tsx +0 -0
  314. /package/templates/src/{home/web → app/web/home}/pages/components/pie-chart.tsx +0 -0
  315. /package/templates/src/{home/web → app/web/home}/pages/index.tsx +0 -0
  316. /package/templates/src/{login/web → app/web/login}/auth-provider.ts +0 -0
  317. /package/templates/src/{login/web → app/web/login}/locales/en-US.json +0 -0
  318. /package/templates/src/{login/web → app/web/login}/locales/zh-CN.json +0 -0
  319. /package/templates/src/{login/web → app/web/login}/pages/change-password.tsx +0 -0
  320. /package/templates/src/{login/web → app/web/login}/pages/login.tsx +0 -0
  321. /package/templates/src/{login/web → app/web/login}/wecom.ts +0 -0
  322. /package/templates/src/{notification/web → app/web/notification}/locales/en-US.json +0 -0
  323. /package/templates/src/{notification/web → app/web/notification}/locales/zh-CN.json +0 -0
  324. /package/templates/src/{personal-center/web → app/web/personal-center}/__client/api.schemas.ts +0 -0
  325. /package/templates/src/{personal-center/web → app/web/personal-center}/locales/en-US.json +0 -0
  326. /package/templates/src/{personal-center/web → app/web/personal-center}/locales/zh-CN.json +0 -0
  327. /package/templates/src/{system/web → app/web/system}/pages/departments/sync-bar.tsx +0 -0
  328. /package/templates/src/{system/web → app/web/system}/pages/departments/tree.ts +0 -0
  329. /package/templates/src/{system/web → app/web/system}/pages/errors.ts +0 -0
  330. /package/templates/src/{system/web → app/web/system}/pages/forbidden.tsx +0 -0
@@ -1,5 +1,6 @@
1
1
  // 账号管理应用服务(管理域 /api/system/users,AC1-7~10 + B6 建号加固)。
2
2
  // 红线:admin 不碰 superadmin/admin 成员资格(AC1-8,service 按 actor 角色自查);
3
+ // 只有 superadmin 能改 superadmin 账号(口令/启停/资料/删除/角色,见 assertCanManageTarget);
3
4
  // 禁止操作自己(AC1-10 注);最后 superadmin 兜底(服务端二次防线);
4
5
  // 角色唯一权威源 sys_user_role + 写时失效(AC1-7/D10);禁用/重置口令级联删 refresh(§3.1)。
5
6
  // bcrypt 经注入(Node 侧全局不可 stub,同 session.ts 约定)。
@@ -59,6 +60,27 @@ function assertNotSelf(actorId: number, targetUid: number): void {
59
60
  fail(400, "cannot operate on yourself");
60
61
  }
61
62
 
63
+ /**
64
+ * 红线(RBAC 保护红线):只有 superadmin 能改 superadmin。
65
+ *
66
+ * 与 `assertMembershipAllowed` 的分工:那条按**被授予的角色**管提权方向(谁能任命 admin /
67
+ * superadmin),这条按**目标账号当前的角色**管反向——谁能动一个已经是 superadmin 的号。
68
+ * 缺了它,同一租户的 admin 可以:重置 superadmin 口令(知道口令即接管账号,而 superadmin
69
+ * 是平台级、能跨租户)/把人禁用/把角色整集清空——后者连 `assertMembershipAllowed`
70
+ * 都拦不住(`next` 里没有 admin/superadmin 时它直接放行)。故口令/启停/资料/删除/角色
71
+ * 五条写路径统一走这里。
72
+ *
73
+ * 读角色走 `rolesOf()`,与写入同受请求租户注入约束:只能影响本租户的人。
74
+ */
75
+ async function assertCanManageTarget(actorId: number, targetUid: number): Promise<void> {
76
+ const targetRoles = await rolesOf(Number(targetUid));
77
+ if (!targetRoles.includes("superadmin"))
78
+ return;
79
+ if ((await rolesOf(Number(actorId))).includes("superadmin"))
80
+ return;
81
+ fail(403, "modifying a superadmin requires superadmin");
82
+ }
83
+
62
84
  /**
63
85
  * AC1-8(已修订):成员资格提权守卫——按**被授予的角色**分层,不再一刀切。
64
86
  *
@@ -200,6 +222,7 @@ export async function updateUserRoles(actorId: number, targetUid: number, roles:
200
222
  const target = await db.table("users").select(["id"]).where({ field: "id", op: "eq", value: targetUid }).all();
201
223
  if (!target.length)
202
224
  fail(404, "user not found");
225
+ await assertCanManageTarget(actorId, targetUid);
203
226
  const next = [...new Set(roles.map(String))];
204
227
  if (next.length) {
205
228
  const known = await db.table("sys_role").select(["name"]).where({ field: "name", op: "in", value: next }).all();
@@ -258,6 +281,7 @@ export async function changePasswordByAdmin(actorId: number, targetUid: number,
258
281
  const rows = await db.table("users").select(["id"]).where({ field: "id", op: "eq", value: targetUid }).all();
259
282
  if (!rows.length)
260
283
  fail(404, "user not found");
284
+ await assertCanManageTarget(actorId, targetUid);
261
285
  const hash = await deps.bcryptHash(String(newPassword));
262
286
  await db.tx(async (t) => {
263
287
  await t.table("users").update({ password_hash: hash, must_change: 1 }).where({ field: "id", op: "eq", value: targetUid }).run();
@@ -273,6 +297,7 @@ export async function setForbidden(actorId: number, targetUid: number, forbidden
273
297
  const rows = await db.table("users").select(["id"]).where({ field: "id", op: "eq", value: targetUid }).all();
274
298
  if (!rows.length)
275
299
  fail(404, "user not found");
300
+ await assertCanManageTarget(actorId, targetUid);
276
301
  await db.tx(async (t) => {
277
302
  await t.table("users").update({ is_forbidden: forbidden ? 1 : 0 }).where({ field: "id", op: "eq", value: targetUid }).run();
278
303
  if (forbidden)
@@ -294,6 +319,7 @@ export async function updateUserProfile(actorId: number, targetUid: number, patc
294
319
  const rows = await db.table("users").select(["id"]).where({ field: "id", op: "eq", value: targetUid }).all();
295
320
  if (!rows.length)
296
321
  fail(404, "user not found");
322
+ await assertCanManageTarget(actorId, targetUid);
297
323
  const cols: Record<string, unknown> = {};
298
324
  if (patch.displayName !== undefined)
299
325
  cols.display_name = patch.displayName == null ? null : String(patch.displayName).trim() || null;
@@ -320,6 +346,7 @@ export async function deleteUser(actorId: number, targetUid: number): Promise<vo
320
346
  const rows = await db.table("users").select(["id", "username"]).where({ field: "id", op: "eq", value: targetUid }).all() as Array<{ id: number, username: string }>;
321
347
  if (!rows.length)
322
348
  fail(404, "user not found");
349
+ await assertCanManageTarget(actorId, targetUid);
323
350
  const roles = await rolesOf(Number(targetUid));
324
351
  if (roles.includes("superadmin")) {
325
352
  const supers = await db.table("sys_user_role").select([{ fn: "count", as: "c" }]).where({ field: "role_name", op: "eq", value: "superadmin" }).all() as Array<{ c: number }>;
@@ -21,7 +21,7 @@ export interface OplogPage {
21
21
  truncated: boolean
22
22
  }
23
23
 
24
- export async function searchOplog(filter: { action?: unknown, username?: unknown, module?: unknown, limit?: unknown }): Promise<OplogPage> {
24
+ export async function searchOplog(filter: { action?: unknown, username?: unknown, module?: unknown, page?: unknown, limit?: unknown }): Promise<OplogPage> {
25
25
  // 构造器(v0.1.82):可选过滤走 whereIf,条件写一份(mk 工厂),count + list 复用;
26
26
  // username 走语义 op contains(引擎转义 %/_/!,等价旧 LIKE ? ESCAPE '!' 的 %kw%)。
27
27
  // 照抄口径:devkit scenarios 场景 42 ④。
@@ -35,6 +35,8 @@ export async function searchOplog(filter: { action?: unknown, username?: unknown
35
35
  .whereIf(action, { field: "action", op: "eq", value: action })
36
36
  .whereIf(module, { field: "module", op: "eq", value: module })
37
37
  .whereIf(username, { field: "username", op: "contains", value: username });
38
- const r = await pagedFactory<OplogRow>(mk, { size: filter.limit });
38
+ // page/limit 成对:limit 是每页条数(pageArgs 钳 200),缺省第 1 页 / 20 条。
39
+ // total 由 count 查询单独给出,页面按 total 算页数——不再靠 truncated 提示「结果被截断」。
40
+ const r = await pagedFactory<OplogRow>(mk, { page: filter.page, size: filter.limit });
39
41
  return { list: r.list, total: r.total, truncated: r.truncated };
40
42
  }
@@ -6,7 +6,7 @@ import { searchContacts } from "../_shared/service/domain/contacts";
6
6
  import { assertTenantSelf } from "../_shared/service/infra/access";
7
7
  import { rateLimit } from "../_shared/service/infra/rate";
8
8
 
9
- export default {
9
+ const handler = {
10
10
  // GET /api/_platform/contacts?keyword=&limit= —— 人员目录(AC1-22~24)。
11
11
  async get() {
12
12
  await respond(async () => {
@@ -23,3 +23,31 @@ export default {
23
23
  });
24
24
  },
25
25
  };
26
+
27
+ // 入参:keyword 必填(服务层红线 2~64 字符),limit 可选(钳 200)
28
+ handler.get.schema = {
29
+ query: {
30
+ type: "object",
31
+ required: ["keyword"],
32
+ properties: {
33
+ keyword: { type: "string", minLength: 2 },
34
+ limit: { type: "number" },
35
+ },
36
+ },
37
+ };
38
+
39
+ // 出参:目录红线只出 id/username/display_name(不含 email/avatar/roles)
40
+ handler.get.response = {
41
+ type: "array",
42
+ items: {
43
+ type: "object",
44
+ required: ["id", "username", "display_name"],
45
+ properties: {
46
+ id: { type: "number" },
47
+ username: { type: "string" },
48
+ display_name: { type: ["string", "null"] },
49
+ },
50
+ },
51
+ };
52
+
53
+ export default handler;
@@ -4,7 +4,7 @@ import { contactDetail } from "../../_shared/service/domain/contacts";
4
4
  import { assertTenantSelf } from "../../_shared/service/infra/access";
5
5
  import { rateLimit } from "../../_shared/service/infra/rate";
6
6
 
7
- export default {
7
+ const handler = {
8
8
  async get() {
9
9
  await respond(async () => {
10
10
  const uid = http.user?.id;
@@ -19,3 +19,27 @@ export default {
19
19
  });
20
20
  },
21
21
  };
22
+
23
+ // 入参:id 必填(详情按 id 取单条)
24
+ handler.get.schema = {
25
+ query: {
26
+ type: "object",
27
+ required: ["id"],
28
+ properties: { id: { type: "number" } },
29
+ },
30
+ };
31
+
32
+ // 出参:详情比目录多 email/avatar(安全评审 P2:仅详情端点给)
33
+ handler.get.response = {
34
+ type: "object",
35
+ required: ["id", "username", "display_name", "email", "avatar_base64"],
36
+ properties: {
37
+ id: { type: "number" },
38
+ username: { type: "string" },
39
+ display_name: { type: ["string", "null"] },
40
+ email: { type: ["string", "null"] },
41
+ avatar_base64: { type: ["string", "null"] },
42
+ },
43
+ };
44
+
45
+ export default handler;
@@ -0,0 +1,7 @@
1
+ -- 0017__users_employee_no(mysql 方言):工号列 + 租户内唯一。列语义同 0017__users_employee_no.sql 头注释。
2
+ -- 差异(沿用 0015__tenant.mysql.sql 惯例):
3
+ -- · 键列不能用 TEXT 落唯一键 → 按业务长度收窄为 VARCHAR(64);
4
+ -- · 无部分索引 → 普通唯一键,NULL 在 MySQL 里可重复出现,效果等同「有值才唯一」;
5
+ -- · (255 + 64) × 4B = 1276B < InnoDB 3072B 上限。
6
+ ALTER TABLE users ADD COLUMN employee_no VARCHAR(64) NULL;
7
+ ALTER TABLE users ADD UNIQUE KEY uk_users_employee_no (tenant_id, employee_no);
@@ -0,0 +1,4 @@
1
+ -- 0017__users_employee_no(postgres 方言):工号列 + 租户内唯一。列语义同 0017__users_employee_no.sql 头注释。
2
+ -- 差异(沿用 0016__org.pg.sql 惯例):TEXT 在 pg 合法且可用于部分唯一索引,无需收窄类型。
3
+ ALTER TABLE users ADD COLUMN employee_no TEXT;
4
+ CREATE UNIQUE INDEX IF NOT EXISTS uk_users_employee_no ON users (tenant_id, employee_no) WHERE employee_no IS NOT NULL;
@@ -0,0 +1,8 @@
1
+ -- 0017__users_employee_no(sqlite 方言):工号列 + 租户内唯一
2
+ --
3
+ -- 为什么单开一列而不是复用 display_name / 外部 origin_id:用户 CSV 导入导出要与源系统
4
+ -- 9 列表头完全互通,「工号」必须是独立可查字段。
5
+ -- 可空是必需的:存量本地账号没有工号,外部同步号也可能不带;唯一性只在有值时有意义,
6
+ -- 故用部分索引(sqlite/pg 都支持 WHERE),mysql 侧靠「NULL 可重复」的唯一键等效。
7
+ ALTER TABLE users ADD COLUMN employee_no TEXT;
8
+ CREATE UNIQUE INDEX IF NOT EXISTS uk_users_employee_no ON users (tenant_id, employee_no) WHERE employee_no IS NOT NULL;
@@ -37,6 +37,11 @@ tables:
37
37
  activated: {type: integer, null: false}
38
38
  # 职务(与来源无关的通用字段)
39
39
  position: {type: text}
40
+ # 工号(0017,可空):与姓名/用户名分开的独立工号列,租户内有值时唯一。
41
+ # 必须在这里声明——列只落库而不进白名单时,任何模块 select 它都会被构造器拒为
42
+ # `unknown column 'employee_no'`(/system/departments 依赖 /system/users 的选人列表,
43
+ # 曾因此在打开部门页时整页报错)。
44
+ employee_no: {type: text}
40
45
  # 外部源侧的状态快照(企微 1/2/4/5)。**绝不**自动改写 is_forbidden——那会和
41
46
  # 本站管理员的手动启停互相覆盖(方案 §4.3)
42
47
  external_status: {type: integer}
@@ -167,7 +167,7 @@ async function listMigrationFiles(env: SolutionsEnv, name: string, version: stri
167
167
  }
168
168
 
169
169
  export function createSolutionsApi(env: SolutionsEnv) {
170
- return {
170
+ const api = {
171
171
  // GET /api/_platform/solutions/routes —— 前端归并数据源(登录即可)。
172
172
  routes: {
173
173
  async get() {
@@ -217,6 +217,65 @@ export function createSolutionsApi(env: SolutionsEnv) {
217
217
  },
218
218
  },
219
219
  };
220
+
221
+ // ---- 契约(v0.1.72:入参 .schema / 出参平级 .response)----
222
+ // 路由行出参:SolutionRouteRow(前端归并与深检共用形状)
223
+ const routeRow = {
224
+ type: "object",
225
+ required: ["path", "module", "category", "order", "title_key", "required_permission", "enabled"],
226
+ properties: {
227
+ path: { type: "string" },
228
+ module: { type: "string" },
229
+ category: { type: "string", enum: ["main", "admin", "hidden"] },
230
+ order: { type: "number" },
231
+ title_key: { type: ["string", "null"] },
232
+ required_permission: { type: ["string", "null"] },
233
+ enabled: { type: "boolean" },
234
+ },
235
+ };
236
+ api.routes.get.response = { type: "array", items: routeRow };
237
+
238
+ // 深检报告出参:VerifyReport(ok/checks/advise)
239
+ const verifyReport = {
240
+ type: "object",
241
+ required: ["ok", "checks", "advise"],
242
+ properties: {
243
+ ok: { type: "boolean" },
244
+ checks: {
245
+ type: "array",
246
+ items: {
247
+ type: "object",
248
+ required: ["id", "severity", "subject", "advise_key"],
249
+ properties: {
250
+ id: { type: "string" },
251
+ severity: { type: "string", enum: ["error", "warn", "info"] },
252
+ subject: { type: "string" },
253
+ advise_key: { type: "string" },
254
+ },
255
+ },
256
+ },
257
+ advise: { type: "array", items: { type: "string" } },
258
+ },
259
+ };
260
+ // verify:name 必填(端点门,缺参 400)
261
+ api.verify.post.schema = {
262
+ query: {
263
+ type: "object",
264
+ required: ["name"],
265
+ properties: { name: { type: "string", minLength: 1 } },
266
+ },
267
+ };
268
+ api.verify.post.response = verifyReport;
269
+ api.verify.last.get.schema = {
270
+ query: {
271
+ type: "object",
272
+ required: ["name"],
273
+ properties: { name: { type: "string", minLength: 1 } },
274
+ },
275
+ };
276
+ api.verify.last.get.response = verifyReport;
277
+
278
+ return api;
220
279
  }
221
280
 
222
281
  export default createSolutionsApi(solutionsEnv());
@@ -43,7 +43,13 @@ export function runtimeReplayGate(): ReplayGate {
43
43
  incr: key => kv.incr(key),
44
44
  expire: (key, ttlSecs) => kv.expire(key, ttlSecs),
45
45
  get: key => kv.get(key),
46
- set: async (key, ttlSecs) => { await kv.set(key, "1"); if (!(await kv.expire(key, ttlSecs))) { log.warn("wecom applied marker ttl not set", "key", key); await kv.del(key); } },
46
+ set: async (key, ttlSecs) => {
47
+ await kv.set(key, "1");
48
+ if (!(await kv.expire(key, ttlSecs))) {
49
+ log.warn("wecom applied marker ttl not set", "key", key);
50
+ await kv.del(key);
51
+ }
52
+ },
47
53
  del: key => kv.del(key),
48
54
  now: () => Math.floor(Date.now() / 1000),
49
55
  };
@@ -61,7 +67,7 @@ export class ReplayAlreadyApplied extends Error {
61
67
 
62
68
  /** 处理成功后调用:标记该 nonce 已落库成功,供重推幂等回 success(避免首次偏慢被企微判超时、重推又被重放守卫挡成 400)。 */
63
69
  export async function markApplied(nonce: string, gate: ReplayGate): Promise<void> {
64
- await gate.set("wecom:applied:" + nonce, NONCE_TTL_SECS);
70
+ await gate.set(`wecom:applied:${nonce}`, NONCE_TTL_SECS);
65
71
  }
66
72
 
67
73
  /**
@@ -85,7 +91,7 @@ export async function guardReplay(timestampRaw: string, nonce: string, gate: Rep
85
91
  if (seen !== 1) {
86
92
  // 重推场景:若首次处理已成功落库(标了 applied 标记),直接幂等回 success,
87
93
  // 不让企微卡在「应用安装失败」。未标记者说明首推仍在进行或已失败(失败会 release 让重推重放),维持 400。
88
- const applied = await gate.get("wecom:applied:" + nonce);
94
+ const applied = await gate.get(`wecom:applied:${nonce}`);
89
95
  if (applied)
90
96
  throw new ReplayAlreadyApplied();
91
97
  fail(400, "回调 nonce 重复:同一条指令回调已被处理过(重放请求已拒绝)");
@@ -61,15 +61,12 @@ function randomPassword(): string {
61
61
 
62
62
  /** 跨租户读:指定租户下的管理员行(asSystem 例外,见文件头) */
63
63
  async function adminRow(tenantId: string): Promise<{ id: number, must_change: number } | null> {
64
- const rows = await db.asSystem().table("users")
65
- .select(["id", "must_change"])
66
- .where({
67
- and: [
68
- { field: "tenant_id", op: "eq", value: tenantId },
69
- { field: "username", op: "eq", value: TENANT_ADMIN_USERNAME },
70
- ],
71
- })
72
- .all() as Array<{ id: number, must_change: number | null }>;
64
+ const rows = await db.asSystem().table("users").select(["id", "must_change"]).where({
65
+ and: [
66
+ { field: "tenant_id", op: "eq", value: tenantId },
67
+ { field: "username", op: "eq", value: TENANT_ADMIN_USERNAME },
68
+ ],
69
+ }).all() as Array<{ id: number, must_change: number | null }>;
73
70
  if (!rows.length)
74
71
  return null;
75
72
  return { id: Number(rows[0]!.id), must_change: Number(rows[0]!.must_change ?? 0) };
@@ -77,15 +74,12 @@ async function adminRow(tenantId: string): Promise<{ id: number, must_change: nu
77
74
 
78
75
  /** 跨租户读:该用户在本租户是否握着 admin 角色(账号存在 ≠ 有角色,可能被收回过) */
79
76
  async function hasAdminRole(tenantId: string, uid: number): Promise<boolean> {
80
- const roles = await db.asSystem().table("sys_user_role")
81
- .select(["role_name"])
82
- .where({
83
- and: [
84
- { field: "tenant_id", op: "eq", value: tenantId },
85
- { field: "user_id", op: "eq", value: uid },
86
- ],
87
- })
88
- .all() as Array<{ role_name: string }>;
77
+ const roles = await db.asSystem().table("sys_user_role").select(["role_name"]).where({
78
+ and: [
79
+ { field: "tenant_id", op: "eq", value: tenantId },
80
+ { field: "user_id", op: "eq", value: uid },
81
+ ],
82
+ }).all() as Array<{ role_name: string }>;
89
83
  return roles.some(r => String(r.role_name) === TENANT_ADMIN_ROLE);
90
84
  }
91
85
 
@@ -176,15 +170,12 @@ export async function setTenantAdminPassword(tenantId: string, password: string)
176
170
  if (!row)
177
171
  throw Object.assign(new Error("租户管理员建档失败"), { status: 500 });
178
172
 
179
- await db.asSystem().table("users")
180
- .update({ password_hash: await bcrypt.hash(password), must_change: 1 })
181
- .where({
182
- and: [
183
- { field: "tenant_id", op: "eq", value: tenantId },
184
- { field: "username", op: "eq", value: TENANT_ADMIN_USERNAME },
185
- ],
186
- })
187
- .run();
173
+ await db.asSystem().table("users").update({ password_hash: await bcrypt.hash(password), must_change: 1 }).where({
174
+ and: [
175
+ { field: "tenant_id", op: "eq", value: tenantId },
176
+ { field: "username", op: "eq", value: TENANT_ADMIN_USERNAME },
177
+ ],
178
+ }).run();
188
179
 
189
180
  // 改密后旧会话必须失效。跨租户删 refresh 走带 tenant_id 条件的裸 SQL
190
181
  // (先例见 store.ts::revokeTenantSessions:守卫是 check-on-miss,带了租户条件即放行)
@@ -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;
@@ -320,6 +320,29 @@ ticket.post.schema = {
320
320
  },
321
321
  },
322
322
  };
323
+ // 出参:TicketBundle(issueExternal 签发,同 TokenBundle)
324
+ ticket.post.response = {
325
+ type: "object",
326
+ required: ["access_token", "refresh_token", "expires_in", "restricted", "user"],
327
+ properties: {
328
+ access_token: { type: "string" },
329
+ refresh_token: { type: "string" },
330
+ expires_in: { type: "number" },
331
+ restricted: { type: "boolean" },
332
+ user: {
333
+ type: "object",
334
+ required: ["id", "username", "display_name", "avatar_base64", "must_change", "roles"],
335
+ properties: {
336
+ id: { type: "number" },
337
+ username: { type: "string" },
338
+ display_name: { type: ["string", "null"] },
339
+ avatar_base64: { type: ["string", "null"] },
340
+ must_change: { type: "boolean" },
341
+ roles: { type: "array", items: { type: "string" } },
342
+ },
343
+ },
344
+ },
345
+ };
323
346
 
324
347
  const event = {
325
348
  // 企微后台「保存回调 URL」时会 GET 一次:解出 echostr 后**原样明文回**(不套信封、不加引号)。
@@ -137,6 +137,11 @@ notifications.read.post.schema = {
137
137
  properties: { id: { type: "string" } },
138
138
  },
139
139
  };
140
+ notifications.read.post.response = { type: "null" };
141
+
142
+ // read-all / clear:无入参、无返回体(json.ok(null))——只声明出参,入参留空。
143
+ notifications["read-all"].post.response = { type: "null" };
144
+ notifications.clear.post.response = { type: "null" };
140
145
 
141
146
  // POST /api/notification/push —— 给指定用户推一条按人通知(发射原语 HTTP 入口)。
142
147
  // 真正逻辑在 _shared/notify.pushNotice(任一后端模块也可直接 import 调用,无需走 HTTP)。
@@ -239,6 +244,12 @@ const broadcast = {
239
244
  },
240
245
  };
241
246
 
247
+ push.post.response = {
248
+ type: "object",
249
+ required: ["id"],
250
+ properties: { id: { type: "string" } },
251
+ };
252
+
242
253
  broadcast.post.schema = {
243
254
  body: {
244
255
  type: "object",
@@ -253,4 +264,10 @@ broadcast.post.schema = {
253
264
  },
254
265
  };
255
266
 
267
+ broadcast.post.response = {
268
+ type: "object",
269
+ required: ["id"],
270
+ properties: { id: { type: "string" } },
271
+ };
272
+
256
273
  export default { notifications, push, "push-broadcast": broadcast };
@@ -49,4 +49,6 @@ handler.post.schema = {
49
49
  },
50
50
  };
51
51
 
52
+ handler.post.response = { type: "null" };
53
+
52
54
  export default handler;
@@ -1,5 +1,5 @@
1
1
  import { respond } from "#/_platform/_shared/respond";
2
- // GET /api/system/oplog?action=&username=&module=&limit= —— 操作日志查询(AC1-29;D13 admin 可查)。
2
+ // GET /api/system/oplog?action=&username=&module=&page=&limit= —— 操作日志查询(AC1-29;D13 admin 可查)。
3
3
  // POST /api/system/oplog —— 操作日志写入(C3;登录态即可,解决方案模块记录自身操作)。
4
4
  import { assertSystemAdmin, currentAccess } from "#/_platform/_shared/service/infra/access";
5
5
  import { recordOp } from "#/_platform/_shared/service/infra/audit";
@@ -16,10 +16,12 @@ get.schema = {
16
16
  type: "object",
17
17
  properties: {
18
18
  action: { type: "string" },
19
- // username 前缀匹配(service likePattern),非精确
19
+ // username 包含匹配(子串,service 走 contains),非精确
20
20
  username: { type: "string" },
21
21
  module: { type: "string" },
22
- // 分页大小,service paged 钳 200
22
+ // 页码(1 起);service 的 pageArgs 非法回落 1
23
+ page: { type: "number", minimum: 1 },
24
+ // 每页条数 1~200(schema 兜住;service 的 pageArgs 再钳一次)
23
25
  limit: { type: "number", minimum: 1, maximum: 200 },
24
26
  },
25
27
  },
@@ -72,4 +72,7 @@ del.schema = {
72
72
  },
73
73
  };
74
74
 
75
+ // 出参无业务载荷(service 返回 void → data 为 null)
76
+ del.response = { type: "null" };
77
+
75
78
  export default { get, post, del };
@@ -26,4 +26,7 @@ put.schema = {
26
26
  },
27
27
  };
28
28
 
29
+ // 出参无业务载荷(service 返回 void → data 为 null)
30
+ put.response = { type: "null" };
31
+
29
32
  export default { put };
@@ -57,4 +57,7 @@ put.schema = {
57
57
  },
58
58
  };
59
59
 
60
+ // 出参无业务载荷(service 返回 void → data 为 null)
61
+ put.response = { type: "null" };
62
+
60
63
  export default { get, put };
@@ -23,4 +23,7 @@ put.schema = {
23
23
  },
24
24
  };
25
25
 
26
+ // 出参无业务载荷(service 返回 void → data 为 null)
27
+ put.response = { type: "null" };
28
+
26
29
  export default { put };
@@ -23,4 +23,7 @@ put.schema = {
23
23
  },
24
24
  };
25
25
 
26
+ // 出参无业务载荷(service 返回 void → data 为 null)
27
+ put.response = { type: "null" };
28
+
26
29
  export default { put };
@@ -24,4 +24,7 @@ put.schema = {
24
24
  },
25
25
  };
26
26
 
27
+ // 出参无业务载荷(service 返回 void → data 为 null)
28
+ put.response = { type: "null" };
29
+
27
30
  export default { put };