@eintrek/erp-shell 0.1.31 → 0.1.33
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/employee-required-gate.d.ts +15 -0
- package/dist/auth/permission-provider.d.ts +3 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.esm.js +64 -7
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +65 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare const EMPLOYEE_RECORD_REQUIRED = "EMPLOYEE_RECORD_REQUIRED";
|
|
2
|
+
export declare const EMPLOYEE_RECORD_REQUIRED_EVENT = "erp:employee-record-required";
|
|
3
|
+
/**
|
|
4
|
+
* Shown when the API refuses every request because the signed-in account has
|
|
5
|
+
* no employee record in this organisation.
|
|
6
|
+
*
|
|
7
|
+
* A gate rather than a banner. Leave, attendance, approvals and payslips all
|
|
8
|
+
* identify a person by that record, so once the server started enforcing it
|
|
9
|
+
* every call fails — and without something that says so, the app becomes a
|
|
10
|
+
* screen of error toasts with no explanation and no way forward.
|
|
11
|
+
*/
|
|
12
|
+
export declare function EmployeeRequiredGate({ platformBaseUrl, }: {
|
|
13
|
+
/** Where employee records are managed, e.g. https://platform.antniti.com */
|
|
14
|
+
platformBaseUrl?: string;
|
|
15
|
+
}): import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -33,7 +33,9 @@ export interface PermissionProviderProps {
|
|
|
33
33
|
* over the optional `fallbackRulesMap`), passes the resolved required roles
|
|
34
34
|
* into the host's `usePermission` probe, and renders one of:
|
|
35
35
|
*
|
|
36
|
-
* 1. rules / probe still loading → `null` (let Suspense /
|
|
36
|
+
* 1. rules / probe still loading → `null` on FIRST load only (let Suspense /
|
|
37
|
+
* loading.tsx show); once this subtree has been granted access, a later
|
|
38
|
+
* loading state keeps the children mounted — see the note below
|
|
37
39
|
* 2. no rule covers this path → deny (fail closed)
|
|
38
40
|
* 3. rule is explicit public (`[]`) → children
|
|
39
41
|
* 4. probe says no access → deny card
|
package/dist/index.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ export { SidebarHeader } from "./navigation/sidebar-header";
|
|
|
20
20
|
export { AppSidebar, type AppSidebarProps } from "./navigation/app-sidebar";
|
|
21
21
|
export { AppLayoutShell, type AppLayoutShellProps, type AppLayoutShellOrganization, } from "./navigation/app-layout-shell";
|
|
22
22
|
export { AccessDenied, type AccessDeniedProps } from "./auth/access-denied";
|
|
23
|
+
export { EmployeeRequiredGate, EMPLOYEE_RECORD_REQUIRED, EMPLOYEE_RECORD_REQUIRED_EVENT, } from "./auth/employee-required-gate";
|
|
23
24
|
export { PermissionGateShell, type PermissionGateShellProps, type PermissionGateSkeletonVariant, } from "./auth/permission-gate-shell";
|
|
24
25
|
export { PermissionProvider, type PermissionProviderProps, type PermissionProbe, type UsePermissionHook, } from "./auth/permission-provider";
|
|
25
26
|
export { getRequiredChildGroups, normalizePathnameForPermissionRules, resolveUiRoutePermission, type UiRoutePermissionLookup, } from "./auth/permission-utils";
|
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, LayoutGrid, ExternalLink, Loader2, LogOut, ChevronDown, ChevronsUpDown, Plus, Ban, Home, Headset } from 'lucide-react';
|
|
8
|
+
import { PanelLeftClose, Menu, ChevronRight, Building2, Palette, ChevronUp, Sun, Moon, Monitor, Globe, User, LayoutGrid, ExternalLink, Loader2, LogOut, ChevronDown, ChevronsUpDown, Plus, Ban, Home, Headset, IdCard, UserPlus } 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';
|
|
@@ -1195,6 +1195,33 @@ function AccessDenied({ title = "ขออภัย, คุณไม่มีส
|
|
|
1195
1195
|
return (jsx("div", { className: wrapperClass, children: jsx("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: jsxs("div", { className: "flex flex-col items-center gap-8 text-center sm:flex-row sm:text-left", children: [jsxs("div", { className: "relative shrink-0", children: [jsx("div", { className: "absolute inset-0 rounded-full bg-destructive/20 blur-2xl" }), 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: jsx("div", { className: "flex h-16 w-16 items-center justify-center rounded-full border border-destructive/40 bg-destructive/10", children: jsx(Ban, { className: "h-8 w-8 text-destructive", strokeWidth: 2.5 }) }) })] }), jsxs("div", { className: "min-w-0 flex-1", children: [jsx("h1", { className: "text-xl font-bold text-foreground sm:text-2xl", children: title }), message ? (jsx("p", { className: "mt-3 whitespace-pre-line text-sm leading-relaxed text-muted-foreground sm:text-base", children: message })) : null, jsxs("div", { className: "mt-6 flex flex-col gap-3 sm:flex-row", children: [jsxs(Button, { type: "button", onClick: () => go(homeTarget), className: "w-full sm:w-auto", children: [jsx(Home, { className: "mr-2 h-4 w-4" }), homeLabel] }), supportHref ? (jsxs(Button, { type: "button", variant: "outline", onClick: () => go(supportHref), className: "w-full sm:w-auto", children: [jsx(Headset, { className: "mr-2 h-4 w-4" }), supportLabel] })) : null] })] })] }) }) }));
|
|
1196
1196
|
}
|
|
1197
1197
|
|
|
1198
|
+
const EMPLOYEE_RECORD_REQUIRED = "EMPLOYEE_RECORD_REQUIRED";
|
|
1199
|
+
const EMPLOYEE_RECORD_REQUIRED_EVENT = "erp:employee-record-required";
|
|
1200
|
+
/**
|
|
1201
|
+
* Shown when the API refuses every request because the signed-in account has
|
|
1202
|
+
* no employee record in this organisation.
|
|
1203
|
+
*
|
|
1204
|
+
* A gate rather than a banner. Leave, attendance, approvals and payslips all
|
|
1205
|
+
* identify a person by that record, so once the server started enforcing it
|
|
1206
|
+
* every call fails — and without something that says so, the app becomes a
|
|
1207
|
+
* screen of error toasts with no explanation and no way forward.
|
|
1208
|
+
*/
|
|
1209
|
+
function EmployeeRequiredGate({ platformBaseUrl, }) {
|
|
1210
|
+
const [blocked, setBlocked] = useState(false);
|
|
1211
|
+
const params = useParams();
|
|
1212
|
+
const code = params?.code ?? "";
|
|
1213
|
+
useEffect(() => {
|
|
1214
|
+
const onBlocked = () => setBlocked(true);
|
|
1215
|
+
window.addEventListener(EMPLOYEE_RECORD_REQUIRED_EVENT, onBlocked);
|
|
1216
|
+
return () => window.removeEventListener(EMPLOYEE_RECORD_REQUIRED_EVENT, onBlocked);
|
|
1217
|
+
}, []);
|
|
1218
|
+
if (!blocked)
|
|
1219
|
+
return null;
|
|
1220
|
+
const base = platformBaseUrl?.replace(/\/$/, "") ?? "";
|
|
1221
|
+
const createHref = code ? `${base}/${code}/employee/create` : `${base}/`;
|
|
1222
|
+
return (jsx("div", { className: "fixed inset-0 z-[100] flex items-center justify-center bg-background/95 p-6 backdrop-blur-sm", children: jsxs("div", { className: "w-full max-w-md space-y-5 text-center", children: [jsx(IdCard, { className: "mx-auto h-12 w-12 text-muted-foreground" }), jsxs("div", { className: "space-y-2", children: [jsx("h1", { className: "text-lg font-semibold tracking-tight", children: "\u0E1A\u0E31\u0E0D\u0E0A\u0E35\u0E19\u0E35\u0E49\u0E22\u0E31\u0E07\u0E44\u0E21\u0E48\u0E44\u0E14\u0E49\u0E1C\u0E39\u0E01\u0E01\u0E31\u0E1A\u0E02\u0E49\u0E2D\u0E21\u0E39\u0E25\u0E1E\u0E19\u0E31\u0E01\u0E07\u0E32\u0E19" }), jsx("p", { className: "text-sm text-muted-foreground", children: "\u0E25\u0E32 \u0E25\u0E07\u0E40\u0E27\u0E25\u0E32 \u0E40\u0E1A\u0E34\u0E01\u0E40\u0E07\u0E34\u0E19 \u0E41\u0E25\u0E30\u0E01\u0E32\u0E23\u0E2D\u0E19\u0E38\u0E21\u0E31\u0E15\u0E34 \u0E23\u0E30\u0E1A\u0E38\u0E15\u0E31\u0E27\u0E04\u0E19\u0E08\u0E32\u0E01\u0E02\u0E49\u0E2D\u0E21\u0E39\u0E25\u0E1E\u0E19\u0E31\u0E01\u0E07\u0E32\u0E19 \u0E08\u0E36\u0E07\u0E22\u0E31\u0E07\u0E43\u0E0A\u0E49\u0E07\u0E32\u0E19\u0E43\u0E19\u0E2D\u0E07\u0E04\u0E4C\u0E01\u0E23\u0E19\u0E35\u0E49\u0E44\u0E21\u0E48\u0E44\u0E14\u0E49" })] }), jsxs("div", { className: "flex flex-wrap justify-center gap-2", children: [jsx(Button, { asChild: true, children: jsxs("a", { href: createHref, children: [jsx(UserPlus, { className: "mr-1 h-4 w-4" }), "\u0E2A\u0E23\u0E49\u0E32\u0E07\u0E02\u0E49\u0E2D\u0E21\u0E39\u0E25\u0E1E\u0E19\u0E31\u0E01\u0E07\u0E32\u0E19"] }) }), jsx(Button, { variant: "outline", onClick: () => window.location.reload(), children: "\u0E25\u0E2D\u0E07\u0E43\u0E2B\u0E21\u0E48" })] }), jsx("p", { className: "text-xs text-muted-foreground", children: "\u0E16\u0E49\u0E32\u0E04\u0E38\u0E13\u0E44\u0E21\u0E48\u0E21\u0E35\u0E2A\u0E34\u0E17\u0E18\u0E34\u0E4C\u0E41\u0E01\u0E49\u0E44\u0E02 \u0E43\u0E2B\u0E49\u0E41\u0E08\u0E49\u0E07\u0E1C\u0E39\u0E49\u0E14\u0E39\u0E41\u0E25\u0E2D\u0E07\u0E04\u0E4C\u0E01\u0E23" })] }) }));
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1198
1225
|
const shellClassName = "flex min-w-0 flex-1 flex-col gap-6 overflow-x-hidden";
|
|
1199
1226
|
/**
|
|
1200
1227
|
* Shared layout for “กำลังตรวจสอบสิทธิ์” (skeleton) and “ไม่มีสิทธิ์” (message).
|
|
@@ -1349,7 +1376,9 @@ function DeniedCard() {
|
|
|
1349
1376
|
* over the optional `fallbackRulesMap`), passes the resolved required roles
|
|
1350
1377
|
* into the host's `usePermission` probe, and renders one of:
|
|
1351
1378
|
*
|
|
1352
|
-
* 1. rules / probe still loading → `null` (let Suspense /
|
|
1379
|
+
* 1. rules / probe still loading → `null` on FIRST load only (let Suspense /
|
|
1380
|
+
* loading.tsx show); once this subtree has been granted access, a later
|
|
1381
|
+
* loading state keeps the children mounted — see the note below
|
|
1353
1382
|
* 2. no rule covers this path → deny (fail closed)
|
|
1354
1383
|
* 3. rule is explicit public (`[]`) → children
|
|
1355
1384
|
* 4. probe says no access → deny card
|
|
@@ -1369,7 +1398,25 @@ function PermissionProvider({ children, usePermission, fallbackRulesMap, }) {
|
|
|
1369
1398
|
const { hasPermission, isLoading } = usePermission({
|
|
1370
1399
|
requiredRoles: lookup.roles,
|
|
1371
1400
|
});
|
|
1372
|
-
|
|
1401
|
+
// Returning null unmounts the page. That is fine before anything has been
|
|
1402
|
+
// shown, and destructive afterwards: next-auth flips its status back to
|
|
1403
|
+
// "loading" on every session update() — one runs every 4 minutes — and a
|
|
1404
|
+
// rules hook that reports loading on that transient took the page down with
|
|
1405
|
+
// it. Users filling a long form watched everything they had typed vanish,
|
|
1406
|
+
// which reads as a spontaneous page refresh.
|
|
1407
|
+
//
|
|
1408
|
+
// So a loading state only blanks the subtree until access has been granted
|
|
1409
|
+
// once. After that the children stay mounted while the check re-settles; the
|
|
1410
|
+
// API still enforces access on every request, and a genuine loss of
|
|
1411
|
+
// permission surfaces as a denied response rather than a wiped form.
|
|
1412
|
+
//
|
|
1413
|
+
// The grant is remembered per pathname, not per mount. This provider lives in
|
|
1414
|
+
// the layout, so it survives every in-app navigation — a bare boolean would
|
|
1415
|
+
// carry one page's grant onto the next route and flash a page the user may
|
|
1416
|
+
// not be allowed to see while its own check is still running.
|
|
1417
|
+
const grantedForPathRef = useRef(null);
|
|
1418
|
+
const grantedHere = grantedForPathRef.current === pathname;
|
|
1419
|
+
if (rulesLoading && !grantedHere) {
|
|
1373
1420
|
return null;
|
|
1374
1421
|
}
|
|
1375
1422
|
// Unknown route → deny. Only explicit `required_child_groups: []` is public.
|
|
@@ -1377,15 +1424,16 @@ function PermissionProvider({ children, usePermission, fallbackRulesMap, }) {
|
|
|
1377
1424
|
return jsx(DeniedCard, {});
|
|
1378
1425
|
}
|
|
1379
1426
|
if (lookup.roles.length === 0) {
|
|
1427
|
+
grantedForPathRef.current = pathname;
|
|
1380
1428
|
return jsx(Fragment$1, { children: children });
|
|
1381
1429
|
}
|
|
1382
|
-
|
|
1383
|
-
if (isLoadingCombined) {
|
|
1430
|
+
if (isLoading && !grantedHere) {
|
|
1384
1431
|
return null;
|
|
1385
1432
|
}
|
|
1386
|
-
if (!hasPermission) {
|
|
1433
|
+
if (!hasPermission && !isLoading) {
|
|
1387
1434
|
return jsx(DeniedCard, {});
|
|
1388
1435
|
}
|
|
1436
|
+
grantedForPathRef.current = pathname;
|
|
1389
1437
|
return jsx(Fragment$1, { children: children });
|
|
1390
1438
|
}
|
|
1391
1439
|
|
|
@@ -1839,6 +1887,15 @@ instance.interceptors.response.use(response => response, async (error) => {
|
|
|
1839
1887
|
await redirectToLogout();
|
|
1840
1888
|
return Promise.reject(toApiError(error));
|
|
1841
1889
|
}
|
|
1890
|
+
// The server refuses every organisation-scoped call when the account has
|
|
1891
|
+
// no employee record. Announce it once so a gate can explain it, instead
|
|
1892
|
+
// of letting each failed query raise its own toast.
|
|
1893
|
+
if (error.response?.status === 403) {
|
|
1894
|
+
const body = error.response.data;
|
|
1895
|
+
if (body?.code === "EMPLOYEE_RECORD_REQUIRED" && typeof window !== "undefined") {
|
|
1896
|
+
window.dispatchEvent(new CustomEvent("erp:employee-record-required"));
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1842
1899
|
return Promise.reject(toApiError(error));
|
|
1843
1900
|
});
|
|
1844
1901
|
|
|
@@ -1881,5 +1938,5 @@ const getAxiosErrorMessage = (error) => {
|
|
|
1881
1938
|
return String(error);
|
|
1882
1939
|
};
|
|
1883
1940
|
|
|
1884
|
-
export { AccessDenied, 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 };
|
|
1941
|
+
export { AccessDenied, ApiError, AppBreadcrumb, AppLayoutShell, AppSidebar, EMPLOYEE_RECORD_REQUIRED, EMPLOYEE_RECORD_REQUIRED_EVENT, EmployeeRequiredGate, 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 };
|
|
1885
1942
|
//# sourceMappingURL=index.esm.js.map
|