@eintrek/erp-shell 0.1.10 → 0.1.12
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-gate-shell.d.ts +22 -0
- package/dist/auth/permission-provider.d.ts +44 -0
- package/dist/auth/permission-utils.d.ts +22 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.esm.js +274 -59
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +275 -55
- package/dist/index.js.map +1 -1
- package/dist/navigation/app-layout-shell.d.ts +59 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -966,6 +966,276 @@ function AppSidebar({ modules, tools, activeModule, orgCode, approvalCounts, clo
|
|
|
966
966
|
return (jsxRuntime.jsxs("div", { className: erpTheme.cn("bg-sidebar shrink-0 flex flex-col overflow-hidden shadow-2xl h-screen w-[var(--sidebar-width-mobile)]", "!w-76 xl:rounded-b-4xl xl:rounded-tl-4xl xl:m-5 xl:h-[calc(100vh-2.5rem)]", className), children: [jsxRuntime.jsx(SidebarHeader, { children: header }), jsxRuntime.jsxs(SidebarContent, { className: "pl-2 pr-2 flex-1 overflow-y-auto", children: [jsxRuntime.jsx(NavMain, { items: modulesWithActiveState, orgCode: orgCode, approvalCounts: approvalCounts, closeOnNavigation: closeOnNavigation }), jsxRuntime.jsx(NavTools, { tools: tools, closeOnNavigation: closeOnNavigation })] }), jsxRuntime.jsx(SidebarFooter, { className: "pl-2 pr-2", children: footer })] }));
|
|
967
967
|
}
|
|
968
968
|
|
|
969
|
+
/**
|
|
970
|
+
* Top-level app chrome: sticky header, sidebar slot, content gutter and the
|
|
971
|
+
* session/org-context plumbing that both Accounting and HR need. Apps wrap
|
|
972
|
+
* it inside their `SidebarProvider` (server-rendered with the cookie-backed
|
|
973
|
+
* `defaultOpen`) and their `ERPProvider`, then pass app-specific bits as
|
|
974
|
+
* props.
|
|
975
|
+
*
|
|
976
|
+
* The padding contract is deliberately owned here so future style tweaks
|
|
977
|
+
* (e.g. the recent left/right/bottom gutter pass) propagate to both apps via
|
|
978
|
+
* an erp-shell bump — domain components must NOT add their own root padding
|
|
979
|
+
* or content gets indented twice.
|
|
980
|
+
*/
|
|
981
|
+
function AppLayoutShell({ children, sidebar, loadingComponent, breadcrumb, permissionProvider, organizationId, setOrganizationId, organizations, signInUrl, selectOrganizationUrl, }) {
|
|
982
|
+
const { data: session, status } = react.useSession();
|
|
983
|
+
const { open, setOpen } = useSidebar();
|
|
984
|
+
const router = navigation.useRouter();
|
|
985
|
+
const params = navigation.useParams();
|
|
986
|
+
const code = params?.code;
|
|
987
|
+
const extendedSession = session;
|
|
988
|
+
// Find the org from the URL `[code]` so we can detect a context mismatch
|
|
989
|
+
// (the user bookmarked a different org than the cookie remembers).
|
|
990
|
+
const orgFromCode = React__namespace.useMemo(() => {
|
|
991
|
+
if (!code || !organizations.length)
|
|
992
|
+
return null;
|
|
993
|
+
return (organizations.find(org => org.code === code || String(org.id) === code) ?? null);
|
|
994
|
+
}, [code, organizations]);
|
|
995
|
+
// Sync state: when the URL code points at a different org than the cookie,
|
|
996
|
+
// trust the URL and update the cookie. Otherwise do nothing — the user
|
|
997
|
+
// wins ties via the in-app org switcher.
|
|
998
|
+
React.useEffect(() => {
|
|
999
|
+
if (!code)
|
|
1000
|
+
return;
|
|
1001
|
+
if (!organizations.length)
|
|
1002
|
+
return;
|
|
1003
|
+
if (!orgFromCode)
|
|
1004
|
+
return;
|
|
1005
|
+
if (String(organizationId) !== String(orgFromCode.id)) {
|
|
1006
|
+
void setOrganizationId(orgFromCode.id);
|
|
1007
|
+
}
|
|
1008
|
+
}, [
|
|
1009
|
+
orgFromCode,
|
|
1010
|
+
organizationId,
|
|
1011
|
+
setOrganizationId,
|
|
1012
|
+
code,
|
|
1013
|
+
organizations.length,
|
|
1014
|
+
]);
|
|
1015
|
+
// ≤1024px: sidebar renders as a modal overlay (backdrop dims content). >1024px:
|
|
1016
|
+
// sidebar is docked and the content gets a left margin to clear it.
|
|
1017
|
+
const [isSidebarModalLayout, setIsSidebarModalLayout] = React__namespace.useState(false);
|
|
1018
|
+
React.useEffect(() => {
|
|
1019
|
+
const mq = window.matchMedia("(max-width: 1024px)");
|
|
1020
|
+
const sync = () => setIsSidebarModalLayout(mq.matches);
|
|
1021
|
+
sync();
|
|
1022
|
+
mq.addEventListener("change", sync);
|
|
1023
|
+
return () => mq.removeEventListener("change", sync);
|
|
1024
|
+
}, []);
|
|
1025
|
+
// Wait for session to load (initial load only). When refetching/updating we
|
|
1026
|
+
// already have a session — don't flash the full-screen loader.
|
|
1027
|
+
if (status === "loading" && !session) {
|
|
1028
|
+
return jsxRuntime.jsx(jsxRuntime.Fragment, { children: loadingComponent });
|
|
1029
|
+
}
|
|
1030
|
+
// Session loaded but no accessToken (e.g. token refresh failed) -> bounce
|
|
1031
|
+
// through signin so we don't end up at a "No Keycloak Context" error.
|
|
1032
|
+
if (status === "authenticated" && !extendedSession?.accessToken) {
|
|
1033
|
+
if (typeof window !== "undefined") {
|
|
1034
|
+
const url = new URL(signInUrl, window.location.origin);
|
|
1035
|
+
url.searchParams.set("error", "NoAccessToken");
|
|
1036
|
+
url.searchParams.set("callbackUrl", window.location.pathname);
|
|
1037
|
+
window.location.href = url.toString();
|
|
1038
|
+
}
|
|
1039
|
+
return jsxRuntime.jsx(jsxRuntime.Fragment, { children: loadingComponent });
|
|
1040
|
+
}
|
|
1041
|
+
if (!extendedSession?.accessToken) {
|
|
1042
|
+
return jsxRuntime.jsx(jsxRuntime.Fragment, { children: loadingComponent });
|
|
1043
|
+
}
|
|
1044
|
+
if (!code) {
|
|
1045
|
+
router.push(selectOrganizationUrl);
|
|
1046
|
+
return jsxRuntime.jsx(jsxRuntime.Fragment, { children: loadingComponent });
|
|
1047
|
+
}
|
|
1048
|
+
// When the sidebar is docked-open it consumes 21.5rem on the left; otherwise
|
|
1049
|
+
// the content takes the full width and we add a left gutter inside.
|
|
1050
|
+
const contentMargin = open && !isSidebarModalLayout ? "ml-[21.5rem]" : "";
|
|
1051
|
+
const needsLeftGutter = !(open && !isSidebarModalLayout);
|
|
1052
|
+
const wrap = permissionProvider ?? ((c) => c);
|
|
1053
|
+
return (jsxRuntime.jsxs("div", { className: "flex min-h-screen w-full bg-background", children: [open && (jsxRuntime.jsx("div", { className: "fixed left-0 top-0 z-50 h-screen", children: sidebar })), open && isSidebarModalLayout && (jsxRuntime.jsx("button", { type: "button", "aria-label": "\u0E1B\u0E34\u0E14\u0E40\u0E21\u0E19\u0E39", className: "fixed inset-0 z-40 bg-black/50", onClick: () => setOpen(false) })), jsxRuntime.jsx("div", { className: cx("relative flex min-w-0 flex-1 flex-col transition-all", contentMargin), children: jsxRuntime.jsxs("main", { className: "flex flex-1 flex-col min-h-screen gap-6", children: [jsxRuntime.jsxs("div", { className: "sticky top-0 z-30 shrink-0 w-full", children: [jsxRuntime.jsx(HeaderBackground, {}), jsxRuntime.jsxs("header", { className: "relative shrink-0 gap-2 py-4 h-auto min-h-[100px] sm:h-[144px] w-full z-10", children: [jsxRuntime.jsxs("div", { className: "flex w-full items-center gap-2 px-3 sm:px-4 lg:px-6", children: [jsxRuntime.jsx(SidebarTrigger, { className: "shrink-0 text-white hover:bg-white/10" }), jsxRuntime.jsx("div", { className: "flex-1 min-w-0", children: breadcrumb })] }), jsxRuntime.jsx("div", { id: "header-title" })] })] }), jsxRuntime.jsx(React.Suspense, { fallback: jsxRuntime.jsx("div", { className: "relative z-10 min-h-[calc(100vh-8rem)]", children: jsxRuntime.jsx("div", { className: "flex h-full items-center justify-center", children: jsxRuntime.jsxs("div", { className: "space-y-4 text-center", children: [jsxRuntime.jsx("div", { className: "mx-auto h-8 w-8 animate-spin rounded-full border-b-2 border-primary" }), jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: "\u0E01\u0E33\u0E25\u0E31\u0E07\u0E42\u0E2B\u0E25\u0E14..." })] }) }) }), children: jsxRuntime.jsx("div", { className: cx("relative z-10 pr-6 pb-12", needsLeftGutter && "pl-6"), children: wrap(children) }, `org-${organizationId ?? "no-org"}`) })] }) })] }));
|
|
1054
|
+
}
|
|
1055
|
+
/** Local tiny classnames helper — avoids pulling clsx/tailwind-merge into
|
|
1056
|
+
* the shell just for the layout file. Falsy values are skipped. */
|
|
1057
|
+
function cx(...parts) {
|
|
1058
|
+
return parts.filter(Boolean).join(" ");
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
const shellClassName = "flex min-w-0 flex-1 flex-col gap-6 overflow-x-hidden";
|
|
1062
|
+
/**
|
|
1063
|
+
* Shared layout for “กำลังตรวจสอบสิทธิ์” (skeleton) and “ไม่มีสิทธิ์” (message).
|
|
1064
|
+
* Renders `children` only when policy is loaded and access is granted.
|
|
1065
|
+
*/
|
|
1066
|
+
function PermissionGateShell({ policyLoading, hasPolicyAccess, deniedMessage, children, skeletonCount = 6, skeletonGridClassName = "grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3", skeletonVariant = "kpi", }) {
|
|
1067
|
+
if (policyLoading) {
|
|
1068
|
+
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))) }) }));
|
|
1069
|
+
}
|
|
1070
|
+
if (!hasPolicyAccess) {
|
|
1071
|
+
return (jsxRuntime.jsx("div", { className: shellClassName, children: jsxRuntime.jsx(erpTheme.Card, { children: jsxRuntime.jsx(erpTheme.CardContent, { className: "text-sm text-muted-foreground", children: deniedMessage }) }) }));
|
|
1072
|
+
}
|
|
1073
|
+
return jsxRuntime.jsx(jsxRuntime.Fragment, { children: children });
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
const PermissionRulesContext = React.createContext(undefined);
|
|
1077
|
+
/**
|
|
1078
|
+
* Generic provider — the consumer app passes in `data` (live fetch) and
|
|
1079
|
+
* `fallbackPolicies` (static JSON). All apps share the same mapping logic
|
|
1080
|
+
* (build a flat map of route_key/policy_key → required child groups).
|
|
1081
|
+
*/
|
|
1082
|
+
function PermissionRulesProvider({ children, data, isLoading = false, isError = false, fallbackPolicies = [], }) {
|
|
1083
|
+
const rulesMap = React.useMemo(() => {
|
|
1084
|
+
const m = {};
|
|
1085
|
+
if (!data?.routes)
|
|
1086
|
+
return m;
|
|
1087
|
+
for (const row of data.routes) {
|
|
1088
|
+
m[row.route_key] = row.required_child_groups ?? [];
|
|
1089
|
+
}
|
|
1090
|
+
return m;
|
|
1091
|
+
}, [data]);
|
|
1092
|
+
const policiesMap = React.useMemo(() => {
|
|
1093
|
+
const m = {};
|
|
1094
|
+
for (const row of fallbackPolicies) {
|
|
1095
|
+
m[row.policy_key] = row.required_child_groups ?? [];
|
|
1096
|
+
}
|
|
1097
|
+
if (data?.policies?.length) {
|
|
1098
|
+
for (const row of data.policies) {
|
|
1099
|
+
m[row.policy_key] = row.required_child_groups ?? [];
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
return m;
|
|
1103
|
+
}, [data, fallbackPolicies]);
|
|
1104
|
+
const value = React.useMemo(() => ({
|
|
1105
|
+
rulesMap,
|
|
1106
|
+
policiesMap,
|
|
1107
|
+
isLoading,
|
|
1108
|
+
isError,
|
|
1109
|
+
updatedAt: data?.updated_at ?? null,
|
|
1110
|
+
}), [rulesMap, policiesMap, isLoading, isError, data?.updated_at]);
|
|
1111
|
+
return (jsxRuntime.jsx(PermissionRulesContext.Provider, { value: value, children: children }));
|
|
1112
|
+
}
|
|
1113
|
+
/**
|
|
1114
|
+
* Read the permission rules. Returns empty maps when used outside the
|
|
1115
|
+
* provider (lets pages render without a hard-fail in tests).
|
|
1116
|
+
*/
|
|
1117
|
+
function usePermissionRules() {
|
|
1118
|
+
const ctx = React.useContext(PermissionRulesContext);
|
|
1119
|
+
if (!ctx) {
|
|
1120
|
+
return {
|
|
1121
|
+
rulesMap: {},
|
|
1122
|
+
policiesMap: {},
|
|
1123
|
+
isLoading: false,
|
|
1124
|
+
isError: false,
|
|
1125
|
+
updatedAt: null,
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
return ctx;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
/**
|
|
1132
|
+
* Strip the org `[code]` prefix from a pathname so we can look it up in the
|
|
1133
|
+
* permission rules map. `/O12345/setting/foo` → `/setting/foo`. Single-segment
|
|
1134
|
+
* paths (e.g. `/select-organization`) keep their leading slash.
|
|
1135
|
+
*
|
|
1136
|
+
* Also normalizes the user-role subtab aliases — both `purchase-doc` and
|
|
1137
|
+
* `sales-doc` are gated by the same `members` rule because the UI shows
|
|
1138
|
+
* the same management screen with different filters.
|
|
1139
|
+
*/
|
|
1140
|
+
function normalizePathnameForPermissionRules(pathname) {
|
|
1141
|
+
const parts = pathname.split("/").filter(Boolean);
|
|
1142
|
+
const path = parts.length <= 1 ? "/" : "/" + parts.slice(1).join("/");
|
|
1143
|
+
return path.replace(/\/user-role\/(purchase-doc|sales-doc)$/, "/user-role/members");
|
|
1144
|
+
}
|
|
1145
|
+
/**
|
|
1146
|
+
* Returns the required child-group segments for a page pathname (the user's
|
|
1147
|
+
* JWT must contain at least one matching group). An exact match on the
|
|
1148
|
+
* normalized path wins; otherwise the function falls back to a segment-by-
|
|
1149
|
+
* segment pattern match where `[param]` placeholders accept any non-empty
|
|
1150
|
+
* value. Returns an empty array when no rule covers the path.
|
|
1151
|
+
*
|
|
1152
|
+
* `rulesMap` is the merged map — apps that want a fallback should layer it
|
|
1153
|
+
* here (`{ ...fallbackMap, ...liveMap }`) before calling.
|
|
1154
|
+
*/
|
|
1155
|
+
function getRequiredChildGroups(pathname, rulesMap) {
|
|
1156
|
+
const normalized = normalizePathnameForPermissionRules(pathname);
|
|
1157
|
+
if (rulesMap[normalized]) {
|
|
1158
|
+
return rulesMap[normalized];
|
|
1159
|
+
}
|
|
1160
|
+
const pathSegments = normalized.split("/").filter(Boolean);
|
|
1161
|
+
for (const [pattern, roles] of Object.entries(rulesMap)) {
|
|
1162
|
+
const patternSegments = pattern.split("/").filter(Boolean);
|
|
1163
|
+
if (patternSegments.length !== pathSegments.length)
|
|
1164
|
+
continue;
|
|
1165
|
+
let matches = true;
|
|
1166
|
+
for (let i = 0; i < patternSegments.length; i++) {
|
|
1167
|
+
const patternSegment = patternSegments[i];
|
|
1168
|
+
const pathSegment = pathSegments[i];
|
|
1169
|
+
if (patternSegment.startsWith("[") && patternSegment.endsWith("]")) {
|
|
1170
|
+
if (!pathSegment) {
|
|
1171
|
+
matches = false;
|
|
1172
|
+
break;
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
else if (patternSegment !== pathSegment) {
|
|
1176
|
+
matches = false;
|
|
1177
|
+
break;
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
if (matches) {
|
|
1181
|
+
return roles;
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
return [];
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
/**
|
|
1188
|
+
* Permission gate for a page subtree. Looks up the current pathname in the
|
|
1189
|
+
* permission rules map (live rules from `PermissionRulesProvider`, layered
|
|
1190
|
+
* over the optional `fallbackRulesMap`), passes the resolved required roles
|
|
1191
|
+
* into the host's `usePermission` probe, and renders one of three states:
|
|
1192
|
+
*
|
|
1193
|
+
* 1. while rules or probe are still loading → centered spinner card
|
|
1194
|
+
* 2. probe says no access → centered "ไม่มีสิทธิ์" card with required vs.
|
|
1195
|
+
* user roles for debugging
|
|
1196
|
+
* 3. otherwise → children
|
|
1197
|
+
*
|
|
1198
|
+
* The host wraps `<PermissionProvider>` around its page chrome (typically
|
|
1199
|
+
* via `AppLayoutShell`'s `permissionProvider` slot).
|
|
1200
|
+
*/
|
|
1201
|
+
function PermissionProvider({ children, usePermission, fallbackRulesMap, }) {
|
|
1202
|
+
const pathname = navigation.usePathname();
|
|
1203
|
+
const { rulesMap, isLoading: rulesLoading } = usePermissionRules();
|
|
1204
|
+
const mergedMap = React.useMemo(() => {
|
|
1205
|
+
if (!fallbackRulesMap)
|
|
1206
|
+
return rulesMap;
|
|
1207
|
+
// live rules win over fallback when both define the same key
|
|
1208
|
+
return { ...fallbackRulesMap, ...rulesMap };
|
|
1209
|
+
}, [rulesMap, fallbackRulesMap]);
|
|
1210
|
+
const requiredRoles = React.useMemo(() => getRequiredChildGroups(pathname, mergedMap), [pathname, mergedMap]);
|
|
1211
|
+
// Hook must run unconditionally (rules of hooks); we just ignore the
|
|
1212
|
+
// result on the fast path below.
|
|
1213
|
+
const { hasPermission, isLoading, userRoles } = usePermission({
|
|
1214
|
+
requiredRoles,
|
|
1215
|
+
});
|
|
1216
|
+
// Fast path — no rule covers this pathname, anyone can see it. Skip the
|
|
1217
|
+
// probe UI entirely; flashing "checking permission" or "no access" on an
|
|
1218
|
+
// unrestricted page is pure noise.
|
|
1219
|
+
if (requiredRoles.length === 0) {
|
|
1220
|
+
return jsxRuntime.jsx(jsxRuntime.Fragment, { children: children });
|
|
1221
|
+
}
|
|
1222
|
+
// While the answer hasn't settled, render nothing and let the surrounding
|
|
1223
|
+
// Next.js loading.tsx / Suspense fallback show. The old behaviour wrapped
|
|
1224
|
+
// a big "กำลังตรวจสอบสิทธิ์..." card over the top, which both duplicated
|
|
1225
|
+
// the app's own loader and — worse — caused a "ไม่มีสิทธิ์" flash on
|
|
1226
|
+
// fast loads because the probe transiently returned `hasPermission:false,
|
|
1227
|
+
// isLoading:false` before its dependencies (org context, role-segments)
|
|
1228
|
+
// arrived.
|
|
1229
|
+
const isLoadingCombined = rulesLoading || isLoading;
|
|
1230
|
+
if (isLoadingCombined) {
|
|
1231
|
+
return null;
|
|
1232
|
+
}
|
|
1233
|
+
if (!hasPermission) {
|
|
1234
|
+
return (jsxRuntime.jsx(erpTheme.Card, { className: "p-6", children: jsxRuntime.jsxs("div", { className: "flex flex-col items-center justify-center gap-3 text-center", children: [jsxRuntime.jsx(lucideReact.AlertCircle, { className: "h-8 w-8 text-destructive" }), jsxRuntime.jsxs("div", { className: "space-y-1", children: [jsxRuntime.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" }), jsxRuntime.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" }), requiredRoles.length > 0 && (jsxRuntime.jsxs("div", { className: "mt-3 space-y-1", children: [jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground", children: "\u0E2A\u0E34\u0E17\u0E18\u0E34\u0E4C\u0E17\u0E35\u0E48\u0E15\u0E49\u0E2D\u0E07\u0E01\u0E32\u0E23:" }), jsxRuntime.jsx("div", { className: "flex flex-wrap gap-1 justify-center", children: requiredRoles.map((role, idx) => (jsxRuntime.jsx("span", { className: "rounded bg-muted px-2 py-0.5 text-xs font-mono", children: role }, idx))) })] })), userRoles.length > 0 && (jsxRuntime.jsxs("div", { className: "mt-3 space-y-1", children: [jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground", children: "\u0E2A\u0E34\u0E17\u0E18\u0E34\u0E4C\u0E02\u0E2D\u0E07\u0E04\u0E38\u0E13:" }), jsxRuntime.jsx("div", { className: "flex flex-wrap gap-1 justify-center", children: userRoles.map((role, idx) => (jsxRuntime.jsx("span", { className: "rounded bg-primary/10 px-2 py-0.5 text-xs font-mono text-primary", children: role }, idx))) })] }))] })] }) }));
|
|
1235
|
+
}
|
|
1236
|
+
return jsxRuntime.jsx(jsxRuntime.Fragment, { children: children });
|
|
1237
|
+
}
|
|
1238
|
+
|
|
969
1239
|
function ThemeProvider({ children, ...props }) {
|
|
970
1240
|
return jsxRuntime.jsx(nextThemes.ThemeProvider, { ...props, children: children });
|
|
971
1241
|
}
|
|
@@ -1067,61 +1337,6 @@ function ReactQueryProvider({ children }) {
|
|
|
1067
1337
|
return (jsxRuntime.jsxs(reactQuery.QueryClientProvider, { client: queryClient, children: [children, jsxRuntime.jsx(reactQueryDevtools.ReactQueryDevtools, { client: queryClient, initialIsOpen: false })] }));
|
|
1068
1338
|
}
|
|
1069
1339
|
|
|
1070
|
-
const PermissionRulesContext = React.createContext(undefined);
|
|
1071
|
-
/**
|
|
1072
|
-
* Generic provider — the consumer app passes in `data` (live fetch) and
|
|
1073
|
-
* `fallbackPolicies` (static JSON). All apps share the same mapping logic
|
|
1074
|
-
* (build a flat map of route_key/policy_key → required child groups).
|
|
1075
|
-
*/
|
|
1076
|
-
function PermissionRulesProvider({ children, data, isLoading = false, isError = false, fallbackPolicies = [], }) {
|
|
1077
|
-
const rulesMap = React.useMemo(() => {
|
|
1078
|
-
const m = {};
|
|
1079
|
-
if (!data?.routes)
|
|
1080
|
-
return m;
|
|
1081
|
-
for (const row of data.routes) {
|
|
1082
|
-
m[row.route_key] = row.required_child_groups ?? [];
|
|
1083
|
-
}
|
|
1084
|
-
return m;
|
|
1085
|
-
}, [data]);
|
|
1086
|
-
const policiesMap = React.useMemo(() => {
|
|
1087
|
-
const m = {};
|
|
1088
|
-
for (const row of fallbackPolicies) {
|
|
1089
|
-
m[row.policy_key] = row.required_child_groups ?? [];
|
|
1090
|
-
}
|
|
1091
|
-
if (data?.policies?.length) {
|
|
1092
|
-
for (const row of data.policies) {
|
|
1093
|
-
m[row.policy_key] = row.required_child_groups ?? [];
|
|
1094
|
-
}
|
|
1095
|
-
}
|
|
1096
|
-
return m;
|
|
1097
|
-
}, [data, fallbackPolicies]);
|
|
1098
|
-
const value = React.useMemo(() => ({
|
|
1099
|
-
rulesMap,
|
|
1100
|
-
policiesMap,
|
|
1101
|
-
isLoading,
|
|
1102
|
-
isError,
|
|
1103
|
-
updatedAt: data?.updated_at ?? null,
|
|
1104
|
-
}), [rulesMap, policiesMap, isLoading, isError, data?.updated_at]);
|
|
1105
|
-
return (jsxRuntime.jsx(PermissionRulesContext.Provider, { value: value, children: children }));
|
|
1106
|
-
}
|
|
1107
|
-
/**
|
|
1108
|
-
* Read the permission rules. Returns empty maps when used outside the
|
|
1109
|
-
* provider (lets pages render without a hard-fail in tests).
|
|
1110
|
-
*/
|
|
1111
|
-
function usePermissionRules() {
|
|
1112
|
-
const ctx = React.useContext(PermissionRulesContext);
|
|
1113
|
-
if (!ctx) {
|
|
1114
|
-
return {
|
|
1115
|
-
rulesMap: {},
|
|
1116
|
-
policiesMap: {},
|
|
1117
|
-
isLoading: false,
|
|
1118
|
-
isError: false,
|
|
1119
|
-
updatedAt: null,
|
|
1120
|
-
};
|
|
1121
|
-
}
|
|
1122
|
-
return ctx;
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
1340
|
/**
|
|
1126
1341
|
* Renders nothing. Runs during render (after SessionProvider, inside
|
|
1127
1342
|
* QueryClientProvider) to seed the cached profile slot from the JWT-backed
|
|
@@ -1436,6 +1651,7 @@ Object.defineProperty(exports, "isAxiosError", {
|
|
|
1436
1651
|
});
|
|
1437
1652
|
exports.ApiError = ApiError;
|
|
1438
1653
|
exports.AppBreadcrumb = AppBreadcrumb;
|
|
1654
|
+
exports.AppLayoutShell = AppLayoutShell;
|
|
1439
1655
|
exports.AppSidebar = AppSidebar;
|
|
1440
1656
|
exports.FeatureFlagsProvider = FeatureFlagsProvider;
|
|
1441
1657
|
exports.HeaderBackground = HeaderBackground;
|
|
@@ -1443,6 +1659,8 @@ exports.NavMain = NavMain;
|
|
|
1443
1659
|
exports.NavTools = NavTools;
|
|
1444
1660
|
exports.NavUser = NavUser;
|
|
1445
1661
|
exports.OrganizationSwitcher = OrganizationSwitcher;
|
|
1662
|
+
exports.PermissionGateShell = PermissionGateShell;
|
|
1663
|
+
exports.PermissionProvider = PermissionProvider;
|
|
1446
1664
|
exports.PermissionRulesProvider = PermissionRulesProvider;
|
|
1447
1665
|
exports.ReactQueryProvider = ReactQueryProvider;
|
|
1448
1666
|
exports.SessionBackedProfileCacheSeeder = SessionBackedProfileCacheSeeder;
|
|
@@ -1468,8 +1686,10 @@ exports.getActiveSubModule = getActiveSubModule;
|
|
|
1468
1686
|
exports.getAxiosErrorMessage = getAxiosErrorMessage;
|
|
1469
1687
|
exports.getOrganizationId = getOrganizationId;
|
|
1470
1688
|
exports.getQueryClientConfig = getQueryClientConfig;
|
|
1689
|
+
exports.getRequiredChildGroups = getRequiredChildGroups;
|
|
1471
1690
|
exports.isLikelyTransientNetworkError = isLikelyTransientNetworkError;
|
|
1472
1691
|
exports.isPathActive = isPathActive;
|
|
1692
|
+
exports.normalizePathnameForPermissionRules = normalizePathnameForPermissionRules;
|
|
1473
1693
|
exports.runGlobalRequestPrecheck = runGlobalRequestPrecheck;
|
|
1474
1694
|
exports.runWithTransientRetry = runWithTransientRetry;
|
|
1475
1695
|
exports.setOrganizationId = setOrganizationId;
|