@oj-module/cli 0.1.21 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/devkit/README.md +1 -1
  2. package/devkit/SKILL.md +17 -3
  3. package/devkit/manual.md +77 -0
  4. package/package.json +2 -2
  5. package/shell-dist/assets/{index-BUyoDvUl.js → index-CLdhl3Qd.js} +61 -12
  6. package/shell-dist/assets/runtime.js +226 -89
  7. package/shell-dist/index.html +3 -3
  8. package/shell-dist/versions.json +3 -3
  9. package/src/build.ts +25 -0
  10. package/src/init.ts +3 -0
  11. package/templates/README.md +54 -0
  12. package/templates/api/config.yaml +14 -0
  13. package/templates/api/seed.mysql.sql +5 -0
  14. package/templates/api/seed.pg.sql +5 -0
  15. package/templates/api/src/_platform/_shared/respond.ts +18 -0
  16. package/templates/api/src/_platform/_shared/service/access.ts +99 -0
  17. package/templates/api/src/_platform/_shared/service/audit.ts +90 -0
  18. package/templates/api/src/_platform/_shared/service/contacts.ts +36 -0
  19. package/templates/api/src/_platform/_shared/service/guard.ts +30 -0
  20. package/templates/api/src/_platform/_shared/service/oplog.ts +47 -0
  21. package/templates/api/src/_platform/_shared/service/query.ts +53 -0
  22. package/templates/api/src/_platform/_shared/service/rate.ts +79 -0
  23. package/templates/api/src/_platform/_shared/service/roles.ts +137 -0
  24. package/templates/api/src/_platform/_shared/service/settings.ts +67 -0
  25. package/templates/api/src/_platform/_shared/service/solution-perms.ts +94 -0
  26. package/templates/api/src/_platform/_shared/service/users.ts +163 -0
  27. package/templates/api/src/_platform/contacts/api.ts +22 -0
  28. package/templates/api/src/_platform/contacts/detail/api.ts +18 -0
  29. package/templates/api/src/_platform/manifest.yaml +9 -1
  30. package/templates/api/src/_platform/migrations/0001__create_users.mysql.sql +9 -0
  31. package/templates/api/src/_platform/migrations/0001__create_users.pg.sql +9 -0
  32. package/templates/api/src/_platform/migrations/0002__add_users_avatar_base64.mysql.sql +3 -0
  33. package/templates/api/src/_platform/migrations/0003__users_source_display_name.mysql.sql +5 -0
  34. package/templates/api/src/_platform/migrations/0003__users_source_display_name.sql +5 -0
  35. package/templates/api/src/_platform/migrations/0004__sys_rbac.mysql.sql +70 -0
  36. package/templates/api/src/_platform/migrations/0004__sys_rbac.pg.sql +67 -0
  37. package/templates/api/src/_platform/migrations/0004__sys_rbac.sql +67 -0
  38. package/templates/api/src/_platform/migrations/0005__users_must_change.mysql.sql +3 -0
  39. package/templates/api/src/_platform/migrations/0005__users_must_change.sql +2 -0
  40. package/templates/api/src/_platform/migrations/0006__users_is_forbidden.mysql.sql +3 -0
  41. package/templates/api/src/_platform/migrations/0006__users_is_forbidden.sql +3 -0
  42. package/templates/api/src/_platform/migrations/0007__users_email.sql +3 -0
  43. package/templates/api/src/_platform/migrations/0008__sys_refresh_token.mysql.sql +10 -0
  44. package/templates/api/src/_platform/migrations/0008__sys_refresh_token.pg.sql +10 -0
  45. package/templates/api/src/_platform/migrations/0008__sys_refresh_token.sql +11 -0
  46. package/templates/api/src/_platform/migrations/0009__sys_throttle.mysql.sql +12 -0
  47. package/templates/api/src/_platform/migrations/0009__sys_throttle.pg.sql +12 -0
  48. package/templates/api/src/_platform/migrations/0009__sys_throttle.sql +13 -0
  49. package/templates/api/src/_platform/schema.yaml +76 -1
  50. package/templates/api/src/_platform/seed.mysql.sql +8 -0
  51. package/templates/api/src/_platform/seed.pg.sql +8 -0
  52. package/templates/api/src/_platform/seed.sql +19 -2
  53. package/templates/api/src/auth/_shared/ldap-login.ts +86 -0
  54. package/templates/api/src/auth/_shared/ldap.ts +14 -0
  55. package/templates/api/src/auth/_shared/login-throttle.ts +29 -0
  56. package/templates/api/src/auth/_shared/session.ts +163 -18
  57. package/templates/api/src/auth/api.ts +54 -44
  58. package/templates/api/src/auth/ldap-login/api.ts +29 -0
  59. package/templates/api/src/system/login-methods/api.ts +19 -0
  60. package/templates/api/src/system/manifest.yaml +5 -0
  61. package/templates/api/src/system/oplog/api.ts +53 -0
  62. package/templates/api/src/system/permissions/api.ts +26 -0
  63. package/templates/api/src/system/roles/api.ts +75 -0
  64. package/templates/api/src/system/roles/bindings/api.ts +29 -0
  65. package/templates/api/src/system/settings/api.ts +60 -0
  66. package/templates/api/src/system/users/api.ts +58 -0
  67. package/templates/api/src/system/users/forbidden/api.ts +26 -0
  68. package/templates/api/src/system/users/password/api.ts +26 -0
  69. package/templates/api/src/system/users/roles/api.ts +27 -0
  70. package/templates/api/src/web/user-info/api.ts +9 -18
  71. package/templates/api/tests/auth-ldap.spec.ts +173 -0
  72. package/templates/api/tests/auth-session.spec.ts +194 -0
  73. package/templates/api/tests/auth-throttle.spec.ts +74 -0
  74. package/templates/api/tests/fixture.ts +50 -0
  75. package/templates/api/tests/guard.spec.ts +62 -0
  76. package/templates/api/tests/query-utils.spec.ts +68 -0
  77. package/templates/api/tests/rbac-audit.spec.ts +105 -0
  78. package/templates/api/tests/rbac-contacts.spec.ts +85 -0
  79. package/templates/api/tests/rbac-guard.spec.ts +192 -0
  80. package/templates/api/tests/rbac-roles.spec.ts +159 -0
  81. package/templates/api/tests/rbac-seed.spec.ts +54 -0
  82. package/templates/api/tests/rbac-settings.spec.ts +79 -0
  83. package/templates/api/tests/rbac-solution-perms.spec.ts +168 -0
  84. package/templates/api/tests/rbac-tables.spec.ts +63 -0
  85. package/templates/api/tests/rbac-users.spec.ts +164 -0
  86. package/templates/api/vitest.config.ts +12 -0
  87. package/templates/openapi.json +1060 -31
  88. package/templates/pnpm-workspace.yaml +9 -0
  89. package/templates/web/src/auth/__client/api.schemas.ts +6 -1
  90. package/templates/web/src/auth/__client/api.ts +33 -1
  91. package/templates/web/src/login/entry.ts +17 -0
  92. package/templates/web/src/login/locales/en-US.json +12 -2
  93. package/templates/web/src/login/locales/zh-CN.json +12 -2
  94. package/templates/web/src/login/pages/change-password.tsx +74 -0
  95. package/templates/web/src/system/__client/api.schemas.ts +150 -0
  96. package/templates/web/src/system/__client/api.ts +358 -0
  97. package/templates/web/src/system/entry.ts +90 -0
  98. package/templates/web/src/system/locales/en-US.json +98 -0
  99. package/templates/web/src/system/locales/zh-CN.json +98 -0
  100. package/templates/web/src/system/pages/errors.ts +51 -0
  101. package/templates/web/src/system/pages/forbidden.tsx +35 -0
  102. package/templates/web/src/system/pages/login-methods/index.tsx +29 -0
  103. package/templates/web/src/system/pages/oplog/index.tsx +91 -0
  104. package/templates/web/src/system/pages/roles/index.tsx +91 -0
  105. package/templates/web/src/system/pages/settings/index.tsx +106 -0
  106. package/templates/web/src/system/pages/users/index.tsx +168 -0
  107. package/templates/web/src/system/pages/users/roles.tsx +68 -0
  108. package/templates/web.config.ts +8 -0
  109. package/vendor/host-versions.json +3 -3
@@ -0,0 +1,358 @@
1
+ /* eslint-disable */
2
+ // 生成物:ojm api 从契约生成,勿手改(改动请改契约文件后重跑 ojm api)
3
+ import { ContractApiError } from "@oj-module/runtime/contract/errors";
4
+ import type { ScopedRequestLike } from "@oj-module/runtime/contract/errors";
5
+ import type { ModuleContext } from "@oj-module/runtime";
6
+ import type { z } from "@oj-module/runtime";
7
+ import type { schemas } from "./api.schemas";
8
+
9
+ /** oj 信封(AC-D16):code=0 成功;非 0 时 HTTP status=code,由 toApiError 归一为 ContractApiError */
10
+ interface OjEnvelope<T> { code: number, msg?: string, data?: T }
11
+
12
+ let req: ScopedRequestLike | undefined;
13
+
14
+ /** 线上 API 前缀(ojm api 从契约抽取,唯一真源,勿手改):请求基址 + 前缀登记用 */
15
+ export const API_PREFIX = "/system";
16
+
17
+ /** 认领身份前缀(契约 apiPrefix,字面等于模块目录名):跨模块认领护栏对比基准 */
18
+ export const MODULE_PREFIX = "/system";
19
+
20
+ /**
21
+ * 模块 entry 的 onInit 里一行创建(构造即 install):完成 apiPrefix 登记 +
22
+ * scoped request 绑定,返回绑好前缀的端点函数集合。一模块一 client;
23
+ * 重复构造幂等安全(前缀重复登记同值、request 重绑同值)。
24
+ */
25
+ export function createSystemClient(ctx: ModuleContext) {
26
+ // 跨模块认领护栏(DEV):工厂知道自己该属于谁(MODULE_PREFIX = 契约 apiPrefix),
27
+ // ctx 又带 module.name——名实不符说明别的模块在替本模块创建 client,会共享 request
28
+ // 单槽、卸载互相耦合;跨模块需求应由本模块创建后经 provider 暴露。
29
+ // 注意对比基准是 MODULE_PREFIX(目录名)而非 API_PREFIX——D-M8 映射时线上前缀
30
+ // 属于别的命名空间,认领身份仍按目录名。
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 暴露。`);
33
+ }
34
+ ctx.register.apiPrefix(API_PREFIX);
35
+ bindRequest(ctx.utils.request);
36
+ return { systemLoginMethodsGet, systemOplogGet, systemPermissionsGet, systemRolesGet, systemRolesPost, systemRolesDelete, systemRolesBindingsPut, systemSettingsGet, systemSettingsPut, systemUsersGet, systemUsersPost, systemUsersForbiddenPut, systemUsersPasswordPut, systemUsersRolesPut };
37
+ }
38
+
39
+ /**
40
+ * @deprecated 请改用 createSystemClient(ctx)——工厂构造即完成
41
+ * 前缀登记 + request 绑定,不会出现「登记了前缀没绑 request」的半接线状态。
42
+ */
43
+ export function bindRequest(r: ScopedRequestLike): void {
44
+ req = r;
45
+ }
46
+
47
+ function ensureReq(): ScopedRequestLike {
48
+ if (!req)
49
+ throw new ContractApiError(-1, "[ojm-api] 请求未绑定——请在模块 entry.ts 的 onInit 里调用 createSystemClient(ctx)(或 bindRequest(ctx.utils.request))。");
50
+ return req;
51
+ }
52
+
53
+ /** ky HTTPError → ContractApiError(错误体为信封时取 code/msg);契约违例与原错误原样透传 */
54
+ async function toApiError(e: unknown): Promise<unknown> {
55
+ if (e instanceof ContractApiError)
56
+ return e;
57
+ // ky 2.x 构造 HTTPError 时已用 getReader() 消费掉原 response 体(res.clone().json() 按规范抛 TypeError,
58
+ // 体方法一律失效),解析好的错误体在公开字段 error.data——故读 error.data,勿改回 clone().json()。
59
+ const err = e as { data?: { code?: unknown, msg?: string }, response?: Response } | null;
60
+ const res = err?.response;
61
+ const d = err?.data;
62
+ if (res instanceof Response && d && typeof d.code === "number")
63
+ return new ContractApiError(d.code, d.msg ?? res.statusText);
64
+ return e;
65
+ }
66
+
67
+ export type SystemLoginMethodsGetData = z.infer<(typeof schemas)["systemLoginMethodsGet"]["data"]>;
68
+
69
+ export async function systemLoginMethodsGet(): Promise<SystemLoginMethodsGetData> {
70
+ const client = ensureReq();
71
+ try {
72
+ const env = await client.get(`system/login-methods`).json<OjEnvelope<SystemLoginMethodsGetData>>();
73
+ // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
74
+ if (typeof env.code === "number" && env.code !== 0)
75
+ throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
76
+ const data = env.data as SystemLoginMethodsGetData;
77
+ if (import.meta.env.DEV) {
78
+ const { schemas } = await import("./api.schemas");
79
+ const r = schemas["systemLoginMethodsGet"].data.safeParse(data);
80
+ if (!r.success)
81
+ throw new ContractApiError(-1, `[契约违例] systemLoginMethodsGet 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
82
+ }
83
+ return data;
84
+ }
85
+ catch (e) {
86
+ throw await toApiError(e);
87
+ }
88
+ }
89
+
90
+ export type SystemOplogGetQuery = z.input<(typeof schemas)["systemOplogGet"]["query"]>;
91
+ export type SystemOplogGetData = z.infer<(typeof schemas)["systemOplogGet"]["data"]>;
92
+
93
+ export async function systemOplogGet(query: SystemOplogGetQuery): Promise<SystemOplogGetData> {
94
+ const client = ensureReq();
95
+ try {
96
+ const env = await client.get(`system/oplog`, { searchParams: query as Record<string, string | number | boolean> }).json<OjEnvelope<SystemOplogGetData>>();
97
+ // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
98
+ if (typeof env.code === "number" && env.code !== 0)
99
+ throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
100
+ const data = env.data as SystemOplogGetData;
101
+ if (import.meta.env.DEV) {
102
+ const { schemas } = await import("./api.schemas");
103
+ const r = schemas["systemOplogGet"].data.safeParse(data);
104
+ if (!r.success)
105
+ throw new ContractApiError(-1, `[契约违例] systemOplogGet 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
106
+ }
107
+ return data;
108
+ }
109
+ catch (e) {
110
+ throw await toApiError(e);
111
+ }
112
+ }
113
+
114
+ export type SystemPermissionsGetData = z.infer<(typeof schemas)["systemPermissionsGet"]["data"]>;
115
+
116
+ export async function systemPermissionsGet(): Promise<SystemPermissionsGetData> {
117
+ const client = ensureReq();
118
+ try {
119
+ const env = await client.get(`system/permissions`).json<OjEnvelope<SystemPermissionsGetData>>();
120
+ // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
121
+ if (typeof env.code === "number" && env.code !== 0)
122
+ throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
123
+ const data = env.data as SystemPermissionsGetData;
124
+ if (import.meta.env.DEV) {
125
+ const { schemas } = await import("./api.schemas");
126
+ const r = schemas["systemPermissionsGet"].data.safeParse(data);
127
+ if (!r.success)
128
+ throw new ContractApiError(-1, `[契约违例] systemPermissionsGet 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
129
+ }
130
+ return data;
131
+ }
132
+ catch (e) {
133
+ throw await toApiError(e);
134
+ }
135
+ }
136
+
137
+ export type SystemRolesGetData = z.infer<(typeof schemas)["systemRolesGet"]["data"]>;
138
+
139
+ export async function systemRolesGet(): Promise<SystemRolesGetData> {
140
+ const client = ensureReq();
141
+ try {
142
+ const env = await client.get(`system/roles`).json<OjEnvelope<SystemRolesGetData>>();
143
+ // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
144
+ if (typeof env.code === "number" && env.code !== 0)
145
+ throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
146
+ const data = env.data as SystemRolesGetData;
147
+ if (import.meta.env.DEV) {
148
+ const { schemas } = await import("./api.schemas");
149
+ const r = schemas["systemRolesGet"].data.safeParse(data);
150
+ if (!r.success)
151
+ throw new ContractApiError(-1, `[契约违例] systemRolesGet 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
152
+ }
153
+ return data;
154
+ }
155
+ catch (e) {
156
+ throw await toApiError(e);
157
+ }
158
+ }
159
+
160
+ export type SystemRolesPostBody = z.input<(typeof schemas)["systemRolesPost"]["body"]>;
161
+ export type SystemRolesPostData = z.infer<(typeof schemas)["systemRolesPost"]["data"]>;
162
+
163
+ export async function systemRolesPost(body: SystemRolesPostBody): Promise<SystemRolesPostData> {
164
+ const client = ensureReq();
165
+ try {
166
+ const env = await client.post(`system/roles`, { json: body }).json<OjEnvelope<SystemRolesPostData>>();
167
+ // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
168
+ if (typeof env.code === "number" && env.code !== 0)
169
+ throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
170
+ const data = env.data as SystemRolesPostData;
171
+ if (import.meta.env.DEV) {
172
+ const { schemas } = await import("./api.schemas");
173
+ const r = schemas["systemRolesPost"].data.safeParse(data);
174
+ if (!r.success)
175
+ throw new ContractApiError(-1, `[契约违例] systemRolesPost 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
176
+ }
177
+ return data;
178
+ }
179
+ catch (e) {
180
+ throw await toApiError(e);
181
+ }
182
+ }
183
+
184
+ export type SystemRolesDeleteQuery = z.input<(typeof schemas)["systemRolesDelete"]["query"]>;
185
+
186
+ export async function systemRolesDelete(query: SystemRolesDeleteQuery): Promise<unknown> {
187
+ const client = ensureReq();
188
+ try {
189
+ const env = await client.delete(`system/roles`, { searchParams: query as Record<string, string | number | boolean> }).json<OjEnvelope<unknown>>();
190
+ // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
191
+ if (typeof env.code === "number" && env.code !== 0)
192
+ throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
193
+ const data = env.data as unknown;
194
+ return data;
195
+ }
196
+ catch (e) {
197
+ throw await toApiError(e);
198
+ }
199
+ }
200
+
201
+ export type SystemRolesBindingsPutQuery = z.input<(typeof schemas)["systemRolesBindingsPut"]["query"]>;
202
+ export type SystemRolesBindingsPutBody = z.input<(typeof schemas)["systemRolesBindingsPut"]["body"]>;
203
+
204
+ export async function systemRolesBindingsPut(input: { query: SystemRolesBindingsPutQuery, body: SystemRolesBindingsPutBody }): Promise<unknown> {
205
+ const client = ensureReq();
206
+ try {
207
+ const env = await client.put(`system/roles/bindings`, { searchParams: input.query as Record<string, string | number | boolean>, json: input.body }).json<OjEnvelope<unknown>>();
208
+ // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
209
+ if (typeof env.code === "number" && env.code !== 0)
210
+ throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
211
+ const data = env.data as unknown;
212
+ return data;
213
+ }
214
+ catch (e) {
215
+ throw await toApiError(e);
216
+ }
217
+ }
218
+
219
+ export type SystemSettingsGetData = z.infer<(typeof schemas)["systemSettingsGet"]["data"]>;
220
+
221
+ export async function systemSettingsGet(): Promise<SystemSettingsGetData> {
222
+ const client = ensureReq();
223
+ try {
224
+ const env = await client.get(`system/settings`).json<OjEnvelope<SystemSettingsGetData>>();
225
+ // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
226
+ if (typeof env.code === "number" && env.code !== 0)
227
+ throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
228
+ const data = env.data as SystemSettingsGetData;
229
+ if (import.meta.env.DEV) {
230
+ const { schemas } = await import("./api.schemas");
231
+ const r = schemas["systemSettingsGet"].data.safeParse(data);
232
+ if (!r.success)
233
+ throw new ContractApiError(-1, `[契约违例] systemSettingsGet 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
234
+ }
235
+ return data;
236
+ }
237
+ catch (e) {
238
+ throw await toApiError(e);
239
+ }
240
+ }
241
+
242
+ export type SystemSettingsPutBody = z.input<(typeof schemas)["systemSettingsPut"]["body"]>;
243
+
244
+ export async function systemSettingsPut(body: SystemSettingsPutBody): Promise<unknown> {
245
+ const client = ensureReq();
246
+ try {
247
+ const env = await client.put(`system/settings`, { json: body }).json<OjEnvelope<unknown>>();
248
+ // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
249
+ if (typeof env.code === "number" && env.code !== 0)
250
+ throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
251
+ const data = env.data as unknown;
252
+ return data;
253
+ }
254
+ catch (e) {
255
+ throw await toApiError(e);
256
+ }
257
+ }
258
+
259
+ export type SystemUsersGetData = z.infer<(typeof schemas)["systemUsersGet"]["data"]>;
260
+
261
+ export async function systemUsersGet(): Promise<SystemUsersGetData> {
262
+ const client = ensureReq();
263
+ try {
264
+ const env = await client.get(`system/users`).json<OjEnvelope<SystemUsersGetData>>();
265
+ // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
266
+ if (typeof env.code === "number" && env.code !== 0)
267
+ throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
268
+ const data = env.data as SystemUsersGetData;
269
+ if (import.meta.env.DEV) {
270
+ const { schemas } = await import("./api.schemas");
271
+ const r = schemas["systemUsersGet"].data.safeParse(data);
272
+ if (!r.success)
273
+ throw new ContractApiError(-1, `[契约违例] systemUsersGet 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
274
+ }
275
+ return data;
276
+ }
277
+ catch (e) {
278
+ throw await toApiError(e);
279
+ }
280
+ }
281
+
282
+ export type SystemUsersPostBody = z.input<(typeof schemas)["systemUsersPost"]["body"]>;
283
+ export type SystemUsersPostData = z.infer<(typeof schemas)["systemUsersPost"]["data"]>;
284
+
285
+ export async function systemUsersPost(body: SystemUsersPostBody): Promise<SystemUsersPostData> {
286
+ const client = ensureReq();
287
+ try {
288
+ const env = await client.post(`system/users`, { json: body }).json<OjEnvelope<SystemUsersPostData>>();
289
+ // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
290
+ if (typeof env.code === "number" && env.code !== 0)
291
+ throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
292
+ const data = env.data as SystemUsersPostData;
293
+ if (import.meta.env.DEV) {
294
+ const { schemas } = await import("./api.schemas");
295
+ const r = schemas["systemUsersPost"].data.safeParse(data);
296
+ if (!r.success)
297
+ throw new ContractApiError(-1, `[契约违例] systemUsersPost 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
298
+ }
299
+ return data;
300
+ }
301
+ catch (e) {
302
+ throw await toApiError(e);
303
+ }
304
+ }
305
+
306
+ export type SystemUsersForbiddenPutQuery = z.input<(typeof schemas)["systemUsersForbiddenPut"]["query"]>;
307
+ export type SystemUsersForbiddenPutBody = z.input<(typeof schemas)["systemUsersForbiddenPut"]["body"]>;
308
+
309
+ export async function systemUsersForbiddenPut(input: { query: SystemUsersForbiddenPutQuery, body: SystemUsersForbiddenPutBody }): Promise<unknown> {
310
+ const client = ensureReq();
311
+ try {
312
+ const env = await client.put(`system/users/forbidden`, { searchParams: input.query as Record<string, string | number | boolean>, json: input.body }).json<OjEnvelope<unknown>>();
313
+ // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
314
+ if (typeof env.code === "number" && env.code !== 0)
315
+ throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
316
+ const data = env.data as unknown;
317
+ return data;
318
+ }
319
+ catch (e) {
320
+ throw await toApiError(e);
321
+ }
322
+ }
323
+
324
+ export type SystemUsersPasswordPutQuery = z.input<(typeof schemas)["systemUsersPasswordPut"]["query"]>;
325
+ export type SystemUsersPasswordPutBody = z.input<(typeof schemas)["systemUsersPasswordPut"]["body"]>;
326
+
327
+ export async function systemUsersPasswordPut(input: { query: SystemUsersPasswordPutQuery, body: SystemUsersPasswordPutBody }): Promise<unknown> {
328
+ const client = ensureReq();
329
+ try {
330
+ const env = await client.put(`system/users/password`, { searchParams: input.query as Record<string, string | number | boolean>, json: input.body }).json<OjEnvelope<unknown>>();
331
+ // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
332
+ if (typeof env.code === "number" && env.code !== 0)
333
+ throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
334
+ const data = env.data as unknown;
335
+ return data;
336
+ }
337
+ catch (e) {
338
+ throw await toApiError(e);
339
+ }
340
+ }
341
+
342
+ export type SystemUsersRolesPutQuery = z.input<(typeof schemas)["systemUsersRolesPut"]["query"]>;
343
+ export type SystemUsersRolesPutBody = z.input<(typeof schemas)["systemUsersRolesPut"]["body"]>;
344
+
345
+ export async function systemUsersRolesPut(input: { query: SystemUsersRolesPutQuery, body: SystemUsersRolesPutBody }): Promise<unknown> {
346
+ const client = ensureReq();
347
+ try {
348
+ const env = await client.put(`system/users/roles`, { searchParams: input.query as Record<string, string | number | boolean>, json: input.body }).json<OjEnvelope<unknown>>();
349
+ // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
350
+ if (typeof env.code === "number" && env.code !== 0)
351
+ throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
352
+ const data = env.data as unknown;
353
+ return data;
354
+ }
355
+ catch (e) {
356
+ throw await toApiError(e);
357
+ }
358
+ }
@@ -0,0 +1,90 @@
1
+ import type { AppRouteRecordRaw } from "@oj-module/runtime";
2
+
3
+ import { SettingOutlined } from "@ant-design/icons";
4
+ import { defineModule } from "@oj-module/runtime";
5
+
6
+ import { createElement, lazy } from "react";
7
+
8
+ import { createSystemClient } from "./__client/api";
9
+
10
+ const Users = lazy(() => import("./pages/users"));
11
+ const UserRoles = lazy(() => import("./pages/users/roles"));
12
+ const Roles = lazy(() => import("./pages/roles"));
13
+ const Oplog = lazy(() => import("./pages/oplog"));
14
+ const Settings = lazy(() => import("./pages/settings"));
15
+ const LoginMethods = lazy(() => import("./pages/login-methods"));
16
+
17
+ const routes: AppRouteRecordRaw[] = [
18
+ {
19
+ path: "/system",
20
+ handle: {
21
+ icon: createElement(SettingOutlined),
22
+ title: "system:menu.system",
23
+ order: 100,
24
+ layout: "container",
25
+ },
26
+ children: [
27
+ {
28
+ path: "/system/users",
29
+ Component: Users,
30
+ handle: { title: "system:menu.users" },
31
+ },
32
+ {
33
+ // 成员资格(superadmin 入口从用户管理页进入,菜单不出现)
34
+ path: "/system/users/:uid/roles",
35
+ Component: UserRoles,
36
+ handle: { title: "system:menu.userRoles", hideInMenu: true },
37
+ },
38
+ {
39
+ path: "/system/roles",
40
+ Component: Roles,
41
+ handle: { title: "system:menu.roles" },
42
+ },
43
+ {
44
+ path: "/system/oplog",
45
+ Component: Oplog,
46
+ handle: { title: "system:menu.oplog" },
47
+ },
48
+ {
49
+ path: "/system/settings",
50
+ Component: Settings,
51
+ handle: { title: "system:menu.settings" },
52
+ },
53
+ {
54
+ path: "/system/login-methods",
55
+ Component: LoginMethods,
56
+ handle: { title: "system:menu.loginMethods" },
57
+ },
58
+ ],
59
+ },
60
+ ];
61
+
62
+ /**
63
+ * system 模块:框架管理面(用户/成员资格/角色权限/操作日志/站点设置/登录方式),
64
+ * 数据源为 oj 后端 /api/system/*(模板 api 模块)。__client 不随模板分发——
65
+ * init 后 `oj openapi -o openapi.json` + `ojm api` 生成(生成前本文件 ts 报红属预期,
66
+ * devkit manual §2.2)。
67
+ */
68
+ export default defineModule({
69
+ name: "system",
70
+ description: "系统管理模块(框架管理面)",
71
+ version: "0.1.0",
72
+ peerRuntime: ">=0.0.0",
73
+ // 路由级门禁(B16):非系统管理员不注入 /system 路由、菜单不可见;
74
+ // API 级 403 由 assertSystemAdmin + service 层硬 enforcement 兜底。
75
+ // 角色口径对齐后端 assertSystemAdmin(access.ts:90)——superadmin **或**
76
+ // admin 均可;seed 的 admin 账号只有 superadmin,写 ["admin"] 会把它自己挡在门外。
77
+ config: { requiredRoles: ["admin", "superadmin"] },
78
+ routes,
79
+ i18n: {
80
+ "zh-CN": () => import("./locales/zh-CN.json"),
81
+ "en-US": () => import("./locales/en-US.json"),
82
+ },
83
+ lifecycle: {
84
+ async onInit(ctx) {
85
+ // 构造即接线:登记 /system 前缀 + 绑定 scoped request(D11 前缀收敛),
86
+ // 页面直接 import 生成的端点函数(模块级单槽 req 已由本调用绑定)。
87
+ createSystemClient(ctx);
88
+ },
89
+ },
90
+ });
@@ -0,0 +1,98 @@
1
+ {
2
+ "menu": {
3
+ "system": "System",
4
+ "users": "Users",
5
+ "userRoles": "Membership",
6
+ "roles": "Roles & Permissions",
7
+ "oplog": "Audit Log",
8
+ "settings": "Site Settings",
9
+ "loginMethods": "Login Methods"
10
+ },
11
+ "common": {
12
+ "confirm": "Confirm",
13
+ "cancel": "Cancel",
14
+ "save": "Save",
15
+ "saved": "Saved",
16
+ "search": "Search",
17
+ "reset": "Reset",
18
+ "create": "Create"
19
+ },
20
+ "users": {
21
+ "create": "Create User",
22
+ "resetPwd": "Reset Password",
23
+ "forbidden": "Forbidden",
24
+ "createOk": "User created: password change required on first login",
25
+ "firstLoginHint": "Must change on first login",
26
+ "membership": "Membership",
27
+ "pwdPlaceholder": "At least 6 characters",
28
+ "columns": {
29
+ "username": "Username",
30
+ "displayName": "Display Name",
31
+ "email": "Email",
32
+ "roles": "Roles",
33
+ "status": "Status",
34
+ "actions": "Actions"
35
+ }
36
+ },
37
+ "userRoles": {
38
+ "title": "Membership",
39
+ "saved": "Roles updated",
40
+ "none": "(no roles)"
41
+ },
42
+ "rolesPage": {
43
+ "tabRoles": "Roles",
44
+ "tabPerms": "Permissions",
45
+ "builtin": "Built-in",
46
+ "deprecated": "Deprecated",
47
+ "holders": "Holders",
48
+ "permissions": "Bound Permissions",
49
+ "titleKey": "Title Key",
50
+ "readonlyHint": "Read-only view: roles and bindings are managed by solution tooling (upsertSolutionPerms)"
51
+ },
52
+ "oplog": {
53
+ "filterAction": "Action",
54
+ "filterUsername": "Username (prefix)",
55
+ "filterModule": "Module",
56
+ "filterLimit": "Limit",
57
+ "truncated": "Results truncated by limit; narrow your filters",
58
+ "columns": {
59
+ "time": "Time",
60
+ "username": "Username",
61
+ "action": "Action",
62
+ "module": "Module",
63
+ "target": "Target",
64
+ "detail": "Detail",
65
+ "level": "Level"
66
+ }
67
+ },
68
+ "settingsPage": {
69
+ "secretHint": "Secret items cannot be edited online",
70
+ "saved": "Settings saved"
71
+ },
72
+ "loginMethodsPage": {
73
+ "title": "Enabled login methods",
74
+ "methodLocal": "Local password",
75
+ "methodLdap": "LDAP",
76
+ "methodSso": "SSO"
77
+ },
78
+ "err": {
79
+ "forbidden": "Forbidden",
80
+ "selfAction": "You cannot perform this action on yourself",
81
+ "pwdTooShort": "Password must be at least 6 characters",
82
+ "usernameExists": "Username already exists",
83
+ "usernameReserved": "This username prefix is reserved for external identity sources",
84
+ "mustChange": {
85
+ "title": "Password change required",
86
+ "hint": "This account must change its password before admin features become available.",
87
+ "go": "Change password now"
88
+ }
89
+ },
90
+ "setting": {
91
+ "site_title": "Site Title",
92
+ "ldap_enabled": "Enable LDAP login",
93
+ "sso_enabled": "Enable SSO login",
94
+ "ldap_base_dn": "LDAP Base DN",
95
+ "ldap_uid_attr": "LDAP UID Attribute",
96
+ "ldap_email_attr": "LDAP Email Attribute"
97
+ }
98
+ }
@@ -0,0 +1,98 @@
1
+ {
2
+ "menu": {
3
+ "system": "系统管理",
4
+ "users": "用户管理",
5
+ "userRoles": "成员资格",
6
+ "roles": "角色权限",
7
+ "oplog": "操作日志",
8
+ "settings": "站点设置",
9
+ "loginMethods": "登录方式"
10
+ },
11
+ "common": {
12
+ "confirm": "确认",
13
+ "cancel": "取消",
14
+ "save": "保存",
15
+ "saved": "已保存",
16
+ "search": "查询",
17
+ "reset": "重置",
18
+ "create": "新建"
19
+ },
20
+ "users": {
21
+ "create": "建号",
22
+ "resetPwd": "重置口令",
23
+ "forbidden": "禁用",
24
+ "createOk": "建号成功,首次登录须改密",
25
+ "firstLoginHint": "首登改密",
26
+ "membership": "成员资格",
27
+ "pwdPlaceholder": "至少 6 位",
28
+ "columns": {
29
+ "username": "用户名",
30
+ "displayName": "显示名",
31
+ "email": "邮箱",
32
+ "roles": "角色",
33
+ "status": "状态",
34
+ "actions": "操作"
35
+ }
36
+ },
37
+ "userRoles": {
38
+ "title": "成员资格",
39
+ "saved": "角色已更新",
40
+ "none": "(无角色)"
41
+ },
42
+ "rolesPage": {
43
+ "tabRoles": "角色",
44
+ "tabPerms": "权限点",
45
+ "builtin": "内置",
46
+ "deprecated": "已废弃",
47
+ "holders": "持有者",
48
+ "permissions": "已绑定权限点",
49
+ "titleKey": "标题键",
50
+ "readonlyHint": "只读视图:角色与绑定由解决方案工具链(upsertSolutionPerms)管理"
51
+ },
52
+ "oplog": {
53
+ "filterAction": "操作 (action)",
54
+ "filterUsername": "用户名(前缀匹配)",
55
+ "filterModule": "模块",
56
+ "filterLimit": "条数上限",
57
+ "truncated": "结果已按上限截断,请缩小过滤范围",
58
+ "columns": {
59
+ "time": "时间",
60
+ "username": "用户名",
61
+ "action": "操作",
62
+ "module": "模块",
63
+ "target": "目标",
64
+ "detail": "详情",
65
+ "level": "级别"
66
+ }
67
+ },
68
+ "settingsPage": {
69
+ "secretHint": "敏感项不可在线修改",
70
+ "saved": "设置已保存"
71
+ },
72
+ "loginMethodsPage": {
73
+ "title": "当前启用的登录方式",
74
+ "methodLocal": "本地口令",
75
+ "methodLdap": "LDAP",
76
+ "methodSso": "SSO 单点登录"
77
+ },
78
+ "err": {
79
+ "forbidden": "无权限",
80
+ "selfAction": "不能对自己执行该操作",
81
+ "pwdTooShort": "口令至少 6 位",
82
+ "usernameExists": "用户名已存在",
83
+ "usernameReserved": "该用户名前缀为外部身份源保留",
84
+ "mustChange": {
85
+ "title": "需要先修改密码",
86
+ "hint": "当前账号尚未完成首次改密,管理功能暂不可用。修改完成后将自动返回。",
87
+ "go": "去修改密码"
88
+ }
89
+ },
90
+ "setting": {
91
+ "site_title": "站点标题",
92
+ "ldap_enabled": "启用 LDAP 登录",
93
+ "sso_enabled": "启用 SSO 登录",
94
+ "ldap_base_dn": "LDAP Base DN",
95
+ "ldap_uid_attr": "LDAP UID 属性",
96
+ "ldap_email_attr": "LDAP 邮箱属性"
97
+ }
98
+ }