Files
ThothII/backend/src/routes/pi-management.ts
T

68 lines
2.8 KiB
TypeScript

import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import { getPrincipal } from "../auth/auth.js";
import type { AppConfig } from "../config.js";
import { PiManagementError, type PiManagementService } from "../pi/management.js";
export function piManagementRoutes(
app: FastifyInstance,
deps: { config: AppConfig; service: PiManagementService },
): void {
app.get("/pi-management/status", async (request, reply) => run(request, reply, deps, () => deps.service.status()));
app.get("/pi-management/options", async (request, reply) => run(request, reply, deps, () => deps.service.options()));
app.put("/pi-management/config", async (request, reply) => run(
request,
reply,
deps,
() => deps.service.configure((request.body ?? {}) as Record<string, unknown>),
));
app.post("/pi-management/test", async (request, reply) => run(request, reply, deps, () => deps.service.test()));
app.get("/pi-management/logs", async (request, reply) => run(request, reply, deps, () => deps.service.logs()));
}
async function run<T>(
request: FastifyRequest,
reply: FastifyReply,
deps: { config: AppConfig; service: PiManagementService },
action: () => Promise<T>,
): Promise<T | FastifyReply> {
const principal = getPrincipal(request);
if (!managementAllowed(deps.config, principal.isAdmin)) {
return reply.code(403).send({ code: "pi_management_forbidden", error: "Pi management is not permitted" });
}
if (deps.config.authMode === "none" && isManagementWrite(request.method)
&& !sameOriginOrNonBrowser(request)) {
return reply.code(403).send({ code: "pi_management_forbidden", error: "Pi management is not permitted" });
}
try {
return await action();
} catch (error) {
if (error instanceof PiManagementError) {
const statusCode = error.code === "pi_management_invalid_config" ? 400 : 503;
return reply.code(statusCode).send({ code: error.code, error: error.message });
}
return reply.code(503).send({ code: "pi_management_unavailable", error: "Pi management is unavailable" });
}
}
function managementAllowed(config: AppConfig, isAdmin: boolean): boolean {
return (config.authMode === "none" && !config.publicExposure)
|| (config.authMode === "upstream" && isAdmin);
}
function isManagementWrite(method: string): boolean {
return method === "POST" || method === "PUT" || method === "PATCH" || method === "DELETE";
}
function sameOriginOrNonBrowser(request: FastifyRequest): boolean {
const origin = request.headers.origin;
if (origin === undefined) return true;
if (typeof origin !== "string" || typeof request.headers.host !== "string") return false;
try {
const supplied = new URL(origin);
const expected = new URL(`${request.protocol}://${request.headers.host}`);
return supplied.origin === expected.origin;
} catch {
return false;
}
}