@eintrek/erp-shell 0.1.22 → 0.1.25
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/auth/permission-provider.d.ts +6 -8
- package/dist/auth/permission-utils.d.ts +24 -7
- package/dist/index.d.ts +2 -2
- package/dist/index.esm.js +154 -49
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +153 -47
- package/dist/index.js.map +1 -1
- package/dist/navigation/nav-user.d.ts +13 -1
- package/dist/navigation/navigation-data-helpers.d.ts +6 -2
- package/dist/providers/permission-rules-context.d.ts +2 -2
- package/package.json +1 -1
|
@@ -31,14 +31,12 @@ export interface PermissionProviderProps {
|
|
|
31
31
|
* Permission gate for a page subtree. Looks up the current pathname in the
|
|
32
32
|
* permission rules map (live rules from `PermissionRulesProvider`, layered
|
|
33
33
|
* over the optional `fallbackRulesMap`), passes the resolved required roles
|
|
34
|
-
* into the host's `usePermission` probe, and renders one of
|
|
34
|
+
* into the host's `usePermission` probe, and renders one of:
|
|
35
35
|
*
|
|
36
|
-
* 1.
|
|
37
|
-
* 2.
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* The host wraps `<PermissionProvider>` around its page chrome (typically
|
|
42
|
-
* via `AppLayoutShell`'s `permissionProvider` slot).
|
|
36
|
+
* 1. rules / probe still loading → `null` (let Suspense / loading.tsx show)
|
|
37
|
+
* 2. no rule covers this path → deny (fail closed)
|
|
38
|
+
* 3. rule is explicit public (`[]`) → children
|
|
39
|
+
* 4. probe says no access → deny card
|
|
40
|
+
* 5. otherwise → children
|
|
43
41
|
*/
|
|
44
42
|
export declare function PermissionProvider({ children, usePermission, fallbackRulesMap, }: PermissionProviderProps): import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -9,14 +9,31 @@ import type { PermissionRulesMap } from "../providers/permission-rules-context";
|
|
|
9
9
|
* the same management screen with different filters.
|
|
10
10
|
*/
|
|
11
11
|
export declare function normalizePathnameForPermissionRules(pathname: string): string;
|
|
12
|
+
export interface UiRoutePermissionLookup {
|
|
13
|
+
/** True when an exact, pattern, or restrictive parent rule was found. */
|
|
14
|
+
matched: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Required child-group segments. Empty array means the route is
|
|
17
|
+
* explicitly public (configured as `required_child_groups: []`).
|
|
18
|
+
* When `matched` is false this is always `[]` and must be treated as deny.
|
|
19
|
+
*/
|
|
20
|
+
roles: string[];
|
|
21
|
+
}
|
|
12
22
|
/**
|
|
13
|
-
*
|
|
14
|
-
* JWT must contain at least one matching group). An exact match on the
|
|
15
|
-
* normalized path wins; otherwise the function falls back to a segment-by-
|
|
16
|
-
* segment pattern match where `[param]` placeholders accept any non-empty
|
|
17
|
-
* value. Returns an empty array when no rule covers the path.
|
|
23
|
+
* Resolve UI route permissions for a pathname.
|
|
18
24
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
25
|
+
* - Exact / `[param]` pattern match → matched
|
|
26
|
+
* - Parent-prefix inherit only when the parent rule has a **non-empty**
|
|
27
|
+
* role list (restrictive inherit). Explicitly public parents (`[]`) do
|
|
28
|
+
* not open all children.
|
|
29
|
+
* - No match → `{ matched: false }` (fail closed — never treat as public)
|
|
30
|
+
*/
|
|
31
|
+
export declare function resolveUiRoutePermission(pathname: string, rulesMap: PermissionRulesMap): UiRoutePermissionLookup;
|
|
32
|
+
/**
|
|
33
|
+
* Returns the required child-group segments for a page pathname.
|
|
34
|
+
*
|
|
35
|
+
* Prefer {@link resolveUiRoutePermission} when you need to distinguish
|
|
36
|
+
* "explicitly public (`[]`)" from "no rule (deny)". This helper returns
|
|
37
|
+
* `[]` for both cases and is kept for backward compatibility.
|
|
21
38
|
*/
|
|
22
39
|
export declare function getRequiredChildGroups(pathname: string, rulesMap: PermissionRulesMap): string[];
|
package/dist/index.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export { useSidebarToggle } from "./navigation/use-sidebar-toggle";
|
|
|
13
13
|
export { TapMenu } from "./navigation/tap-menu";
|
|
14
14
|
export { NavTools } from "./navigation/nav-tools";
|
|
15
15
|
export { AppBreadcrumb } from "./navigation/app-breadcrumb";
|
|
16
|
-
export { NavUser, type NavUserProps } from "./navigation/nav-user";
|
|
16
|
+
export { NavUser, type NavUserProps, type NavUserSystemLink } from "./navigation/nav-user";
|
|
17
17
|
export { NavMain, type NavMainProps } from "./navigation/nav-main";
|
|
18
18
|
export { OrganizationSwitcher, type OrganizationSwitcherProps, type OrganizationLike, } from "./navigation/organization-switcher";
|
|
19
19
|
export { SidebarHeader } from "./navigation/sidebar-header";
|
|
@@ -21,7 +21,7 @@ export { AppSidebar, type AppSidebarProps } from "./navigation/app-sidebar";
|
|
|
21
21
|
export { AppLayoutShell, type AppLayoutShellProps, type AppLayoutShellOrganization, } from "./navigation/app-layout-shell";
|
|
22
22
|
export { PermissionGateShell, type PermissionGateShellProps, type PermissionGateSkeletonVariant, } from "./auth/permission-gate-shell";
|
|
23
23
|
export { PermissionProvider, type PermissionProviderProps, type PermissionProbe, type UsePermissionHook, } from "./auth/permission-provider";
|
|
24
|
-
export { getRequiredChildGroups, normalizePathnameForPermissionRules, } from "./auth/permission-utils";
|
|
24
|
+
export { getRequiredChildGroups, normalizePathnameForPermissionRules, resolveUiRoutePermission, type UiRoutePermissionLookup, } from "./auth/permission-utils";
|
|
25
25
|
export * from "./navigation/navigation-helpers";
|
|
26
26
|
export type { Module, SubPage, SubModule, Tool, Company, NavigationData, } from "./navigation/navigation-types";
|
|
27
27
|
export { canAccessNavPermissionRoute, filterNavigationToolsByPermission, filterNavigationModulesByPermission, getActiveModule, getActiveSubModule, buildUrlWithCode, } from "./navigation/navigation-data-helpers";
|
package/dist/index.esm.js
CHANGED
|
@@ -5,7 +5,7 @@ import { useSession, signOut } from 'next-auth/react';
|
|
|
5
5
|
import { useQueryClient, MutationCache, QueryCache, QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
6
6
|
import { jsx, jsxs, Fragment as Fragment$1 } from 'react/jsx-runtime';
|
|
7
7
|
import { cn, Button, Breadcrumb, BreadcrumbList, BreadcrumbItem, BreadcrumbLink, BreadcrumbSeparator, BreadcrumbPage, Avatar, AvatarImage, AvatarFallback, Collapsible, CollapsibleTrigger, CollapsibleContent, Badge, Skeleton, DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuLabel, DropdownMenuItem, DropdownMenuSeparator, Card, CardHeader, CardContent } from '@eintrek/erp-theme';
|
|
8
|
-
import { PanelLeftClose, Menu, ChevronRight, Building2, Palette, ChevronUp, Sun, Moon, Monitor, Globe, User, Loader2, LogOut, ChevronDown, ChevronsUpDown, Plus, AlertCircle } from 'lucide-react';
|
|
8
|
+
import { PanelLeftClose, Menu, ChevronRight, Building2, ExternalLink, Palette, ChevronUp, Sun, Moon, Monitor, Globe, User, Loader2, LogOut, ChevronDown, ChevronsUpDown, Plus, AlertCircle } from 'lucide-react';
|
|
9
9
|
import { create } from 'zustand';
|
|
10
10
|
import { persist, createJSONStorage } from 'zustand/middleware';
|
|
11
11
|
import { useRouter, usePathname, useParams } from 'next/navigation';
|
|
@@ -604,12 +604,19 @@ SidebarMenuSubButton.displayName = "SidebarMenuSubButton";
|
|
|
604
604
|
|
|
605
605
|
/**
|
|
606
606
|
* True when the user's child-group segments include at least one of the
|
|
607
|
-
* groups required for `permissionRouteKey`.
|
|
608
|
-
*
|
|
607
|
+
* groups required for `permissionRouteKey`.
|
|
608
|
+
*
|
|
609
|
+
* Fail-closed:
|
|
610
|
+
* - Nav item without `permissionRouteKey` → deny (must declare a key)
|
|
611
|
+
* - Key absent from `rulesMap` → deny (unknown route is not public)
|
|
612
|
+
* - Key present with `required_child_groups: []` → allow (explicit public)
|
|
609
613
|
*/
|
|
610
614
|
function canAccessNavPermissionRoute(permissionRouteKey, rulesMap, userChildSegments) {
|
|
611
615
|
if (!permissionRouteKey)
|
|
612
|
-
return
|
|
616
|
+
return false;
|
|
617
|
+
if (!Object.prototype.hasOwnProperty.call(rulesMap, permissionRouteKey)) {
|
|
618
|
+
return false;
|
|
619
|
+
}
|
|
613
620
|
const required = rulesMap[permissionRouteKey] ?? [];
|
|
614
621
|
if (required.length === 0)
|
|
615
622
|
return true;
|
|
@@ -740,7 +747,10 @@ function AppBreadcrumb({ labels = {}, homeLabel = "แดชบอร์ด", ro
|
|
|
740
747
|
})] }) }));
|
|
741
748
|
}
|
|
742
749
|
|
|
743
|
-
function
|
|
750
|
+
function isExternalHref(href) {
|
|
751
|
+
return /^https?:\/\//i.test(href) || href.startsWith("//");
|
|
752
|
+
}
|
|
753
|
+
function NavUser({ organization, avatarSrc, profileHref, selectOrganizationHref = "/select-organization", signInRedirectHref = "/auth/signin", systemLinks, } = {}) {
|
|
744
754
|
const { data: session, status } = useSession();
|
|
745
755
|
const router = useRouter();
|
|
746
756
|
const { theme, setTheme } = useTheme();
|
|
@@ -792,6 +802,14 @@ function NavUser({ organization, avatarSrc, profileHref, selectOrganizationHref
|
|
|
792
802
|
if (profileHref)
|
|
793
803
|
router.push(profileHref);
|
|
794
804
|
};
|
|
805
|
+
const handleSystemLink = (href) => {
|
|
806
|
+
setIsOpen(false);
|
|
807
|
+
if (isExternalHref(href)) {
|
|
808
|
+
window.location.assign(href);
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
router.push(href);
|
|
812
|
+
};
|
|
795
813
|
const handleThemeChange = (newTheme) => {
|
|
796
814
|
setTheme(newTheme);
|
|
797
815
|
setShowThemeMenu(false);
|
|
@@ -824,6 +842,7 @@ function NavUser({ organization, avatarSrc, profileHref, selectOrganizationHref
|
|
|
824
842
|
}
|
|
825
843
|
if (!user)
|
|
826
844
|
return null;
|
|
845
|
+
const visibleSystemLinks = (systemLinks ?? []).filter(link => link.href && link.label);
|
|
827
846
|
const dropdownContent = isOpen ? (jsxs("div", { "data-dropdown": "nav-user", className: "fixed w-52 sm:w-60 md:w-80 rounded-lg bg-sidebar border border-sidebar-border shadow-xl z-50 overflow-hidden\n -translate-x-[65%] -translate-y-[110%] sm:translate-x-0 sm:-translate-y-full", style: { top: `${position.top}px`, left: `${position.left}px` }, children: [organization && (jsx("div", { className: "p-4 border-b border-sidebar-border bg-sidebar-accent/30", children: jsxs("div", { className: "flex items-center gap-3", children: [organization.logoUrl ? (
|
|
828
847
|
// eslint-disable-next-line @next/next/no-img-element
|
|
829
848
|
jsx("img", { src: organization.logoUrl, alt: organization.name ?? "โลโก้บริษัท", className: "h-8 w-8 flex-shrink-0 rounded object-contain bg-white",
|
|
@@ -831,7 +850,7 @@ function NavUser({ organization, avatarSrc, profileHref, selectOrganizationHref
|
|
|
831
850
|
// sidebar; drop back to the generic icon's empty slot.
|
|
832
851
|
onError: e => {
|
|
833
852
|
e.currentTarget.style.display = "none";
|
|
834
|
-
} })) : (jsx(Building2, { className: "h-5 w-5 text-sidebar-foreground/70 flex-shrink-0" })), jsxs("div", { className: "flex-1 min-w-0", children: [jsx("div", { className: "font-medium text-sm sm:truncate", children: organization.name }), organization.description && (jsx("div", { className: "text-xs text-sidebar-foreground/70 truncate mt-0.5", children: organization.description }))] })] }) })), jsxs("div", { className: "p-2", children: [jsxs("div", { className: "relative", children: [jsxs("button", { onClick: () => {
|
|
853
|
+
} })) : (jsx(Building2, { className: "h-5 w-5 text-sidebar-foreground/70 flex-shrink-0" })), jsxs("div", { className: "flex-1 min-w-0", children: [jsx("div", { className: "font-medium text-sm sm:truncate", children: organization.name }), organization.description && (jsx("div", { className: "text-xs text-sidebar-foreground/70 truncate mt-0.5", children: organization.description }))] })] }) })), jsxs("div", { className: "p-2", children: [visibleSystemLinks.length > 0 ? (jsxs(Fragment$1, { children: [visibleSystemLinks.map(link => (jsxs("button", { type: "button", onClick: () => handleSystemLink(link.href), className: "flex w-full items-center gap-3 px-3 py-2.5 text-sm text-sidebar-foreground hover:bg-sidebar-accent rounded-md transition-colors", children: [link.icon ?? jsx(ExternalLink, { className: "h-4 w-4" }), jsx("span", { className: "flex-1 text-left", children: link.label })] }, `${link.label}:${link.href}`))), jsx("div", { className: "my-1 border-t border-sidebar-border" })] })) : null, jsxs("div", { className: "relative", children: [jsxs("button", { onClick: () => {
|
|
835
854
|
setShowThemeMenu(!showThemeMenu);
|
|
836
855
|
setShowLanguageMenu(false);
|
|
837
856
|
}, className: "flex w-full items-center gap-3 px-3 py-2.5 text-sm text-sidebar-foreground hover:bg-sidebar-accent rounded-md transition-colors", children: [jsx(Palette, { className: "h-4 w-4" }), jsx("span", { className: "flex-1 text-left", children: "\u0E40\u0E1B\u0E25\u0E35\u0E48\u0E22\u0E19\u0E18\u0E35\u0E21" }), jsx(ChevronUp, { className: `h-4 w-4 transition-transform ${showThemeMenu ? "rotate-180" : ""}` })] }), showThemeMenu && (jsx("div", { className: "mt-1 ml-4 space-y-1", children: [
|
|
@@ -1185,8 +1204,8 @@ function PermissionRulesProvider({ children, data, isLoading = false, isError =
|
|
|
1185
1204
|
return (jsx(PermissionRulesContext.Provider, { value: value, children: children }));
|
|
1186
1205
|
}
|
|
1187
1206
|
/**
|
|
1188
|
-
* Read the permission rules.
|
|
1189
|
-
*
|
|
1207
|
+
* Read the permission rules. Outside the provider, report loading so
|
|
1208
|
+
* fail-closed consumers (nav / gates) do not treat empty maps as "allow all".
|
|
1190
1209
|
*/
|
|
1191
1210
|
function usePermissionRules() {
|
|
1192
1211
|
const ctx = useContext(PermissionRulesContext);
|
|
@@ -1194,7 +1213,7 @@ function usePermissionRules() {
|
|
|
1194
1213
|
return {
|
|
1195
1214
|
rulesMap: {},
|
|
1196
1215
|
policiesMap: {},
|
|
1197
|
-
isLoading:
|
|
1216
|
+
isLoading: true,
|
|
1198
1217
|
isError: false,
|
|
1199
1218
|
updatedAt: null,
|
|
1200
1219
|
};
|
|
@@ -1216,20 +1235,22 @@ function normalizePathnameForPermissionRules(pathname) {
|
|
|
1216
1235
|
const path = parts.length <= 1 ? "/" : "/" + parts.slice(1).join("/");
|
|
1217
1236
|
return path.replace(/\/user-role\/(purchase-doc|sales-doc)$/, "/user-role/members");
|
|
1218
1237
|
}
|
|
1238
|
+
function hasRule(rulesMap, routeKey) {
|
|
1239
|
+
return Object.prototype.hasOwnProperty.call(rulesMap, routeKey);
|
|
1240
|
+
}
|
|
1219
1241
|
/**
|
|
1220
|
-
*
|
|
1221
|
-
* JWT must contain at least one matching group). An exact match on the
|
|
1222
|
-
* normalized path wins; otherwise the function falls back to a segment-by-
|
|
1223
|
-
* segment pattern match where `[param]` placeholders accept any non-empty
|
|
1224
|
-
* value. Returns an empty array when no rule covers the path.
|
|
1242
|
+
* Resolve UI route permissions for a pathname.
|
|
1225
1243
|
*
|
|
1226
|
-
*
|
|
1227
|
-
*
|
|
1244
|
+
* - Exact / `[param]` pattern match → matched
|
|
1245
|
+
* - Parent-prefix inherit only when the parent rule has a **non-empty**
|
|
1246
|
+
* role list (restrictive inherit). Explicitly public parents (`[]`) do
|
|
1247
|
+
* not open all children.
|
|
1248
|
+
* - No match → `{ matched: false }` (fail closed — never treat as public)
|
|
1228
1249
|
*/
|
|
1229
|
-
function
|
|
1250
|
+
function resolveUiRoutePermission(pathname, rulesMap) {
|
|
1230
1251
|
const normalized = normalizePathnameForPermissionRules(pathname);
|
|
1231
|
-
if (rulesMap
|
|
1232
|
-
return rulesMap[normalized];
|
|
1252
|
+
if (hasRule(rulesMap, normalized)) {
|
|
1253
|
+
return { matched: true, roles: rulesMap[normalized] ?? [] };
|
|
1233
1254
|
}
|
|
1234
1255
|
const pathSegments = normalized.split("/").filter(Boolean);
|
|
1235
1256
|
for (const [pattern, roles] of Object.entries(rulesMap)) {
|
|
@@ -1252,25 +1273,43 @@ function getRequiredChildGroups(pathname, rulesMap) {
|
|
|
1252
1273
|
}
|
|
1253
1274
|
}
|
|
1254
1275
|
if (matches) {
|
|
1255
|
-
return roles;
|
|
1276
|
+
return { matched: true, roles: roles ?? [] };
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
// Parent-prefix: /leave/types/abc → /leave/types (non-empty roles only)
|
|
1280
|
+
for (let len = pathSegments.length - 1; len >= 1; len--) {
|
|
1281
|
+
const parent = "/" + pathSegments.slice(0, len).join("/");
|
|
1282
|
+
if (hasRule(rulesMap, parent) && (rulesMap[parent]?.length ?? 0) > 0) {
|
|
1283
|
+
return { matched: true, roles: rulesMap[parent] ?? [] };
|
|
1256
1284
|
}
|
|
1257
1285
|
}
|
|
1258
|
-
return [];
|
|
1286
|
+
return { matched: false, roles: [] };
|
|
1287
|
+
}
|
|
1288
|
+
/**
|
|
1289
|
+
* Returns the required child-group segments for a page pathname.
|
|
1290
|
+
*
|
|
1291
|
+
* Prefer {@link resolveUiRoutePermission} when you need to distinguish
|
|
1292
|
+
* "explicitly public (`[]`)" from "no rule (deny)". This helper returns
|
|
1293
|
+
* `[]` for both cases and is kept for backward compatibility.
|
|
1294
|
+
*/
|
|
1295
|
+
function getRequiredChildGroups(pathname, rulesMap) {
|
|
1296
|
+
return resolveUiRoutePermission(pathname, rulesMap).roles;
|
|
1259
1297
|
}
|
|
1260
1298
|
|
|
1299
|
+
function DeniedCard() {
|
|
1300
|
+
return (jsx(Card, { className: "p-6", children: jsxs("div", { className: "flex flex-col items-center justify-center gap-3 text-center", children: [jsx(AlertCircle, { className: "h-8 w-8 text-destructive" }), jsxs("div", { className: "space-y-1", children: [jsx("h3", { className: "text-lg font-semibold text-destructive", children: "\u0E44\u0E21\u0E48\u0E21\u0E35\u0E2A\u0E34\u0E17\u0E18\u0E34\u0E4C\u0E40\u0E02\u0E49\u0E32\u0E16\u0E36\u0E07" }), jsx("p", { className: "text-sm text-muted-foreground", children: "\u0E04\u0E38\u0E13\u0E44\u0E21\u0E48\u0E21\u0E35\u0E2A\u0E34\u0E17\u0E18\u0E34\u0E4C\u0E43\u0E19\u0E01\u0E32\u0E23\u0E40\u0E02\u0E49\u0E32\u0E16\u0E36\u0E07\u0E2B\u0E19\u0E49\u0E32\u0E19\u0E35\u0E49 \u0E2B\u0E32\u0E01\u0E04\u0E34\u0E14\u0E27\u0E48\u0E32\u0E04\u0E27\u0E23\u0E40\u0E02\u0E49\u0E32\u0E16\u0E36\u0E07\u0E44\u0E14\u0E49 \u0E01\u0E23\u0E38\u0E13\u0E32\u0E15\u0E34\u0E14\u0E15\u0E48\u0E2D\u0E1C\u0E39\u0E49\u0E14\u0E39\u0E41\u0E25\u0E23\u0E30\u0E1A\u0E1A\u0E02\u0E2D\u0E07\u0E2D\u0E07\u0E04\u0E4C\u0E01\u0E23" })] })] }) }));
|
|
1301
|
+
}
|
|
1261
1302
|
/**
|
|
1262
1303
|
* Permission gate for a page subtree. Looks up the current pathname in the
|
|
1263
1304
|
* permission rules map (live rules from `PermissionRulesProvider`, layered
|
|
1264
1305
|
* over the optional `fallbackRulesMap`), passes the resolved required roles
|
|
1265
|
-
* into the host's `usePermission` probe, and renders one of
|
|
1266
|
-
*
|
|
1267
|
-
* 1. while rules or probe are still loading → centered spinner card
|
|
1268
|
-
* 2. probe says no access → centered "ไม่มีสิทธิ์" card with required vs.
|
|
1269
|
-
* user roles for debugging
|
|
1270
|
-
* 3. otherwise → children
|
|
1306
|
+
* into the host's `usePermission` probe, and renders one of:
|
|
1271
1307
|
*
|
|
1272
|
-
*
|
|
1273
|
-
*
|
|
1308
|
+
* 1. rules / probe still loading → `null` (let Suspense / loading.tsx show)
|
|
1309
|
+
* 2. no rule covers this path → deny (fail closed)
|
|
1310
|
+
* 3. rule is explicit public (`[]`) → children
|
|
1311
|
+
* 4. probe says no access → deny card
|
|
1312
|
+
* 5. otherwise → children
|
|
1274
1313
|
*/
|
|
1275
1314
|
function PermissionProvider({ children, usePermission, fallbackRulesMap, }) {
|
|
1276
1315
|
const pathname = usePathname();
|
|
@@ -1281,37 +1320,103 @@ function PermissionProvider({ children, usePermission, fallbackRulesMap, }) {
|
|
|
1281
1320
|
// live rules win over fallback when both define the same key
|
|
1282
1321
|
return { ...fallbackRulesMap, ...rulesMap };
|
|
1283
1322
|
}, [rulesMap, fallbackRulesMap]);
|
|
1284
|
-
const
|
|
1285
|
-
// Hook must run unconditionally (rules of hooks)
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
requiredRoles,
|
|
1323
|
+
const lookup = useMemo(() => resolveUiRoutePermission(pathname, mergedMap), [pathname, mergedMap]);
|
|
1324
|
+
// Hook must run unconditionally (rules of hooks).
|
|
1325
|
+
const { hasPermission, isLoading } = usePermission({
|
|
1326
|
+
requiredRoles: lookup.roles,
|
|
1289
1327
|
});
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1328
|
+
if (rulesLoading) {
|
|
1329
|
+
return null;
|
|
1330
|
+
}
|
|
1331
|
+
// Unknown route → deny. Only explicit `required_child_groups: []` is public.
|
|
1332
|
+
if (!lookup.matched) {
|
|
1333
|
+
return jsx(DeniedCard, {});
|
|
1334
|
+
}
|
|
1335
|
+
if (lookup.roles.length === 0) {
|
|
1294
1336
|
return jsx(Fragment$1, { children: children });
|
|
1295
1337
|
}
|
|
1296
|
-
|
|
1297
|
-
// Next.js loading.tsx / Suspense fallback show. The old behaviour wrapped
|
|
1298
|
-
// a big "กำลังตรวจสอบสิทธิ์..." card over the top, which both duplicated
|
|
1299
|
-
// the app's own loader and — worse — caused a "ไม่มีสิทธิ์" flash on
|
|
1300
|
-
// fast loads because the probe transiently returned `hasPermission:false,
|
|
1301
|
-
// isLoading:false` before its dependencies (org context, role-segments)
|
|
1302
|
-
// arrived.
|
|
1303
|
-
const isLoadingCombined = rulesLoading || isLoading;
|
|
1338
|
+
const isLoadingCombined = isLoading;
|
|
1304
1339
|
if (isLoadingCombined) {
|
|
1305
1340
|
return null;
|
|
1306
1341
|
}
|
|
1307
1342
|
if (!hasPermission) {
|
|
1308
|
-
return
|
|
1343
|
+
return jsx(DeniedCard, {});
|
|
1309
1344
|
}
|
|
1310
1345
|
return jsx(Fragment$1, { children: children });
|
|
1311
1346
|
}
|
|
1312
1347
|
|
|
1348
|
+
/** Cookie name for the cross-subdomain theme choice. */
|
|
1349
|
+
const THEME_COOKIE = "erp-theme";
|
|
1350
|
+
/**
|
|
1351
|
+
* Parent domain to scope the cookie to, so every `*.antniti.com` app reads the
|
|
1352
|
+
* same value. Returns `null` on localhost / bare IPs (host-only cookie).
|
|
1353
|
+
*/
|
|
1354
|
+
function sharedCookieDomain() {
|
|
1355
|
+
if (typeof window === "undefined")
|
|
1356
|
+
return null;
|
|
1357
|
+
const host = window.location.hostname;
|
|
1358
|
+
if (host === "localhost" || /^[0-9.]+$/.test(host))
|
|
1359
|
+
return null;
|
|
1360
|
+
const parts = host.split(".");
|
|
1361
|
+
if (parts.length < 2)
|
|
1362
|
+
return null;
|
|
1363
|
+
return "." + parts.slice(-2).join(".");
|
|
1364
|
+
}
|
|
1365
|
+
function readThemeCookie() {
|
|
1366
|
+
if (typeof document === "undefined")
|
|
1367
|
+
return null;
|
|
1368
|
+
const match = document.cookie.match(new RegExp("(?:^|; )" + THEME_COOKIE + "=([^;]*)"));
|
|
1369
|
+
return match ? decodeURIComponent(match[1]) : null;
|
|
1370
|
+
}
|
|
1371
|
+
function writeThemeCookie(value) {
|
|
1372
|
+
if (typeof document === "undefined")
|
|
1373
|
+
return;
|
|
1374
|
+
const domain = sharedCookieDomain();
|
|
1375
|
+
const parts = [
|
|
1376
|
+
`${THEME_COOKIE}=${encodeURIComponent(value)}`,
|
|
1377
|
+
"path=/",
|
|
1378
|
+
"max-age=31536000", // 1 year
|
|
1379
|
+
"SameSite=Lax",
|
|
1380
|
+
];
|
|
1381
|
+
if (domain)
|
|
1382
|
+
parts.push(`domain=${domain}`);
|
|
1383
|
+
document.cookie = parts.join("; ");
|
|
1384
|
+
}
|
|
1385
|
+
/**
|
|
1386
|
+
* Bridges next-themes (per-origin localStorage) with a cookie scoped to the
|
|
1387
|
+
* parent domain, so the light/dark/system choice is shared across the
|
|
1388
|
+
* accounting / HR / platform subdomains. On load it adopts the shared cookie;
|
|
1389
|
+
* on every later change it writes the cookie back.
|
|
1390
|
+
*/
|
|
1391
|
+
function ThemeCookieSync() {
|
|
1392
|
+
const { theme, setTheme } = useTheme();
|
|
1393
|
+
const skipFirstWrite = React.useRef(true);
|
|
1394
|
+
React.useEffect(() => {
|
|
1395
|
+
const cookie = readThemeCookie();
|
|
1396
|
+
if (cookie) {
|
|
1397
|
+
if (cookie !== theme)
|
|
1398
|
+
setTheme(cookie);
|
|
1399
|
+
}
|
|
1400
|
+
else if (theme) {
|
|
1401
|
+
writeThemeCookie(theme); // seed the shared cookie the first time
|
|
1402
|
+
}
|
|
1403
|
+
// Run once on mount — `theme` is intentionally not a dependency here.
|
|
1404
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1405
|
+
}, []);
|
|
1406
|
+
React.useEffect(() => {
|
|
1407
|
+
// Skip the initial render; only persist genuine changes (incl. the
|
|
1408
|
+
// setTheme() triggered by adopting the cookie above — harmless re-write).
|
|
1409
|
+
if (skipFirstWrite.current) {
|
|
1410
|
+
skipFirstWrite.current = false;
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1413
|
+
if (theme)
|
|
1414
|
+
writeThemeCookie(theme);
|
|
1415
|
+
}, [theme]);
|
|
1416
|
+
return null;
|
|
1417
|
+
}
|
|
1313
1418
|
function ThemeProvider({ children, ...props }) {
|
|
1314
|
-
return
|
|
1419
|
+
return (jsxs(ThemeProvider$1, { ...props, children: [jsx(ThemeCookieSync, {}), children] }));
|
|
1315
1420
|
}
|
|
1316
1421
|
|
|
1317
1422
|
const defaultFlags = {
|
|
@@ -1732,5 +1837,5 @@ const getAxiosErrorMessage = (error) => {
|
|
|
1732
1837
|
return String(error);
|
|
1733
1838
|
};
|
|
1734
1839
|
|
|
1735
|
-
export { ApiError, AppBreadcrumb, AppLayoutShell, AppSidebar, FeatureFlagsProvider, HeaderBackground, NavMain, NavTools, NavUser, OrganizationSwitcher, PermissionGateShell, PermissionProvider, PermissionRulesProvider, ReactQueryProvider, SessionBackedProfileCacheSeeder, SessionRefreshTrigger, SidebarContent, SidebarFooter, SidebarHeader, SidebarProvider, SidebarTrigger, TapMenu, ThemeProvider, instance as axiosInstance, buildUrlWithCode, canAccessNavPermissionRoute, clearAccessTokenCache, clearOrganizationId, decodeJWT, filterNavigationModulesByPermission, filterNavigationToolsByPermission, getAccessToken, getActiveModule, getActiveSubModule, getAxiosErrorMessage, getOrganizationId, getQueryClientConfig, getRequiredChildGroups, isLikelyTransientNetworkError, isPathActive, normalizePathnameForPermissionRules, runGlobalRequestPrecheck, runWithTransientRetry, setOrganizationId, toApiError, unwrapApiResponse, updateOrganizationId, useClearOrgOnUserChange, useFeatureFlag, useFeatureFlags, useInvalidateQueriesOnOrgChange, useKeycloakRoles, useOrganizationIdState, usePermissionRules, useSessionRefresh, useSidebar, useSidebarToggle };
|
|
1840
|
+
export { ApiError, AppBreadcrumb, AppLayoutShell, AppSidebar, FeatureFlagsProvider, HeaderBackground, NavMain, NavTools, NavUser, OrganizationSwitcher, PermissionGateShell, PermissionProvider, PermissionRulesProvider, ReactQueryProvider, SessionBackedProfileCacheSeeder, SessionRefreshTrigger, SidebarContent, SidebarFooter, SidebarHeader, SidebarProvider, SidebarTrigger, TapMenu, ThemeProvider, instance as axiosInstance, buildUrlWithCode, canAccessNavPermissionRoute, clearAccessTokenCache, clearOrganizationId, decodeJWT, filterNavigationModulesByPermission, filterNavigationToolsByPermission, getAccessToken, getActiveModule, getActiveSubModule, getAxiosErrorMessage, getOrganizationId, getQueryClientConfig, getRequiredChildGroups, isLikelyTransientNetworkError, isPathActive, normalizePathnameForPermissionRules, resolveUiRoutePermission, runGlobalRequestPrecheck, runWithTransientRetry, setOrganizationId, toApiError, unwrapApiResponse, updateOrganizationId, useClearOrgOnUserChange, useFeatureFlag, useFeatureFlags, useInvalidateQueriesOnOrgChange, useKeycloakRoles, useOrganizationIdState, usePermissionRules, useSessionRefresh, useSidebar, useSidebarToggle };
|
|
1736
1841
|
//# sourceMappingURL=index.esm.js.map
|