@shield-acl/react
Version:
Sistema ACL (Access Control List) inteligente e granular para aplicações React
498 lines (486 loc) • 13.6 kB
JavaScript
// 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