@goplusvn/core 0.1.78 → 0.1.79
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/CHANGELOG.md +56 -0
- package/package.json +1 -1
- package/src/rbac/__tests__/route-handlers.test.ts +183 -0
- package/src/rbac/components/roles/role-card.tsx +36 -17
- package/src/rbac/pages/role-form-page.tsx +135 -4
- package/src/rbac/pages/role-list-page.tsx +5 -2
- package/src/rbac/role-service.ts +82 -1
- package/src/rbac/route-handlers.ts +180 -10
- package/src/rbac/types.ts +9 -0
- package/src/user/user-service.ts +57 -0
- package/src/workspace/__tests__/workspace-delegation.test.ts +102 -0
- package/src/workspace/__tests__/workspace-scope.test.ts +98 -0
- package/src/workspace/delegation.ts +148 -0
- package/src/workspace/index.ts +5 -0
- package/src/workspace/scope.ts +41 -3
- package/src/workspace/types.ts +25 -0
|
@@ -590,3 +590,101 @@ describe("memberScopeWhere", () => {
|
|
|
590
590
|
).toEqual({});
|
|
591
591
|
});
|
|
592
592
|
});
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* CÔ LẬP DỮ LIỆU THEO VAI TRÒ (`Role.dataScope` → `getScopeLevel`).
|
|
596
|
+
*
|
|
597
|
+
* Điểm mấu chốt của thiết kế: bốn nấc `none/workspace/subtree/all` được GẤP vào
|
|
598
|
+
* chính đối tượng phạm vi lúc dựng, nên mọi `scopedWhere` / `memberScopeWhere`
|
|
599
|
+
* / guard lớp 2 đang có tự tuân theo — không trang nào phải sửa. Test dưới đây
|
|
600
|
+
* canh đúng tính chất đó; hỏng nó là hỏng toàn bộ cách tiếp cận.
|
|
601
|
+
*/
|
|
602
|
+
describe("nấc cô lập dữ liệu gấp vào phạm vi", () => {
|
|
603
|
+
const TREE = [
|
|
604
|
+
{ id: "spa", path: "/spa/" },
|
|
605
|
+
{ id: "spa-qc", path: "/spa/spa-qc/" },
|
|
606
|
+
{ id: "spa-kho", path: "/spa/spa-kho/" },
|
|
607
|
+
];
|
|
608
|
+
|
|
609
|
+
function configure(level: string | null) {
|
|
610
|
+
resetWorkspaceConfig();
|
|
611
|
+
configureWorkspaces<{ id: string }>({
|
|
612
|
+
scopeField: "workspaceId",
|
|
613
|
+
getUserId: (s) => s.id,
|
|
614
|
+
canViewAll: () => false,
|
|
615
|
+
getScopeLevel: () => level as never,
|
|
616
|
+
getMemberships: () => [{ workspaceId: "spa", isAdmin: true }],
|
|
617
|
+
db: {
|
|
618
|
+
userWorkspace: { findMany: async () => [] },
|
|
619
|
+
workspace: {
|
|
620
|
+
findMany: async ({ where }: { where: any }) =>
|
|
621
|
+
where?.id?.in
|
|
622
|
+
? TREE.filter((n) => where.id.in.includes(n.id))
|
|
623
|
+
: TREE.filter((n) =>
|
|
624
|
+
(where?.OR ?? []).some((c: any) =>
|
|
625
|
+
n.path.startsWith(c.path.startsWith),
|
|
626
|
+
),
|
|
627
|
+
),
|
|
628
|
+
},
|
|
629
|
+
},
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
beforeEach(() => resetWorkspaceConfig());
|
|
634
|
+
|
|
635
|
+
it("mặc định (không khai nấc) = subtree — bung con cháu, y như trước", async () => {
|
|
636
|
+
configure(null);
|
|
637
|
+
const scope = await getWorkspaceScope({ id: "u1" });
|
|
638
|
+
expect(scope.allowedIds).toEqual(["spa", "spa-qc", "spa-kho"]);
|
|
639
|
+
expect(scopedWhere(scope)).toEqual({
|
|
640
|
+
OR: [
|
|
641
|
+
{ workspaceId: { in: ["spa", "spa-qc", "spa-kho"] } },
|
|
642
|
+
{ workspaceId: null },
|
|
643
|
+
],
|
|
644
|
+
});
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
it("nấc `workspace` KHÔNG bung con cháu", async () => {
|
|
648
|
+
configure("workspace");
|
|
649
|
+
const scope = await getWorkspaceScope({ id: "u1" });
|
|
650
|
+
expect(scope.allowedIds).toEqual(["spa"]);
|
|
651
|
+
expect(scopedWhere(scope)).toEqual({
|
|
652
|
+
OR: [{ workspaceId: { in: ["spa"] } }, { workspaceId: null }],
|
|
653
|
+
});
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
it("nấc `all` bỏ cô lập — kể cả khi không có quyền view-all", async () => {
|
|
657
|
+
configure("all");
|
|
658
|
+
const scope = await getWorkspaceScope({ id: "u1" });
|
|
659
|
+
expect(scope.canViewAll).toBe(true);
|
|
660
|
+
expect(scopedWhere(scope)).toEqual({});
|
|
661
|
+
expect(memberScopeWhere(scope)).toEqual({});
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
it("nấc `none` đóng sạch — sentinel chứ không phải 'không lọc'", async () => {
|
|
665
|
+
configure("none");
|
|
666
|
+
const scope = await getWorkspaceScope({ id: "u1" });
|
|
667
|
+
expect(scope.canViewAll).toBe(false);
|
|
668
|
+
expect(scope.allowedIds).toEqual([NO_WORKSPACE_ACCESS]);
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
it("nấc `own` lọc theo cột chủ sở hữu, và THIẾU cột thì đóng chứ không nới", async () => {
|
|
672
|
+
configure("own");
|
|
673
|
+
const scope = await getWorkspaceScope({ id: "u1" });
|
|
674
|
+
expect(scopedWhere(scope, { ownerField: "createdBy" })).toEqual({
|
|
675
|
+
createdBy: "u1",
|
|
676
|
+
});
|
|
677
|
+
// Không khai ownerField ⇒ fail-closed.
|
|
678
|
+
expect(scopedWhere(scope)).toEqual({ id: NO_WORKSPACE_ACCESS });
|
|
679
|
+
// Trên chính bảng người dùng: chỉ thấy hồ sơ của mình.
|
|
680
|
+
expect(memberScopeWhere(scope)).toEqual({ id: "u1" });
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
it("nấc `own` qua quan hệ", async () => {
|
|
684
|
+
configure("own");
|
|
685
|
+
const scope = await getWorkspaceScope({ id: "u1" });
|
|
686
|
+
expect(
|
|
687
|
+
scopedWhere(scope, { relation: "employee", ownerField: "user_id" }),
|
|
688
|
+
).toEqual({ employee: { user_id: "u1" } });
|
|
689
|
+
});
|
|
690
|
+
});
|
|
@@ -278,6 +278,154 @@ export function assignableRoles<T extends DelegationRole>(
|
|
|
278
278
|
});
|
|
279
279
|
}
|
|
280
280
|
|
|
281
|
+
/**
|
|
282
|
+
* Vai trò đang được TẠO hoặc SỬA — đầu vào của đường quản trị vai trò.
|
|
283
|
+
*
|
|
284
|
+
* Khác `DelegationRole` (thứ đã có trong DB, dùng để hỏi "gán đi được không"):
|
|
285
|
+
* đây là bản nháp người dùng vừa bấm lưu, chưa qua cổng nào.
|
|
286
|
+
*/
|
|
287
|
+
export interface RoleDraft {
|
|
288
|
+
code: string;
|
|
289
|
+
/** Bỏ trống = để engine đóng dấu theo nhánh của người tạo. */
|
|
290
|
+
workspaceId?: string | null;
|
|
291
|
+
rank?: number | null;
|
|
292
|
+
isSystem?: boolean;
|
|
293
|
+
permissions: readonly string[];
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Vai trò DÙNG CHUNG (`workspaceId === null`) là của cấp vận hành: admin nhánh
|
|
298
|
+
* NHÌN thấy để biết mình đang gán gì, nhưng không sửa được — một nhánh sửa thì
|
|
299
|
+
* mọi đơn vị khác lãnh đủ.
|
|
300
|
+
*/
|
|
301
|
+
export function canManageRole(
|
|
302
|
+
actor: DelegationActor,
|
|
303
|
+
role: Pick<DelegationRole, "workspaceId" | "isSystem">,
|
|
304
|
+
): boolean {
|
|
305
|
+
if (actor.canManageAll) return true;
|
|
306
|
+
if (role.isSystem) return false;
|
|
307
|
+
if (!role.workspaceId) return false;
|
|
308
|
+
return isWorkspaceAdmin(actor.scope, role.workspaceId);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Nhánh mà vai trò mới sẽ mang dấu. Người quản trị nhiều nhánh thì lấy nhánh
|
|
313
|
+
* mặc định nếu nó nằm trong quyền quản trị, không thì nhánh đầu tiên.
|
|
314
|
+
*/
|
|
315
|
+
export function defaultRoleWorkspaceIdFor(
|
|
316
|
+
actor: DelegationActor,
|
|
317
|
+
): string | null {
|
|
318
|
+
if (actor.canManageAll) return null;
|
|
319
|
+
const { defaultId, adminIds } = actor.scope;
|
|
320
|
+
if (defaultId && adminIds.includes(defaultId)) return defaultId;
|
|
321
|
+
return adminIds[0] ?? null;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Cổng đầy đủ cho đường TẠO / SỬA một vai trò (D1 + D2 + D4).
|
|
326
|
+
*
|
|
327
|
+
* Đây là lỗ hổng đối xứng với `assertCanCreateUser`: chặn *gán* vai trò mạnh mà
|
|
328
|
+
* để ngỏ đường *tạo* vai trò mạnh thì admin nhánh chỉ cần đi vòng một bước —
|
|
329
|
+
* tạo vai trò "Trợ lý" có `role:update` rồi tự gán cho tài khoản mình vừa lập.
|
|
330
|
+
*
|
|
331
|
+
* `existing` là vai trò trong DB (null khi tạo mới). Trả về `workspaceId` +
|
|
332
|
+
* `rank` đã chuẩn hoá để route ghi thẳng, khỏi tự suy lại rồi suy sai.
|
|
333
|
+
*/
|
|
334
|
+
export function assertCanManageRole(
|
|
335
|
+
actor: DelegationActor,
|
|
336
|
+
existing: DelegationRole | null,
|
|
337
|
+
draft: RoleDraft,
|
|
338
|
+
): { workspaceId: string | null; rank: number } {
|
|
339
|
+
const requestedRank = draft.rank ?? existing?.rank ?? 100;
|
|
340
|
+
|
|
341
|
+
if (actor.canManageAll) {
|
|
342
|
+
return {
|
|
343
|
+
workspaceId:
|
|
344
|
+
draft.workspaceId !== undefined
|
|
345
|
+
? draft.workspaceId
|
|
346
|
+
: (existing?.workspaceId ?? null),
|
|
347
|
+
rank: requestedRank,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (actor.scope.adminIds.length === 0) {
|
|
352
|
+
throw new DelegationError(
|
|
353
|
+
"NO_DELEGATION",
|
|
354
|
+
"Bạn không được uỷ quyền quản trị workspace nào.",
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (existing && !canManageRole(actor, existing)) {
|
|
359
|
+
throw new DelegationError(
|
|
360
|
+
existing.isSystem ? "D4_DANGEROUS_ROLE" : "D1_OUT_OF_SCOPE",
|
|
361
|
+
existing.isSystem
|
|
362
|
+
? `Vai trò "${existing.code}" là vai trò hệ thống — chỉ quản trị toàn hệ thống sửa được.`
|
|
363
|
+
: `Vai trò "${existing.code}" ${existing.workspaceId ? "thuộc đơn vị ngoài phạm vi của bạn" : "là vai trò dùng chung của cấp trên"} — bạn xem được nhưng không sửa được.`,
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (draft.isSystem) {
|
|
368
|
+
throw new DelegationError(
|
|
369
|
+
"D4_DANGEROUS_ROLE",
|
|
370
|
+
"Không thể đánh dấu vai trò là vai trò hệ thống.",
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Đóng dấu nhánh: vai trò không mang dấu là vai trò DÙNG CHUNG, tức là admin
|
|
375
|
+
// nhánh vừa tạo ra thứ mà mọi đơn vị khác cũng gán được.
|
|
376
|
+
const workspaceId =
|
|
377
|
+
draft.workspaceId ??
|
|
378
|
+
existing?.workspaceId ??
|
|
379
|
+
defaultRoleWorkspaceIdFor(actor);
|
|
380
|
+
if (!workspaceId || !isWorkspaceAdmin(actor.scope, workspaceId)) {
|
|
381
|
+
throw new DelegationError(
|
|
382
|
+
"D1_OUT_OF_SCOPE",
|
|
383
|
+
"Vai trò phải thuộc một đơn vị trong phạm vi quản trị của bạn.",
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// D2 lúc TẠO: vai trò yếu hơn chính người tạo. Không có luật này thì họ tạo
|
|
388
|
+
// vai trò rank 0 rồi nhờ người khác gán ngược lại cho mình.
|
|
389
|
+
if (requestedRank <= actor.rank) {
|
|
390
|
+
throw new DelegationError(
|
|
391
|
+
"D2_EXCEEDS_CEILING",
|
|
392
|
+
`Vai trò phải có cấp thấp hơn bạn (rank > ${actor.rank}).`,
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const dangerous = draft.permissions.filter(isDangerousPermission);
|
|
397
|
+
if (dangerous.length > 0) {
|
|
398
|
+
throw new DelegationError(
|
|
399
|
+
"D4_DANGEROUS_ROLE",
|
|
400
|
+
`Không thể đưa quyền quản trị hệ thống vào vai trò: ${dangerous.join(", ")}.`,
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const exceeded = draft.permissions.filter((p) => !actor.permissions.has(p));
|
|
405
|
+
if (exceeded.length > 0) {
|
|
406
|
+
throw new DelegationError(
|
|
407
|
+
"D2_EXCEEDS_CEILING",
|
|
408
|
+
`Vai trò chứa quyền bạn không có: ${exceeded.slice(0, 5).join(", ")}${exceeded.length > 5 ? "…" : ""}.`,
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return { workspaceId, rank: requestedRank };
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** Cổng cho đường XOÁ vai trò — cùng luật với sửa, không có phần bộ quyền. */
|
|
416
|
+
export function assertCanDeleteRole(
|
|
417
|
+
actor: DelegationActor,
|
|
418
|
+
role: DelegationRole,
|
|
419
|
+
): void {
|
|
420
|
+
if (actor.canManageAll) return;
|
|
421
|
+
if (!canManageRole(actor, role)) {
|
|
422
|
+
throw new DelegationError(
|
|
423
|
+
role.isSystem ? "D4_DANGEROUS_ROLE" : "D1_OUT_OF_SCOPE",
|
|
424
|
+
`Vai trò "${role.code}" nằm ngoài phạm vi quản trị của bạn — không xoá được.`,
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
281
429
|
/**
|
|
282
430
|
* D2 bản CHẠY — quyền hiệu lực = GIAO của quyền vai trò và trần quyền.
|
|
283
431
|
*
|
package/src/workspace/index.ts
CHANGED
|
@@ -97,6 +97,10 @@ export {
|
|
|
97
97
|
assertNotProtected,
|
|
98
98
|
assertRoleAssignable,
|
|
99
99
|
assignableRoles,
|
|
100
|
+
canManageRole,
|
|
101
|
+
defaultRoleWorkspaceIdFor,
|
|
102
|
+
assertCanManageRole,
|
|
103
|
+
assertCanDeleteRole,
|
|
100
104
|
applyPermissionCeiling,
|
|
101
105
|
assertBoundaryIntact,
|
|
102
106
|
assertCanUpdateUser,
|
|
@@ -107,6 +111,7 @@ export {
|
|
|
107
111
|
type DelegationTarget,
|
|
108
112
|
type DelegationRole,
|
|
109
113
|
type DelegationChanges,
|
|
114
|
+
type RoleDraft,
|
|
110
115
|
} from "./delegation";
|
|
111
116
|
|
|
112
117
|
export {
|
package/src/workspace/scope.ts
CHANGED
|
@@ -66,6 +66,8 @@ export function createWorkspaceScope(input: {
|
|
|
66
66
|
allowedIds?: string[];
|
|
67
67
|
adminIds?: string[];
|
|
68
68
|
defaultId?: string;
|
|
69
|
+
level?: ScopeLevel;
|
|
70
|
+
userId?: string;
|
|
69
71
|
}): WorkspaceScope {
|
|
70
72
|
if (input.canViewAll) {
|
|
71
73
|
return {
|
|
@@ -73,6 +75,8 @@ export function createWorkspaceScope(input: {
|
|
|
73
75
|
rootIds: input.rootIds ?? [],
|
|
74
76
|
adminIds: input.adminIds ?? [],
|
|
75
77
|
defaultId: input.defaultId,
|
|
78
|
+
level: input.level,
|
|
79
|
+
userId: input.userId,
|
|
76
80
|
};
|
|
77
81
|
}
|
|
78
82
|
const allowed =
|
|
@@ -86,6 +90,8 @@ export function createWorkspaceScope(input: {
|
|
|
86
90
|
allowedBranchIds: allowed,
|
|
87
91
|
adminIds: input.adminIds ?? [],
|
|
88
92
|
defaultId: input.defaultId,
|
|
93
|
+
level: input.level,
|
|
94
|
+
userId: input.userId,
|
|
89
95
|
};
|
|
90
96
|
}
|
|
91
97
|
|
|
@@ -97,8 +103,18 @@ export async function getWorkspaceScope<TSession>(
|
|
|
97
103
|
session: TSession,
|
|
98
104
|
): Promise<WorkspaceScope> {
|
|
99
105
|
const config = requireConfigured();
|
|
106
|
+
const userId = config.getUserId(session) ?? undefined;
|
|
107
|
+
// Nấc cô lập của vai trò. `all` mở toàn bộ, `none` đóng sạch — hai nấc này
|
|
108
|
+
// quyết định xong là không cần đọc membership nữa.
|
|
109
|
+
const level = (await config.getScopeLevel?.(session)) ?? undefined;
|
|
110
|
+
if (level === "all") {
|
|
111
|
+
return createWorkspaceScope({ canViewAll: true, level, userId });
|
|
112
|
+
}
|
|
113
|
+
if (level === "none") {
|
|
114
|
+
return createWorkspaceScope({ canViewAll: false, allowedIds: [], level, userId });
|
|
115
|
+
}
|
|
100
116
|
if (config.canViewAll(session)) {
|
|
101
|
-
return createWorkspaceScope({ canViewAll: true });
|
|
117
|
+
return createWorkspaceScope({ canViewAll: true, level, userId });
|
|
102
118
|
}
|
|
103
119
|
|
|
104
120
|
const memberships = await readMemberships(config, session);
|
|
@@ -109,7 +125,10 @@ export async function getWorkspaceScope<TSession>(
|
|
|
109
125
|
const defaultId =
|
|
110
126
|
memberships.find((m) => m.isDefault)?.workspaceId ?? rootIds[0];
|
|
111
127
|
|
|
112
|
-
|
|
128
|
+
// `workspace` = đúng nút được gán, KHÔNG bung con cháu. Gấp ngay vào
|
|
129
|
+
// `allowedIds` ở đây thay vì bắt từng trang nhớ gọi `scopeLevelWhere`.
|
|
130
|
+
const allowedIds =
|
|
131
|
+
level === "workspace" ? rootIds : await expand(config, rootIds);
|
|
113
132
|
// Quản trị viên của nhánh nào thì quản trị được cả con cháu nhánh đó — cùng
|
|
114
133
|
// luật cộng dồn với phạm vi đọc, nếu không thì admin nút cha lại không đụng
|
|
115
134
|
// được phòng ban con của chính mình.
|
|
@@ -122,6 +141,8 @@ export async function getWorkspaceScope<TSession>(
|
|
|
122
141
|
allowedIds,
|
|
123
142
|
adminIds,
|
|
124
143
|
defaultId,
|
|
144
|
+
level,
|
|
145
|
+
userId,
|
|
125
146
|
});
|
|
126
147
|
}
|
|
127
148
|
|
|
@@ -252,6 +273,8 @@ export function memberScopeWhere(
|
|
|
252
273
|
scope: WorkspaceScope,
|
|
253
274
|
options?: { relation?: string; field?: string; adminOnly?: boolean },
|
|
254
275
|
): Record<string, unknown> {
|
|
276
|
+
// Nấc `own` trên chính bảng NGƯỜI DÙNG = chỉ thấy hồ sơ của mình.
|
|
277
|
+
if (scope.level === "own") return { id: scope.userId ?? NO_WORKSPACE_ACCESS };
|
|
255
278
|
if (scope.canViewAll) return {};
|
|
256
279
|
const config = configured;
|
|
257
280
|
const relation =
|
|
@@ -274,8 +297,23 @@ export function memberScopeWhere(
|
|
|
274
297
|
*/
|
|
275
298
|
export function scopedWhere(
|
|
276
299
|
scope: WorkspaceScope,
|
|
277
|
-
options?: { field?: string; relation?: string },
|
|
300
|
+
options?: { field?: string; relation?: string; ownerField?: string },
|
|
278
301
|
): Record<string, unknown> {
|
|
302
|
+
// Nấc `own` là nấc DUY NHẤT không gấp được vào `allowedIds` — nó lọc theo
|
|
303
|
+
// người chứ không theo không gian. Không khai `ownerField` (ở đây hoặc trong
|
|
304
|
+
// `configureWorkspaces`) thì đóng lại: fail-closed, đừng lặng lẽ nới thành
|
|
305
|
+
// `subtree`.
|
|
306
|
+
if (scope.level === "own") {
|
|
307
|
+
const ownerField = options?.ownerField ?? configured?.ownerField;
|
|
308
|
+
if (options?.relation) {
|
|
309
|
+
return ownerField
|
|
310
|
+
? { [options.relation]: { [ownerField]: scope.userId ?? NO_WORKSPACE_ACCESS } }
|
|
311
|
+
: { [options.relation]: { id: NO_WORKSPACE_ACCESS } };
|
|
312
|
+
}
|
|
313
|
+
return ownerField
|
|
314
|
+
? { [ownerField]: scope.userId ?? NO_WORKSPACE_ACCESS }
|
|
315
|
+
: { id: NO_WORKSPACE_ACCESS };
|
|
316
|
+
}
|
|
279
317
|
if (scope.canViewAll) return {};
|
|
280
318
|
const field = options?.field ?? workspaceScopeField();
|
|
281
319
|
const ids = scope.allowedIds ?? [NO_WORKSPACE_ACCESS];
|
package/src/workspace/types.ts
CHANGED
|
@@ -84,6 +84,18 @@ export interface WorkspaceScope extends BranchScope {
|
|
|
84
84
|
adminIds: string[];
|
|
85
85
|
/** Không gian mở sẵn khi đăng nhập / gán cho bản ghi mới tạo. */
|
|
86
86
|
defaultId?: string;
|
|
87
|
+
/**
|
|
88
|
+
* Nấc cô lập dữ liệu ĐÃ ÁP dụng khi dựng phạm vi này (Δ1). Bỏ trống = `subtree`,
|
|
89
|
+
* đúng hành vi cũ.
|
|
90
|
+
*
|
|
91
|
+
* Bốn nấc `none/workspace/subtree/all` được **gấp thẳng vào** `canViewAll` +
|
|
92
|
+
* `allowedIds` lúc dựng, nên mọi `scopedWhere` / `memberScopeWhere` / guard
|
|
93
|
+
* lớp 2 đang có tự tuân theo mà không phải sửa một call-site nào. Riêng `own`
|
|
94
|
+
* cần biết cột chủ sở hữu nên phải khai ở call-site (`ownerField`).
|
|
95
|
+
*/
|
|
96
|
+
level?: ScopeLevel;
|
|
97
|
+
/** Chủ phiên — chỉ dùng cho nấc `own`. */
|
|
98
|
+
userId?: string;
|
|
87
99
|
}
|
|
88
100
|
|
|
89
101
|
/**
|
|
@@ -175,6 +187,19 @@ export interface WorkspaceScopeConfig<TSession = unknown> {
|
|
|
175
187
|
getUserId: (session: TSession) => string | null | undefined;
|
|
176
188
|
/** "Xem mọi không gian" — app tự quyết (vai trò quản trị / quyền `view-all-workspaces`). */
|
|
177
189
|
canViewAll: (session: TSession) => boolean;
|
|
190
|
+
/**
|
|
191
|
+
* NẤC CÔ LẬP DỮ LIỆU của phiên (Δ1) — thường là nấc RỘNG NHẤT trong các vai
|
|
192
|
+
* trò của người này (`Role.dataScope`). Bỏ trống ⇒ `subtree`, y hệt hôm nay.
|
|
193
|
+
*
|
|
194
|
+
* Trả `all` là bỏ cô lập (thấy hết), `workspace` là **không** bung con cháu,
|
|
195
|
+
* `none` là không thấy gì. Nấc này được gấp vào phạm vi lúc dựng nên không
|
|
196
|
+
* trang nào phải sửa — đó là lý do nó nằm ở đây chứ không rải ra call-site.
|
|
197
|
+
*/
|
|
198
|
+
getScopeLevel?: (
|
|
199
|
+
session: TSession,
|
|
200
|
+
) => ScopeLevel | null | undefined | Promise<ScopeLevel | null | undefined>;
|
|
201
|
+
/** Cột chủ sở hữu mặc định cho nấc `own` (VD `createdBy`). */
|
|
202
|
+
ownerField?: string;
|
|
178
203
|
/** Trần độ sâu cây. Mặc định 4 (Δ9). */
|
|
179
204
|
maxDepth?: number;
|
|
180
205
|
/** Ngưỡng cảnh báo số con trực tiếp. Mặc định 200. */
|