create-windy 0.6.21 → 0.7.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.
- package/dist/cli.js +11 -4
- package/package.json +1 -1
- package/template/.windy-template.json +2 -2
- package/template/apps/server/src/audit/policy.ts +6 -1
- package/template/apps/server/src/auth/bootstrap-admin.ts +10 -1
- package/template/apps/server/src/auth/composition.ts +5 -0
- package/template/apps/server/src/auth/credential-authorization.test.ts +257 -0
- package/template/apps/server/src/auth/credential-routes.test.ts +1 -0
- package/template/apps/server/src/auth/credential-service.ts +47 -12
- package/template/apps/server/src/auth/drizzle-repository.ts +13 -5
- package/template/apps/server/src/auth/drizzle-session-operations.ts +1 -0
- package/template/apps/server/src/auth/repository.ts +9 -6
- package/template/apps/server/src/auth/routes.test-support.ts +1 -0
- package/template/apps/server/src/auth/runtime.ts +4 -0
- package/template/apps/server/src/auth/service-options.ts +2 -0
- package/template/apps/server/src/auth/service.ts +2 -0
- package/template/apps/server/src/auth/user-credential-access.ts +37 -0
- package/template/apps/server/src/foundation-activation.test.ts +24 -1
- package/template/apps/server/src/foundation.ts +0 -2
- package/template/apps/server/src/index.ts +1 -0
- package/template/apps/server/src/runtime-development.ts +2 -0
- package/template/apps/server/src/runtime.test.ts +13 -4
- package/template/apps/server/src/system/department-organization.ts +58 -59
- package/template/apps/server/src/system/department-read-routes.ts +54 -0
- package/template/apps/server/src/system/department-routes.test.ts +27 -19
- package/template/apps/server/src/system/department-routes.ts +95 -33
- package/template/apps/server/src/system/department-transition-safety.test.ts +148 -0
- package/template/apps/server/src/system/department-validation.ts +79 -0
- package/template/apps/server/src/system/drizzle-repository.ts +14 -0
- package/template/apps/server/src/system/drizzle-system-configs.ts +2 -0
- package/template/apps/server/src/system/identity-read-model.ts +105 -0
- package/template/apps/server/src/system/identity-role-assignment.test.ts +199 -0
- package/template/apps/server/src/system/identity-role-assignment.ts +148 -0
- package/template/apps/server/src/system/identity-route-config.ts +29 -7
- package/template/apps/server/src/system/identity-write-security.test.ts +127 -0
- package/template/apps/server/src/system/identity-write-validation.ts +170 -0
- package/template/apps/server/src/system/organization-read-security.test.ts +221 -0
- package/template/apps/server/src/system/redis-health.test.ts +1 -1
- package/template/apps/server/src/system/repository.ts +29 -2
- package/template/apps/server/src/system/resource-routes.ts +116 -72
- package/template/apps/server/src/system/route-audit.test.ts +117 -0
- package/template/apps/server/src/system/route-helpers.ts +14 -0
- package/template/apps/server/src/system/route-types.ts +11 -2
- package/template/apps/server/src/system/routes.test.ts +0 -68
- package/template/apps/server/src/system/routes.ts +29 -2
- package/template/apps/server/src/system/user-account-policy.test.ts +3 -1
- package/template/apps/server/src/system/user-account-policy.ts +26 -9
- package/template/apps/web/src/pages/system/SystemResourceRoutePage.vue +27 -13
- package/template/apps/web/src/pages/system/SystemResourceRoutePage.webtest.ts +68 -0
- package/template/apps/web/src/pages/system/components/PagedSelectionList.vue +131 -0
- package/template/apps/web/src/pages/system/components/PagedSelectionList.webtest.ts +32 -0
- package/template/apps/web/src/pages/system/components/SystemRecordDetailDialog.vue +67 -0
- package/template/apps/web/src/pages/system/components/SystemResourceEditor.vue +114 -38
- package/template/apps/web/src/pages/system/components/SystemResourceEditor.webtest.ts +180 -0
- package/template/apps/web/src/pages/system/components/SystemResourceFilters.vue +8 -0
- package/template/apps/web/src/pages/system/components/SystemResourceForm.vue +8 -2
- package/template/apps/web/src/pages/system/components/SystemResourcePage.department.webtest.ts +89 -0
- package/template/apps/web/src/pages/system/components/SystemResourcePage.vue +42 -46
- package/template/apps/web/src/pages/system/components/SystemResourcePage.webtest.ts +12 -55
- package/template/apps/web/src/pages/system/components/SystemResourceTable.vue +14 -2
- package/template/apps/web/src/pages/system/components/department/DepartmentOrganizationForm.vue +121 -85
- package/template/apps/web/src/pages/system/components/department/DepartmentOrganizationForm.webtest.ts +57 -0
- package/template/apps/web/src/pages/system/components/department/DepartmentOrganizationTable.vue +227 -0
- package/template/apps/web/src/pages/system/components/department/DepartmentOrganizationTable.webtest.ts +62 -0
- package/template/apps/web/src/pages/system/components/department/DepartmentTransitionDialog.vue +72 -52
- package/template/apps/web/src/pages/system/components/role-policy/RolePermissionTree.vue +150 -79
- package/template/apps/web/src/pages/system/components/role-policy/RolePolicyForm.vue +98 -8
- package/template/apps/web/src/pages/system/components/role-policy/RolePolicyForm.webtest.ts +108 -0
- package/template/apps/web/src/pages/system/components/selection-options.ts +71 -0
- package/template/apps/web/src/pages/system/components/selection-options.webtest.ts +49 -0
- package/template/apps/web/src/pages/system/components/user-account/CreateDepartmentDialog.vue +19 -27
- package/template/apps/web/src/pages/system/components/user-account/CreateDepartmentDialog.webtest.ts +2 -6
- package/template/apps/web/src/pages/system/components/user-account/CreateRoleDialog.vue +128 -0
- package/template/apps/web/src/pages/system/components/user-account/CreateRoleDialog.webtest.ts +76 -0
- package/template/apps/web/src/pages/system/components/user-account/RolePermissionSummary.vue +61 -3
- package/template/apps/web/src/pages/system/components/user-account/UserAccountForm.vue +144 -90
- package/template/apps/web/src/pages/system/components/user-account/UserAccountForm.webtest.ts +95 -0
- package/template/apps/web/src/pages/system/components/user-account/UserListFilters.vue +168 -0
- package/template/apps/web/src/pages/system/components/user-account/UserRoleSelector.vue +33 -57
- package/template/apps/web/src/pages/system/components/user-account/UserRoleSelector.webtest.ts +4 -4
- package/template/apps/web/src/pages/system/components/user-account/save-user-account.ts +4 -1
- package/template/apps/web/src/pages/system/components/user-account/user-account-draft.ts +81 -0
- package/template/apps/web/src/pages/system/composables/useDepartmentTransitions.ts +6 -10
- package/template/apps/web/src/pages/system/composables/useResourceEditor.ts +72 -0
- package/template/apps/web/src/pages/system/composables/useResourceEditor.webtest.ts +110 -0
- package/template/apps/web/src/pages/system/composables/useSystemResource.ts +27 -1
- package/template/apps/web/src/pages/system/resource-config.ts +2 -0
- package/template/apps/web/src/services/system-api.ts +25 -0
- package/template/docs/architecture/foundation-infrastructure.md +12 -25
- package/template/docs/development/crud-generator.md +1 -1
- package/template/docs/operations/legacy-data-migration.md +73 -0
- package/template/docs/platform/bulk-data-jobs.md +1 -1
- package/template/docs/platform/module-configuration.md +2 -2
- package/template/docs/platform/organization-membership.md +37 -6
- package/template/docs/platform/scheduled-task-executor.md +1 -1
- package/template/package.json +2 -1
- package/template/packages/config/index.test.ts +1 -5
- package/template/packages/config/package.json +2 -2
- package/template/packages/config/src/platform-boundary.test.ts +39 -0
- package/template/packages/config/src/platform.ts +0 -28
- package/template/packages/database/drizzle/0063_license_project_lifecycle.sql +28 -0
- package/template/packages/database/drizzle/meta/0063_snapshot.json +15053 -0
- package/template/packages/database/drizzle/meta/_journal.json +7 -0
- package/template/packages/database/package.json +2 -2
- package/template/packages/database/src/agent-runtime-schema.test.ts +5 -1
- package/template/packages/database/src/schema/index.ts +1 -0
- package/template/packages/database/src/schema/license-issuance-requests.ts +59 -0
- package/template/packages/database/src/schema/license-v1.ts +5 -0
- package/template/packages/jobs/package.json +2 -2
- package/template/packages/modules/package.json +1 -1
- package/template/packages/modules/src/system-api-permissions.ts +14 -0
- package/template/packages/modules/src/system-features.ts +0 -11
- package/template/packages/modules/src/system-module-catalog.test.ts +4 -0
- package/template/packages/modules/src/system-module-catalog.ts +0 -1
- package/template/packages/server-sdk/index.ts +1 -49
- package/template/packages/server-sdk/package.json +3 -2
- package/template/packages/server-sdk/src/ai-runtime-readiness-exports.ts +49 -0
- package/template/packages/shared/index.ts +2 -1
- package/template/packages/shared/package.json +1 -1
- package/template/packages/shared/src/license-console-capabilities.ts +163 -0
- package/template/packages/shared/src/license-console.ts +94 -0
- package/template/packages/shared/src/license.ts +5 -0
- package/template/docs/adapters/ruoyi-vue-compatibility.md +0 -93
- package/template/packages/shared/src/ruoyi.ts +0 -98
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import type { ManagedRole, ManagedUser } from "./entities.js";
|
|
2
|
+
import type { EntityRepository } from "./repository.js";
|
|
3
|
+
import type { ResourcePolicyResult } from "./route-types.js";
|
|
4
|
+
|
|
5
|
+
export const userWritableFields = [
|
|
6
|
+
"username",
|
|
7
|
+
"displayName",
|
|
8
|
+
"email",
|
|
9
|
+
"phone",
|
|
10
|
+
"departmentId",
|
|
11
|
+
"roleIds",
|
|
12
|
+
"postIds",
|
|
13
|
+
"status",
|
|
14
|
+
] as const;
|
|
15
|
+
export const roleWritableFields = [
|
|
16
|
+
"code",
|
|
17
|
+
"name",
|
|
18
|
+
"dataScope",
|
|
19
|
+
"permissionKeys",
|
|
20
|
+
"menuKeys",
|
|
21
|
+
"departmentIds",
|
|
22
|
+
"status",
|
|
23
|
+
] as const;
|
|
24
|
+
|
|
25
|
+
export function validateWritableFields(
|
|
26
|
+
body: unknown,
|
|
27
|
+
fields: readonly PropertyKey[],
|
|
28
|
+
): string | undefined {
|
|
29
|
+
if (!body || typeof body !== "object" || Array.isArray(body))
|
|
30
|
+
return "请求必须是对象";
|
|
31
|
+
return Object.keys(body).some((key) => !fields.includes(key))
|
|
32
|
+
? "请求包含不可写字段"
|
|
33
|
+
: undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function validateUserFields(
|
|
37
|
+
input: Partial<ManagedUser>,
|
|
38
|
+
repository: EntityRepository<ManagedUser>,
|
|
39
|
+
current?: ManagedUser,
|
|
40
|
+
): Promise<ResourcePolicyResult> {
|
|
41
|
+
const error =
|
|
42
|
+
textField(input.username, "用户名", 80, !current) ||
|
|
43
|
+
textField(input.displayName, "姓名", 120, !current) ||
|
|
44
|
+
textField(input.email, "邮箱", 160, false, true) ||
|
|
45
|
+
textField(input.phone, "手机号", 32, false, true) ||
|
|
46
|
+
statusField(input.status) ||
|
|
47
|
+
stringArray(input.roleIds, "角色", !current) ||
|
|
48
|
+
stringArray(input.postIds, "岗位", false);
|
|
49
|
+
if (error) return error;
|
|
50
|
+
if (input.username !== undefined && /\s/.test(input.username))
|
|
51
|
+
return "用户名不能包含空白字符";
|
|
52
|
+
if (input.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input.email))
|
|
53
|
+
return "邮箱格式不正确";
|
|
54
|
+
if (input.phone && !/^\+?[0-9][0-9 -]{5,30}[0-9]$/.test(input.phone))
|
|
55
|
+
return "手机号格式不正确";
|
|
56
|
+
const users = await repository.all({ includeDeleted: true });
|
|
57
|
+
if (
|
|
58
|
+
users.some(
|
|
59
|
+
(user) => user.id !== current?.id && user.username === input.username,
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
return duplicate("用户名已存在");
|
|
63
|
+
if (
|
|
64
|
+
input.phone &&
|
|
65
|
+
users.some((user) => user.id !== current?.id && user.phone === input.phone)
|
|
66
|
+
)
|
|
67
|
+
return duplicate("手机号已被使用");
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function validateRoleFields(
|
|
72
|
+
input: Partial<ManagedRole>,
|
|
73
|
+
repository: EntityRepository<ManagedRole>,
|
|
74
|
+
current?: ManagedRole,
|
|
75
|
+
): Promise<ResourcePolicyResult> {
|
|
76
|
+
const error =
|
|
77
|
+
textField(input.code, "角色编码", 80, !current) ||
|
|
78
|
+
textField(input.name, "角色名称", 120, !current) ||
|
|
79
|
+
statusField(input.status) ||
|
|
80
|
+
stringArray(input.permissionKeys, "权限", !current) ||
|
|
81
|
+
stringArray(input.menuKeys, "菜单", !current) ||
|
|
82
|
+
stringArray(input.departmentIds, "部门范围", false);
|
|
83
|
+
if (error) return error;
|
|
84
|
+
if (
|
|
85
|
+
(!current || input.dataScope !== undefined) &&
|
|
86
|
+
![
|
|
87
|
+
"all",
|
|
88
|
+
"tenant",
|
|
89
|
+
"self",
|
|
90
|
+
"department",
|
|
91
|
+
"department-and-children",
|
|
92
|
+
"custom",
|
|
93
|
+
].includes(input.dataScope || "")
|
|
94
|
+
)
|
|
95
|
+
return "角色数据范围无效";
|
|
96
|
+
if (input.code !== undefined && !/^[A-Za-z0-9_.-]+$/.test(input.code))
|
|
97
|
+
return "角色编码格式不正确";
|
|
98
|
+
const roles = await repository.all({ includeDeleted: true });
|
|
99
|
+
if (roles.some((role) => role.id !== current?.id && role.code === input.code))
|
|
100
|
+
return duplicate("角色编码已存在");
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function textField(
|
|
105
|
+
value: unknown,
|
|
106
|
+
label: string,
|
|
107
|
+
max: number,
|
|
108
|
+
required: boolean,
|
|
109
|
+
empty = false,
|
|
110
|
+
): string | undefined {
|
|
111
|
+
if (value === undefined && !required) return undefined;
|
|
112
|
+
if (
|
|
113
|
+
typeof value !== "string" ||
|
|
114
|
+
value.length > max ||
|
|
115
|
+
(!empty && !value.trim()) ||
|
|
116
|
+
value !== value.trim()
|
|
117
|
+
)
|
|
118
|
+
return `${label}必须为${empty ? "不超过" : "非空且不超过"} ${max} 个字符的文本`;
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
function statusField(value: unknown): string | undefined {
|
|
122
|
+
return value !== undefined &&
|
|
123
|
+
!["active", "disabled", "archived"].includes(String(value))
|
|
124
|
+
? "账号或角色状态无效"
|
|
125
|
+
: undefined;
|
|
126
|
+
}
|
|
127
|
+
function stringArray(
|
|
128
|
+
value: unknown,
|
|
129
|
+
label: string,
|
|
130
|
+
required: boolean,
|
|
131
|
+
): string | undefined {
|
|
132
|
+
if (value === undefined && !required) return undefined;
|
|
133
|
+
return !Array.isArray(value) ||
|
|
134
|
+
value.length > 1000 ||
|
|
135
|
+
value.some(
|
|
136
|
+
(entry) =>
|
|
137
|
+
typeof entry !== "string" || !entry.trim() || entry.length > 160,
|
|
138
|
+
) ||
|
|
139
|
+
new Set(value).size !== value.length
|
|
140
|
+
? `${label}必须为不重复的非空字符串数组`
|
|
141
|
+
: undefined;
|
|
142
|
+
}
|
|
143
|
+
function duplicate(message: string) {
|
|
144
|
+
return { error: "DUPLICATE_IDENTITY", message };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export async function identityMutationResponse<T>(
|
|
148
|
+
mutation: () => Promise<T>,
|
|
149
|
+
): Promise<T | Response> {
|
|
150
|
+
try {
|
|
151
|
+
return await mutation();
|
|
152
|
+
} catch (error) {
|
|
153
|
+
const seen = new Set<unknown>();
|
|
154
|
+
let cause: unknown = error;
|
|
155
|
+
while (cause && typeof cause === "object" && !seen.has(cause)) {
|
|
156
|
+
seen.add(cause);
|
|
157
|
+
const record = cause as Record<string, unknown>;
|
|
158
|
+
if (record.code === "23505")
|
|
159
|
+
return Response.json(
|
|
160
|
+
{
|
|
161
|
+
error: "DUPLICATE_IDENTITY",
|
|
162
|
+
message: "唯一字段已被使用,请检查用户名、手机号或编码",
|
|
163
|
+
},
|
|
164
|
+
{ status: 409 },
|
|
165
|
+
);
|
|
166
|
+
cause = record.cause;
|
|
167
|
+
}
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { createFoundationSnapshot } from "../foundation.js";
|
|
3
|
+
import { buildActorScopeContexts } from "../data-access/context.js";
|
|
4
|
+
import { createSystemRepositories } from "./seed.js";
|
|
5
|
+
import { createRouteTestRuntime } from "./route-test-runtime.js";
|
|
6
|
+
import { registerSystemRoutes } from "./routes.js";
|
|
7
|
+
import { FakeRouteApp } from "./route-test-app.js";
|
|
8
|
+
import type { ManagedUser } from "./entities.js";
|
|
9
|
+
|
|
10
|
+
async function fixture() {
|
|
11
|
+
const snapshot = createFoundationSnapshot();
|
|
12
|
+
const repositories = createSystemRepositories(snapshot);
|
|
13
|
+
const runtime = createRouteTestRuntime(snapshot, {
|
|
14
|
+
WINDY_DEV_ADMIN_TOKEN: "organization-security",
|
|
15
|
+
});
|
|
16
|
+
const app = new FakeRouteApp();
|
|
17
|
+
registerSystemRoutes(app, repositories, runtime, snapshot.features);
|
|
18
|
+
const guardContext = await runtime.createGuardContext(
|
|
19
|
+
new Request("http://localhost/api/system/departments", {
|
|
20
|
+
headers: { authorization: "Bearer organization-security" },
|
|
21
|
+
}),
|
|
22
|
+
);
|
|
23
|
+
return {
|
|
24
|
+
repositories,
|
|
25
|
+
app,
|
|
26
|
+
context: { guardContext, query: {}, params: {} },
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe("organization read and identity filters", () => {
|
|
31
|
+
test("组织快照超过100条仍完整,数字排序稳定,叶节点子级数为零", async () => {
|
|
32
|
+
const { repositories, app, context } = await fixture();
|
|
33
|
+
for (let index = 0; index < 121; index++)
|
|
34
|
+
await repositories.departments.create(
|
|
35
|
+
{
|
|
36
|
+
id: `department-${index}`,
|
|
37
|
+
code: `department-${index}`,
|
|
38
|
+
name: `部门 ${index}`,
|
|
39
|
+
parentId: "dept_platform",
|
|
40
|
+
order: index,
|
|
41
|
+
},
|
|
42
|
+
context.guardContext.actor,
|
|
43
|
+
);
|
|
44
|
+
const response = (await app.invoke(
|
|
45
|
+
"GET",
|
|
46
|
+
"/system/departments/organization",
|
|
47
|
+
context,
|
|
48
|
+
)) as {
|
|
49
|
+
total: number;
|
|
50
|
+
items: Array<{ id: string; order: number; childDepartmentCount: number }>;
|
|
51
|
+
};
|
|
52
|
+
expect(response.total).toBe((await repositories.departments.all()).length);
|
|
53
|
+
expect(response.total).toBeGreaterThan(100);
|
|
54
|
+
expect(
|
|
55
|
+
response.items.find((item) => item.id === "department-120")
|
|
56
|
+
?.childDepartmentCount,
|
|
57
|
+
).toBe(0);
|
|
58
|
+
const subset = response.items.filter((item) =>
|
|
59
|
+
item.id.startsWith("department-"),
|
|
60
|
+
);
|
|
61
|
+
expect(subset.map((item) => item.order)).toEqual(
|
|
62
|
+
Array.from({ length: 121 }, (_, index) => index),
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("缺用户读取权限不投影成员数量,缺部门读取权限不能读取组织快照", async () => {
|
|
67
|
+
const { app, context } = await fixture();
|
|
68
|
+
const guardContext = {
|
|
69
|
+
...context.guardContext,
|
|
70
|
+
actor: {
|
|
71
|
+
...context.guardContext.actor,
|
|
72
|
+
roleCodes: ["reader"],
|
|
73
|
+
permissionKeys: ["system.department.read"],
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
const response = (await app.invoke(
|
|
77
|
+
"GET",
|
|
78
|
+
"/system/departments/organization",
|
|
79
|
+
{ ...context, guardContext },
|
|
80
|
+
)) as { items: Record<string, unknown>[] };
|
|
81
|
+
expect(response.items.length).toBeGreaterThan(0);
|
|
82
|
+
expect(response.items.every((item) => !("userCount" in item))).toBe(true);
|
|
83
|
+
const forbidden = (await app.invoke(
|
|
84
|
+
"GET",
|
|
85
|
+
"/system/departments/organization",
|
|
86
|
+
{
|
|
87
|
+
...context,
|
|
88
|
+
guardContext: {
|
|
89
|
+
...guardContext,
|
|
90
|
+
actor: { ...guardContext.actor, permissionKeys: [] },
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
)) as Response;
|
|
94
|
+
expect(forbidden.status).toBe(403);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("影响预览只允许部门管理权限,统计包含直接子部门、成员和角色范围", async () => {
|
|
98
|
+
const { repositories, app, context } = await fixture();
|
|
99
|
+
await repositories.departments.create(
|
|
100
|
+
{
|
|
101
|
+
id: "preview",
|
|
102
|
+
code: "preview",
|
|
103
|
+
name: "待预览",
|
|
104
|
+
parentId: "dept_platform",
|
|
105
|
+
},
|
|
106
|
+
context.guardContext.actor,
|
|
107
|
+
);
|
|
108
|
+
await repositories.departments.create(
|
|
109
|
+
{
|
|
110
|
+
id: "preview-child",
|
|
111
|
+
code: "preview-child",
|
|
112
|
+
name: "下级",
|
|
113
|
+
parentId: "preview",
|
|
114
|
+
},
|
|
115
|
+
context.guardContext.actor,
|
|
116
|
+
);
|
|
117
|
+
await repositories.users.create(
|
|
118
|
+
{
|
|
119
|
+
username: "preview-user",
|
|
120
|
+
displayName: "成员",
|
|
121
|
+
departmentId: "preview",
|
|
122
|
+
roleIds: ["role_user"],
|
|
123
|
+
},
|
|
124
|
+
context.guardContext.actor,
|
|
125
|
+
);
|
|
126
|
+
await repositories.roles.create(
|
|
127
|
+
{
|
|
128
|
+
code: "preview-role",
|
|
129
|
+
name: "范围角色",
|
|
130
|
+
dataScope: "custom",
|
|
131
|
+
departmentIds: ["preview"],
|
|
132
|
+
permissionKeys: [],
|
|
133
|
+
menuKeys: [],
|
|
134
|
+
},
|
|
135
|
+
context.guardContext.actor,
|
|
136
|
+
);
|
|
137
|
+
const params = { id: "preview" };
|
|
138
|
+
expect(
|
|
139
|
+
await app.invoke("GET", "/system/departments/:id/transition-preview", {
|
|
140
|
+
...context,
|
|
141
|
+
params,
|
|
142
|
+
}),
|
|
143
|
+
).toEqual({ childDepartmentCount: 1, userCount: 1, roleCount: 1 });
|
|
144
|
+
const forbidden = (await app.invoke(
|
|
145
|
+
"GET",
|
|
146
|
+
"/system/departments/:id/transition-preview",
|
|
147
|
+
{
|
|
148
|
+
...context,
|
|
149
|
+
params,
|
|
150
|
+
guardContext: {
|
|
151
|
+
...context.guardContext,
|
|
152
|
+
actor: {
|
|
153
|
+
...context.guardContext.actor,
|
|
154
|
+
roleCodes: ["reader"],
|
|
155
|
+
permissionKeys: ["system.department.read"],
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
)) as Response;
|
|
160
|
+
expect(forbidden.status).toBe(403);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("账户部门、角色、关键字筛选与scope共同影响分页总数,读取投影不旁路名称权限", async () => {
|
|
164
|
+
const { repositories, app, context } = await fixture();
|
|
165
|
+
for (const [id, departmentId, roleIds] of [
|
|
166
|
+
["match-1", "dept_platform", ["role_user"]],
|
|
167
|
+
["match-2", "dept_platform", ["role_user"]],
|
|
168
|
+
["wrong-role", "dept_platform", ["role_developer"]],
|
|
169
|
+
["outside", "other", ["role_user"]],
|
|
170
|
+
] as const)
|
|
171
|
+
await repositories.users.create(
|
|
172
|
+
{
|
|
173
|
+
id,
|
|
174
|
+
username: id,
|
|
175
|
+
displayName: `匹配${id}`,
|
|
176
|
+
departmentId,
|
|
177
|
+
roleIds: [...roleIds],
|
|
178
|
+
},
|
|
179
|
+
context.guardContext.actor,
|
|
180
|
+
);
|
|
181
|
+
const actor = {
|
|
182
|
+
...context.guardContext.actor,
|
|
183
|
+
roleCodes: ["reader"],
|
|
184
|
+
permissionKeys: ["system.user.read"],
|
|
185
|
+
...buildActorScopeContexts("reader", "dept_platform", [
|
|
186
|
+
{
|
|
187
|
+
id: "read-role",
|
|
188
|
+
code: "reader",
|
|
189
|
+
dataScope: "department",
|
|
190
|
+
departmentIds: [],
|
|
191
|
+
},
|
|
192
|
+
]),
|
|
193
|
+
};
|
|
194
|
+
const result = (await app.invoke("GET", "/system/users", {
|
|
195
|
+
...context,
|
|
196
|
+
guardContext: { ...context.guardContext, actor },
|
|
197
|
+
query: {
|
|
198
|
+
departmentId: "dept_platform",
|
|
199
|
+
roleId: "role_user",
|
|
200
|
+
q: "匹配",
|
|
201
|
+
page: "1",
|
|
202
|
+
pageSize: "1",
|
|
203
|
+
},
|
|
204
|
+
})) as {
|
|
205
|
+
total: number;
|
|
206
|
+
items: Array<
|
|
207
|
+
ManagedUser & { departmentName?: string; roleNames?: string[] }
|
|
208
|
+
>;
|
|
209
|
+
};
|
|
210
|
+
expect(result.total).toBe(2);
|
|
211
|
+
expect(result.items).toHaveLength(1);
|
|
212
|
+
expect(result.items[0]?.departmentName).toBeUndefined();
|
|
213
|
+
expect(result.items[0]?.roleNames).toBeUndefined();
|
|
214
|
+
const outside = (await app.invoke("GET", "/system/users", {
|
|
215
|
+
...context,
|
|
216
|
+
guardContext: { ...context.guardContext, actor },
|
|
217
|
+
query: { departmentId: "other", roleId: "role_user" },
|
|
218
|
+
})) as { total: number };
|
|
219
|
+
expect(outside.total).toBe(0);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
@@ -15,7 +15,7 @@ describe("Redis health probe", () => {
|
|
|
15
15
|
});
|
|
16
16
|
});
|
|
17
17
|
|
|
18
|
-
test("
|
|
18
|
+
test("退役旧库变量不能替代平台 REDIS_URL", async () => {
|
|
19
19
|
const snapshot = await createRedisHealthProbeFromEnv({
|
|
20
20
|
RUOYI_REDIS_URL: "redis://legacy-cache:6379/0",
|
|
21
21
|
})();
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { randomUUIDv7 } from "bun";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
LifecycleStatus,
|
|
4
|
+
PageResult,
|
|
5
|
+
SortRule,
|
|
6
|
+
} from "@southwind-ai/shared";
|
|
3
7
|
import type { RequestActor } from "../guards.js";
|
|
4
8
|
import type { ManagedEntity } from "./entities.js";
|
|
5
9
|
|
|
@@ -8,6 +12,8 @@ export interface EntityListQuery {
|
|
|
8
12
|
pageSize?: number;
|
|
9
13
|
q?: string;
|
|
10
14
|
status?: LifecycleStatus;
|
|
15
|
+
departmentId?: string;
|
|
16
|
+
roleId?: string;
|
|
11
17
|
includeDeleted?: boolean;
|
|
12
18
|
sort?: SortRule[];
|
|
13
19
|
}
|
|
@@ -79,6 +85,7 @@ export class InMemoryEntityRepository<
|
|
|
79
85
|
if (query.status && item.status !== query.status) {
|
|
80
86
|
return false;
|
|
81
87
|
}
|
|
88
|
+
if (!matchesIdentityFilters(item, query)) return false;
|
|
82
89
|
return normalizedKeyword
|
|
83
90
|
? matchesKeyword(item, searchFields, normalizedKeyword)
|
|
84
91
|
: true;
|
|
@@ -103,6 +110,7 @@ export class InMemoryEntityRepository<
|
|
|
103
110
|
if (query.status && item.status !== query.status) {
|
|
104
111
|
return false;
|
|
105
112
|
}
|
|
113
|
+
if (!matchesIdentityFilters(item, query)) return false;
|
|
106
114
|
return normalizedKeyword
|
|
107
115
|
? matchesKeyword(item, searchFields, normalizedKeyword)
|
|
108
116
|
: true;
|
|
@@ -201,6 +209,20 @@ export class InMemoryEntityRepository<
|
|
|
201
209
|
}
|
|
202
210
|
}
|
|
203
211
|
|
|
212
|
+
function matchesIdentityFilters(
|
|
213
|
+
item: ManagedEntity,
|
|
214
|
+
query: EntityListQuery,
|
|
215
|
+
): boolean {
|
|
216
|
+
const identity = item as ManagedEntity & {
|
|
217
|
+
departmentId?: string;
|
|
218
|
+
roleIds?: string[];
|
|
219
|
+
};
|
|
220
|
+
return (
|
|
221
|
+
(!query.departmentId || identity.departmentId === query.departmentId) &&
|
|
222
|
+
(!query.roleId || Boolean(identity.roleIds?.includes(query.roleId)))
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
204
226
|
function matchesKeyword<T extends ManagedEntity>(
|
|
205
227
|
item: T,
|
|
206
228
|
fields: Array<keyof T>,
|
|
@@ -224,7 +246,12 @@ function sortItems<T extends ManagedEntity>(
|
|
|
224
246
|
for (const rule of rules) {
|
|
225
247
|
const leftValue = String(left[rule.field as keyof T] || "");
|
|
226
248
|
const rightValue = String(right[rule.field as keyof T] || "");
|
|
227
|
-
const comparison =
|
|
249
|
+
const comparison =
|
|
250
|
+
typeof left[rule.field as keyof T] === "number" &&
|
|
251
|
+
typeof right[rule.field as keyof T] === "number"
|
|
252
|
+
? Number(left[rule.field as keyof T]) -
|
|
253
|
+
Number(right[rule.field as keyof T])
|
|
254
|
+
: leftValue.localeCompare(rightValue);
|
|
228
255
|
if (comparison !== 0) {
|
|
229
256
|
return rule.order === "asc" ? comparison : -comparison;
|
|
230
257
|
}
|