175 lines
6.8 KiB
TypeScript
175 lines
6.8 KiB
TypeScript
import { EventRouter } from "@/app/events/eventRouter";
|
|
import { Fastify } from "../types";
|
|
import { z } from "zod";
|
|
import { db } from "@/storage/db";
|
|
import { log } from "@/utils/log";
|
|
import { randomKeyNaked } from "@/utils/randomKeyNaked";
|
|
import { allocateUserSeq } from "@/storage/seq";
|
|
import { buildNewMachineUpdate, buildUpdateMachineUpdate } from "@/app/events/eventRouter";
|
|
|
|
export function machinesRoutes(app: Fastify, eventRouter: EventRouter) {
|
|
app.post('/v1/machines', {
|
|
preHandler: app.authenticate,
|
|
schema: {
|
|
body: z.object({
|
|
id: z.string(),
|
|
metadata: z.string(), // Encrypted metadata
|
|
daemonState: z.string().optional(), // Encrypted daemon state
|
|
dataEncryptionKey: z.string().nullish()
|
|
})
|
|
}
|
|
}, async (request, reply) => {
|
|
const userId = request.userId;
|
|
const { id, metadata, daemonState, dataEncryptionKey } = request.body;
|
|
|
|
// Check if machine exists (like sessions do)
|
|
const machine = await db.machine.findFirst({
|
|
where: {
|
|
accountId: userId,
|
|
id: id
|
|
}
|
|
});
|
|
|
|
if (machine) {
|
|
// Machine exists - just return it
|
|
log({ module: 'machines', machineId: id, userId }, 'Found existing machine');
|
|
return reply.send({
|
|
machine: {
|
|
id: machine.id,
|
|
metadata: machine.metadata,
|
|
metadataVersion: machine.metadataVersion,
|
|
daemonState: machine.daemonState,
|
|
daemonStateVersion: machine.daemonStateVersion,
|
|
dataEncryptionKey: machine.dataEncryptionKey ? Buffer.from(machine.dataEncryptionKey).toString('base64') : null,
|
|
active: machine.active,
|
|
activeAt: machine.lastActiveAt.getTime(), // Return as activeAt for API consistency
|
|
createdAt: machine.createdAt.getTime(),
|
|
updatedAt: machine.updatedAt.getTime()
|
|
}
|
|
});
|
|
} else {
|
|
// Create new machine
|
|
log({ module: 'machines', machineId: id, userId }, 'Creating new machine');
|
|
|
|
const newMachine = await db.machine.create({
|
|
data: {
|
|
id,
|
|
accountId: userId,
|
|
metadata,
|
|
metadataVersion: 1,
|
|
daemonState: daemonState || null,
|
|
daemonStateVersion: daemonState ? 1 : 0,
|
|
dataEncryptionKey: dataEncryptionKey ? Buffer.from(dataEncryptionKey, 'base64') : undefined,
|
|
// Default to offline - in case the user does not start daemon
|
|
active: false,
|
|
// lastActiveAt and activeAt defaults to now() in schema
|
|
}
|
|
});
|
|
|
|
// Emit both new-machine and update-machine events for backward compatibility
|
|
const updSeq1 = await allocateUserSeq(userId);
|
|
const updSeq2 = await allocateUserSeq(userId);
|
|
|
|
// Emit new-machine event with all data including dataEncryptionKey
|
|
const newMachinePayload = buildNewMachineUpdate(newMachine, updSeq1, randomKeyNaked(12));
|
|
eventRouter.emitUpdate({
|
|
userId,
|
|
payload: newMachinePayload
|
|
});
|
|
|
|
// Emit update-machine event for backward compatibility (without dataEncryptionKey)
|
|
const machineMetadata = {
|
|
version: 1,
|
|
value: metadata
|
|
};
|
|
const updatePayload = buildUpdateMachineUpdate(newMachine.id, updSeq2, randomKeyNaked(12), machineMetadata);
|
|
eventRouter.emitUpdate({
|
|
userId,
|
|
payload: updatePayload
|
|
});
|
|
|
|
return reply.send({
|
|
machine: {
|
|
id: newMachine.id,
|
|
metadata: newMachine.metadata,
|
|
metadataVersion: newMachine.metadataVersion,
|
|
daemonState: newMachine.daemonState,
|
|
daemonStateVersion: newMachine.daemonStateVersion,
|
|
dataEncryptionKey: newMachine.dataEncryptionKey ? Buffer.from(newMachine.dataEncryptionKey).toString('base64') : null,
|
|
active: newMachine.active,
|
|
activeAt: newMachine.lastActiveAt.getTime(), // Return as activeAt for API consistency
|
|
createdAt: newMachine.createdAt.getTime(),
|
|
updatedAt: newMachine.updatedAt.getTime()
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
|
|
// Machines API
|
|
app.get('/v1/machines', {
|
|
preHandler: app.authenticate,
|
|
}, async (request, reply) => {
|
|
const userId = request.userId;
|
|
|
|
const machines = await db.machine.findMany({
|
|
where: { accountId: userId },
|
|
orderBy: { lastActiveAt: 'desc' }
|
|
});
|
|
|
|
return machines.map(m => ({
|
|
id: m.id,
|
|
metadata: m.metadata,
|
|
metadataVersion: m.metadataVersion,
|
|
daemonState: m.daemonState,
|
|
daemonStateVersion: m.daemonStateVersion,
|
|
dataEncryptionKey: m.dataEncryptionKey ? Buffer.from(m.dataEncryptionKey).toString('base64') : null,
|
|
seq: m.seq,
|
|
active: m.active,
|
|
activeAt: m.lastActiveAt.getTime(),
|
|
createdAt: m.createdAt.getTime(),
|
|
updatedAt: m.updatedAt.getTime()
|
|
}));
|
|
});
|
|
|
|
// GET /v1/machines/:id - Get single machine by ID
|
|
app.get('/v1/machines/:id', {
|
|
preHandler: app.authenticate,
|
|
schema: {
|
|
params: z.object({
|
|
id: z.string()
|
|
})
|
|
}
|
|
}, async (request, reply) => {
|
|
const userId = request.userId;
|
|
const { id } = request.params;
|
|
|
|
const machine = await db.machine.findFirst({
|
|
where: {
|
|
accountId: userId,
|
|
id: id
|
|
}
|
|
});
|
|
|
|
if (!machine) {
|
|
return reply.code(404).send({ error: 'Machine not found' });
|
|
}
|
|
|
|
return {
|
|
machine: {
|
|
id: machine.id,
|
|
metadata: machine.metadata,
|
|
metadataVersion: machine.metadataVersion,
|
|
daemonState: machine.daemonState,
|
|
daemonStateVersion: machine.daemonStateVersion,
|
|
dataEncryptionKey: machine.dataEncryptionKey ? Buffer.from(machine.dataEncryptionKey).toString('base64') : null,
|
|
seq: machine.seq,
|
|
active: machine.active,
|
|
activeAt: machine.lastActiveAt.getTime(),
|
|
createdAt: machine.createdAt.getTime(),
|
|
updatedAt: machine.updatedAt.getTime()
|
|
}
|
|
};
|
|
});
|
|
|
|
} |