mirror of
https://github.com/tale/headplane.git
synced 2026-08-10 14:06:51 +00:00
193 lines
5.1 KiB
TypeScript
193 lines
5.1 KiB
TypeScript
import { data, redirect } from "react-router";
|
|
|
|
import { isDataWithApiError } from "~/server/headscale/api/error-client";
|
|
import { Capabilities } from "~/server/web/roles";
|
|
|
|
import type { Route } from "./+types/machine";
|
|
|
|
export async function machineAction({ request, context }: Route.ActionArgs) {
|
|
const principal = await context.auth.require(request);
|
|
|
|
const formData = await request.formData();
|
|
const api = context.hsApi.getRuntimeClient(
|
|
context.auth.getHeadscaleApiKey(principal, context.oidc?.apiKey),
|
|
);
|
|
|
|
const action = formData.get("action_id")?.toString();
|
|
if (!action) {
|
|
throw data("Missing `action_id` in the form data.", {
|
|
status: 400,
|
|
});
|
|
}
|
|
|
|
// Fast track register since it doesn't require an existing machine
|
|
if (action === "register") {
|
|
if (!context.auth.can(principal, Capabilities.write_machines)) {
|
|
throw data("You do not have permission to manage machines", {
|
|
status: 403,
|
|
});
|
|
}
|
|
|
|
const registrationKey = formData.get("register_key")?.toString();
|
|
if (!registrationKey) {
|
|
throw data("Missing `register_key` in the form data.", {
|
|
status: 400,
|
|
});
|
|
}
|
|
|
|
const user = formData.get("user")?.toString();
|
|
if (!user) {
|
|
throw data("Missing `user` in the form data.", {
|
|
status: 400,
|
|
});
|
|
}
|
|
|
|
const node = await api.registerNode(user, registrationKey);
|
|
return redirect(`/machines/${node.id}`);
|
|
}
|
|
|
|
// Check if the user has permission to manage this machine
|
|
const nodeId = formData.get("node_id")?.toString();
|
|
if (!nodeId) {
|
|
throw data("Missing `node_id` in the form data.", {
|
|
status: 400,
|
|
});
|
|
}
|
|
|
|
const node = await api.getNode(nodeId);
|
|
if (!node) {
|
|
throw data(`Machine with ID ${nodeId} not found`, {
|
|
status: 404,
|
|
});
|
|
}
|
|
|
|
if (!context.auth.canManageNode(principal, node)) {
|
|
throw data("You do not have permission to act on this machine", {
|
|
status: 403,
|
|
});
|
|
}
|
|
|
|
switch (action) {
|
|
case "rename": {
|
|
const newName = formData.get("name")?.toString();
|
|
if (!newName) {
|
|
throw data("Missing `name` in the form data.", {
|
|
status: 400,
|
|
});
|
|
}
|
|
|
|
const name = String(formData.get("name"));
|
|
await api.renameNode(nodeId, name);
|
|
return { message: "Machine renamed" };
|
|
}
|
|
|
|
case "delete": {
|
|
await api.deleteNode(nodeId);
|
|
return redirect("/machines");
|
|
}
|
|
|
|
case "expire": {
|
|
await api.expireNode(nodeId);
|
|
return { message: "Machine expired" };
|
|
}
|
|
|
|
case "update_tags": {
|
|
const tags = formData.get("tags")?.toString().split(",") ?? [];
|
|
if (tags.length === 0) {
|
|
throw data("Missing `tags` in the form data.", {
|
|
status: 400,
|
|
});
|
|
}
|
|
|
|
try {
|
|
await api.setNodeTags(
|
|
nodeId,
|
|
tags.map((tag) => tag.trim()).filter((tag) => tag !== ""),
|
|
);
|
|
|
|
return { success: true as const, message: "Tags updated" };
|
|
} catch (error) {
|
|
if (isDataWithApiError(error) && error.data.statusCode === 400) {
|
|
return data(
|
|
{
|
|
success: false as const,
|
|
error:
|
|
"One or more tags are not defined in your ACL policy. Please add them to your policy before assigning them to a machine.",
|
|
},
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
case "update_routes": {
|
|
const newApproved = node.approvedRoutes;
|
|
const routes = formData.get("routes")?.toString();
|
|
if (!routes) {
|
|
throw data("Missing `routes` in the form data.", {
|
|
status: 400,
|
|
});
|
|
}
|
|
|
|
const allRoutes = routes.split(",").map((route) => route.trim());
|
|
if (allRoutes.length === 0) {
|
|
throw data("No routes provided to update", {
|
|
status: 400,
|
|
});
|
|
}
|
|
|
|
const enabled = formData.get("enabled")?.toString();
|
|
if (enabled === undefined) {
|
|
throw data("Missing `enabled` in the form data.", {
|
|
status: 400,
|
|
});
|
|
}
|
|
|
|
if (enabled === "true") {
|
|
for (const route of allRoutes) {
|
|
// If already approved, skip, otherwise add to approved
|
|
if (newApproved.includes(route)) {
|
|
continue;
|
|
}
|
|
|
|
newApproved.push(route);
|
|
}
|
|
} else {
|
|
for (const route of allRoutes) {
|
|
// If not approved, skip, otherwise remove from approved
|
|
if (!newApproved.includes(route)) {
|
|
continue;
|
|
}
|
|
|
|
const index = newApproved.indexOf(route);
|
|
if (index > -1) {
|
|
newApproved.splice(index, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
await api.approveNodeRoutes(nodeId, newApproved);
|
|
return { message: "Routes updated" };
|
|
}
|
|
|
|
case "reassign": {
|
|
const user = formData.get("user_id")?.toString();
|
|
if (!user) {
|
|
throw data("Missing `user_id` in the form data.", {
|
|
status: 400,
|
|
});
|
|
}
|
|
|
|
await api.setNodeUser(nodeId, user);
|
|
return { message: "Machine reassigned" };
|
|
}
|
|
|
|
default:
|
|
throw data("Invalid action", {
|
|
status: 400,
|
|
});
|
|
}
|
|
}
|