@eintrek/erp-shell 0.1.24 → 0.1.26
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/access-denied.d.ts +22 -0
- package/dist/auth/permission-provider.d.ts +6 -7
- package/dist/auth/permission-utils.d.ts +24 -7
- package/dist/index.d.ts +2 -1
- package/dist/index.esm.js +90 -45
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +90 -43
- package/dist/index.js.map +1 -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
package/dist/index.js
CHANGED
|
@@ -623,12 +623,19 @@ SidebarMenuSubButton.displayName = "SidebarMenuSubButton";
|
|
|
623
623
|
|
|
624
624
|
/**
|
|
625
625
|
* True when the user's child-group segments include at least one of the
|
|
626
|
-
* groups required for `permissionRouteKey`.
|
|
627
|
-
*
|
|
626
|
+
* groups required for `permissionRouteKey`.
|
|
627
|
+
*
|
|
628
|
+
* Fail-closed:
|
|
629
|
+
* - Nav item without `permissionRouteKey` → deny (must declare a key)
|
|
630
|
+
* - Key absent from `rulesMap` → deny (unknown route is not public)
|
|
631
|
+
* - Key present with `required_child_groups: []` → allow (explicit public)
|
|
628
632
|
*/
|
|
629
633
|
function canAccessNavPermissionRoute(permissionRouteKey, rulesMap, userChildSegments) {
|
|
630
634
|
if (!permissionRouteKey)
|
|
631
|
-
return
|
|
635
|
+
return false;
|
|
636
|
+
if (!Object.prototype.hasOwnProperty.call(rulesMap, permissionRouteKey)) {
|
|
637
|
+
return false;
|
|
638
|
+
}
|
|
632
639
|
const required = rulesMap[permissionRouteKey] ?? [];
|
|
633
640
|
if (required.length === 0)
|
|
634
641
|
return true;
|
|
@@ -1163,6 +1170,27 @@ function cx(...parts) {
|
|
|
1163
1170
|
return parts.filter(Boolean).join(" ");
|
|
1164
1171
|
}
|
|
1165
1172
|
|
|
1173
|
+
/**
|
|
1174
|
+
* Central "no access" screen shared by every policy check (route-level
|
|
1175
|
+
* PermissionProvider and page-level PermissionGateShell). One look everywhere:
|
|
1176
|
+
* an illustration, a 403 headline, the page-specific reason, and recovery
|
|
1177
|
+
* actions.
|
|
1178
|
+
*/
|
|
1179
|
+
function AccessDenied({ title = "ขออภัย, คุณไม่มีสิทธิ์เข้าถึงหน้านี้ (403)", message = "คุณไม่มีสิทธิ์เข้าถึงหน้านี้ ขออภัยในความไม่สะดวก", homeHref = "/", homeLabel = "กลับไปหน้าหลัก", supportHref, supportLabel = "ติดต่อฝ่ายสนับสนุน", fullScreen = true, className, }) {
|
|
1180
|
+
const go = (href) => {
|
|
1181
|
+
if (typeof window !== "undefined")
|
|
1182
|
+
window.location.href = href;
|
|
1183
|
+
};
|
|
1184
|
+
const wrapperClass = [
|
|
1185
|
+
"flex min-w-0 flex-1 items-center justify-center p-4",
|
|
1186
|
+
fullScreen ? "min-h-[70vh]" : "",
|
|
1187
|
+
className ?? "",
|
|
1188
|
+
]
|
|
1189
|
+
.filter(Boolean)
|
|
1190
|
+
.join(" ");
|
|
1191
|
+
return (jsxRuntime.jsx("div", { className: wrapperClass, children: jsxRuntime.jsxs("div", { className: "relative w-full max-w-3xl overflow-hidden rounded-2xl border border-border/60 bg-card/60 p-8 shadow-sm backdrop-blur sm:p-10", children: [jsxRuntime.jsx(lucideReact.Sparkles, { className: "pointer-events-none absolute right-6 top-6 h-5 w-5 text-primary/40" }), jsxRuntime.jsxs("div", { className: "flex flex-col items-center gap-8 text-center sm:flex-row sm:text-left", children: [jsxRuntime.jsxs("div", { className: "relative shrink-0", children: [jsxRuntime.jsx("div", { className: "absolute inset-0 rounded-full bg-destructive/20 blur-2xl" }), jsxRuntime.jsx("div", { className: "relative flex h-28 w-28 items-center justify-center rounded-3xl border border-border/60 bg-gradient-to-br from-muted/40 to-background shadow-inner", children: jsxRuntime.jsx("div", { className: "flex h-16 w-16 items-center justify-center rounded-full border border-destructive/40 bg-destructive/10", children: jsxRuntime.jsx(lucideReact.Ban, { className: "h-8 w-8 text-destructive", strokeWidth: 2.5 }) }) })] }), jsxRuntime.jsxs("div", { className: "min-w-0 flex-1", children: [jsxRuntime.jsx("h1", { className: "text-xl font-bold text-foreground sm:text-2xl", children: title }), message ? (jsxRuntime.jsx("p", { className: "mt-3 whitespace-pre-line text-sm leading-relaxed text-muted-foreground sm:text-base", children: message })) : null, jsxRuntime.jsxs("div", { className: "mt-6 flex flex-col gap-3 sm:flex-row", children: [jsxRuntime.jsxs(erpTheme.Button, { type: "button", onClick: () => go(homeHref), className: "w-full sm:w-auto", children: [jsxRuntime.jsx(lucideReact.Home, { className: "mr-2 h-4 w-4" }), homeLabel] }), supportHref ? (jsxRuntime.jsxs(erpTheme.Button, { type: "button", variant: "outline", onClick: () => go(supportHref), className: "w-full sm:w-auto", children: [jsxRuntime.jsx(lucideReact.Headset, { className: "mr-2 h-4 w-4" }), supportLabel] })) : null] })] })] })] }) }));
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1166
1194
|
const shellClassName = "flex min-w-0 flex-1 flex-col gap-6 overflow-x-hidden";
|
|
1167
1195
|
/**
|
|
1168
1196
|
* Shared layout for “กำลังตรวจสอบสิทธิ์” (skeleton) and “ไม่มีสิทธิ์” (message).
|
|
@@ -1173,7 +1201,7 @@ function PermissionGateShell({ policyLoading, hasPolicyAccess, deniedMessage, ch
|
|
|
1173
1201
|
return (jsxRuntime.jsx("div", { className: shellClassName, children: jsxRuntime.jsx("div", { className: skeletonGridClassName, children: Array.from({ length: skeletonCount }).map((_, i) => (jsxRuntime.jsx(erpTheme.Card, { children: skeletonVariant === "kpi" ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsxs(erpTheme.CardHeader, { className: "flex flex-row items-center justify-between pb-2", children: [jsxRuntime.jsx(erpTheme.Skeleton, { className: "h-4 w-24" }), jsxRuntime.jsx(erpTheme.Skeleton, { className: "h-4 w-4 rounded" })] }), jsxRuntime.jsxs(erpTheme.CardContent, { children: [jsxRuntime.jsx(erpTheme.Skeleton, { className: "h-8 w-32 mb-2" }), jsxRuntime.jsx(erpTheme.Skeleton, { className: "h-3 w-20" })] })] })) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(erpTheme.CardHeader, { className: "pb-2", children: jsxRuntime.jsx(erpTheme.Skeleton, { className: "h-4 w-24" }) }), jsxRuntime.jsx(erpTheme.CardContent, { children: jsxRuntime.jsx(erpTheme.Skeleton, { className: "h-8 w-16" }) })] })) }, i))) }) }));
|
|
1174
1202
|
}
|
|
1175
1203
|
if (!hasPolicyAccess) {
|
|
1176
|
-
return (jsxRuntime.jsx("div", { className: shellClassName, children: jsxRuntime.jsx(
|
|
1204
|
+
return (jsxRuntime.jsx("div", { className: shellClassName, children: jsxRuntime.jsx(AccessDenied, { message: deniedMessage }) }));
|
|
1177
1205
|
}
|
|
1178
1206
|
return jsxRuntime.jsx(jsxRuntime.Fragment, { children: children });
|
|
1179
1207
|
}
|
|
@@ -1216,8 +1244,8 @@ function PermissionRulesProvider({ children, data, isLoading = false, isError =
|
|
|
1216
1244
|
return (jsxRuntime.jsx(PermissionRulesContext.Provider, { value: value, children: children }));
|
|
1217
1245
|
}
|
|
1218
1246
|
/**
|
|
1219
|
-
* Read the permission rules.
|
|
1220
|
-
*
|
|
1247
|
+
* Read the permission rules. Outside the provider, report loading so
|
|
1248
|
+
* fail-closed consumers (nav / gates) do not treat empty maps as "allow all".
|
|
1221
1249
|
*/
|
|
1222
1250
|
function usePermissionRules() {
|
|
1223
1251
|
const ctx = React.useContext(PermissionRulesContext);
|
|
@@ -1225,7 +1253,7 @@ function usePermissionRules() {
|
|
|
1225
1253
|
return {
|
|
1226
1254
|
rulesMap: {},
|
|
1227
1255
|
policiesMap: {},
|
|
1228
|
-
isLoading:
|
|
1256
|
+
isLoading: true,
|
|
1229
1257
|
isError: false,
|
|
1230
1258
|
updatedAt: null,
|
|
1231
1259
|
};
|
|
@@ -1247,20 +1275,22 @@ function normalizePathnameForPermissionRules(pathname) {
|
|
|
1247
1275
|
const path = parts.length <= 1 ? "/" : "/" + parts.slice(1).join("/");
|
|
1248
1276
|
return path.replace(/\/user-role\/(purchase-doc|sales-doc)$/, "/user-role/members");
|
|
1249
1277
|
}
|
|
1278
|
+
function hasRule(rulesMap, routeKey) {
|
|
1279
|
+
return Object.prototype.hasOwnProperty.call(rulesMap, routeKey);
|
|
1280
|
+
}
|
|
1250
1281
|
/**
|
|
1251
|
-
*
|
|
1252
|
-
* JWT must contain at least one matching group). An exact match on the
|
|
1253
|
-
* normalized path wins; otherwise the function falls back to a segment-by-
|
|
1254
|
-
* segment pattern match where `[param]` placeholders accept any non-empty
|
|
1255
|
-
* value. Returns an empty array when no rule covers the path.
|
|
1282
|
+
* Resolve UI route permissions for a pathname.
|
|
1256
1283
|
*
|
|
1257
|
-
*
|
|
1258
|
-
*
|
|
1284
|
+
* - Exact / `[param]` pattern match → matched
|
|
1285
|
+
* - Parent-prefix inherit only when the parent rule has a **non-empty**
|
|
1286
|
+
* role list (restrictive inherit). Explicitly public parents (`[]`) do
|
|
1287
|
+
* not open all children.
|
|
1288
|
+
* - No match → `{ matched: false }` (fail closed — never treat as public)
|
|
1259
1289
|
*/
|
|
1260
|
-
function
|
|
1290
|
+
function resolveUiRoutePermission(pathname, rulesMap) {
|
|
1261
1291
|
const normalized = normalizePathnameForPermissionRules(pathname);
|
|
1262
|
-
if (rulesMap
|
|
1263
|
-
return rulesMap[normalized];
|
|
1292
|
+
if (hasRule(rulesMap, normalized)) {
|
|
1293
|
+
return { matched: true, roles: rulesMap[normalized] ?? [] };
|
|
1264
1294
|
}
|
|
1265
1295
|
const pathSegments = normalized.split("/").filter(Boolean);
|
|
1266
1296
|
for (const [pattern, roles] of Object.entries(rulesMap)) {
|
|
@@ -1283,24 +1313,43 @@ function getRequiredChildGroups(pathname, rulesMap) {
|
|
|
1283
1313
|
}
|
|
1284
1314
|
}
|
|
1285
1315
|
if (matches) {
|
|
1286
|
-
return roles;
|
|
1316
|
+
return { matched: true, roles: roles ?? [] };
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
// Parent-prefix: /leave/types/abc → /leave/types (non-empty roles only)
|
|
1320
|
+
for (let len = pathSegments.length - 1; len >= 1; len--) {
|
|
1321
|
+
const parent = "/" + pathSegments.slice(0, len).join("/");
|
|
1322
|
+
if (hasRule(rulesMap, parent) && (rulesMap[parent]?.length ?? 0) > 0) {
|
|
1323
|
+
return { matched: true, roles: rulesMap[parent] ?? [] };
|
|
1287
1324
|
}
|
|
1288
1325
|
}
|
|
1289
|
-
return [];
|
|
1326
|
+
return { matched: false, roles: [] };
|
|
1327
|
+
}
|
|
1328
|
+
/**
|
|
1329
|
+
* Returns the required child-group segments for a page pathname.
|
|
1330
|
+
*
|
|
1331
|
+
* Prefer {@link resolveUiRoutePermission} when you need to distinguish
|
|
1332
|
+
* "explicitly public (`[]`)" from "no rule (deny)". This helper returns
|
|
1333
|
+
* `[]` for both cases and is kept for backward compatibility.
|
|
1334
|
+
*/
|
|
1335
|
+
function getRequiredChildGroups(pathname, rulesMap) {
|
|
1336
|
+
return resolveUiRoutePermission(pathname, rulesMap).roles;
|
|
1290
1337
|
}
|
|
1291
1338
|
|
|
1339
|
+
function DeniedCard() {
|
|
1340
|
+
return (jsxRuntime.jsx(AccessDenied, { message: "\u0E04\u0E38\u0E13\u0E44\u0E21\u0E48\u0E21\u0E35\u0E2A\u0E34\u0E17\u0E18\u0E34\u0E4C\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" }));
|
|
1341
|
+
}
|
|
1292
1342
|
/**
|
|
1293
1343
|
* Permission gate for a page subtree. Looks up the current pathname in the
|
|
1294
1344
|
* permission rules map (live rules from `PermissionRulesProvider`, layered
|
|
1295
1345
|
* over the optional `fallbackRulesMap`), passes the resolved required roles
|
|
1296
|
-
* into the host's `usePermission` probe, and renders one of
|
|
1297
|
-
*
|
|
1298
|
-
* 1. while rules or probe are still loading → centered spinner card
|
|
1299
|
-
* 2. probe says no access → centered "ไม่มีสิทธิ์" card
|
|
1300
|
-
* 3. otherwise → children
|
|
1346
|
+
* into the host's `usePermission` probe, and renders one of:
|
|
1301
1347
|
*
|
|
1302
|
-
*
|
|
1303
|
-
*
|
|
1348
|
+
* 1. rules / probe still loading → `null` (let Suspense / loading.tsx show)
|
|
1349
|
+
* 2. no rule covers this path → deny (fail closed)
|
|
1350
|
+
* 3. rule is explicit public (`[]`) → children
|
|
1351
|
+
* 4. probe says no access → deny card
|
|
1352
|
+
* 5. otherwise → children
|
|
1304
1353
|
*/
|
|
1305
1354
|
function PermissionProvider({ children, usePermission, fallbackRulesMap, }) {
|
|
1306
1355
|
const pathname = navigation.usePathname();
|
|
@@ -1311,31 +1360,27 @@ function PermissionProvider({ children, usePermission, fallbackRulesMap, }) {
|
|
|
1311
1360
|
// live rules win over fallback when both define the same key
|
|
1312
1361
|
return { ...fallbackRulesMap, ...rulesMap };
|
|
1313
1362
|
}, [rulesMap, fallbackRulesMap]);
|
|
1314
|
-
const
|
|
1315
|
-
// Hook must run unconditionally (rules of hooks)
|
|
1316
|
-
// result on the fast path below.
|
|
1363
|
+
const lookup = React.useMemo(() => resolveUiRoutePermission(pathname, mergedMap), [pathname, mergedMap]);
|
|
1364
|
+
// Hook must run unconditionally (rules of hooks).
|
|
1317
1365
|
const { hasPermission, isLoading } = usePermission({
|
|
1318
|
-
requiredRoles,
|
|
1366
|
+
requiredRoles: lookup.roles,
|
|
1319
1367
|
});
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1368
|
+
if (rulesLoading) {
|
|
1369
|
+
return null;
|
|
1370
|
+
}
|
|
1371
|
+
// Unknown route → deny. Only explicit `required_child_groups: []` is public.
|
|
1372
|
+
if (!lookup.matched) {
|
|
1373
|
+
return jsxRuntime.jsx(DeniedCard, {});
|
|
1374
|
+
}
|
|
1375
|
+
if (lookup.roles.length === 0) {
|
|
1324
1376
|
return jsxRuntime.jsx(jsxRuntime.Fragment, { children: children });
|
|
1325
1377
|
}
|
|
1326
|
-
|
|
1327
|
-
// Next.js loading.tsx / Suspense fallback show. The old behaviour wrapped
|
|
1328
|
-
// a big "กำลังตรวจสอบสิทธิ์..." card over the top, which both duplicated
|
|
1329
|
-
// the app's own loader and — worse — caused a "ไม่มีสิทธิ์" flash on
|
|
1330
|
-
// fast loads because the probe transiently returned `hasPermission:false,
|
|
1331
|
-
// isLoading:false` before its dependencies (org context, role-segments)
|
|
1332
|
-
// arrived.
|
|
1333
|
-
const isLoadingCombined = rulesLoading || isLoading;
|
|
1378
|
+
const isLoadingCombined = isLoading;
|
|
1334
1379
|
if (isLoadingCombined) {
|
|
1335
1380
|
return null;
|
|
1336
1381
|
}
|
|
1337
1382
|
if (!hasPermission) {
|
|
1338
|
-
return
|
|
1383
|
+
return jsxRuntime.jsx(DeniedCard, {});
|
|
1339
1384
|
}
|
|
1340
1385
|
return jsxRuntime.jsx(jsxRuntime.Fragment, { children: children });
|
|
1341
1386
|
}
|
|
@@ -1836,6 +1881,7 @@ Object.defineProperty(exports, "isAxiosError", {
|
|
|
1836
1881
|
enumerable: true,
|
|
1837
1882
|
get: function () { return axios.isAxiosError; }
|
|
1838
1883
|
});
|
|
1884
|
+
exports.AccessDenied = AccessDenied;
|
|
1839
1885
|
exports.ApiError = ApiError;
|
|
1840
1886
|
exports.AppBreadcrumb = AppBreadcrumb;
|
|
1841
1887
|
exports.AppLayoutShell = AppLayoutShell;
|
|
@@ -1877,6 +1923,7 @@ exports.getRequiredChildGroups = getRequiredChildGroups;
|
|
|
1877
1923
|
exports.isLikelyTransientNetworkError = isLikelyTransientNetworkError;
|
|
1878
1924
|
exports.isPathActive = isPathActive;
|
|
1879
1925
|
exports.normalizePathnameForPermissionRules = normalizePathnameForPermissionRules;
|
|
1926
|
+
exports.resolveUiRoutePermission = resolveUiRoutePermission;
|
|
1880
1927
|
exports.runGlobalRequestPrecheck = runGlobalRequestPrecheck;
|
|
1881
1928
|
exports.runWithTransientRetry = runWithTransientRetry;
|
|
1882
1929
|
exports.setOrganizationId = setOrganizationId;
|