hub-api/src/routes/instances.tsblame
View source
135dfe51import type { FastifyInstance } from "fastify";
135dfe52import { z } from "zod";
135dfe53
135dfe54const createInstanceSchema = z.object({
135dfe55 name: z.string().default("grove"),
135dfe56 domain: z.string().optional(),
966d71f7 region: z.string().default("self-hosted"),
966d71f8 size: z.string().default("custom"),
135dfe59});
135dfe510
135dfe511const updateInstanceSchema = z.object({
135dfe512 status: z
135dfe513 .enum(["creating", "active", "error", "destroyed"])
135dfe514 .optional(),
135dfe515 ip: z.string().optional(),
135dfe516});
135dfe517
135dfe518export async function instanceRoutes(app: FastifyInstance) {
135dfe519 const db = (app as any).db;
135dfe520
135dfe521 // List user's instances
135dfe522 app.get("/", {
135dfe523 preHandler: [(app as any).authenticate],
135dfe524 handler: async (request) => {
135dfe525 const user = request.user as any;
135dfe526
135dfe527 const instances = db
135dfe528 .prepare(
135dfe529 "SELECT * FROM instances WHERE user_id = ? ORDER BY created_at DESC"
135dfe530 )
135dfe531 .all(user.id);
135dfe532
135dfe533 return { instances };
135dfe534 },
135dfe535 });
135dfe536
966d71f37 // Register a new instance
135dfe538 app.post("/", {
135dfe539 preHandler: [(app as any).authenticate],
135dfe540 handler: async (request, reply) => {
135dfe541 const parsed = createInstanceSchema.safeParse(request.body);
135dfe542 if (!parsed.success) {
135dfe543 return reply.code(400).send({ error: parsed.error.flatten() });
135dfe544 }
135dfe545
135dfe546 const user = request.user as any;
966d71f47 const { name, domain, region, size } = parsed.data;
135dfe548
135dfe549 const result = db
135dfe550 .prepare(`
966d71f51 INSERT INTO instances (user_id, name, domain, region, size, status)
966d71f52 VALUES (?, ?, ?, ?, ?, 'creating')
135dfe553 `)
966d71f54 .run(user.id, name, domain ?? null, region, size);
135dfe555
135dfe556 const instance = db
135dfe557 .prepare("SELECT * FROM instances WHERE id = ?")
135dfe558 .get(result.lastInsertRowid);
135dfe559
135dfe560 return reply.code(201).send({ instance });
135dfe561 },
135dfe562 });
135dfe563
135dfe564 // Update instance
135dfe565 app.patch("/:id", {
135dfe566 preHandler: [(app as any).authenticate],
135dfe567 handler: async (request, reply) => {
135dfe568 const { id } = request.params as { id: string };
135dfe569 const parsed = updateInstanceSchema.safeParse(request.body);
135dfe570 if (!parsed.success) {
135dfe571 return reply.code(400).send({ error: parsed.error.flatten() });
135dfe572 }
135dfe573
135dfe574 const user = request.user as any;
135dfe575
135dfe576 const instance = db
135dfe577 .prepare("SELECT * FROM instances WHERE id = ? AND user_id = ?")
135dfe578 .get(parseInt(id), user.id) as any;
135dfe579
135dfe580 if (!instance) {
135dfe581 return reply.code(404).send({ error: "Instance not found" });
135dfe582 }
135dfe583
135dfe584 const { status, ip } = parsed.data;
135dfe585 const updates: string[] = ["updated_at = datetime('now')"];
135dfe586 const values: any[] = [];
135dfe587
135dfe588 if (status) {
135dfe589 updates.push("status = ?");
135dfe590 values.push(status);
135dfe591 }
135dfe592 if (ip) {
135dfe593 updates.push("ip = ?");
135dfe594 values.push(ip);
135dfe595 }
135dfe596
135dfe597 values.push(instance.id);
135dfe598 db.prepare(
135dfe599 `UPDATE instances SET ${updates.join(", ")} WHERE id = ?`
135dfe5100 ).run(...values);
135dfe5101
135dfe5102 const updated = db
135dfe5103 .prepare("SELECT * FROM instances WHERE id = ?")
135dfe5104 .get(instance.id);
135dfe5105
135dfe5106 return { instance: updated };
135dfe5107 },
135dfe5108 });
135dfe5109
966d71f110 // Instance readiness callback (called by cloud-init when setup completes)
4a006da111 app.post("/:id/ready", async (request, reply) => {
4a006da112 const { id } = request.params as { id: string };
966d71f113 const body = request.body as { ip?: string; jwt_secret?: string };
4a006da114
4a006da115 const instance = db
4a006da116 .prepare("SELECT * FROM instances WHERE id = ?")
4a006da117 .get(parseInt(id)) as any;
4a006da118
4a006da119 if (!instance) {
4a006da120 return reply.code(404).send({ error: "Instance not found" });
4a006da121 }
4a006da122
4a006da123 if (instance.status !== "creating") {
4a006da124 return reply.code(400).send({ error: "Instance is not in creating state" });
4a006da125 }
4a006da126
4a006da127 const updates: string[] = [
4a006da128 "status = 'active'",
4a006da129 "updated_at = datetime('now')",
4a006da130 ];
4a006da131 const values: any[] = [];
4a006da132
4a006da133 if (body.ip) {
4a006da134 updates.push("ip = ?");
4a006da135 values.push(body.ip);
4a006da136 }
966d71f137 if (body.jwt_secret) {
966d71f138 updates.push("jwt_secret = ?");
966d71f139 values.push(body.jwt_secret);
966d71f140 }
4a006da141
4a006da142 values.push(instance.id);
4a006da143 db.prepare(
4a006da144 `UPDATE instances SET ${updates.join(", ")} WHERE id = ?`
4a006da145 ).run(...values);
4a006da146
4a006da147 const updated = db
4a006da148 .prepare("SELECT * FROM instances WHERE id = ?")
4a006da149 .get(instance.id);
4a006da150
4a006da151 return { instance: updated };
4a006da152 });
4a006da153
135dfe5154 // Delete instance record
135dfe5155 app.delete("/:id", {
135dfe5156 preHandler: [(app as any).authenticate],
135dfe5157 handler: async (request, reply) => {
135dfe5158 const { id } = request.params as { id: string };
135dfe5159 const user = request.user as any;
135dfe5160
135dfe5161 const result = db
135dfe5162 .prepare("DELETE FROM instances WHERE id = ? AND user_id = ?")
135dfe5163 .run(parseInt(id), user.id);
135dfe5164
135dfe5165 if (result.changes === 0) {
135dfe5166 return reply.code(404).send({ error: "Instance not found" });
135dfe5167 }
135dfe5168
135dfe5169 return reply.code(204).send();
135dfe5170 },
135dfe5171 });
135dfe5172}