@goplusvn/core 0.1.56 → 0.1.57
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
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goplusvn/core",
|
|
3
3
|
"description": "GoPlusVN Platform Kit - ERP kernel: layout, RBAC, CRUD, multi-tenant, system pages",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.57",
|
|
5
5
|
"private": false,
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"registry": "https://registry.npmjs.org",
|
|
@@ -119,7 +119,6 @@
|
|
|
119
119
|
"eslint-plugin-react-hooks": "^7.0.1",
|
|
120
120
|
"globals": "16.5.0",
|
|
121
121
|
"jsdom": "^27.2.0",
|
|
122
|
-
"next-auth": "4.24.11",
|
|
123
122
|
"tsup": "^8.5.1",
|
|
124
123
|
"typescript": "^5.7.3",
|
|
125
124
|
"typescript-eslint": "^8.50.1",
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
|
|
6
|
+
// Đọc theo cwd chứ không theo import.meta.url: môi trường test là jsdom nên
|
|
7
|
+
// import.meta.url là URL http, fileURLToPath ném "URL must be of scheme file".
|
|
8
|
+
const source = readFileSync(resolve(process.cwd(), "src/auth/proxy-gate.ts"), "utf8")
|
|
9
|
+
// Bỏ chú thích: phần header có ví dụ dùng, trong đó cũng có chữ `import`.
|
|
10
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
11
|
+
.replace(/^\s*\/\/.*$/gm, "");
|
|
12
|
+
|
|
13
|
+
describe("proxy-gate không kéo theo thư viện auth nào", () => {
|
|
14
|
+
// Bẫy đã trả giá: proxy-gate từng `await import("next-auth/jwt")` trong nhánh
|
|
15
|
+
// mặc định. Bundler phân giải TĨNH cả dynamic import, nên mọi app Better Auth
|
|
16
|
+
// (không cài next-auth) đều gãy middleware bằng "Module not found" — mà trong
|
|
17
|
+
// workspace này thì không lộ, vì next-auth nằm ở devDependencies của core.
|
|
18
|
+
// Middleware chạy ở Edge: đừng thêm import nào không phải next/server.
|
|
19
|
+
it("chỉ import next/server", () => {
|
|
20
|
+
const specifiers = [
|
|
21
|
+
...source.matchAll(/(?:^|\s)import\s+(?:type\s+)?[^"']*from\s*["']([^"']+)["']/gm),
|
|
22
|
+
...source.matchAll(/\bimport\(\s*["']([^"']+)["']\s*\)/g),
|
|
23
|
+
].map((m) => m[1]);
|
|
24
|
+
|
|
25
|
+
expect([...new Set(specifiers)].sort()).toEqual(["next/server"]);
|
|
26
|
+
});
|
|
27
|
+
});
|
package/src/auth/proxy-gate.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @goerp/core/auth/proxy-gate — server-only request-gate for the Next.js
|
|
2
2
|
// proxy/middleware. Default-DENY authentication (authN); route-level authZ still
|
|
3
3
|
// happens via getCrudPermissions/checkPermission. Isolated in its own subpath so
|
|
4
|
-
// `next/server`
|
|
4
|
+
// `next/server` never leaks into client bundles via the auth barrel.
|
|
5
5
|
//
|
|
6
6
|
// Usage (app side):
|
|
7
7
|
// // src/proxy.ts
|
|
@@ -14,7 +14,7 @@ import { NextResponse } from "next/server";
|
|
|
14
14
|
import type { NextRequest } from "next/server";
|
|
15
15
|
|
|
16
16
|
export interface AuthProxyOptions {
|
|
17
|
-
/** API prefixes served without a session (
|
|
17
|
+
/** API prefixes served without a session (auth handler + public). Default: /api/auth, /api/public. */
|
|
18
18
|
publicApiPrefixes?: string[];
|
|
19
19
|
/** Pages reachable while logged out. Default: /sign-in. */
|
|
20
20
|
publicPages?: string[];
|
|
@@ -22,33 +22,49 @@ export interface AuthProxyOptions {
|
|
|
22
22
|
signInPath?: string;
|
|
23
23
|
/** Where to send a logged-in user who hits a guest page. Default: "/". */
|
|
24
24
|
homePath?: string;
|
|
25
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* Session reader. STRONGLY recommended — pass the one your auth library ships
|
|
27
|
+
* (Better Auth: `getSessionCookie` from "better-auth/cookies"; NextAuth:
|
|
28
|
+
* `getToken` from "next-auth/jwt"). Default: presence of a known session
|
|
29
|
+
* cookie, see SESSION_COOKIE_NAMES.
|
|
30
|
+
*/
|
|
26
31
|
getToken?: (req: NextRequest) => Promise<unknown | null>;
|
|
27
32
|
}
|
|
28
33
|
|
|
29
34
|
const startsWithAny = (pathname: string, list: string[]) =>
|
|
30
35
|
list.some((p) => pathname === p || pathname.startsWith(`${p}/`));
|
|
31
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Cookie tên gì thì coi như "có phiên" — dùng cho trường hợp app không truyền
|
|
39
|
+
* getToken. Middleware chạy ở Edge nên đây CHỈ là rào authN thô; chữ ký/hạn
|
|
40
|
+
* dùng vẫn do từng route kiểm qua getSession(). Không import next-auth ở đây:
|
|
41
|
+
* bundler phân giải tĩnh cả `await import()`, nên một dòng import next-auth
|
|
42
|
+
* trong nhánh chết cũng đủ làm mọi app Better Auth gãy middleware bằng
|
|
43
|
+
* "Module not found: Can't resolve 'next-auth/jwt'".
|
|
44
|
+
*/
|
|
45
|
+
const SESSION_COOKIE_NAMES = [
|
|
46
|
+
"better-auth.session_token",
|
|
47
|
+
"__Secure-better-auth.session_token",
|
|
48
|
+
"next-auth.session-token",
|
|
49
|
+
"__Secure-next-auth.session-token",
|
|
50
|
+
"authjs.session-token",
|
|
51
|
+
"__Secure-authjs.session-token",
|
|
52
|
+
];
|
|
53
|
+
|
|
32
54
|
export function createAuthProxy(options: AuthProxyOptions = {}) {
|
|
33
55
|
const publicApiPrefixes = options.publicApiPrefixes ?? ["/api/auth", "/api/public"];
|
|
34
56
|
const publicPages = options.publicPages ?? ["/sign-in"];
|
|
35
57
|
const signInPath = options.signInPath ?? "/sign-in";
|
|
36
58
|
const homePath = options.homePath ?? "/";
|
|
37
|
-
// next-auth chỉ được LAZY-load khi app không truyền getToken riêng — app đã
|
|
38
|
-
// sang Better Auth (không cài next-auth) sẽ không dính module-not-found lúc
|
|
39
|
-
// import proxy-gate (trước đây import top-level, next-auth lại chỉ nằm ở
|
|
40
|
-
// devDependencies của core).
|
|
41
59
|
const readToken =
|
|
42
60
|
options.getToken ??
|
|
43
|
-
(async (req: NextRequest) =>
|
|
44
|
-
|
|
45
|
-
return getToken({ req });
|
|
46
|
-
});
|
|
61
|
+
(async (req: NextRequest) =>
|
|
62
|
+
SESSION_COOKIE_NAMES.some((name) => req.cookies.has(name)) ? { cookie: true } : null);
|
|
47
63
|
|
|
48
64
|
return async function proxy(request: NextRequest) {
|
|
49
65
|
const { pathname, search } = request.nextUrl;
|
|
50
66
|
|
|
51
|
-
// API routes that authenticate themselves (
|
|
67
|
+
// API routes that authenticate themselves (the auth handler) or are public → pass.
|
|
52
68
|
if (startsWithAny(pathname, publicApiPrefixes)) return NextResponse.next();
|
|
53
69
|
|
|
54
70
|
const token = await readToken(request);
|
|
@@ -245,13 +245,13 @@ export function CommandMenu({
|
|
|
245
245
|
buttonClassName,
|
|
246
246
|
)}
|
|
247
247
|
onClick={() => setOpen(true)}
|
|
248
|
-
aria-label={dictionary
|
|
248
|
+
aria-label={dictionary?.search?.search ?? "Tìm kiếm"}
|
|
249
249
|
{...props}
|
|
250
250
|
>
|
|
251
251
|
<Search className={cn("h-4 w-4", variant !== "icon" && "me-2")} />
|
|
252
252
|
{variant !== "icon" && (
|
|
253
253
|
<>
|
|
254
|
-
<span>{dictionary
|
|
254
|
+
<span>{dictionary?.search?.search ?? "Tìm kiếm"}</span>
|
|
255
255
|
{variant !== "fiori" && <Keyboard className="ms-auto">K</Keyboard>}
|
|
256
256
|
</>
|
|
257
257
|
)}
|
|
@@ -270,7 +270,7 @@ export function CommandMenu({
|
|
|
270
270
|
<Search className="absolute left-4 top-1/2 -translate-y-1/2 h-4 w-4 shrink-0 opacity-50" />
|
|
271
271
|
<input
|
|
272
272
|
className="flex h-11 w-full rounded-md bg-transparent py-3 pl-10 pr-10 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50"
|
|
273
|
-
placeholder={dictionary
|
|
273
|
+
placeholder={dictionary?.search?.typeCommand ?? "Nhập lệnh hoặc từ khoá…"}
|
|
274
274
|
value={query}
|
|
275
275
|
onChange={(e) => setQuery(e.target.value)}
|
|
276
276
|
autoFocus
|
|
@@ -288,7 +288,7 @@ export function CommandMenu({
|
|
|
288
288
|
</div>
|
|
289
289
|
) : (
|
|
290
290
|
!searchResults?.length &&
|
|
291
|
-
query && <CommandEmpty>{dictionary
|
|
291
|
+
query && <CommandEmpty>{dictionary?.search?.noResults ?? "Không có kết quả"}</CommandEmpty>
|
|
292
292
|
)}
|
|
293
293
|
|
|
294
294
|
<ScrollArea className="h-[300px] max-h-[300px]">
|
|
@@ -73,10 +73,10 @@ export function NotificationDropdown({
|
|
|
73
73
|
<Card className="border-0 shadow-none">
|
|
74
74
|
<div className="flex items-center justify-between border-b border-border p-3">
|
|
75
75
|
<h3 className="text-sm font-semibold">
|
|
76
|
-
{dictionary
|
|
76
|
+
{dictionary?.navigation?.notifications?.notifications ?? "Thông báo"}
|
|
77
77
|
</h3>
|
|
78
78
|
<Button variant="link" className="text-primary h-auto p-0">
|
|
79
|
-
{dictionary
|
|
79
|
+
{dictionary?.navigation?.notifications?.dismissAll ?? "Bỏ qua tất cả"}
|
|
80
80
|
</Button>
|
|
81
81
|
</div>
|
|
82
82
|
<ScrollArea className="max-h-[300px]">
|
|
@@ -117,7 +117,7 @@ export function NotificationDropdown({
|
|
|
117
117
|
"text-primary text-center",
|
|
118
118
|
)}
|
|
119
119
|
>
|
|
120
|
-
{dictionary
|
|
120
|
+
{dictionary?.navigation?.notifications?.seeAllNotifications ?? "Xem tất cả thông báo"}
|
|
121
121
|
</Link>
|
|
122
122
|
</CardFooter>
|
|
123
123
|
</Card>
|
|
@@ -149,7 +149,7 @@ export function UserDropdown({
|
|
|
149
149
|
className="flex items-center w-full"
|
|
150
150
|
>
|
|
151
151
|
<User className="me-2 size-4 text-muted-foreground group-hover:text-primary group-focus:text-primary transition-colors" />
|
|
152
|
-
<span>{dictionary
|
|
152
|
+
<span>{dictionary?.navigation?.userNav?.profile ?? "Hồ sơ"}</span>
|
|
153
153
|
</Link>
|
|
154
154
|
</DropdownMenuItem>
|
|
155
155
|
{/* <DropdownMenuItem
|
|
@@ -161,7 +161,7 @@ export function UserDropdown({
|
|
|
161
161
|
className="flex items-center w-full"
|
|
162
162
|
>
|
|
163
163
|
<UserCog className="me-2 size-4 text-muted-foreground group-hover:text-primary group-focus:text-primary transition-colors" />
|
|
164
|
-
<span>{dictionary
|
|
164
|
+
<span>{dictionary?.navigation?.userNav?.settings ?? "Cài đặt"}</span>
|
|
165
165
|
</Link>
|
|
166
166
|
</DropdownMenuItem> */}
|
|
167
167
|
</DropdownMenuGroup>
|
|
@@ -173,7 +173,7 @@ export function UserDropdown({
|
|
|
173
173
|
className="h-8 px-2 rounded-md cursor-pointer text-red-600 focus:bg-red-50 focus:text-red-700 dark:text-red-400 dark:focus:bg-red-950/30 dark:focus:text-red-300 transition-colors duration-200 group text-sm"
|
|
174
174
|
>
|
|
175
175
|
<LogOut className="me-2 size-4 group-hover:text-red-700 dark:group-hover:text-red-300 transition-colors" />
|
|
176
|
-
<span>{dictionary
|
|
176
|
+
<span>{dictionary?.navigation?.userNav?.signOut ?? "Đăng xuất"}</span>
|
|
177
177
|
</DropdownMenuItem>
|
|
178
178
|
</DropdownMenuContent>
|
|
179
179
|
</DropdownMenu>
|
package/src/utils/index.ts
CHANGED
|
@@ -432,14 +432,19 @@ export function generateId(prefix?: string): string {
|
|
|
432
432
|
}
|
|
433
433
|
|
|
434
434
|
/**
|
|
435
|
-
* Get dictionary value safely
|
|
435
|
+
* Get dictionary value safely.
|
|
436
|
+
*
|
|
437
|
+
* `section` được phép undefined: app mới bắt đầu với `dictionary = {}` (chưa
|
|
438
|
+
* localize gì) thì `dictionary.navigation` là undefined, và trước đây cả shell
|
|
439
|
+
* đổ "Cannot read properties of undefined" — hỏng TOÀN BỘ trang chứ không phải
|
|
440
|
+
* hiện sai một nhãn. Thiếu từ điển thì rơi về chính key.
|
|
436
441
|
*/
|
|
437
442
|
export function getDictionaryValue(
|
|
438
443
|
key: string,
|
|
439
|
-
section: Record<string, unknown
|
|
444
|
+
section: Record<string, unknown> | undefined | null,
|
|
440
445
|
fallback?: string,
|
|
441
446
|
): string {
|
|
442
|
-
const value = section[key];
|
|
447
|
+
const value = section?.[key];
|
|
443
448
|
|
|
444
449
|
if (typeof value !== "string") {
|
|
445
450
|
if (fallback !== undefined) {
|
|
@@ -447,7 +452,7 @@ export function getDictionaryValue(
|
|
|
447
452
|
}
|
|
448
453
|
|
|
449
454
|
const normalizedKey = key.replace(/[-_]/g, "");
|
|
450
|
-
const normalizedValue = section[normalizedKey];
|
|
455
|
+
const normalizedValue = section?.[normalizedKey];
|
|
451
456
|
|
|
452
457
|
if (typeof normalizedValue === "string") {
|
|
453
458
|
return normalizedValue;
|