91 lines
2.7 KiB
TypeScript
91 lines
2.7 KiB
TypeScript
import type { Context } from "@wrnexus/core";
|
|
import { can } from "@wrnexus/authz";
|
|
|
|
export class HttpError extends Error {
|
|
constructor(
|
|
public readonly status: number,
|
|
message: string,
|
|
public readonly details?: unknown,
|
|
) {
|
|
super(message);
|
|
this.name = "HttpError";
|
|
}
|
|
}
|
|
|
|
export function subjectId(ctx: Pick<Context, "user">): string {
|
|
return String((ctx.user as { id?: unknown } | undefined)?.id ?? "").trim();
|
|
}
|
|
|
|
export function requireUser(ctx: Pick<Context, "user">): { id: string; user: unknown } {
|
|
const id = subjectId(ctx);
|
|
if (!id) throw new HttpError(401, "Unauthorized");
|
|
return { id, user: ctx.user };
|
|
}
|
|
|
|
export function requireParam(
|
|
ctx: Pick<Context, "params">,
|
|
name: string,
|
|
options: { integer?: boolean; positive?: boolean } = {},
|
|
): string | number {
|
|
const raw = String(ctx.params[name] ?? "").trim();
|
|
if (!raw) throw new HttpError(400, `Route parameter '${name}' is required.`);
|
|
if (!options.integer) return raw;
|
|
const value = Number(raw);
|
|
if (!Number.isInteger(value) || (options.positive && value < 1)) {
|
|
throw new HttpError(400, `Route parameter '${name}' must be a positive integer.`);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
export async function requireJson<T = Record<string, unknown>>(
|
|
ctx: Pick<Context, "req">,
|
|
): Promise<T> {
|
|
try {
|
|
return (await ctx.req.json()) as T;
|
|
} catch {
|
|
throw new HttpError(400, "Request body must be valid JSON.");
|
|
}
|
|
}
|
|
|
|
export async function requirePermission(
|
|
ctx: Context,
|
|
permission: string,
|
|
resource?: unknown,
|
|
): Promise<void> {
|
|
if (!(await can(ctx, permission, resource))) throw new HttpError(403, "Forbidden");
|
|
}
|
|
|
|
export const json = {
|
|
ok: <T>(value: T) => Response.json(value),
|
|
created: <T>(value: T) => Response.json(value, { status: 201 }),
|
|
noContent: () => new Response(null, { status: 204 }),
|
|
error: (status: number, message: string, details?: unknown) =>
|
|
Response.json({ error: message, ...(details === undefined ? {} : { details }) }, { status }),
|
|
};
|
|
|
|
export type ApiHandler = (ctx: Context) => Response | Promise<Response>;
|
|
|
|
/** Convert thrown HttpError values into the framework's standard JSON error shape. */
|
|
export function defineApiRoute(handler: ApiHandler): ApiHandler {
|
|
return async (ctx) => {
|
|
try {
|
|
return await handler(ctx);
|
|
} catch (error) {
|
|
if (error instanceof HttpError) return json.error(error.status, error.message, error.details);
|
|
throw error;
|
|
}
|
|
};
|
|
}
|
|
|
|
export function authorized(
|
|
permission: string,
|
|
handler: ApiHandler,
|
|
resource?: (ctx: Context) => unknown | Promise<unknown>,
|
|
): ApiHandler {
|
|
return defineApiRoute(async (ctx) => {
|
|
requireUser(ctx);
|
|
await requirePermission(ctx, permission, await resource?.(ctx));
|
|
return handler(ctx);
|
|
});
|
|
}
|