happy-server/sources/app/api/routes/accessKeysRoutes.ts

289 lines
9.8 KiB
TypeScript

import { Fastify } from "../types";
import { z } from "zod";
import { db } from "@/storage/db";
import { log } from "@/utils/log";
export function accessKeysRoutes(app: Fastify) {
// Get Access Key API
app.get('/v1/access-keys/:sessionId/:machineId', {
preHandler: app.authenticate,
schema: {
params: z.object({
sessionId: z.string(),
machineId: z.string()
}),
response: {
200: z.object({
accessKey: z.object({
data: z.string(),
dataVersion: z.number(),
createdAt: z.number(),
updatedAt: z.number()
}).nullable()
}),
404: z.object({
error: z.literal('Session or machine not found')
}),
500: z.object({
error: z.literal('Failed to get access key')
})
}
}
}, async (request, reply) => {
const userId = request.userId;
const { sessionId, machineId } = request.params;
try {
// Verify session and machine belong to user
const [session, machine] = await Promise.all([
db.session.findFirst({
where: { id: sessionId, accountId: userId }
}),
db.machine.findFirst({
where: { id: machineId, accountId: userId }
})
]);
if (!session || !machine) {
return reply.code(404).send({ error: 'Session or machine not found' });
}
// Get access key
const accessKey = await db.accessKey.findUnique({
where: {
accountId_machineId_sessionId: {
accountId: userId,
machineId,
sessionId
}
}
});
if (!accessKey) {
return reply.send({ accessKey: null });
}
return reply.send({
accessKey: {
data: accessKey.data,
dataVersion: accessKey.dataVersion,
createdAt: accessKey.createdAt.getTime(),
updatedAt: accessKey.updatedAt.getTime()
}
});
} catch (error) {
log({ module: 'api', level: 'error' }, `Failed to get access key: ${error}`);
return reply.code(500).send({ error: 'Failed to get access key' });
}
});
// Create Access Key API
app.post('/v1/access-keys/:sessionId/:machineId', {
preHandler: app.authenticate,
schema: {
params: z.object({
sessionId: z.string(),
machineId: z.string()
}),
body: z.object({
data: z.string()
}),
response: {
200: z.object({
success: z.boolean(),
accessKey: z.object({
data: z.string(),
dataVersion: z.number(),
createdAt: z.number(),
updatedAt: z.number()
}).optional(),
error: z.string().optional()
}),
404: z.object({
error: z.literal('Session or machine not found')
}),
409: z.object({
error: z.literal('Access key already exists')
}),
500: z.object({
error: z.literal('Failed to create access key')
})
}
}
}, async (request, reply) => {
const userId = request.userId;
const { sessionId, machineId } = request.params;
const { data } = request.body;
try {
// Verify session and machine belong to user
const [session, machine] = await Promise.all([
db.session.findFirst({
where: { id: sessionId, accountId: userId }
}),
db.machine.findFirst({
where: { id: machineId, accountId: userId }
})
]);
if (!session || !machine) {
return reply.code(404).send({ error: 'Session or machine not found' });
}
// Check if access key already exists
const existing = await db.accessKey.findUnique({
where: {
accountId_machineId_sessionId: {
accountId: userId,
machineId,
sessionId
}
}
});
if (existing) {
return reply.code(409).send({ error: 'Access key already exists' });
}
// Create access key
const accessKey = await db.accessKey.create({
data: {
accountId: userId,
machineId,
sessionId,
data,
dataVersion: 1
}
});
log({ module: 'access-keys', userId, sessionId, machineId }, 'Created new access key');
return reply.send({
success: true,
accessKey: {
data: accessKey.data,
dataVersion: accessKey.dataVersion,
createdAt: accessKey.createdAt.getTime(),
updatedAt: accessKey.updatedAt.getTime()
}
});
} catch (error) {
log({ module: 'api', level: 'error' }, `Failed to create access key: ${error}`);
return reply.code(500).send({ error: 'Failed to create access key' });
}
});
// Update Access Key API
app.put('/v1/access-keys/:sessionId/:machineId', {
preHandler: app.authenticate,
schema: {
params: z.object({
sessionId: z.string(),
machineId: z.string()
}),
body: z.object({
data: z.string(),
expectedVersion: z.number().int().min(0)
}),
response: {
200: z.union([
z.object({
success: z.literal(true),
version: z.number()
}),
z.object({
success: z.literal(false),
error: z.literal('version-mismatch'),
currentVersion: z.number(),
currentData: z.string()
})
]),
404: z.object({
error: z.literal('Access key not found')
}),
500: z.object({
success: z.literal(false),
error: z.literal('Failed to update access key')
})
}
}
}, async (request, reply) => {
const userId = request.userId;
const { sessionId, machineId } = request.params;
const { data, expectedVersion } = request.body;
try {
// Get current access key for version check
const currentAccessKey = await db.accessKey.findUnique({
where: {
accountId_machineId_sessionId: {
accountId: userId,
machineId,
sessionId
}
}
});
if (!currentAccessKey) {
return reply.code(404).send({ error: 'Access key not found' });
}
// Check version
if (currentAccessKey.dataVersion !== expectedVersion) {
return reply.code(200).send({
success: false,
error: 'version-mismatch',
currentVersion: currentAccessKey.dataVersion,
currentData: currentAccessKey.data
});
}
// Update with version check
const { count } = await db.accessKey.updateMany({
where: {
accountId: userId,
machineId,
sessionId,
dataVersion: expectedVersion
},
data: {
data,
dataVersion: expectedVersion + 1,
updatedAt: new Date()
}
});
if (count === 0) {
// Re-fetch to get current version
const accessKey = await db.accessKey.findUnique({
where: {
accountId_machineId_sessionId: {
accountId: userId,
machineId,
sessionId
}
}
});
return reply.code(200).send({
success: false,
error: 'version-mismatch',
currentVersion: accessKey?.dataVersion || 0,
currentData: accessKey?.data || ''
});
}
log({ module: 'access-keys', userId, sessionId, machineId }, `Updated access key to version ${expectedVersion + 1}`);
return reply.send({
success: true,
version: expectedVersion + 1
});
} catch (error) {
log({ module: 'api', level: 'error' }, `Failed to update access key: ${error}`);
return reply.code(500).send({
success: false,
error: 'Failed to update access key'
});
}
});
}