permix
Version:
Permix is a lightweight, framework-agnostic, type-safe permissions management library for JavaScript applications on the client and server sides.
114 lines (113 loc) • 3.43 kB
JavaScript
import { PermixNotFoundError, createCheckContext, createHooks, createPermix as createPermix$1, createTemplate } from "../core/index.mjs";
import { ForbiddenException, SetMetadata, UseGuards, applyDecorators } from "@nestjs/common";
//#region src/nest/permix.ts
function getRequest(context) {
return context.switchToHttp().getRequest();
}
function readCheckArgs(metadataKey, context) {
const handler = context.getHandler();
const classRef = context.getClass();
const fromHandler = Reflect.getMetadata(metadataKey, handler);
if (fromHandler) return fromHandler;
return Reflect.getMetadata(metadataKey, classRef);
}
function buildPermix(resolveKey, options = {}) {
const checkMetadataKey = Symbol("permix:check");
const onForbidden = options.onForbidden ?? (() => {
throw new ForbiddenException({ error: "Forbidden" });
});
const hooks = createHooks();
function get(req) {
return req[resolveKey()] ?? null;
}
function getOrThrow(req) {
const instance = get(req);
if (!instance) throw new PermixNotFoundError(resolveKey());
return instance;
}
function attach(req, rules) {
const instance = createPermix$1(rules);
instance.hook("check", (context) => {
hooks.callHook("check", context);
});
req[resolveKey()] = instance;
return instance;
}
/**
* Nest guard that sets up a per-request Permix instance. Register globally
* with `APP_GUARD`, or per-controller / per-route with `@UseGuards`.
*
* Non-HTTP contexts (RPC, WebSockets, GraphQL) are left untouched.
*/
function guard(callbackOrRules) {
return { async canActivate(context) {
if (context.getType() !== "http") return true;
const req = getRequest(context);
attach(req, typeof callbackOrRules === "function" ? await callbackOrRules({
req,
context
}) : callbackOrRules);
return true;
} };
}
const enforce = { async canActivate(context) {
const args = readCheckArgs(checkMetadataKey, context);
if (!args) return true;
if (context.getType() !== "http") throw new PermixNotFoundError(resolveKey());
const req = getRequest(context);
if (getOrThrow(req).check(...args)) return true;
await onForbidden({
req,
context,
...createCheckContext(...args)
});
return false;
} };
/**
* Method or class decorator that enforces a permission. A handler-level
* `@Check` overrides a controller-level one.
*
* Throws `PermixNotFoundError` when `guard()` has not run for the request,
* so a forgotten setup guard is a loud failure, not an unprotected route.
*/
const Check = (...args) => applyDecorators(SetMetadata(checkMetadataKey, args), UseGuards(enforce));
function getRules(req) {
return get(req)?.getRules() ?? null;
}
function template(rules) {
return createTemplate(rules);
}
return {
guard,
Check,
template,
get,
getOrThrow,
getRules,
hook: hooks.hook,
hookOnce: hooks.hookOnce,
get key() {
return resolveKey();
},
$inferDefinition: void 0,
$inferPath: void 0
};
}
/**
* Create a guard factory that wires Permix into NestJS routes.
*
* Use `.contextKey('name')` to set a custom request key (defaults to a unique
* `Symbol('permix')`).
*
* @link https://permix.letstri.dev/docs/integrations/nest
*/
function createPermix(options = {}) {
let key = Symbol("permix");
const permix = buildPermix(() => key, options);
return Object.assign(permix, { contextKey(newKey) {
key = newKey;
return permix;
} });
}
//#endregion
export { createPermix };