mirror of
https://github.com/tale/headplane.git
synced 2026-07-26 07:48:14 +00:00
0512565f8e
Apparently I didn't use my brain cells and rely on the /version endpoint that Headscale has exposed since 0.26 (our lowest supported version). Switching to that significantly simplifies the API surface.
203 lines
5.6 KiB
TypeScript
203 lines
5.6 KiB
TypeScript
import { data, redirect } from "react-router";
|
|
|
|
import { isDataWithApiError } from "~/server/headscale/api/error-client";
|
|
import { nodesResource } from "~/server/headscale/live-store";
|
|
import { Capabilities } from "~/server/web/roles";
|
|
|
|
import type { Route } from "./+types/machine";
|
|
|
|
export async function machineAction({ request, context }: Route.ActionArgs) {
|
|
const { principal, api } = await context.apiForRequest(request);
|
|
|
|
const formData = await request.formData();
|
|
|
|
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.nodes.register(user, registrationKey);
|
|
await context.hsLive.refresh(nodesResource, api);
|
|
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.nodes.get(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.nodes.rename(nodeId, name);
|
|
await context.hsLive.refresh(nodesResource, api);
|
|
return { message: "Machine renamed" };
|
|
}
|
|
|
|
case "delete": {
|
|
await api.nodes.delete(nodeId);
|
|
await context.hsLive.refresh(nodesResource, api);
|
|
return redirect("/machines");
|
|
}
|
|
|
|
case "expire": {
|
|
await api.nodes.expire(nodeId);
|
|
await context.hsLive.refresh(nodesResource, api);
|
|
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.nodes.setTags(
|
|
nodeId,
|
|
tags.map((tag) => tag.trim()).filter((tag) => tag !== ""),
|
|
);
|
|
|
|
await context.hsLive.refresh(nodesResource, api);
|
|
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.nodes.approveRoutes(nodeId, newApproved);
|
|
await context.hsLive.refresh(nodesResource, api);
|
|
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,
|
|
});
|
|
}
|
|
|
|
if (!api.nodes.reassignUser) {
|
|
throw data("Reassigning a node owner is no longer supported on this Headscale version.", {
|
|
status: 400,
|
|
});
|
|
}
|
|
await api.nodes.reassignUser(nodeId, user);
|
|
await context.hsLive.refresh(nodesResource, api);
|
|
return { message: "Machine reassigned" };
|
|
}
|
|
|
|
default:
|
|
throw data("Invalid action", {
|
|
status: 400,
|
|
});
|
|
}
|
|
}
|