create-windy 0.6.21 → 0.8.0

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 (129) hide show
  1. package/README.md +6 -1
  2. package/dist/cli.js +19 -7
  3. package/package.json +1 -1
  4. package/template/.windy-template.json +2 -2
  5. package/template/AGENTS.md +1 -1
  6. package/template/apps/server/src/audit/policy.ts +6 -1
  7. package/template/apps/server/src/auth/bootstrap-admin.ts +10 -1
  8. package/template/apps/server/src/auth/composition.ts +5 -0
  9. package/template/apps/server/src/auth/credential-authorization.test.ts +257 -0
  10. package/template/apps/server/src/auth/credential-routes.test.ts +1 -0
  11. package/template/apps/server/src/auth/credential-service.ts +47 -12
  12. package/template/apps/server/src/auth/drizzle-repository.ts +13 -5
  13. package/template/apps/server/src/auth/drizzle-session-operations.ts +1 -0
  14. package/template/apps/server/src/auth/repository.ts +9 -6
  15. package/template/apps/server/src/auth/routes.test-support.ts +1 -0
  16. package/template/apps/server/src/auth/runtime.ts +4 -0
  17. package/template/apps/server/src/auth/service-options.ts +2 -0
  18. package/template/apps/server/src/auth/service.ts +2 -0
  19. package/template/apps/server/src/auth/user-credential-access.ts +37 -0
  20. package/template/apps/server/src/foundation-activation.test.ts +24 -1
  21. package/template/apps/server/src/foundation.ts +0 -2
  22. package/template/apps/server/src/index.ts +1 -0
  23. package/template/apps/server/src/runtime-development.ts +2 -0
  24. package/template/apps/server/src/runtime.test.ts +13 -4
  25. package/template/apps/server/src/system/department-organization.ts +58 -59
  26. package/template/apps/server/src/system/department-read-routes.ts +54 -0
  27. package/template/apps/server/src/system/department-routes.test.ts +27 -19
  28. package/template/apps/server/src/system/department-routes.ts +95 -33
  29. package/template/apps/server/src/system/department-transition-safety.test.ts +148 -0
  30. package/template/apps/server/src/system/department-validation.ts +79 -0
  31. package/template/apps/server/src/system/drizzle-repository.ts +14 -0
  32. package/template/apps/server/src/system/drizzle-system-configs.ts +2 -0
  33. package/template/apps/server/src/system/identity-read-model.ts +105 -0
  34. package/template/apps/server/src/system/identity-role-assignment.test.ts +199 -0
  35. package/template/apps/server/src/system/identity-role-assignment.ts +148 -0
  36. package/template/apps/server/src/system/identity-route-config.ts +29 -7
  37. package/template/apps/server/src/system/identity-write-security.test.ts +127 -0
  38. package/template/apps/server/src/system/identity-write-validation.ts +170 -0
  39. package/template/apps/server/src/system/organization-read-security.test.ts +221 -0
  40. package/template/apps/server/src/system/redis-health.test.ts +1 -1
  41. package/template/apps/server/src/system/repository.ts +29 -2
  42. package/template/apps/server/src/system/resource-routes.ts +116 -72
  43. package/template/apps/server/src/system/route-audit.test.ts +117 -0
  44. package/template/apps/server/src/system/route-helpers.ts +14 -0
  45. package/template/apps/server/src/system/route-types.ts +11 -2
  46. package/template/apps/server/src/system/routes.test.ts +0 -68
  47. package/template/apps/server/src/system/routes.ts +29 -2
  48. package/template/apps/server/src/system/user-account-policy.test.ts +3 -1
  49. package/template/apps/server/src/system/user-account-policy.ts +26 -9
  50. package/template/apps/web/src/pages/system/SystemResourceRoutePage.vue +27 -13
  51. package/template/apps/web/src/pages/system/SystemResourceRoutePage.webtest.ts +68 -0
  52. package/template/apps/web/src/pages/system/components/PagedSelectionList.vue +131 -0
  53. package/template/apps/web/src/pages/system/components/PagedSelectionList.webtest.ts +32 -0
  54. package/template/apps/web/src/pages/system/components/SystemRecordDetailDialog.vue +67 -0
  55. package/template/apps/web/src/pages/system/components/SystemResourceEditor.vue +114 -38
  56. package/template/apps/web/src/pages/system/components/SystemResourceEditor.webtest.ts +180 -0
  57. package/template/apps/web/src/pages/system/components/SystemResourceFilters.vue +8 -0
  58. package/template/apps/web/src/pages/system/components/SystemResourceForm.vue +8 -2
  59. package/template/apps/web/src/pages/system/components/SystemResourcePage.department.webtest.ts +89 -0
  60. package/template/apps/web/src/pages/system/components/SystemResourcePage.vue +42 -46
  61. package/template/apps/web/src/pages/system/components/SystemResourcePage.webtest.ts +12 -55
  62. package/template/apps/web/src/pages/system/components/SystemResourceTable.vue +14 -2
  63. package/template/apps/web/src/pages/system/components/department/DepartmentOrganizationForm.vue +121 -85
  64. package/template/apps/web/src/pages/system/components/department/DepartmentOrganizationForm.webtest.ts +57 -0
  65. package/template/apps/web/src/pages/system/components/department/DepartmentOrganizationTable.vue +227 -0
  66. package/template/apps/web/src/pages/system/components/department/DepartmentOrganizationTable.webtest.ts +62 -0
  67. package/template/apps/web/src/pages/system/components/department/DepartmentTransitionDialog.vue +72 -52
  68. package/template/apps/web/src/pages/system/components/role-policy/RolePermissionTree.vue +150 -79
  69. package/template/apps/web/src/pages/system/components/role-policy/RolePolicyForm.vue +98 -8
  70. package/template/apps/web/src/pages/system/components/role-policy/RolePolicyForm.webtest.ts +108 -0
  71. package/template/apps/web/src/pages/system/components/selection-options.ts +71 -0
  72. package/template/apps/web/src/pages/system/components/selection-options.webtest.ts +49 -0
  73. package/template/apps/web/src/pages/system/components/user-account/CreateDepartmentDialog.vue +19 -27
  74. package/template/apps/web/src/pages/system/components/user-account/CreateDepartmentDialog.webtest.ts +2 -6
  75. package/template/apps/web/src/pages/system/components/user-account/CreateRoleDialog.vue +128 -0
  76. package/template/apps/web/src/pages/system/components/user-account/CreateRoleDialog.webtest.ts +76 -0
  77. package/template/apps/web/src/pages/system/components/user-account/RolePermissionSummary.vue +61 -3
  78. package/template/apps/web/src/pages/system/components/user-account/UserAccountForm.vue +144 -90
  79. package/template/apps/web/src/pages/system/components/user-account/UserAccountForm.webtest.ts +95 -0
  80. package/template/apps/web/src/pages/system/components/user-account/UserListFilters.vue +168 -0
  81. package/template/apps/web/src/pages/system/components/user-account/UserRoleSelector.vue +33 -57
  82. package/template/apps/web/src/pages/system/components/user-account/UserRoleSelector.webtest.ts +4 -4
  83. package/template/apps/web/src/pages/system/components/user-account/save-user-account.ts +4 -1
  84. package/template/apps/web/src/pages/system/components/user-account/user-account-draft.ts +81 -0
  85. package/template/apps/web/src/pages/system/composables/useDepartmentTransitions.ts +6 -10
  86. package/template/apps/web/src/pages/system/composables/useResourceEditor.ts +72 -0
  87. package/template/apps/web/src/pages/system/composables/useResourceEditor.webtest.ts +110 -0
  88. package/template/apps/web/src/pages/system/composables/useSystemResource.ts +27 -1
  89. package/template/apps/web/src/pages/system/resource-config.ts +2 -0
  90. package/template/apps/web/src/services/system-api.ts +25 -0
  91. package/template/docker-compose.yml +3 -3
  92. package/template/docs/architecture/foundation-infrastructure.md +12 -25
  93. package/template/docs/development/crud-generator.md +1 -1
  94. package/template/docs/operations/legacy-data-migration.md +73 -0
  95. package/template/docs/platform/bulk-data-jobs.md +1 -1
  96. package/template/docs/platform/module-configuration.md +3 -3
  97. package/template/docs/platform/organization-membership.md +38 -7
  98. package/template/docs/platform/platform-authentication.md +1 -1
  99. package/template/docs/platform/scheduled-task-executor.md +1 -1
  100. package/template/package.json +2 -1
  101. package/template/packages/config/index.test.ts +1 -5
  102. package/template/packages/config/package.json +2 -2
  103. package/template/packages/config/src/platform-boundary.test.ts +39 -0
  104. package/template/packages/config/src/platform.ts +0 -28
  105. package/template/packages/database/drizzle/0063_license_project_lifecycle.sql +28 -0
  106. package/template/packages/database/drizzle/meta/0063_snapshot.json +15053 -0
  107. package/template/packages/database/drizzle/meta/_journal.json +7 -0
  108. package/template/packages/database/package.json +2 -2
  109. package/template/packages/database/src/agent-runtime-schema.test.ts +5 -1
  110. package/template/packages/database/src/schema/index.ts +1 -0
  111. package/template/packages/database/src/schema/license-issuance-requests.ts +59 -0
  112. package/template/packages/database/src/schema/license-v1.ts +5 -0
  113. package/template/packages/jobs/package.json +2 -2
  114. package/template/packages/modules/package.json +1 -1
  115. package/template/packages/modules/src/system-api-permissions.ts +14 -0
  116. package/template/packages/modules/src/system-features.ts +0 -11
  117. package/template/packages/modules/src/system-module-catalog.test.ts +4 -0
  118. package/template/packages/modules/src/system-module-catalog.ts +0 -1
  119. package/template/packages/server-sdk/index.ts +1 -49
  120. package/template/packages/server-sdk/package.json +3 -2
  121. package/template/packages/server-sdk/src/ai-runtime-readiness-exports.ts +49 -0
  122. package/template/packages/shared/index.ts +2 -1
  123. package/template/packages/shared/package.json +1 -1
  124. package/template/packages/shared/src/license-console-capabilities.ts +163 -0
  125. package/template/packages/shared/src/license-console.ts +94 -0
  126. package/template/packages/shared/src/license.ts +5 -0
  127. package/template/scripts/lib/postgres-regression.ts +1 -1
  128. package/template/docs/adapters/ruoyi-vue-compatibility.md +0 -93
  129. package/template/packages/shared/src/ruoyi.ts +0 -98
@@ -1,3 +1,5 @@
1
+ import { identityMutationResponse } from "./identity-write-validation.js";
2
+ import { departmentWritableFields } from "./department-validation.js";
1
3
  import type { FeatureFlagDefinition } from "@southwind-ai/shared";
2
4
  import { recordMutationAudit } from "../audit/policy.js";
3
5
  import { evaluateRouteGuards } from "../route-guards.js";
@@ -36,6 +38,14 @@ export function registerDepartmentQuickCreateRoute(
36
38
  const parsed = parseDepartment(body);
37
39
  if (parsed instanceof Response) return parsed;
38
40
  const create = async () => {
41
+ const validationError = await policy?.validateInput(parsed);
42
+ if (validationError)
43
+ return Response.json(
44
+ typeof validationError === "string"
45
+ ? { message: validationError }
46
+ : validationError,
47
+ { status: 400 },
48
+ );
39
49
  const policyError = await policy?.beforeCreate(parsed);
40
50
  if (policyError) {
41
51
  return Response.json(policyError, { status: 409 });
@@ -50,13 +60,24 @@ export function registerDepartmentQuickCreateRoute(
50
60
  );
51
61
  return item;
52
62
  };
53
- return mutationRunner ? mutationRunner.run(create) : create();
63
+ return identityMutationResponse(() =>
64
+ mutationRunner ? mutationRunner.run(create) : create(),
65
+ );
54
66
  });
55
67
  }
56
68
 
57
69
  function parseDepartment(body: unknown): Partial<ManagedDepartment> | Response {
58
70
  const value =
59
71
  body && typeof body === "object" ? (body as Record<string, unknown>) : {};
72
+ if (
73
+ Object.keys(value).some(
74
+ (key) => !departmentWritableFields.some((field) => field === key),
75
+ )
76
+ )
77
+ return Response.json(
78
+ { error: "INVALID_FIELDS", message: "包含不可写字段" },
79
+ { status: 400 },
80
+ );
60
81
  const code = typeof value.code === "string" ? value.code.trim() : "";
61
82
  const name = typeof value.name === "string" ? value.name.trim() : "";
62
83
  if (!/^[a-zA-Z][a-zA-Z0-9_-]{1,79}$/.test(code) || !name) {
@@ -85,10 +106,10 @@ function parseDepartment(body: unknown): Partial<ManagedDepartment> | Response {
85
106
  code,
86
107
  name,
87
108
  parentId,
88
- order:
89
- typeof value.order === "number" && Number.isFinite(value.order)
90
- ? value.order
91
- : 100,
109
+ ...(value.status === undefined
110
+ ? {}
111
+ : { status: value.status as ManagedDepartment["status"] }),
112
+ order: value.order === undefined ? 100 : (value.order as number),
92
113
  };
93
114
  }
94
115
 
@@ -112,35 +133,38 @@ export function registerDepartmentTransitionRoute(
112
133
  if (!decision.allowed) return forbidden(decision.reason);
113
134
  const parsed = parseTransition(body);
114
135
  if (parsed instanceof Response) return parsed;
115
- return mutationRunner.run(async () => {
116
- const result = await policy.transition(
117
- String(params.id || ""),
118
- parsed.action,
119
- parsed.targetDepartmentId,
120
- guardContext.actor,
121
- );
122
- if ("error" in result) {
123
- return Response.json(result, { status: 409 });
124
- }
125
- await recordMutationAudit(
126
- runtime,
127
- "departments",
128
- resourceEvent(
129
- guardContext,
130
- "organization.department.transition",
136
+ return identityMutationResponse(() =>
137
+ mutationRunner.run(async () => {
138
+ const result = await policy.transition(
139
+ String(params.id || ""),
140
+ parsed.action,
141
+ parsed.targetDepartmentId,
142
+ guardContext.actor,
143
+ parsed.updates,
144
+ );
145
+ if ("error" in result) {
146
+ return Response.json(result, { status: 409 });
147
+ }
148
+ await recordMutationAudit(
149
+ runtime,
131
150
  "departments",
132
- result.department.id,
133
- {
134
- action: parsed.action,
135
- targetDepartmentId: parsed.targetDepartmentId,
136
- movedChildren: result.movedChildren,
137
- movedUsers: result.movedUsers,
138
- updatedRoles: result.updatedRoles,
139
- },
140
- ),
141
- );
142
- return result;
143
- });
151
+ resourceEvent(
152
+ guardContext,
153
+ "organization.department.transition",
154
+ "departments",
155
+ result.department.id,
156
+ {
157
+ action: parsed.action,
158
+ targetDepartmentId: parsed.targetDepartmentId,
159
+ movedChildren: result.movedChildren,
160
+ movedUsers: result.movedUsers,
161
+ updatedRoles: result.updatedRoles,
162
+ },
163
+ ),
164
+ );
165
+ return result;
166
+ }),
167
+ );
144
168
  });
145
169
  }
146
170
 
@@ -148,6 +172,7 @@ function parseTransition(body: unknown):
148
172
  | {
149
173
  action: DepartmentTransitionAction;
150
174
  targetDepartmentId?: string;
175
+ updates?: Partial<ManagedDepartment>;
151
176
  }
152
177
  | Response {
153
178
  const value =
@@ -161,8 +186,45 @@ function parseTransition(body: unknown):
161
186
  { status: 400 },
162
187
  );
163
188
  }
189
+ if (
190
+ Object.keys(value).some(
191
+ (key) => !["action", "targetDepartmentId", "updates"].includes(key),
192
+ ) ||
193
+ (value.targetDepartmentId !== undefined &&
194
+ typeof value.targetDepartmentId !== "string")
195
+ )
196
+ return Response.json(
197
+ { error: "INVALID_DEPARTMENT_TRANSITION", message: "部门处置字段不合法" },
198
+ { status: 400 },
199
+ );
200
+ if (
201
+ value.updates !== undefined &&
202
+ (!value.updates ||
203
+ typeof value.updates !== "object" ||
204
+ Array.isArray(value.updates) ||
205
+ Object.keys(value.updates).some(
206
+ (key) => !["name", "code", "order", "parentId"].includes(key),
207
+ ))
208
+ )
209
+ return Response.json(
210
+ { error: "INVALID_DEPARTMENT_TRANSITION", message: "部门编辑字段不合法" },
211
+ { status: 400 },
212
+ );
213
+ if (
214
+ value.action === "delete" &&
215
+ value.updates !== undefined &&
216
+ Object.keys(value.updates as object).length
217
+ )
218
+ return Response.json(
219
+ {
220
+ error: "INVALID_DEPARTMENT_TRANSITION",
221
+ message: "删除部门时不能同时修改资料",
222
+ },
223
+ { status: 400 },
224
+ );
164
225
  return {
165
226
  action: value.action,
227
+ updates: value.updates as Partial<ManagedDepartment> | undefined,
166
228
  targetDepartmentId:
167
229
  typeof value.targetDepartmentId === "string" && value.targetDepartmentId
168
230
  ? value.targetDepartmentId
@@ -0,0 +1,148 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { createFoundationSnapshot } from "../foundation.js";
3
+ import { createSystemRepositories } from "./seed.js";
4
+ import { createRouteTestRuntime } from "./route-test-runtime.js";
5
+ import { registerSystemRoutes } from "./routes.js";
6
+ import { FakeRouteApp } from "./route-test-app.js";
7
+
8
+ async function fixture() {
9
+ const snapshot = createFoundationSnapshot();
10
+ const repositories = createSystemRepositories(snapshot);
11
+ const runtime = createRouteTestRuntime(snapshot, {
12
+ WINDY_DEV_ADMIN_TOKEN: "transition-security",
13
+ });
14
+ const app = new FakeRouteApp();
15
+ registerSystemRoutes(app, repositories, runtime, snapshot.features);
16
+ const guardContext = await runtime.createGuardContext(
17
+ new Request("http://localhost/api/system/departments", {
18
+ headers: { authorization: "Bearer transition-security" },
19
+ }),
20
+ );
21
+ await repositories.departments.create(
22
+ {
23
+ id: "source",
24
+ code: "source",
25
+ name: "原名称",
26
+ order: 1,
27
+ parentId: "dept_platform",
28
+ },
29
+ guardContext.actor,
30
+ );
31
+ await repositories.departments.create(
32
+ { id: "child", code: "child", name: "下级", order: 1, parentId: "source" },
33
+ guardContext.actor,
34
+ );
35
+ await repositories.users.create(
36
+ {
37
+ id: "member",
38
+ username: "member",
39
+ displayName: "成员",
40
+ departmentId: "source",
41
+ roleIds: ["role_user"],
42
+ },
43
+ guardContext.actor,
44
+ );
45
+ return {
46
+ repositories,
47
+ app,
48
+ context: { guardContext, query: {}, params: { id: "source" } },
49
+ };
50
+ }
51
+
52
+ describe("department mutation validation", () => {
53
+ test("资料编辑和停用合并请求在迁移校验失败时不保存任何部分", async () => {
54
+ const { app, repositories, context } = await fixture();
55
+ const before = {
56
+ departments: await repositories.departments.all(),
57
+ users: await repositories.users.all(),
58
+ };
59
+ const response = (await app.invoke(
60
+ "POST",
61
+ "/system/departments/:id/transition",
62
+ {
63
+ ...context,
64
+ body: {
65
+ action: "disable",
66
+ targetDepartmentId: "child",
67
+ updates: { name: "新名称", order: 5 },
68
+ },
69
+ },
70
+ )) as Response;
71
+ expect(response.status).toBe(409);
72
+ expect(await repositories.departments.all()).toEqual(before.departments);
73
+ expect(await repositories.users.all()).toEqual(before.users);
74
+ const succeeded = (await app.invoke(
75
+ "POST",
76
+ "/system/departments/:id/transition",
77
+ {
78
+ ...context,
79
+ body: {
80
+ action: "disable",
81
+ targetDepartmentId: "dept_platform",
82
+ updates: { name: "新名称", order: 5 },
83
+ },
84
+ },
85
+ )) as { department: { status: string; name: string; order: number } };
86
+ expect(succeeded.department).toMatchObject({
87
+ status: "disabled",
88
+ name: "新名称",
89
+ order: 5,
90
+ });
91
+ expect((await repositories.users.get("member"))?.departmentId).toBe(
92
+ "dept_platform",
93
+ );
94
+ });
95
+
96
+ test("快捷创建、通用编辑和处置updates拒绝审计字段注入", async () => {
97
+ const { app, repositories, context } = await fixture();
98
+ const current = await repositories.departments.get("source");
99
+ for (const [method, path, body] of [
100
+ [
101
+ "POST",
102
+ "/system/departments/quick",
103
+ {
104
+ code: "injected",
105
+ name: "注入",
106
+ parentId: "dept_platform",
107
+ deletedAt: "2026-09-10T00:00:00Z",
108
+ },
109
+ ],
110
+ ["PUT", "/system/departments/:id", { createdBy: "spoofed" }],
111
+ [
112
+ "POST",
113
+ "/system/departments/:id/transition",
114
+ {
115
+ action: "disable",
116
+ targetDepartmentId: "dept_platform",
117
+ updates: { deletedAt: "2026-09-10T00:00:00Z" },
118
+ },
119
+ ],
120
+ [
121
+ "POST",
122
+ "/system/departments/:id/transition",
123
+ {
124
+ action: "delete",
125
+ targetDepartmentId: "dept_platform",
126
+ updates: { name: "不能静默丢弃" },
127
+ },
128
+ ],
129
+ ] as const) {
130
+ const response = (await app.invoke(method, path, {
131
+ ...context,
132
+ body,
133
+ })) as Response;
134
+ expect(response.status).toBe(400);
135
+ expect(await repositories.departments.get("source")).toEqual(current);
136
+ }
137
+ });
138
+
139
+ test("部门排序超出PostgreSQL整数上限在写入前被拒绝", async () => {
140
+ const { app, repositories, context } = await fixture();
141
+ const response = (await app.invoke("PUT", "/system/departments/:id", {
142
+ ...context,
143
+ body: { order: 2147483648 },
144
+ })) as Response;
145
+ expect(response.status).toBe(400);
146
+ expect((await repositories.departments.get("source"))?.order).toBe(1);
147
+ });
148
+ });
@@ -0,0 +1,79 @@
1
+ import type { LifecycleStatus } from "@southwind-ai/shared";
2
+ import type { ManagedDepartment } from "./entities.js";
3
+ import type {
4
+ ResourcePolicyResult,
5
+ ResourcePolicyError,
6
+ } from "./route-types.js";
7
+ export const departmentWritableFields = [
8
+ "parentId",
9
+ "name",
10
+ "code",
11
+ "order",
12
+ "status",
13
+ ] as const;
14
+ export async function validateDepartmentInput(
15
+ input: Partial<ManagedDepartment>,
16
+ current?: ManagedDepartment,
17
+ ): Promise<ResourcePolicyResult> {
18
+ if (
19
+ input.code !== undefined &&
20
+ (typeof input.code !== "string" ||
21
+ !/^[a-zA-Z][a-zA-Z0-9_-]{1,79}$/.test(input.code.trim()))
22
+ ) {
23
+ return invalid(
24
+ "INVALID_DEPARTMENT_CODE",
25
+ "部门编码需以字母开头且至少 2 位",
26
+ );
27
+ }
28
+ if (
29
+ input.name !== undefined &&
30
+ (typeof input.name !== "string" ||
31
+ !input.name.trim() ||
32
+ input.name.trim().length > 120)
33
+ ) {
34
+ return invalid("INVALID_DEPARTMENT_NAME", "部门名称不能为空");
35
+ }
36
+ if (
37
+ input.order !== undefined &&
38
+ (!Number.isSafeInteger(input.order) ||
39
+ input.order < 0 ||
40
+ input.order > 2147483647)
41
+ ) {
42
+ return invalid(
43
+ "INVALID_DEPARTMENT_ORDER",
44
+ "部门排序必须是 0 到 2147483647 之间的整数",
45
+ );
46
+ }
47
+ if (input.status !== undefined && !isLifecycleStatus(input.status)) {
48
+ return invalid("INVALID_DEPARTMENT_STATUS", "部门状态不合法");
49
+ }
50
+ if (
51
+ !current &&
52
+ (typeof input.code !== "string" ||
53
+ typeof input.name !== "string" ||
54
+ typeof input.parentId !== "string" ||
55
+ !input.parentId)
56
+ ) {
57
+ return invalid(
58
+ "DEPARTMENT_PARENT_REQUIRED",
59
+ "平台只允许一个根组织,新部门必须选择上级部门",
60
+ );
61
+ }
62
+ if (typeof input.name === "string") input.name = input.name.trim();
63
+ if (typeof input.code === "string") input.code = input.code.trim();
64
+ if (
65
+ input.parentId !== undefined &&
66
+ (typeof input.parentId !== "string" ||
67
+ !input.parentId.trim() ||
68
+ input.parentId.length > 64)
69
+ )
70
+ return invalid("INVALID_DEPARTMENT_PARENT", "上级部门不合法");
71
+ return undefined;
72
+ }
73
+
74
+ function invalid(error: string, message: string): ResourcePolicyError {
75
+ return { error, message };
76
+ }
77
+ function isLifecycleStatus(value: unknown): value is LifecycleStatus {
78
+ return ["draft", "active", "disabled", "archived"].includes(String(value));
79
+ }
@@ -240,6 +240,20 @@ export class DrizzleEntityRepository<
240
240
  if (!query.includeDeleted && deletedAt) clauses.push(isNull(deletedAt));
241
241
  const status = this.column("status");
242
242
  if (query.status && status) clauses.push(eq(status, query.status));
243
+ if (query.departmentId) {
244
+ const department = this.column("departmentId");
245
+ clauses.push(
246
+ department ? eq(department, query.departmentId) : sql`false`,
247
+ );
248
+ }
249
+ if (query.roleId) {
250
+ const roles = this.column("roleIds");
251
+ clauses.push(
252
+ roles
253
+ ? sql`${roles} @> ${JSON.stringify([query.roleId])}::jsonb`
254
+ : sql`false`,
255
+ );
256
+ }
243
257
  const keyword = query.q?.trim();
244
258
  const search = keyword
245
259
  ? searchFields.map((field) => this.column(String(field))).filter(Boolean)
@@ -87,6 +87,8 @@ export function userConfig(
87
87
  "postIds",
88
88
  "status",
89
89
  ]),
90
+ phone: item.phone || null,
91
+ email: item.email || null,
90
92
  }),
91
93
  toEntity: (row) =>
92
94
  ({
@@ -0,0 +1,105 @@
1
+ import type { FeatureFlagDefinition } from "@southwind-ai/shared";
2
+ import type { RequestGuardContext } from "../guards.js";
3
+ import { evaluateRouteGuards, findSystemFeature } from "../route-guards.js";
4
+ import type { createServerRuntime } from "../runtime.js";
5
+ import type { ManagedUser } from "./entities.js";
6
+ import { departmentDescendantResolver } from "../data-access/department-tree.js";
7
+ import { DataScopedResourceService } from "../data-access/scoped-resource.js";
8
+ import type { SystemRepositories } from "./seed.js";
9
+ import { projectMaskedUser } from "./user-projection.js";
10
+
11
+ type Runtime = ReturnType<typeof createServerRuntime>;
12
+ export function identityReadModel(
13
+ repositories: SystemRepositories,
14
+ runtime: Runtime,
15
+ features: FeatureFlagDefinition[],
16
+ ) {
17
+ const canRead = async (
18
+ resource: "department" | "role" | "user",
19
+ context: RequestGuardContext,
20
+ ) =>
21
+ (
22
+ await evaluateRouteGuards(
23
+ runtime,
24
+ context,
25
+ {
26
+ permission: {
27
+ method: "GET",
28
+ path: `/api/system/${resource === "department" ? "departments" : resource === "role" ? "roles" : "users"}`,
29
+ permissionKey: `system.${resource}.read`,
30
+ },
31
+ feature: findSystemFeature(features, resource),
32
+ },
33
+ { record: false },
34
+ )
35
+ ).allowed;
36
+ return {
37
+ async users(users: ManagedUser[], context: RequestGuardContext) {
38
+ const [readDepartments, readRoles] = await Promise.all([
39
+ canRead("department", context),
40
+ canRead("role", context),
41
+ ]);
42
+ const [departments, roles] = await Promise.all([
43
+ readDepartments ? repositories.departments.all() : [],
44
+ readRoles ? repositories.roles.all() : [],
45
+ ]);
46
+ const departmentNames = new Map(
47
+ departments.map((item) => [item.id, item.name]),
48
+ );
49
+ const roleNames = new Map(roles.map((item) => [item.id, item.name]));
50
+ return users.map((user) => ({
51
+ ...projectMaskedUser(user),
52
+ ...(readDepartments
53
+ ? {
54
+ departmentName:
55
+ departmentNames.get(user.departmentId ?? "") ?? "未分配部门",
56
+ }
57
+ : {}),
58
+ ...(readRoles
59
+ ? {
60
+ roleNames: user.roleIds.map(
61
+ (id) => roleNames.get(id) ?? "已删除角色",
62
+ ),
63
+ }
64
+ : {}),
65
+ }));
66
+ },
67
+ async organization(context: RequestGuardContext) {
68
+ return repositories.organizationMutationRunner.run(async () => {
69
+ const departments = await repositories.departments.all({
70
+ sort: [
71
+ { field: "order", order: "asc" },
72
+ { field: "name", order: "asc" },
73
+ { field: "id", order: "asc" },
74
+ ],
75
+ });
76
+ const readUsers = await canRead("user", context);
77
+ const users = readUsers
78
+ ? await new DataScopedResourceService(
79
+ repositories.users,
80
+ departmentDescendantResolver(repositories.departments),
81
+ ).all({}, [], context.actor)
82
+ : [];
83
+ const children = new Map<string, number>();
84
+ const members = new Map<string, number>();
85
+ for (const item of departments)
86
+ if (item.parentId)
87
+ children.set(item.parentId, (children.get(item.parentId) ?? 0) + 1);
88
+ for (const user of users)
89
+ if (user.departmentId)
90
+ members.set(
91
+ user.departmentId,
92
+ (members.get(user.departmentId) ?? 0) + 1,
93
+ );
94
+ return {
95
+ items: departments.map((item) => ({
96
+ ...item,
97
+ childDepartmentCount: children.get(item.id) ?? 0,
98
+ ...(readUsers ? { userCount: members.get(item.id) ?? 0 } : {}),
99
+ })),
100
+ total: departments.length,
101
+ };
102
+ });
103
+ },
104
+ };
105
+ }