Files
headplane/app/routes/machines/machine-actions.ts
T
2025-11-04 22:44:58 -05:00

179 lines
4.1 KiB
TypeScript

import { data, redirect } from 'react-router';
import { Capabilities } from '~/server/web/roles';
import type { Route } from './+types/machine';
export async function machineAction({ request, context }: Route.ActionArgs) {
const session = await context.sessions.auth(request);
const check = await context.sessions.check(
request,
Capabilities.write_machines,
);
const formData = await request.formData();
const api = context.hsApi.getRuntimeClient(session.api_key);
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 (!check) {
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 (
node.user.providerId?.split('/').pop() !== session.user.subject &&
!check
) {
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,
});
}
await api.setNodeTags(
nodeId,
tags.map((tag) => tag.trim()).filter((tag) => tag !== ''),
);
return { message: 'Tags updated' };
}
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,
});
}
}