import { type ActionFunctionArgs, data, redirect } from 'react-router'; import type { LoadContext } from '~/server'; import { Capabilities } from '~/server/web/roles'; import { Machine } from '~/types'; export async function machineAction({ request, context, }: ActionFunctionArgs) { const session = await context.sessions.auth(request); const check = await context.sessions.check( request, Capabilities.write_machines, ); const formData = await request.formData(); const apiKey = session.get('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, }); } return registerMachine(formData, apiKey, context); } // 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 { nodes } = await context.client.get<{ nodes: Machine[] }>( 'v1/node', apiKey, ); const node = nodes.find((node) => node.id === nodeId); if (!node) { throw data(`Machine with ID ${nodeId} not found`, { status: 404, }); } if ( node.user.providerId?.split('/').pop() !== session.get('user')!.subject && !check ) { throw data('You do not have permission to act on this machine', { status: 403, }); } switch (action) { case 'rename': { return renameMachine(formData, apiKey, nodeId, context); } case 'delete': { return deleteMachine(apiKey, nodeId, context); } case 'expire': { return expireMachine(apiKey, nodeId, context); } case 'update_tags': { return updateTags(formData, apiKey, nodeId, context); } case 'update_routes': { return updateRoutes(formData, apiKey, nodeId, context); } case 'reassign': { return reassignMachine(formData, apiKey, nodeId, context); } default: throw data('Invalid action', { status: 400, }); } } async function registerMachine( formData: FormData, apiKey: string, context: LoadContext, ) { 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 qp = new URLSearchParams(); qp.append('user', user); qp.append('key', registrationKey); const url = `v1/node/register?${qp.toString()}`; const { node } = await context.client.post<{ node: Machine }>(url, apiKey, { user, key: registrationKey, }); return redirect(`/machines/${node.id}`); } async function renameMachine( formData: FormData, apiKey: string, nodeId: string, context: LoadContext, ) { 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 context.client.post(`v1/node/${nodeId}/rename/${name}`, apiKey); return { message: 'Machine renamed' }; } async function deleteMachine( apiKey: string, nodeId: string, context: LoadContext, ) { await context.client.delete(`v1/node/${nodeId}`, apiKey); return redirect('/machines'); } async function expireMachine( apiKey: string, nodeId: string, context: LoadContext, ) { await context.client.post(`v1/node/${nodeId}/expire`, apiKey); return { message: 'Machine expired' }; } async function updateTags( formData: FormData, apiKey: string, nodeId: string, context: LoadContext, ) { const tags = formData.get('tags')?.toString().split(',') ?? []; if (tags.length === 0) { throw data('Missing `tags` in the form data.', { status: 400, }); } await context.client.post(`v1/node/${nodeId}/tags`, apiKey, { tags: tags.map((tag) => tag.trim()).filter((tag) => tag !== ''), }); return { message: 'Tags updated' }; } async function updateRoutes( formData: FormData, apiKey: string, nodeId: string, context: LoadContext, ) { 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, }); } const postfix = enabled === 'true' ? 'enable' : 'disable'; await Promise.all( allRoutes.map(async (route) => { await context.client.post(`v1/routes/${route}/${postfix}`, apiKey); }), ); return { message: 'Routes updated' }; } async function reassignMachine( formData: FormData, apiKey: string, nodeId: string, context: LoadContext, ) { const user = formData.get('user')?.toString(); if (!user) { throw data('Missing `user` in the form data.', { status: 400, }); } await context.client.post(`v1/node/${nodeId}/user`, apiKey, { user, }); return { message: 'Machine reassigned' }; }