UNPKG

@shield-acl/react

Version:

Sistema ACL (Access Control List) inteligente e granular para aplicações React

498 lines (486 loc) 13.6 kB
// src/context.tsx import { createContext, useContext, useState, useEffect, useCallback, useMemo, useSyncExternalStore } from "react"; // src/hooks/internal.ts function stableKey(value) { var _a; try { return (_a = JSON.stringify(value)) != null ? _a : "undefined"; } catch (e) { return "unserializable"; } } function mergeEnv(base, override) { if (!base && !override) return void 0; return { ...base, ...override }; } // src/context.tsx import { jsx } from "react/jsx-runtime"; var ACLContext = createContext(void 0); function ACLProvider({ engine, user: userProp, scope = "*", environment, children }) { const [user, setUser] = useState(userProp != null ? userProp : null); useEffect(() => { if (userProp !== void 0) setUser(userProp); }, [userProp]); const revision = useSyncExternalStore( engine.subscribe, () => engine.revision, () => engine.revision ); const can = useCallback( (action, resource, opts) => { var _a; if (!user) return false; return engine.can( user, (_a = opts == null ? void 0 : opts.scope) != null ? _a : scope, action, resource, toEval(opts, environment) ); }, // revision garante reavaliação quando o catálogo de roles muda [engine, user, scope, environment, revision] ); const evaluate = useCallback( (action, resource, opts) => { var _a; const activeScope = (_a = opts == null ? void 0 : opts.scope) != null ? _a : scope; if (!user) { return { allowed: false, reason: "No user authenticated", scope: activeScope }; } return engine.evaluate( user, activeScope, action, resource, toEval(opts, environment) ); }, [engine, user, scope, environment, revision] ); const canAsync = useCallback( (action, resource, opts) => { var _a; const activeScope = (_a = opts == null ? void 0 : opts.scope) != null ? _a : scope; if (!user) { return Promise.resolve({ allowed: false, reason: "No user authenticated", scope: activeScope }); } return engine.evaluateAsync( user, activeScope, action, resource, toEval(opts, environment) ); }, [engine, user, scope, environment, revision] ); const value = useMemo( () => ({ engine, scope, environment, user, setUser, revision, can, evaluate, canAsync }), [engine, scope, environment, user, revision, can, evaluate, canAsync] ); return /* @__PURE__ */ jsx(ACLContext.Provider, { value, children }); } function toEval(opts, providerEnv) { return { resource: opts == null ? void 0 : opts.record, environment: mergeEnv(providerEnv, opts == null ? void 0 : opts.environment) }; } function useACLContext() { const context = useContext(ACLContext); if (!context) { throw new Error("useACLContext must be used within an ACLProvider"); } return context; } // src/hooks/useAcl.ts import { useCallback as useCallback2 } from "react"; function useAcl() { const ctx = useACLContext(); const cannot = useCallback2( (action, resource, opts) => !ctx.can(action, resource, opts), [ctx] ); const clearCache = useCallback2(() => ctx.engine.clearCache(), [ctx.engine]); return { ...ctx, cannot, clearCache }; } // src/hooks/useCan.ts import { useMemo as useMemo2 } from "react"; function useCan(action, resource, opts) { const { can, user, revision } = useACLContext(); const key = stableKey(opts); return useMemo2( () => can(action, resource, opts), // eslint-disable-next-line react-hooks/exhaustive-deps [can, action, resource, key, user, revision] ); } function useCannot(action, resource, opts) { return !useCan(action, resource, opts); } // src/hooks/useEvaluate.ts import { useMemo as useMemo3 } from "react"; function useEvaluate(action, resource, opts) { const { evaluate, user, revision } = useACLContext(); const key = stableKey(opts); return useMemo3( () => evaluate(action, resource, opts), // eslint-disable-next-line react-hooks/exhaustive-deps [evaluate, action, resource, key, user, revision] ); } // src/hooks/useCanAsync.ts import { useEffect as useEffect2, useState as useState2, useCallback as useCallback3 } from "react"; function useCanAsync(action, resource, opts) { const { canAsync, user, revision } = useACLContext(); const [state, setState] = useState2({ allowed: void 0, loading: true, error: null }); const [nonce, setNonce] = useState2(0); const refetch = useCallback3(() => setNonce((n) => n + 1), []); const key = stableKey(opts); useEffect2(() => { let alive = true; setState((s) => s.loading ? s : { ...s, loading: true }); canAsync(action, resource, opts).then((r) => { if (alive) setState({ allowed: r.allowed, loading: false, error: null }); }).catch((e) => { if (alive) setState({ allowed: void 0, loading: false, error: e }); }); return () => { alive = false; }; }, [canAsync, action, resource, key, user, revision, nonce]); return { ...state, refetch }; } // src/hooks/useChecks.ts import { useMemo as useMemo4 } from "react"; function useChecks(map) { const { can, user, revision } = useACLContext(); const key = stableKey(map); return useMemo4(() => { const out = {}; Object.keys(map).forEach((k) => { const [action, resource, opts] = map[k]; out[k] = can(action, resource, opts); }); return out; }, [can, key, user, revision]); } var anyOf = (result) => Object.values(result).some(Boolean); var allOf = (result) => Object.values(result).every(Boolean); // src/hooks/useResource.ts import { useMemo as useMemo5 } from "react"; function useResource(type, record, opts) { const { can, evaluate, user, revision } = useACLContext(); const scope = opts == null ? void 0 : opts.scope; const environment = opts == null ? void 0 : opts.environment; return useMemo5(() => { const base = { record, scope, environment }; const check = (action, extra) => can(action, type, { ...base, ...extra }); return { resource: type, record, can: check, evaluate: (action) => evaluate(action, type, base), canRead: () => check("read") || check(`${type}.read`), canCreate: () => check("create") || check(`${type}.create`), canUpdate: () => check("update") || check(`${type}.update`), canDelete: () => check("delete") || check(`${type}.delete`), canManage: () => check("manage") || check("*") }; }, [ can, evaluate, type, stableKey(record), scope, stableKey(environment), user, revision ]); } // src/hooks/useRoles.ts import { useMemo as useMemo6, useCallback as useCallback4 } from "react"; function useGrantedRoles(opts) { var _a; const { engine, user, scope, revision } = useACLContext(); const active = (_a = opts == null ? void 0 : opts.scope) != null ? _a : scope; return useMemo6( () => user ? engine.getGrantedRoles(user, active) : [], // eslint-disable-next-line react-hooks/exhaustive-deps [engine, user, active, revision] ); } function useRoleHierarchy(opts) { var _a; const { engine, user, scope, revision } = useACLContext(); const active = (_a = opts == null ? void 0 : opts.scope) != null ? _a : scope; const { allRoles, directRoles, inheritedRoles } = useMemo6(() => { const direct = user ? engine.getGrantedRoles(user, active) : []; const all = new Set(direct); const walk = (name, seen = /* @__PURE__ */ new Set()) => { var _a2, _b; if (seen.has(name)) return; seen.add(name); (_b = (_a2 = engine.getRole(name)) == null ? void 0 : _a2.inherits) == null ? void 0 : _b.forEach((inherited) => { all.add(inherited); walk(inherited, seen); }); }; direct.forEach((d) => walk(d)); const allArr = [...all]; return { allRoles: allArr, directRoles: direct, inheritedRoles: allArr.filter((r) => !direct.includes(r)) }; }, [engine, user, active, revision]); const hasRole = useCallback4( (n) => allRoles.includes(n), [allRoles] ); const hasAnyRole = useCallback4( (ns) => ns.some((n) => allRoles.includes(n)), [allRoles] ); const hasAllRoles = useCallback4( (ns) => ns.every((n) => allRoles.includes(n)), [allRoles] ); const getRole = useCallback4( (n) => engine.getRole(n), // eslint-disable-next-line react-hooks/exhaustive-deps [engine, revision] ); return { allRoles, directRoles, inheritedRoles, hasRole, hasAnyRole, hasAllRoles, getRole }; } // src/hooks/usePermissions.ts import { useMemo as useMemo7 } from "react"; var EMPTY = { all: [], direct: [], byRole: {}, denials: [], allowances: [], actions: [], resources: [], count: 0 }; function usePermissions(opts) { var _a; const { engine, user, scope, revision } = useACLContext(); const active = (_a = opts == null ? void 0 : opts.scope) != null ? _a : scope; return useMemo7(() => { if (!user) return EMPTY; const all = engine.getUserPermissions(user, active); const direct = user.grants.filter((g) => g.scope === active).flatMap((g) => { var _a2; return (_a2 = g.permissions) != null ? _a2 : []; }); const byRole = {}; engine.getGrantedRoles(user, active).forEach((roleName) => { const role = engine.getRole(roleName); if (role) byRole[roleName] = role.permissions; }); const actions = /* @__PURE__ */ new Set(); const resources = /* @__PURE__ */ new Set(); for (const p of all) { (Array.isArray(p.action) ? p.action : [p.action]).forEach( (a) => actions.add(a) ); if (p.resource !== void 0) { (Array.isArray(p.resource) ? p.resource : [p.resource]).forEach( (r) => resources.add(r) ); } } return { all, direct, byRole, denials: all.filter((p) => p.deny === true), allowances: all.filter((p) => p.deny !== true), actions: [...actions].sort(), resources: [...resources].sort(), count: all.length }; }, [engine, user, active, revision]); } // src/hooks/useSync.ts import { useEffect as useEffect3, useRef, useCallback as useCallback5 } from "react"; function useUserSync(subscribe) { const { setUser } = useACLContext(); const ref = useRef(subscribe); ref.current = subscribe; useEffect3(() => { const cleanup = ref.current(setUser); return () => { if (typeof cleanup === "function") cleanup(); }; }, [setUser]); } function useRolesSync(subscribe) { const { engine } = useACLContext(); const reload = useCallback5( (roles) => engine.setRoles(roles), [engine] ); const ref = useRef(subscribe); ref.current = subscribe; useEffect3(() => { const cleanup = ref.current(reload); return () => { if (typeof cleanup === "function") cleanup(); }; }, [reload]); } function usePermissionEffect(action, resource, handlers, opts) { const allowed = useCan(action, resource, opts); const prev = useRef(null); useEffect3(() => { var _a, _b; if (prev.current !== null) { if (allowed && !prev.current) (_a = handlers.onGain) == null ? void 0 : _a.call(handlers); else if (!allowed && prev.current) (_b = handlers.onLose) == null ? void 0 : _b.call(handlers); } prev.current = allowed; }, [allowed]); } // src/components.tsx import { Fragment, jsx as jsx2 } from "react/jsx-runtime"; function CanBase({ action, resource, record, scope, environment, fallback = null, children }) { const allowed = useCan(action, resource, { record, scope, environment }); return /* @__PURE__ */ jsx2(Fragment, { children: allowed ? children : fallback }); } function Cannot({ action, resource, record, scope, environment, children }) { const allowed = useCan(action, resource, { record, scope, environment }); return /* @__PURE__ */ jsx2(Fragment, { children: allowed ? null : children }); } function toMap(checks) { const map = {}; checks.forEach((c, i) => { map[String(i)] = c; }); return map; } function CanAny({ checks, fallback = null, children }) { const result = useChecks(toMap(checks)); return /* @__PURE__ */ jsx2(Fragment, { children: anyOf(result) ? children : fallback }); } function CanAll({ checks, fallback = null, children }) { const result = useChecks(toMap(checks)); return /* @__PURE__ */ jsx2(Fragment, { children: allOf(result) ? children : fallback }); } function CanAsync({ action, resource, record, scope, environment, pending = null, fallback = null, children }) { const { allowed, loading } = useCanAsync(action, resource, { record, scope, environment }); if (loading) return /* @__PURE__ */ jsx2(Fragment, { children: pending }); return /* @__PURE__ */ jsx2(Fragment, { children: allowed ? children : fallback }); } var Can = Object.assign(CanBase, { Any: CanAny, All: CanAll, Async: CanAsync }); export { ACLProvider, Can, Cannot, allOf, anyOf, useACLContext, useAcl, useCan, useCanAsync, useCannot, useChecks, useEvaluate, useGrantedRoles, usePermissionEffect, usePermissions, useResource, useRoleHierarchy, useRolesSync, useUserSync }; //# sourceMappingURL=index.mjs.map