preact-missing-hooks 4.4.0 → 4.6.0
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/Readme.md +144 -17
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.modern.mjs +1 -1
- package/dist/index.modern.mjs.map +1 -1
- package/dist/index.module.js +1 -1
- package/dist/index.module.js.map +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/index.umd.js.map +1 -1
- package/dist/react.js +310 -0
- package/dist/usePrefetch.d.ts +44 -0
- package/dist/useRBAC.d.ts +102 -0
- package/docs/README.md +107 -0
- package/docs/index.html +50 -0
- package/docs/main.js +132 -0
- package/llm.package.json +105 -216
- package/llm.package.txt +29 -47
- package/package.json +13 -2
- package/src/index.ts +2 -0
- package/src/usePrefetch.ts +92 -0
- package/src/useRBAC.ts +380 -0
- package/tests/usePrefetch.test.tsx +107 -0
- package/tests/useRBAC.test.tsx +212 -0
package/dist/react.js
CHANGED
|
@@ -1776,6 +1776,314 @@ function useRefPrint(printRef, options = {}) {
|
|
|
1776
1776
|
return { print };
|
|
1777
1777
|
}
|
|
1778
1778
|
|
|
1779
|
+
const WILDCARD = "*";
|
|
1780
|
+
function parseUserFromStorage(type, key) {
|
|
1781
|
+
if (typeof window === "undefined")
|
|
1782
|
+
return null;
|
|
1783
|
+
const storage = type === "localStorage" ? window.localStorage : window.sessionStorage;
|
|
1784
|
+
try {
|
|
1785
|
+
const raw = storage.getItem(key);
|
|
1786
|
+
if (raw == null)
|
|
1787
|
+
return null;
|
|
1788
|
+
const data = JSON.parse(raw);
|
|
1789
|
+
return data && typeof data === "object" ? data : null;
|
|
1790
|
+
}
|
|
1791
|
+
catch (_a) {
|
|
1792
|
+
return null;
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
function parseCapabilitiesFromStorage(type, key) {
|
|
1796
|
+
if (typeof window === "undefined")
|
|
1797
|
+
return [];
|
|
1798
|
+
const storage = type === "localStorage" ? window.localStorage : window.sessionStorage;
|
|
1799
|
+
try {
|
|
1800
|
+
const raw = storage.getItem(key);
|
|
1801
|
+
if (raw == null)
|
|
1802
|
+
return [];
|
|
1803
|
+
const data = JSON.parse(raw);
|
|
1804
|
+
return Array.isArray(data)
|
|
1805
|
+
? data.filter((x) => typeof x === "string")
|
|
1806
|
+
: [];
|
|
1807
|
+
}
|
|
1808
|
+
catch (_a) {
|
|
1809
|
+
return [];
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
function computeRoles(user, roleDefinitions) {
|
|
1813
|
+
return roleDefinitions
|
|
1814
|
+
.filter((def) => def.condition(user))
|
|
1815
|
+
.map((def) => def.role);
|
|
1816
|
+
}
|
|
1817
|
+
function computeCapabilitiesFromRoles(roles, roleCapabilities) {
|
|
1818
|
+
const set = new Set();
|
|
1819
|
+
for (const role of roles) {
|
|
1820
|
+
const caps = roleCapabilities[role];
|
|
1821
|
+
if (Array.isArray(caps)) {
|
|
1822
|
+
for (const c of caps)
|
|
1823
|
+
set.add(c);
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
return Array.from(set);
|
|
1827
|
+
}
|
|
1828
|
+
function hasCapabilityImpl(capabilities, capability) {
|
|
1829
|
+
if (capabilities.includes(WILDCARD))
|
|
1830
|
+
return true;
|
|
1831
|
+
return capabilities.includes(capability);
|
|
1832
|
+
}
|
|
1833
|
+
/**
|
|
1834
|
+
* Frontend-only role-based access control hook. Define roles with conditions,
|
|
1835
|
+
* assign capabilities per role, and plug in user source (localStorage, sessionStorage, API, or custom).
|
|
1836
|
+
* Supports full flexibility: frontend-only with storage or pluggable API.
|
|
1837
|
+
*
|
|
1838
|
+
* @param options - userSource, roleDefinitions, roleCapabilities, optional capabilitiesOverride
|
|
1839
|
+
* @returns user, roles, capabilities, hasRole, hasCapability, can, isReady, error, refetch, and storage helpers
|
|
1840
|
+
*
|
|
1841
|
+
* @example
|
|
1842
|
+
* ```tsx
|
|
1843
|
+
* const { hasRole, can, roles, setUserInStorage } = useRBAC({
|
|
1844
|
+
* userSource: { type: 'localStorage', key: 'user' },
|
|
1845
|
+
* roleDefinitions: [
|
|
1846
|
+
* { role: 'admin', condition: (u) => u?.role === 'admin' },
|
|
1847
|
+
* { role: 'editor', condition: (u) => u?.role === 'editor' || u?.role === 'admin' },
|
|
1848
|
+
* ],
|
|
1849
|
+
* roleCapabilities: {
|
|
1850
|
+
* admin: ['*'],
|
|
1851
|
+
* editor: ['posts:edit', 'posts:create'],
|
|
1852
|
+
* },
|
|
1853
|
+
* });
|
|
1854
|
+
* if (can('posts:edit')) { ... }
|
|
1855
|
+
* ```
|
|
1856
|
+
*/
|
|
1857
|
+
function useRBAC(options) {
|
|
1858
|
+
const { userSource } = options;
|
|
1859
|
+
const optsRef = react.useRef(options);
|
|
1860
|
+
optsRef.current = options;
|
|
1861
|
+
const [user, setUser] = react.useState(null);
|
|
1862
|
+
const [roles, setRoles] = react.useState([]);
|
|
1863
|
+
const [capabilities, setCapabilities] = react.useState([]);
|
|
1864
|
+
const [isReady, setIsReady] = react.useState(false);
|
|
1865
|
+
const [error, setError] = react.useState(null);
|
|
1866
|
+
const resolveAuth = react.useCallback(() => __awaiter(this, void 0, void 0, function* () {
|
|
1867
|
+
var _a, _b, _c;
|
|
1868
|
+
const { userSource: us, roleDefinitions: rdefs, roleCapabilities: rcaps, capabilitiesOverride: capOver, } = optsRef.current;
|
|
1869
|
+
setError(null);
|
|
1870
|
+
let resolvedUser = null;
|
|
1871
|
+
let resolvedRoles = [];
|
|
1872
|
+
let resolvedCapabilities = [];
|
|
1873
|
+
try {
|
|
1874
|
+
if (us.type === "localStorage") {
|
|
1875
|
+
resolvedUser = parseUserFromStorage("localStorage", us.key);
|
|
1876
|
+
resolvedRoles = computeRoles(resolvedUser, rdefs);
|
|
1877
|
+
resolvedCapabilities = computeCapabilitiesFromRoles(resolvedRoles, rcaps);
|
|
1878
|
+
}
|
|
1879
|
+
else if (us.type === "sessionStorage") {
|
|
1880
|
+
resolvedUser = parseUserFromStorage("sessionStorage", us.key);
|
|
1881
|
+
resolvedRoles = computeRoles(resolvedUser, rdefs);
|
|
1882
|
+
resolvedCapabilities = computeCapabilitiesFromRoles(resolvedRoles, rcaps);
|
|
1883
|
+
}
|
|
1884
|
+
else if (us.type === "api") {
|
|
1885
|
+
resolvedUser = yield us.fetch();
|
|
1886
|
+
resolvedRoles = computeRoles(resolvedUser, rdefs);
|
|
1887
|
+
resolvedCapabilities = computeCapabilitiesFromRoles(resolvedRoles, rcaps);
|
|
1888
|
+
}
|
|
1889
|
+
else if (us.type === "memory") {
|
|
1890
|
+
resolvedUser = us.getUser();
|
|
1891
|
+
resolvedRoles = computeRoles(resolvedUser, rdefs);
|
|
1892
|
+
resolvedCapabilities = computeCapabilitiesFromRoles(resolvedRoles, rcaps);
|
|
1893
|
+
}
|
|
1894
|
+
else if (us.type === "custom") {
|
|
1895
|
+
const auth = yield Promise.resolve(us.getAuth());
|
|
1896
|
+
resolvedUser = (_a = auth.user) !== null && _a !== void 0 ? _a : null;
|
|
1897
|
+
resolvedRoles = (_b = auth.roles) !== null && _b !== void 0 ? _b : computeRoles(resolvedUser, rdefs);
|
|
1898
|
+
resolvedCapabilities =
|
|
1899
|
+
(_c = auth.capabilities) !== null && _c !== void 0 ? _c : computeCapabilitiesFromRoles(resolvedRoles, rcaps);
|
|
1900
|
+
}
|
|
1901
|
+
if (capOver) {
|
|
1902
|
+
if (capOver.type === "localStorage") {
|
|
1903
|
+
const override = parseCapabilitiesFromStorage("localStorage", capOver.key);
|
|
1904
|
+
if (override.length > 0)
|
|
1905
|
+
resolvedCapabilities = override;
|
|
1906
|
+
}
|
|
1907
|
+
else if (capOver.type === "sessionStorage") {
|
|
1908
|
+
const override = parseCapabilitiesFromStorage("sessionStorage", capOver.key);
|
|
1909
|
+
if (override.length > 0)
|
|
1910
|
+
resolvedCapabilities = override;
|
|
1911
|
+
}
|
|
1912
|
+
else if (capOver.type === "api") {
|
|
1913
|
+
const override = yield capOver.fetch();
|
|
1914
|
+
resolvedCapabilities = override;
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
setUser(resolvedUser);
|
|
1918
|
+
setRoles(resolvedRoles);
|
|
1919
|
+
setCapabilities(resolvedCapabilities);
|
|
1920
|
+
}
|
|
1921
|
+
catch (e) {
|
|
1922
|
+
setError(e instanceof Error ? e : new Error(String(e)));
|
|
1923
|
+
setUser(null);
|
|
1924
|
+
setRoles([]);
|
|
1925
|
+
setCapabilities([]);
|
|
1926
|
+
}
|
|
1927
|
+
finally {
|
|
1928
|
+
setIsReady(true);
|
|
1929
|
+
}
|
|
1930
|
+
}), []);
|
|
1931
|
+
react.useEffect(() => {
|
|
1932
|
+
resolveAuth();
|
|
1933
|
+
}, [resolveAuth]);
|
|
1934
|
+
// Listen to storage events when using localStorage/sessionStorage so we refetch when another tab changes data
|
|
1935
|
+
react.useEffect(() => {
|
|
1936
|
+
if (typeof window === "undefined")
|
|
1937
|
+
return;
|
|
1938
|
+
const key = userSource.type === "localStorage" || userSource.type === "sessionStorage"
|
|
1939
|
+
? userSource.key
|
|
1940
|
+
: null;
|
|
1941
|
+
if (!key)
|
|
1942
|
+
return;
|
|
1943
|
+
const handler = (e) => {
|
|
1944
|
+
if (e.key === key)
|
|
1945
|
+
void resolveAuth();
|
|
1946
|
+
};
|
|
1947
|
+
window.addEventListener("storage", handler);
|
|
1948
|
+
return () => window.removeEventListener("storage", handler);
|
|
1949
|
+
}, [
|
|
1950
|
+
userSource.type,
|
|
1951
|
+
userSource.type === "localStorage" || userSource.type === "sessionStorage"
|
|
1952
|
+
? userSource.key
|
|
1953
|
+
: "",
|
|
1954
|
+
resolveAuth,
|
|
1955
|
+
]);
|
|
1956
|
+
const hasRole = react.useCallback((role) => roles.includes(role), [roles]);
|
|
1957
|
+
const hasCapability = react.useCallback((capability) => hasCapabilityImpl(capabilities, capability), [capabilities]);
|
|
1958
|
+
const can = hasCapability;
|
|
1959
|
+
const refetch = react.useCallback(() => resolveAuth(), [resolveAuth]);
|
|
1960
|
+
const setUserInStorage = react.useCallback((newUser, storage, key) => {
|
|
1961
|
+
if (typeof window === "undefined")
|
|
1962
|
+
return;
|
|
1963
|
+
const s = storage === "localStorage"
|
|
1964
|
+
? window.localStorage
|
|
1965
|
+
: window.sessionStorage;
|
|
1966
|
+
if (newUser == null)
|
|
1967
|
+
s.removeItem(key);
|
|
1968
|
+
else
|
|
1969
|
+
s.setItem(key, JSON.stringify(newUser));
|
|
1970
|
+
if ((userSource.type === "localStorage" &&
|
|
1971
|
+
storage === "localStorage" &&
|
|
1972
|
+
userSource.key === key) ||
|
|
1973
|
+
(userSource.type === "sessionStorage" &&
|
|
1974
|
+
storage === "sessionStorage" &&
|
|
1975
|
+
userSource.key === key)) {
|
|
1976
|
+
void resolveAuth();
|
|
1977
|
+
}
|
|
1978
|
+
}, [userSource, resolveAuth]);
|
|
1979
|
+
const setRolesInStorage = react.useCallback((newRoles, storage, key) => {
|
|
1980
|
+
if (typeof window === "undefined")
|
|
1981
|
+
return;
|
|
1982
|
+
const s = storage === "localStorage"
|
|
1983
|
+
? window.localStorage
|
|
1984
|
+
: window.sessionStorage;
|
|
1985
|
+
s.setItem(key, JSON.stringify(newRoles));
|
|
1986
|
+
}, []);
|
|
1987
|
+
const setCapabilitiesInStorage = react.useCallback((newCaps, storage, key) => {
|
|
1988
|
+
if (typeof window === "undefined")
|
|
1989
|
+
return;
|
|
1990
|
+
const s = storage === "localStorage"
|
|
1991
|
+
? window.localStorage
|
|
1992
|
+
: window.sessionStorage;
|
|
1993
|
+
s.setItem(key, JSON.stringify(newCaps));
|
|
1994
|
+
}, []);
|
|
1995
|
+
return {
|
|
1996
|
+
user,
|
|
1997
|
+
roles,
|
|
1998
|
+
capabilities,
|
|
1999
|
+
isReady,
|
|
2000
|
+
error,
|
|
2001
|
+
hasRole,
|
|
2002
|
+
hasCapability,
|
|
2003
|
+
can,
|
|
2004
|
+
refetch,
|
|
2005
|
+
setUserInStorage,
|
|
2006
|
+
setRolesInStorage,
|
|
2007
|
+
setCapabilitiesInStorage,
|
|
2008
|
+
};
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
function prefetchDocument(url) {
|
|
2012
|
+
if (typeof document === "undefined")
|
|
2013
|
+
return;
|
|
2014
|
+
const links = document.querySelectorAll('link[rel="prefetch"]');
|
|
2015
|
+
for (let i = 0; i < links.length; i++) {
|
|
2016
|
+
if (links[i].href === url)
|
|
2017
|
+
return;
|
|
2018
|
+
}
|
|
2019
|
+
const link = document.createElement("link");
|
|
2020
|
+
link.rel = "prefetch";
|
|
2021
|
+
link.href = url;
|
|
2022
|
+
document.head.appendChild(link);
|
|
2023
|
+
}
|
|
2024
|
+
function prefetchFetch(url) {
|
|
2025
|
+
if (typeof fetch === "undefined")
|
|
2026
|
+
return;
|
|
2027
|
+
fetch(url, { method: "GET", mode: "cors" }).catch(() => {
|
|
2028
|
+
/* ignore; prefetch is best-effort */
|
|
2029
|
+
});
|
|
2030
|
+
}
|
|
2031
|
+
/**
|
|
2032
|
+
* A Preact hook that returns a stable prefetch function to preload URLs (documents or data)
|
|
2033
|
+
* so they are cached before the user navigates or needs them. Useful for link hover or
|
|
2034
|
+
* route preloading.
|
|
2035
|
+
*
|
|
2036
|
+
* @returns Object with prefetch(url, options?) and isPrefetched(url)
|
|
2037
|
+
*
|
|
2038
|
+
* @example
|
|
2039
|
+
* ```tsx
|
|
2040
|
+
* function NavLink({ href, children }) {
|
|
2041
|
+
* const { prefetch } = usePrefetch();
|
|
2042
|
+
* return (
|
|
2043
|
+
* <a
|
|
2044
|
+
* href={href}
|
|
2045
|
+
* onMouseEnter={() => prefetch(href)}
|
|
2046
|
+
* >
|
|
2047
|
+
* {children}
|
|
2048
|
+
* </a>
|
|
2049
|
+
* );
|
|
2050
|
+
* }
|
|
2051
|
+
* ```
|
|
2052
|
+
*
|
|
2053
|
+
* @example
|
|
2054
|
+
* ```tsx
|
|
2055
|
+
* // Prefetch API data
|
|
2056
|
+
* const { prefetch } = usePrefetch();
|
|
2057
|
+
* prefetch('/api/user', { as: 'fetch' });
|
|
2058
|
+
* ```
|
|
2059
|
+
*/
|
|
2060
|
+
function usePrefetch() {
|
|
2061
|
+
const prefetchedRef = react.useRef(new Set());
|
|
2062
|
+
const [, setTick] = react.useState(0);
|
|
2063
|
+
const prefetch = react.useCallback((url, options) => {
|
|
2064
|
+
var _a;
|
|
2065
|
+
const trimmed = url === null || url === void 0 ? void 0 : url.trim();
|
|
2066
|
+
if (!trimmed)
|
|
2067
|
+
return;
|
|
2068
|
+
if (prefetchedRef.current.has(trimmed))
|
|
2069
|
+
return;
|
|
2070
|
+
const as = (_a = options === null || options === void 0 ? void 0 : options.as) !== null && _a !== void 0 ? _a : "document";
|
|
2071
|
+
if (as === "document") {
|
|
2072
|
+
prefetchDocument(trimmed);
|
|
2073
|
+
}
|
|
2074
|
+
else {
|
|
2075
|
+
prefetchFetch(trimmed);
|
|
2076
|
+
}
|
|
2077
|
+
prefetchedRef.current.add(trimmed);
|
|
2078
|
+
setTick((n) => n + 1);
|
|
2079
|
+
}, []);
|
|
2080
|
+
const isPrefetched = react.useCallback((url) => {
|
|
2081
|
+
var _a;
|
|
2082
|
+
return prefetchedRef.current.has((_a = url === null || url === void 0 ? void 0 : url.trim()) !== null && _a !== void 0 ? _a : "");
|
|
2083
|
+
}, []);
|
|
2084
|
+
return { prefetch, isPrefetched };
|
|
2085
|
+
}
|
|
2086
|
+
|
|
1779
2087
|
exports.useClipboard = useClipboard;
|
|
1780
2088
|
exports.useEventBus = useEventBus;
|
|
1781
2089
|
exports.useIndexedDB = useIndexedDB;
|
|
@@ -1783,6 +2091,8 @@ exports.useLLMMetadata = useLLMMetadata;
|
|
|
1783
2091
|
exports.useMutationObserver = useMutationObserver;
|
|
1784
2092
|
exports.useNetworkState = useNetworkState;
|
|
1785
2093
|
exports.usePreferredTheme = usePreferredTheme;
|
|
2094
|
+
exports.usePrefetch = usePrefetch;
|
|
2095
|
+
exports.useRBAC = useRBAC;
|
|
1786
2096
|
exports.useRageClick = useRageClick;
|
|
1787
2097
|
exports.useRefPrint = useRefPrint;
|
|
1788
2098
|
exports.useThreadedWorker = useThreadedWorker;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export interface UsePrefetchOptions {
|
|
2
|
+
/**
|
|
3
|
+
* Resource type for prefetch.
|
|
4
|
+
* - "document": uses <link rel="prefetch"> (default, for next navigation)
|
|
5
|
+
* - "fetch": uses fetch() to warm the HTTP cache (e.g. for API or same-origin data)
|
|
6
|
+
*/
|
|
7
|
+
as?: "document" | "fetch";
|
|
8
|
+
}
|
|
9
|
+
export interface UsePrefetchReturn {
|
|
10
|
+
/** Prefetch a URL so it is cached for later use. No-op if URL was already prefetched or empty. */
|
|
11
|
+
prefetch: (url: string, options?: UsePrefetchOptions) => void;
|
|
12
|
+
/** Check whether a URL has already been prefetched in this hook instance. */
|
|
13
|
+
isPrefetched: (url: string) => boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* A Preact hook that returns a stable prefetch function to preload URLs (documents or data)
|
|
17
|
+
* so they are cached before the user navigates or needs them. Useful for link hover or
|
|
18
|
+
* route preloading.
|
|
19
|
+
*
|
|
20
|
+
* @returns Object with prefetch(url, options?) and isPrefetched(url)
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```tsx
|
|
24
|
+
* function NavLink({ href, children }) {
|
|
25
|
+
* const { prefetch } = usePrefetch();
|
|
26
|
+
* return (
|
|
27
|
+
* <a
|
|
28
|
+
* href={href}
|
|
29
|
+
* onMouseEnter={() => prefetch(href)}
|
|
30
|
+
* >
|
|
31
|
+
* {children}
|
|
32
|
+
* </a>
|
|
33
|
+
* );
|
|
34
|
+
* }
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```tsx
|
|
39
|
+
* // Prefetch API data
|
|
40
|
+
* const { prefetch } = usePrefetch();
|
|
41
|
+
* prefetch('/api/user', { as: 'fetch' });
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export declare function usePrefetch(): UsePrefetchReturn;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/** Flexible user object; your app can use any shape. */
|
|
2
|
+
export type RBACUser = Record<string, unknown>;
|
|
3
|
+
/** Get auth state (user + optional roles/capabilities). Used by custom source. */
|
|
4
|
+
export interface RBACAuthState {
|
|
5
|
+
user?: RBACUser | null;
|
|
6
|
+
roles?: string[];
|
|
7
|
+
capabilities?: string[];
|
|
8
|
+
}
|
|
9
|
+
/** Pluggable source for current user (and optionally roles/capabilities). */
|
|
10
|
+
export type RBACUserSource = {
|
|
11
|
+
type: "localStorage";
|
|
12
|
+
key: string;
|
|
13
|
+
} | {
|
|
14
|
+
type: "sessionStorage";
|
|
15
|
+
key: string;
|
|
16
|
+
} | {
|
|
17
|
+
type: "api";
|
|
18
|
+
fetch: () => Promise<RBACUser>;
|
|
19
|
+
} | {
|
|
20
|
+
type: "memory";
|
|
21
|
+
getUser: () => RBACUser | null;
|
|
22
|
+
} | {
|
|
23
|
+
type: "custom";
|
|
24
|
+
getAuth: () => RBACAuthState | Promise<RBACAuthState>;
|
|
25
|
+
};
|
|
26
|
+
/** Role definition: name + condition to grant this role based on user. */
|
|
27
|
+
export interface RBACRoleDefinition {
|
|
28
|
+
role: string;
|
|
29
|
+
condition: (user: RBACUser | null) => boolean;
|
|
30
|
+
}
|
|
31
|
+
/** Role name -> list of capability strings. Use '*' for full access. */
|
|
32
|
+
export type RBACRoleCapabilities = Record<string, string[]>;
|
|
33
|
+
/** Optional override: get capabilities directly (e.g. from API) instead of deriving from roles. */
|
|
34
|
+
export type RBACCapabilitiesOverride = {
|
|
35
|
+
type: "localStorage";
|
|
36
|
+
key: string;
|
|
37
|
+
} | {
|
|
38
|
+
type: "sessionStorage";
|
|
39
|
+
key: string;
|
|
40
|
+
} | {
|
|
41
|
+
type: "api";
|
|
42
|
+
fetch: () => Promise<string[]>;
|
|
43
|
+
};
|
|
44
|
+
export interface UseRBACOptions {
|
|
45
|
+
/** Where to get the current user (and optionally roles/capabilities if type is 'custom'). */
|
|
46
|
+
userSource: RBACUserSource;
|
|
47
|
+
/** Role definitions: each role has a condition(user) to determine if the user has that role. */
|
|
48
|
+
roleDefinitions: RBACRoleDefinition[];
|
|
49
|
+
/** Capabilities per role. User gets union of capabilities for all their roles. Use '*' for admin. */
|
|
50
|
+
roleCapabilities: RBACRoleCapabilities;
|
|
51
|
+
/** Optional: fetch capabilities directly (overrides role-derived capabilities when provided). */
|
|
52
|
+
capabilitiesOverride?: RBACCapabilitiesOverride;
|
|
53
|
+
}
|
|
54
|
+
export interface UseRBACReturn {
|
|
55
|
+
/** Current user from source, or null. */
|
|
56
|
+
user: RBACUser | null;
|
|
57
|
+
/** Resolved roles for the current user. */
|
|
58
|
+
roles: string[];
|
|
59
|
+
/** Resolved capabilities (union of role capabilities, or from override). */
|
|
60
|
+
capabilities: string[];
|
|
61
|
+
/** True when user/roles/capabilities have been resolved (or failed). */
|
|
62
|
+
isReady: boolean;
|
|
63
|
+
/** Error from source (e.g. API or parse). */
|
|
64
|
+
error: Error | null;
|
|
65
|
+
/** Check if the user has the given role. */
|
|
66
|
+
hasRole: (role: string) => boolean;
|
|
67
|
+
/** Check if the user has the given capability (or '*' ). */
|
|
68
|
+
hasCapability: (capability: string) => boolean;
|
|
69
|
+
/** Alias for hasCapability. */
|
|
70
|
+
can: (capability: string) => boolean;
|
|
71
|
+
/** Re-fetch user/roles/capabilities from source. */
|
|
72
|
+
refetch: () => Promise<void>;
|
|
73
|
+
/** Helpers to persist auth to storage (for frontend-only flows). */
|
|
74
|
+
setUserInStorage: (user: RBACUser | null, storage: "localStorage" | "sessionStorage", key: string) => void;
|
|
75
|
+
setRolesInStorage: (roles: string[], storage: "localStorage" | "sessionStorage", key: string) => void;
|
|
76
|
+
setCapabilitiesInStorage: (capabilities: string[], storage: "localStorage" | "sessionStorage", key: string) => void;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Frontend-only role-based access control hook. Define roles with conditions,
|
|
80
|
+
* assign capabilities per role, and plug in user source (localStorage, sessionStorage, API, or custom).
|
|
81
|
+
* Supports full flexibility: frontend-only with storage or pluggable API.
|
|
82
|
+
*
|
|
83
|
+
* @param options - userSource, roleDefinitions, roleCapabilities, optional capabilitiesOverride
|
|
84
|
+
* @returns user, roles, capabilities, hasRole, hasCapability, can, isReady, error, refetch, and storage helpers
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```tsx
|
|
88
|
+
* const { hasRole, can, roles, setUserInStorage } = useRBAC({
|
|
89
|
+
* userSource: { type: 'localStorage', key: 'user' },
|
|
90
|
+
* roleDefinitions: [
|
|
91
|
+
* { role: 'admin', condition: (u) => u?.role === 'admin' },
|
|
92
|
+
* { role: 'editor', condition: (u) => u?.role === 'editor' || u?.role === 'admin' },
|
|
93
|
+
* ],
|
|
94
|
+
* roleCapabilities: {
|
|
95
|
+
* admin: ['*'],
|
|
96
|
+
* editor: ['posts:edit', 'posts:create'],
|
|
97
|
+
* },
|
|
98
|
+
* });
|
|
99
|
+
* if (can('posts:edit')) { ... }
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
export declare function useRBAC(options: UseRBACOptions): UseRBACReturn;
|
package/docs/README.md
CHANGED
|
@@ -41,6 +41,7 @@ Open `docs/index.html` in a browser that supports ES modules and import maps. Th
|
|
|
41
41
|
| **useWrappedChildren** | Children buttons get injected styles. |
|
|
42
42
|
| **usePreferredTheme** | Shows light / dark / no-preference from system. |
|
|
43
43
|
| **useNetworkState** | Online/offline and connection type. |
|
|
44
|
+
| **usePrefetch** | Hover or click to prefetch a URL (document or fetch); see prefetched status. |
|
|
44
45
|
| **useClipboard** | Copy and paste; see “Copied!” and pasted text. |
|
|
45
46
|
| **useRageClick** | Click the area 3+ times quickly; rage click count. |
|
|
46
47
|
| **useThreadedWorker** | Run a task; see loading and result. |
|
|
@@ -48,6 +49,8 @@ Open `docs/index.html` in a browser that supports ES modules and import maps. Th
|
|
|
48
49
|
| **useWebRTCIP** | Detect IP via WebRTC (may take a few seconds). |
|
|
49
50
|
| **useWasmCompute** | Run WASM in a worker (needs `add.wasm` in docs). |
|
|
50
51
|
| **useWorkerNotifications** | Run/fail tasks and queue updates; toasts show events. |
|
|
52
|
+
| **useRefPrint** | Bind a ref to a section and click “Print / Save as PDF”; only that section is printed via `@media print`. |
|
|
53
|
+
| **useRBAC** | Login as Admin / Editor / Viewer (localStorage or sessionStorage); see roles and capabilities; conditional UI by `can(...)`. |
|
|
51
54
|
| **useLLMMetadata** | Change “route” with buttons; see injected script info in the Live panel and `<script data-llm="true">` in the document head. Safe with `null`/`undefined` config (minimal payload with `route: "/"`). |
|
|
52
55
|
|
|
53
56
|
---
|
|
@@ -88,6 +91,110 @@ function App() {
|
|
|
88
91
|
}
|
|
89
92
|
```
|
|
90
93
|
|
|
94
|
+
### useRefPrint example
|
|
95
|
+
|
|
96
|
+
Print only a specific section via the native print dialog (user can save as PDF):
|
|
97
|
+
|
|
98
|
+
```js
|
|
99
|
+
import { h, render } from "preact";
|
|
100
|
+
import { useRef } from "preact/hooks";
|
|
101
|
+
import { useRefPrint } from "preact-missing-hooks";
|
|
102
|
+
|
|
103
|
+
function Report() {
|
|
104
|
+
const printRef = useRef(null);
|
|
105
|
+
const { print } = useRefPrint(printRef, {
|
|
106
|
+
documentTitle: "My Report",
|
|
107
|
+
downloadAsPdf: true,
|
|
108
|
+
});
|
|
109
|
+
return h(
|
|
110
|
+
"div",
|
|
111
|
+
{},
|
|
112
|
+
h(
|
|
113
|
+
"div",
|
|
114
|
+
{ ref: printRef, style: { padding: "1rem", background: "#f5f5f5" } },
|
|
115
|
+
"Only this section is printed when you click the button."
|
|
116
|
+
),
|
|
117
|
+
h("button", { onClick: print }, "Print / Save as PDF")
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
render(h(Report), document.getElementById("root"));
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### useRBAC example
|
|
124
|
+
|
|
125
|
+
Frontend-only role-based access: define roles with conditions, assign capabilities per role, and read the current user from localStorage (or sessionStorage / API):
|
|
126
|
+
|
|
127
|
+
```js
|
|
128
|
+
import { h, render } from "preact";
|
|
129
|
+
import { useRBAC } from "preact-missing-hooks";
|
|
130
|
+
|
|
131
|
+
const roleDefinitions = [
|
|
132
|
+
{ role: "admin", condition: (u) => u && u.role === "admin" },
|
|
133
|
+
{
|
|
134
|
+
role: "editor",
|
|
135
|
+
condition: (u) => u && (u.role === "editor" || u.role === "admin"),
|
|
136
|
+
},
|
|
137
|
+
{ role: "viewer", condition: (u) => u && u.id != null },
|
|
138
|
+
];
|
|
139
|
+
const roleCapabilities = {
|
|
140
|
+
admin: ["*"],
|
|
141
|
+
editor: ["posts:edit", "posts:create", "posts:read"],
|
|
142
|
+
viewer: ["posts:read"],
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
function App() {
|
|
146
|
+
const { user, roles, can, setUserInStorage } = useRBAC({
|
|
147
|
+
userSource: { type: "localStorage", key: "app-user" },
|
|
148
|
+
roleDefinitions,
|
|
149
|
+
roleCapabilities,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
if (!user) {
|
|
153
|
+
return h(
|
|
154
|
+
"div",
|
|
155
|
+
{},
|
|
156
|
+
h(
|
|
157
|
+
"button",
|
|
158
|
+
{
|
|
159
|
+
onClick: () =>
|
|
160
|
+
setUserInStorage(
|
|
161
|
+
{ id: 1, role: "admin" },
|
|
162
|
+
"localStorage",
|
|
163
|
+
"app-user"
|
|
164
|
+
),
|
|
165
|
+
},
|
|
166
|
+
"Login as Admin"
|
|
167
|
+
),
|
|
168
|
+
h(
|
|
169
|
+
"button",
|
|
170
|
+
{
|
|
171
|
+
onClick: () =>
|
|
172
|
+
setUserInStorage(
|
|
173
|
+
{ id: 2, role: "viewer" },
|
|
174
|
+
"localStorage",
|
|
175
|
+
"app-user"
|
|
176
|
+
),
|
|
177
|
+
},
|
|
178
|
+
"Login as Viewer"
|
|
179
|
+
)
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
return h(
|
|
183
|
+
"div",
|
|
184
|
+
{},
|
|
185
|
+
h("p", {}, "Roles: " + roles.join(", ")),
|
|
186
|
+
can("posts:edit") && h("button", {}, "Edit post"),
|
|
187
|
+
can("*") && h("button", {}, "Admin panel"),
|
|
188
|
+
h(
|
|
189
|
+
"button",
|
|
190
|
+
{ onClick: () => setUserInStorage(null, "localStorage", "app-user") },
|
|
191
|
+
"Logout"
|
|
192
|
+
)
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
render(h(App), document.getElementById("root"));
|
|
196
|
+
```
|
|
197
|
+
|
|
91
198
|
### useLLMMetadata in the demo
|
|
92
199
|
|
|
93
200
|
The **useLLMMetadata** Live panel simulates route changes. Click “Route: /”, “Route: /blog”, or “Route: /docs”. Each change:
|
package/docs/index.html
CHANGED
|
@@ -323,6 +323,56 @@
|
|
|
323
323
|
transform: translateX(0);
|
|
324
324
|
}
|
|
325
325
|
}
|
|
326
|
+
/* useRBAC demo */
|
|
327
|
+
.rbac-demo {
|
|
328
|
+
display: flex;
|
|
329
|
+
flex-direction: column;
|
|
330
|
+
gap: 0.75rem;
|
|
331
|
+
}
|
|
332
|
+
.rbac-toolbar {
|
|
333
|
+
display: flex;
|
|
334
|
+
align-items: center;
|
|
335
|
+
flex-wrap: wrap;
|
|
336
|
+
gap: 0.25rem;
|
|
337
|
+
}
|
|
338
|
+
.rbac-toolbar button {
|
|
339
|
+
padding: 0.25rem 0.5rem;
|
|
340
|
+
font-size: 0.8rem;
|
|
341
|
+
cursor: pointer;
|
|
342
|
+
background: var(--surface2);
|
|
343
|
+
border: 1px solid var(--border);
|
|
344
|
+
border-radius: var(--radiusSm);
|
|
345
|
+
color: var(--text);
|
|
346
|
+
}
|
|
347
|
+
.rbac-toolbar button:hover {
|
|
348
|
+
background: var(--border);
|
|
349
|
+
}
|
|
350
|
+
.rbac-actions {
|
|
351
|
+
display: flex;
|
|
352
|
+
flex-wrap: wrap;
|
|
353
|
+
gap: 0.35rem;
|
|
354
|
+
}
|
|
355
|
+
.rbac-state {
|
|
356
|
+
background: var(--surface2);
|
|
357
|
+
border-radius: var(--radiusSm);
|
|
358
|
+
padding: 0.6rem 0.75rem;
|
|
359
|
+
font-size: 0.85rem;
|
|
360
|
+
}
|
|
361
|
+
.rbac-state-row {
|
|
362
|
+
margin-bottom: 0.35rem;
|
|
363
|
+
}
|
|
364
|
+
.rbac-state-row:last-child {
|
|
365
|
+
margin-bottom: 0;
|
|
366
|
+
}
|
|
367
|
+
.rbac-state-row code {
|
|
368
|
+
margin-left: 0.25rem;
|
|
369
|
+
}
|
|
370
|
+
.rbac-conditional {
|
|
371
|
+
display: flex;
|
|
372
|
+
flex-wrap: wrap;
|
|
373
|
+
align-items: center;
|
|
374
|
+
gap: 0.25rem;
|
|
375
|
+
}
|
|
326
376
|
</style>
|
|
327
377
|
</head>
|
|
328
378
|
<body>
|