41 lines
1.5 KiB
TypeScript
41 lines
1.5 KiB
TypeScript
import { HttpError } from "./errors";
|
|
|
|
/* There is deliberately no authentication (access is controlled by firewall
|
|
* rules). The one guard: mutating requests from a browser must originate
|
|
* from this app, so a page on another site can't POST to the LAN service. */
|
|
function checkOrigin(req: Request): void {
|
|
const origin = req.headers.get("origin");
|
|
if (!origin) return; // curl and other non-browser clients
|
|
let originHost: string;
|
|
try {
|
|
originHost = new URL(origin).host;
|
|
} catch {
|
|
throw new HttpError(403, "bad origin");
|
|
}
|
|
if (originHost !== req.headers.get("host")) throw new HttpError(403, "cross-origin request refused");
|
|
}
|
|
|
|
/** Wraps a route handler: origin check for writes + HttpError -> JSON response. */
|
|
export function route<C>(handler: (req: Request, ctx: C) => Promise<Response>) {
|
|
return async (req: Request, ctx: C): Promise<Response> => {
|
|
try {
|
|
if (req.method !== "GET" && req.method !== "HEAD") checkOrigin(req);
|
|
return await handler(req, ctx);
|
|
} catch (err) {
|
|
if (err instanceof HttpError) {
|
|
return Response.json({ error: err.message, details: err.details }, { status: err.status });
|
|
}
|
|
console.error("[blog-manager]", err);
|
|
return Response.json({ error: "internal error" }, { status: 500 });
|
|
}
|
|
};
|
|
}
|
|
|
|
export async function readJson<T = Record<string, unknown>>(req: Request): Promise<T> {
|
|
try {
|
|
return (await req.json()) as T;
|
|
} catch {
|
|
throw new HttpError(400, "request body must be JSON");
|
|
}
|
|
}
|