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": {
|
"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
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,
|
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";
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user