@goplusvn/core 0.1.76 → 0.1.78
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/package.json +2 -1
- package/src/guardrails/__tests__/guardrails.test.ts +82 -0
- package/src/guardrails/rules/rbac.ts +120 -0
- package/src/navigation/index.ts +49 -0
- package/src/rbac/__tests__/landing-path.test.ts +148 -0
- package/src/rbac/__tests__/route-handlers.test.ts +147 -0
- package/src/rbac/landing-path.ts +140 -0
- package/src/rbac/pages/role-form-page.tsx +99 -0
- package/src/rbac/route-handlers.ts +22 -1
- package/src/schemas/role.schema.ts +6 -0
- package/src/ui/auth/sign-in-form.tsx +36 -4
- package/src/user/components/unified-profile-dialog.tsx +160 -0
- package/src/user/pages/users-client-page.tsx +12 -0
- package/src/workspace/__tests__/workspace-delegation.test.ts +1 -1
- package/src/workspace/__tests__/workspace-member-handlers.test.ts +407 -0
- package/src/workspace/__tests__/workspace-route-handlers.test.ts +35 -0
- package/src/workspace/__tests__/workspace-service.test.ts +1 -1
- package/src/workspace/components/scope-level-select.tsx +4 -4
- package/src/workspace/components/workspace-members-panel.tsx +454 -0
- package/src/workspace/components/workspace-org-block.tsx +293 -0
- package/src/workspace/components/workspace-switcher.tsx +2 -2
- package/src/workspace/components/workspace-tree-view.tsx +66 -25
- package/src/workspace/delegation.ts +7 -7
- package/src/workspace/index.ts +16 -0
- package/src/workspace/pages/workspace-list-page.tsx +425 -53
- package/src/workspace/route-handlers.ts +278 -2
- package/src/workspace/service.ts +4 -4
- package/src/workspace/tree.ts +1 -1
- package/src/workspace/types.ts +1 -1
- package/templates/starter-app/src/app/[lang]/(main)/layout.tsx +14 -2
|
@@ -13,7 +13,14 @@
|
|
|
13
13
|
// prisma, getSession, canManageAll: (s) => checkPermission(s, "workspace", "update"),
|
|
14
14
|
// })
|
|
15
15
|
|
|
16
|
-
import {
|
|
16
|
+
import { DelegationError, assertCanUpdateUser } from "./delegation";
|
|
17
|
+
import type { DelegationActor } from "./delegation";
|
|
18
|
+
import {
|
|
19
|
+
getWorkspaceConfig,
|
|
20
|
+
getWorkspaceScope,
|
|
21
|
+
isWorkspaceAdmin,
|
|
22
|
+
memberScopeWhere,
|
|
23
|
+
} from "./scope";
|
|
17
24
|
import {
|
|
18
25
|
buildTree,
|
|
19
26
|
createWorkspace,
|
|
@@ -35,6 +42,14 @@ export interface WorkspaceHandlerDeps {
|
|
|
35
42
|
canManageAll?: (session: any) => MaybePromise<boolean>;
|
|
36
43
|
/** Gate đọc; bỏ trống = ai đăng nhập cũng đọc được cây (trong phạm vi của mình). */
|
|
37
44
|
canRead?: (session: any) => MaybePromise<boolean>;
|
|
45
|
+
/**
|
|
46
|
+
* Nhãn cấp của app, để GET trả kèm `kindLabel`.
|
|
47
|
+
*
|
|
48
|
+
* Bỏ trống thì cây tải lại sau mỗi lần thêm/sửa sẽ MẤT nhãn cấp — trang server
|
|
49
|
+
* render có nhãn, `reload()` thì không, và huy hiệu "Bộ phận" lặng lẽ biến mất
|
|
50
|
+
* cho tới khi người dùng F5.
|
|
51
|
+
*/
|
|
52
|
+
kinds?: { key: string; label: string }[];
|
|
38
53
|
onError?: (error: unknown, req: Request) => Response | Promise<Response>;
|
|
39
54
|
}
|
|
40
55
|
|
|
@@ -128,15 +143,33 @@ export function createWorkspaceCollectionHandlers(deps: WorkspaceHandlerDeps) {
|
|
|
128
143
|
? rows.filter((r: any) => allowed.has(r.id))
|
|
129
144
|
: rows;
|
|
130
145
|
|
|
146
|
+
// Số quản trị đếm riêng bằng groupBy: `_count` của Prisma không lọc được
|
|
147
|
+
// theo `isAdmin`, mà đây là con số phân biệt "có người cầm trịch nhánh
|
|
148
|
+
// này" với "một đống thành viên không ai quản" — không suy ra được từ
|
|
149
|
+
// tổng thành viên.
|
|
150
|
+
const adminRows = await deps.prisma.userWorkspace.groupBy({
|
|
151
|
+
by: ["workspaceId"],
|
|
152
|
+
where: { isAdmin: true },
|
|
153
|
+
_count: { _all: true },
|
|
154
|
+
});
|
|
155
|
+
const adminCounts = new Map<string, number>(
|
|
156
|
+
adminRows.map((r: any) => [r.workspaceId, r._count?._all ?? 0]),
|
|
157
|
+
);
|
|
158
|
+
const kindLabels = new Map<string, string>(
|
|
159
|
+
(deps.kinds ?? []).map((k) => [k.key, k.label]),
|
|
160
|
+
);
|
|
161
|
+
|
|
131
162
|
const nodes = visible.map((r: any) => ({
|
|
132
163
|
id: r.id,
|
|
133
164
|
code: r.code,
|
|
134
165
|
name: r.name,
|
|
135
166
|
kind: r.kind,
|
|
167
|
+
kindLabel: kindLabels.get(r.kind) ?? undefined,
|
|
136
168
|
parentId: r.parentId,
|
|
137
169
|
path: r.path,
|
|
138
170
|
isActive: r.isActive,
|
|
139
171
|
memberCount: r._count?.members ?? 0,
|
|
172
|
+
adminCount: adminCounts.get(r.id) ?? 0,
|
|
140
173
|
}));
|
|
141
174
|
return json(toClient(buildTree(nodes)));
|
|
142
175
|
} catch (error) {
|
|
@@ -158,7 +191,7 @@ export function createWorkspaceCollectionHandlers(deps: WorkspaceHandlerDeps) {
|
|
|
158
191
|
if (!parentId) {
|
|
159
192
|
if (!actor.manageAll) {
|
|
160
193
|
return json(
|
|
161
|
-
{ error: "Chỉ quản trị toàn hệ thống tạo được
|
|
194
|
+
{ error: "Chỉ quản trị toàn hệ thống tạo được workspace gốc." },
|
|
162
195
|
403,
|
|
163
196
|
);
|
|
164
197
|
}
|
|
@@ -234,6 +267,249 @@ export function createWorkspaceItemHandlers(deps: WorkspaceHandlerDeps) {
|
|
|
234
267
|
return { PATCH, DELETE };
|
|
235
268
|
}
|
|
236
269
|
|
|
270
|
+
/**
|
|
271
|
+
* GET (thành viên + tìm ứng viên) / POST (gán, đổi cờ) / DELETE (gỡ) cho
|
|
272
|
+
* `/api/workspaces/[id]/members`.
|
|
273
|
+
*
|
|
274
|
+
* Đây là đường DUY NHẤT ghi bảng membership từ giao diện. Mọi thao tác đi qua
|
|
275
|
+
* `assertCanUpdateUser` — không tự gọi lẻ từng luật D1…D7, vì quên một cái là
|
|
276
|
+
* thủng một bất biến, mà thủng thì không có gì báo.
|
|
277
|
+
*/
|
|
278
|
+
export function createWorkspaceMemberHandlers(deps: WorkspaceHandlerDeps) {
|
|
279
|
+
type Ctx = { params: Promise<{ id: string }> | { id: string } };
|
|
280
|
+
|
|
281
|
+
const readId = async (ctx: Ctx) => (await ctx.params).id;
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Actor cho tầng uỷ quyền.
|
|
285
|
+
*
|
|
286
|
+
* `permissions` rỗng + `rank` thấp nhất là CỐ Ý: endpoint này chỉ ghi
|
|
287
|
+
* membership, KHÔNG gán vai trò, nên D2 (trần quyền) và D4 (cấm vai trò nguy
|
|
288
|
+
* hiểm) không có gì để soi — hai luật đó đọc đúng hai trường này.
|
|
289
|
+
* **Đừng tái dùng actor này cho đường gán vai trò**: ở đó `permissions`/`rank`
|
|
290
|
+
* rỗng nghĩa là hai luật kia thành no-op, tức mở toang đường leo thang.
|
|
291
|
+
*/
|
|
292
|
+
const toDelegationActor = (actor: Actor): DelegationActor => ({
|
|
293
|
+
// Lấy id qua `getUserId` của config, KHÔNG đọc `session.user.id`: hình dạng
|
|
294
|
+
// session là chuyện của app (NextAuth, Better Auth, session tự chế…). Đoán
|
|
295
|
+
// sai thì `userId` thành chuỗi rỗng, và D3 — luật cấm tự nâng chính mình —
|
|
296
|
+
// im lặng không khớp ai cả.
|
|
297
|
+
userId: String(getWorkspaceConfig()?.getUserId(actor.session) ?? ""),
|
|
298
|
+
scope: actor.scope,
|
|
299
|
+
canManageAll: actor.manageAll,
|
|
300
|
+
permissions: new Set<string>(),
|
|
301
|
+
rank: Number.MAX_SAFE_INTEGER,
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
/** Lỗi uỷ quyền là 403 kèm nguyên văn câu giải thích — người dùng cần biết VÌ SAO. */
|
|
305
|
+
const delegationFailure = (error: unknown) =>
|
|
306
|
+
error instanceof DelegationError
|
|
307
|
+
? json({ error: error.message, code: error.code }, 403)
|
|
308
|
+
: null;
|
|
309
|
+
|
|
310
|
+
/** Membership ĐẦY ĐỦ của target, không phải phần giao với phạm vi actor (D5/D7 cần cả). */
|
|
311
|
+
async function loadTarget(userId: string) {
|
|
312
|
+
const user = await deps.prisma.user.findUnique({
|
|
313
|
+
where: { id: userId },
|
|
314
|
+
select: {
|
|
315
|
+
id: true,
|
|
316
|
+
isProtected: true,
|
|
317
|
+
permissionCeilingRoleId: true,
|
|
318
|
+
userWorkspaces: { select: { workspaceId: true } },
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
if (!user) return null;
|
|
322
|
+
return {
|
|
323
|
+
id: user.id,
|
|
324
|
+
isProtected: user.isProtected ?? false,
|
|
325
|
+
permissionCeilingRoleId: user.permissionCeilingRoleId ?? null,
|
|
326
|
+
workspaceIds: user.userWorkspaces.map(
|
|
327
|
+
(m: any) => m.workspaceId as string,
|
|
328
|
+
) as string[],
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async function GET(req: Request, ctx: Ctx) {
|
|
333
|
+
try {
|
|
334
|
+
const actor = await resolveActor(deps);
|
|
335
|
+
if (actor instanceof Response) return actor;
|
|
336
|
+
const id = await readId(ctx);
|
|
337
|
+
|
|
338
|
+
// Đọc thì chỉ cần THẤY được nút; ghi mới cần quản trị được nút.
|
|
339
|
+
if (
|
|
340
|
+
!actor.scope.canViewAll &&
|
|
341
|
+
!(actor.scope.allowedIds ?? []).includes(id)
|
|
342
|
+
) {
|
|
343
|
+
return json({ error: "Không gian này ngoài phạm vi của bạn." }, 403);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const url = new URL(req.url);
|
|
347
|
+
const query = (url.searchParams.get("q") ?? "").trim();
|
|
348
|
+
|
|
349
|
+
if (url.searchParams.get("candidates") === "1") {
|
|
350
|
+
const denied = assertWritable(actor, id);
|
|
351
|
+
if (denied) return denied;
|
|
352
|
+
|
|
353
|
+
// Ứng viên chỉ lấy trong nhánh mình QUẢN TRỊ (adminOnly), không phải
|
|
354
|
+
// nhánh mình xem được: xem được cả công ty mà thêm được ai cũng vào
|
|
355
|
+
// nhánh mình thì D1 chỉ còn là trang trí.
|
|
356
|
+
const rows = await deps.prisma.user.findMany({
|
|
357
|
+
where: {
|
|
358
|
+
AND: [
|
|
359
|
+
memberScopeWhere(actor.scope, { adminOnly: true }),
|
|
360
|
+
{ userWorkspaces: { none: { workspaceId: id } } },
|
|
361
|
+
query
|
|
362
|
+
? {
|
|
363
|
+
OR: [
|
|
364
|
+
{ name: { contains: query, mode: "insensitive" } },
|
|
365
|
+
{ email: { contains: query, mode: "insensitive" } },
|
|
366
|
+
],
|
|
367
|
+
}
|
|
368
|
+
: {},
|
|
369
|
+
],
|
|
370
|
+
},
|
|
371
|
+
select: { id: true, name: true, email: true, isActive: true },
|
|
372
|
+
orderBy: { name: "asc" },
|
|
373
|
+
take: 20,
|
|
374
|
+
});
|
|
375
|
+
return json(rows);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const rows = await deps.prisma.userWorkspace.findMany({
|
|
379
|
+
where: { workspaceId: id },
|
|
380
|
+
select: {
|
|
381
|
+
isAdmin: true,
|
|
382
|
+
isDefault: true,
|
|
383
|
+
user: {
|
|
384
|
+
select: { id: true, name: true, email: true, isActive: true },
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
orderBy: [{ isAdmin: "desc" }, { user: { name: "asc" } }],
|
|
388
|
+
});
|
|
389
|
+
return json(
|
|
390
|
+
rows.map((r: any) => ({
|
|
391
|
+
userId: r.user.id,
|
|
392
|
+
name: r.user.name,
|
|
393
|
+
email: r.user.email,
|
|
394
|
+
isActive: r.user.isActive,
|
|
395
|
+
isAdmin: r.isAdmin,
|
|
396
|
+
isDefault: r.isDefault,
|
|
397
|
+
})),
|
|
398
|
+
);
|
|
399
|
+
} catch (error) {
|
|
400
|
+
return failure(error, req, deps);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** Gán người vào không gian, hoặc đổi cờ quản trị / mặc định của người đã ở trong. */
|
|
405
|
+
async function POST(req: Request, ctx: Ctx) {
|
|
406
|
+
try {
|
|
407
|
+
const actor = await resolveActor(deps);
|
|
408
|
+
if (actor instanceof Response) return actor;
|
|
409
|
+
const id = await readId(ctx);
|
|
410
|
+
const denied = assertWritable(actor, id);
|
|
411
|
+
if (denied) return denied;
|
|
412
|
+
|
|
413
|
+
const body = await req.json();
|
|
414
|
+
const userId = String(body?.userId ?? "").trim();
|
|
415
|
+
if (!userId) return json({ error: "Thiếu userId." }, 400);
|
|
416
|
+
|
|
417
|
+
const target = await loadTarget(userId);
|
|
418
|
+
if (!target) return json({ error: "Không tìm thấy người dùng." }, 404);
|
|
419
|
+
|
|
420
|
+
const already = target.workspaceIds.includes(id);
|
|
421
|
+
const next = already ? target.workspaceIds : [...target.workspaceIds, id];
|
|
422
|
+
try {
|
|
423
|
+
assertCanUpdateUser(toDelegationActor(actor), target, {
|
|
424
|
+
workspaceIds: next,
|
|
425
|
+
});
|
|
426
|
+
} catch (error) {
|
|
427
|
+
const denied403 = delegationFailure(error);
|
|
428
|
+
if (denied403) return denied403;
|
|
429
|
+
throw error;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const isAdmin = Boolean(body?.isAdmin);
|
|
433
|
+
const isDefault = Boolean(body?.isDefault);
|
|
434
|
+
|
|
435
|
+
await deps.prisma.$transaction(async (tx: any) => {
|
|
436
|
+
// "Mặc định" là DUY NHẤT trên mỗi người: không hạ cờ cũ xuống thì lúc
|
|
437
|
+
// đăng nhập biết mở không gian nào.
|
|
438
|
+
if (isDefault) {
|
|
439
|
+
await tx.userWorkspace.updateMany({
|
|
440
|
+
where: { userId, isDefault: true, NOT: { workspaceId: id } },
|
|
441
|
+
data: { isDefault: false },
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
await tx.userWorkspace.upsert({
|
|
445
|
+
where: { userId_workspaceId: { userId, workspaceId: id } },
|
|
446
|
+
create: { userId, workspaceId: id, isAdmin, isDefault },
|
|
447
|
+
update: { isAdmin, isDefault },
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
return json(
|
|
452
|
+
{ userId, workspaceId: id, isAdmin, isDefault },
|
|
453
|
+
already ? 200 : 201,
|
|
454
|
+
);
|
|
455
|
+
} catch (error) {
|
|
456
|
+
return failure(error, req, deps);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
async function DELETE(req: Request, ctx: Ctx) {
|
|
461
|
+
try {
|
|
462
|
+
const actor = await resolveActor(deps);
|
|
463
|
+
if (actor instanceof Response) return actor;
|
|
464
|
+
const id = await readId(ctx);
|
|
465
|
+
const denied = assertWritable(actor, id);
|
|
466
|
+
if (denied) return denied;
|
|
467
|
+
|
|
468
|
+
const url = new URL(req.url);
|
|
469
|
+
const userId = String(url.searchParams.get("userId") ?? "").trim();
|
|
470
|
+
if (!userId) return json({ error: "Thiếu userId." }, 400);
|
|
471
|
+
|
|
472
|
+
const target = await loadTarget(userId);
|
|
473
|
+
if (!target) return json({ error: "Không tìm thấy người dùng." }, 404);
|
|
474
|
+
if (!target.workspaceIds.includes(id)) {
|
|
475
|
+
return json({ removed: 0 });
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const next = target.workspaceIds.filter((w) => w !== id);
|
|
479
|
+
try {
|
|
480
|
+
assertCanUpdateUser(toDelegationActor(actor), target, {
|
|
481
|
+
workspaceIds: next,
|
|
482
|
+
});
|
|
483
|
+
} catch (error) {
|
|
484
|
+
const denied403 = delegationFailure(error);
|
|
485
|
+
if (denied403) return denied403;
|
|
486
|
+
throw error;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Gỡ hết thì người này không còn phạm vi nào — với admin nhánh đó là ngõ
|
|
490
|
+
// cụt (D5 chặn mọi thao tác sau đó), nên chặn ngay và nói rõ.
|
|
491
|
+
if (!actor.manageAll && next.length === 0) {
|
|
492
|
+
return json(
|
|
493
|
+
{
|
|
494
|
+
error:
|
|
495
|
+
"Không thể gỡ workspace cuối cùng của người dùng — hãy chuyển họ sang workspace khác trước.",
|
|
496
|
+
},
|
|
497
|
+
400,
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const result = await deps.prisma.userWorkspace.deleteMany({
|
|
502
|
+
where: { userId, workspaceId: id },
|
|
503
|
+
});
|
|
504
|
+
return json({ removed: result.count ?? 0 });
|
|
505
|
+
} catch (error) {
|
|
506
|
+
return failure(error, req, deps);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return { GET, POST, DELETE };
|
|
511
|
+
}
|
|
512
|
+
|
|
237
513
|
/** POST cho `/api/workspaces/[id]/move`. */
|
|
238
514
|
export function createWorkspaceMoveHandler(deps: WorkspaceHandlerDeps) {
|
|
239
515
|
type Ctx = { params: Promise<{ id: string }> | { id: string } };
|
package/src/workspace/service.ts
CHANGED
|
@@ -79,7 +79,7 @@ function assertKindAllowed(
|
|
|
79
79
|
if (!parent) return;
|
|
80
80
|
if (parent.canHaveChildren === false) {
|
|
81
81
|
throw new WorkspaceTreeError(
|
|
82
|
-
`"${parent.label}" không được có
|
|
82
|
+
`"${parent.label}" không được có workspace con.`,
|
|
83
83
|
);
|
|
84
84
|
}
|
|
85
85
|
if (
|
|
@@ -140,7 +140,7 @@ export async function createWorkspace(
|
|
|
140
140
|
select: { id: true, path: true, kind: true },
|
|
141
141
|
});
|
|
142
142
|
if (!parent) {
|
|
143
|
-
throw new WorkspaceTreeError("Không tìm thấy
|
|
143
|
+
throw new WorkspaceTreeError("Không tìm thấy workspace cha.");
|
|
144
144
|
}
|
|
145
145
|
assertKindAllowed(parent.kind, input.kind);
|
|
146
146
|
parentPath = parent.path;
|
|
@@ -184,14 +184,14 @@ export async function moveWorkspace(
|
|
|
184
184
|
|
|
185
185
|
const moved = nodes.find((n) => n.id === input.id);
|
|
186
186
|
if (!moved)
|
|
187
|
-
throw new WorkspaceTreeError("Không tìm thấy
|
|
187
|
+
throw new WorkspaceTreeError("Không tìm thấy workspace cần chuyển.");
|
|
188
188
|
|
|
189
189
|
assertNoCycle(input.id, input.newParentId, nodes);
|
|
190
190
|
|
|
191
191
|
let newParentPath: string | null = null;
|
|
192
192
|
if (input.newParentId) {
|
|
193
193
|
const parent = nodes.find((n) => n.id === input.newParentId);
|
|
194
|
-
if (!parent) throw new WorkspaceTreeError("Không tìm thấy
|
|
194
|
+
if (!parent) throw new WorkspaceTreeError("Không tìm thấy workspace cha.");
|
|
195
195
|
assertKindAllowed(parent.kind, moved.kind);
|
|
196
196
|
newParentPath = parent.path;
|
|
197
197
|
}
|
package/src/workspace/tree.ts
CHANGED
|
@@ -170,7 +170,7 @@ export function assertNoCycle(
|
|
|
170
170
|
if (!parent || !moved) return;
|
|
171
171
|
if (isInSubtree(parent.path, subtreePrefix(moved.path))) {
|
|
172
172
|
throw new WorkspaceTreeError(
|
|
173
|
-
"Không thể chuyển một
|
|
173
|
+
"Không thể chuyển một workspace vào bên trong nhánh con của chính nó.",
|
|
174
174
|
);
|
|
175
175
|
}
|
|
176
176
|
}
|
package/src/workspace/types.ts
CHANGED
|
@@ -106,7 +106,7 @@ export const SCOPE_LEVELS: readonly ScopeLevel[] = [
|
|
|
106
106
|
export const SCOPE_LEVEL_LABELS: Record<ScopeLevel, string> = {
|
|
107
107
|
none: "Không",
|
|
108
108
|
own: "Của tôi",
|
|
109
|
-
workspace: "
|
|
109
|
+
workspace: "Workspace của tôi",
|
|
110
110
|
subtree: "Cả nhánh con",
|
|
111
111
|
all: "Tất cả",
|
|
112
112
|
};
|
|
@@ -1,13 +1,25 @@
|
|
|
1
|
+
import { buildNavigationForSession } from "@goerp/core/navigation"
|
|
2
|
+
|
|
1
3
|
import type { ReactNode } from "react"
|
|
2
4
|
|
|
3
5
|
import { dictionary } from "@/data/dictionary"
|
|
4
6
|
import { navigations } from "@/data/navigations"
|
|
7
|
+
import { getSession } from "@/lib/auth"
|
|
5
8
|
|
|
6
9
|
import { MainLayoutWrapper } from "@/components/layout/main-layout-wrapper"
|
|
7
10
|
|
|
8
|
-
export default function MainAreaLayout({
|
|
11
|
+
export default async function MainAreaLayout({
|
|
12
|
+
children,
|
|
13
|
+
}: {
|
|
14
|
+
children: ReactNode
|
|
15
|
+
}) {
|
|
16
|
+
// Cây menu thô là bản đồ TOÀN BỘ hệ thống — truyền thẳng xuống layout thì ai
|
|
17
|
+
// đăng nhập cũng thấy đủ mục rồi bấm vào bị cổng gác đá về. Lọc theo quyền
|
|
18
|
+
// ngay ở server: mục không có quyền không nằm trong payload gửi xuống.
|
|
19
|
+
const navigation = buildNavigationForSession(navigations, await getSession())
|
|
20
|
+
|
|
9
21
|
return (
|
|
10
|
-
<MainLayoutWrapper dictionary={dictionary} navigation={
|
|
22
|
+
<MainLayoutWrapper dictionary={dictionary} navigation={navigation}>
|
|
11
23
|
{children}
|
|
12
24
|
</MainLayoutWrapper>
|
|
13
25
|
)
|