60 lines
2.3 KiB
TypeScript
60 lines
2.3 KiB
TypeScript
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);
|
|
},
|
|
});
|
|
}
|