add permission
This commit is contained in:
parent
0a65628ec3
commit
6d1006f784
1772
dist/index.js
vendored
1772
dist/index.js
vendored
File diff suppressed because it is too large
Load Diff
@ -12,11 +12,13 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@chakra-ui/react": "^3.0.0",
|
||||
"@tanstack/react-query": "^5.0.0",
|
||||
"react": "^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^18.0.0 || ^19.0.0",
|
||||
"react-icons": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tanstack/react-query": "^5.0.0",
|
||||
"@vitejs/plugin-react": "4.3.4",
|
||||
"vite": "5.4.19"
|
||||
},
|
||||
|
||||
240
src/core/permission.jsx
Normal file
240
src/core/permission.jsx
Normal 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;
|
||||
};
|
||||
16
src/index.js
16
src/index.js
@ -14,3 +14,19 @@ export {
|
||||
logoutKeycloak,
|
||||
loginKeycloak,
|
||||
} from "./core/keycloak.jsx";
|
||||
|
||||
export {
|
||||
PermissionProvider,
|
||||
usePermission,
|
||||
usePermissionsQuery,
|
||||
Can,
|
||||
AdminGuard,
|
||||
PanelAccessGuard,
|
||||
getCachedPermissions,
|
||||
setCachedPermissions,
|
||||
clearCachedPermissions,
|
||||
createPermissionSet,
|
||||
checkPermission,
|
||||
permissionKeys,
|
||||
getPermissions,
|
||||
} from "./core/permission.jsx";
|
||||
|
||||
@ -16,6 +16,8 @@ export default defineConfig({
|
||||
"react-dom",
|
||||
"react/jsx-runtime",
|
||||
"@chakra-ui/react",
|
||||
"keycloak-js",
|
||||
"@tanstack/react-query",
|
||||
"react-icons",
|
||||
"react-icons/fa",
|
||||
"react-icons/fa6",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user