add permission

This commit is contained in:
Mohamadzadeh 2026-06-13 11:01:16 +03:30
parent 0a65628ec3
commit 6d1006f784
5 changed files with 650 additions and 1382 deletions

1772
dist/index.js vendored

File diff suppressed because it is too large Load Diff

View File

@ -12,11 +12,13 @@
}, },
"peerDependencies": { "peerDependencies": {
"@chakra-ui/react": "^3.0.0", "@chakra-ui/react": "^3.0.0",
"@tanstack/react-query": "^5.0.0",
"react": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0",
"react-icons": "^5.0.0" "react-icons": "^5.0.0"
}, },
"devDependencies": { "devDependencies": {
"@tanstack/react-query": "^5.0.0",
"@vitejs/plugin-react": "4.3.4", "@vitejs/plugin-react": "4.3.4",
"vite": "5.4.19" "vite": "5.4.19"
}, },

240
src/core/permission.jsx Normal file
View File

@ -0,0 +1,240 @@
import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from "react";
import { useQuery } from "@tanstack/react-query";
import {
getKeycloakToken,
isKeycloakAuthenticated,
logoutKeycloak,
} from "./keycloak.jsx";
// contexts
const PermissionContext = createContext({
user: null,
can: () => false,
cannot: () => true,
permissions: undefined,
isLoading: false,
error: null,
});
// providers
export const PermissionProvider = ({
user = null,
children,
clientId,
permissionsUrl,
storageKey = "permissions",
loading = <div>Loading permissions...</div>,
errorFallback = <div>Error loading permissions</div>,
}) => {
const [cachedPermissions, setCachedPermissionsState] = useState(() =>
getCachedPermissions(storageKey),
);
const { data, isLoading, error } = usePermissionsQuery({
clientId,
permissionsUrl,
});
const permissions = data ?? cachedPermissions;
useEffect(() => {
if (data) {
setCachedPermissions(storageKey, data);
setCachedPermissionsState(data);
}
}, [data, storageKey]);
const permissionSet = useMemo(
() => createPermissionSet(permissions),
[permissions],
);
const can = useCallback(
(perm = "") => checkPermission(permissionSet, perm),
[permissionSet],
);
const cannot = useCallback((perm = "") => !can(perm), [can]);
const value = useMemo(
() => ({
user,
can,
cannot,
permissions,
isLoading,
error,
}),
[user, can, cannot, permissions, isLoading, error],
);
if (error && !permissions) {
return errorFallback;
}
if (isLoading && !permissions) {
return loading;
}
return (
<PermissionContext.Provider value={value}>
{children}
</PermissionContext.Provider>
);
};
// storages
export const getCachedPermissions = (storageKey = "permissions") => {
if (typeof window === "undefined") return undefined;
try {
const localPermissions = localStorage.getItem(storageKey);
return localPermissions ? JSON.parse(localPermissions) : undefined;
} catch {
return undefined;
}
};
export const setCachedPermissions = (
storageKey = "permissions",
permissions,
) => {
if (typeof window === "undefined") return;
try {
localStorage.setItem(storageKey, JSON.stringify(permissions));
} catch {}
};
export const clearCachedPermissions = (storageKey = "permissions") => {
if (typeof window === "undefined") return;
try {
localStorage.removeItem(storageKey);
} catch {}
};
// utils
export const createPermissionSet = (permissions) => {
if (!permissions) return new Set();
return new Set(permissions.map((permission) => permission.name));
};
export const checkPermission = (permissionSet, perm = "") => {
if (!perm) return false;
if (permissionSet.has(perm)) return true;
if ([...permissionSet].some((x) => x.startsWith(perm))) return true;
let current = perm;
while (current.includes(":")) {
current = current.slice(0, current.lastIndexOf(":"));
if (permissionSet.has(`${current}:*`)) return true;
}
return false;
};
// keys
export const permissionKeys = {
all: ["permissions"],
list: (clientId) => [...permissionKeys.all, clientId],
};
// apis
export const getPermissions = async ({ permissionsUrl, clientId }) => {
const token = getKeycloakToken();
const url = new URL(permissionsUrl);
url.searchParams.set("client_id", clientId);
const response = await fetch(url.toString(), {
method: "GET",
headers: {
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
});
if (!response.ok) {
throw new Error("Failed to load permissions");
}
return response.json();
};
// hooks
export const usePermission = () => {
const context = useContext(PermissionContext);
if (!context) {
throw new Error("usePermission must be used within PermissionProvider");
}
return context;
};
export const usePermissionsQuery = ({ clientId, permissionsUrl }) => {
return useQuery({
queryKey: permissionKeys.list(clientId),
queryFn: () => getPermissions({ clientId, permissionsUrl }),
enabled:
Boolean(clientId) &&
Boolean(permissionsUrl) &&
isKeycloakAuthenticated(),
staleTime: Infinity,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
});
};
// components
export const Can = ({
perm,
children,
fallback = <div>You are not allowed to do this</div>,
}) => {
const { can } = usePermission();
return can(perm) ? children : fallback;
};
export const AdminGuard = ({ children }) => {
const { can, permissions } = usePermission();
if (!permissions) return null;
return can("admin") ? children : null;
};
export const PanelAccessGuard = ({ children }) => {
const { can, isLoading } = usePermission();
useEffect(() => {
if (isLoading) return;
if (!can("panel_access")) {
logoutKeycloak();
}
}, [isLoading, can]);
return children;
};

View File

@ -14,3 +14,19 @@ export {
logoutKeycloak, logoutKeycloak,
loginKeycloak, loginKeycloak,
} from "./core/keycloak.jsx"; } from "./core/keycloak.jsx";
export {
PermissionProvider,
usePermission,
usePermissionsQuery,
Can,
AdminGuard,
PanelAccessGuard,
getCachedPermissions,
setCachedPermissions,
clearCachedPermissions,
createPermissionSet,
checkPermission,
permissionKeys,
getPermissions,
} from "./core/permission.jsx";

View File

@ -16,6 +16,8 @@ export default defineConfig({
"react-dom", "react-dom",
"react/jsx-runtime", "react/jsx-runtime",
"@chakra-ui/react", "@chakra-ui/react",
"keycloak-js",
"@tanstack/react-query",
"react-icons", "react-icons",
"react-icons/fa", "react-icons/fa",
"react-icons/fa6", "react-icons/fa6",