feat: centralize application framework primitives
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
import type { Context } from "@wrnexus/core";
|
||||
import { decideFor } from "./middleware.ts";
|
||||
|
||||
export interface AuthorizedHandlerOptions<Resource, Result extends Response = Response> {
|
||||
permission: string;
|
||||
resource?: (ctx: Context) => Resource | null | undefined | Promise<Resource | null | undefined>;
|
||||
exposeReason?: boolean;
|
||||
mayDiscover?: (ctx: Context) => boolean | Promise<boolean>;
|
||||
handle(ctx: Context & { resource: Resource }): Result | Promise<Result>;
|
||||
}
|
||||
|
||||
/** Define an API handler whose resource loading and permission check cannot be skipped. */
|
||||
export function defineAuthorizedHandler<Resource = undefined, Result extends Response = Response>(
|
||||
options: AuthorizedHandlerOptions<Resource, Result>,
|
||||
): (ctx: Context) => Promise<Response> {
|
||||
return async (ctx) => {
|
||||
if (!ctx.user) return Response.json({ ok: false, error: "Unauthorized" }, { status: 401 });
|
||||
const resource = options.resource ? await options.resource(ctx) : (undefined as Resource);
|
||||
if (options.resource && resource == null) {
|
||||
const mayDiscover = await options.mayDiscover?.(ctx);
|
||||
return mayDiscover
|
||||
? Response.json({ ok: false, error: "Not Found" }, { status: 404 })
|
||||
: Response.json({ ok: false, error: "Forbidden" }, { status: 403 });
|
||||
}
|
||||
const decision = await decideFor(ctx, options.permission, resource);
|
||||
if (!decision.allowed) {
|
||||
return ctx.authz!.forbidden(decision, { exposeReason: options.exposeReason });
|
||||
}
|
||||
ctx.resource = resource;
|
||||
return options.handle(ctx as Context & { resource: Resource });
|
||||
};
|
||||
}
|
||||
|
||||
export interface OwnedResourceDefinition<Resource> {
|
||||
name: string;
|
||||
owner(resource: Resource): string | null | undefined;
|
||||
permissions: {
|
||||
read?: string;
|
||||
create?: string;
|
||||
update?: string;
|
||||
delete?: string;
|
||||
readAny?: string;
|
||||
writeAny?: string;
|
||||
};
|
||||
}
|
||||
|
||||
/** Shared ownership and override vocabulary for application resource modules. */
|
||||
export function defineOwnedResource<Resource>(definition: OwnedResourceDefinition<Resource>) {
|
||||
return Object.freeze({
|
||||
...definition,
|
||||
attributes(resource: Resource) {
|
||||
return { ownerId: definition.owner(resource) };
|
||||
},
|
||||
isOwner(subjectId: string, resource: Resource) {
|
||||
const ownerId = definition.owner(resource);
|
||||
return Boolean(subjectId && ownerId && subjectId === ownerId);
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user