4.6 KB173 lines
Blame
1import type { FastifyInstance } from "fastify";
2import { z } from "zod";
3
4const createInstanceSchema = z.object({
5 name: z.string().default("grove"),
6 domain: z.string().optional(),
7 region: z.string().default("self-hosted"),
8 size: z.string().default("custom"),
9});
10
11const updateInstanceSchema = z.object({
12 status: z
13 .enum(["creating", "active", "error", "destroyed"])
14 .optional(),
15 ip: z.string().optional(),
16});
17
18export async function instanceRoutes(app: FastifyInstance) {
19 const db = (app as any).db;
20
21 // List user's instances
22 app.get("/", {
23 preHandler: [(app as any).authenticate],
24 handler: async (request) => {
25 const user = request.user as any;
26
27 const instances = db
28 .prepare(
29 "SELECT * FROM instances WHERE user_id = ? ORDER BY created_at DESC"
30 )
31 .all(user.id);
32
33 return { instances };
34 },
35 });
36
37 // Register a new instance
38 app.post("/", {
39 preHandler: [(app as any).authenticate],
40 handler: async (request, reply) => {
41 const parsed = createInstanceSchema.safeParse(request.body);
42 if (!parsed.success) {
43 return reply.code(400).send({ error: parsed.error.flatten() });
44 }
45
46 const user = request.user as any;
47 const { name, domain, region, size } = parsed.data;
48
49 const result = db
50 .prepare(`
51 INSERT INTO instances (user_id, name, domain, region, size, status)
52 VALUES (?, ?, ?, ?, ?, 'creating')
53 `)
54 .run(user.id, name, domain ?? null, region, size);
55
56 const instance = db
57 .prepare("SELECT * FROM instances WHERE id = ?")
58 .get(result.lastInsertRowid);
59
60 return reply.code(201).send({ instance });
61 },
62 });
63
64 // Update instance
65 app.patch("/:id", {
66 preHandler: [(app as any).authenticate],
67 handler: async (request, reply) => {
68 const { id } = request.params as { id: string };
69 const parsed = updateInstanceSchema.safeParse(request.body);
70 if (!parsed.success) {
71 return reply.code(400).send({ error: parsed.error.flatten() });
72 }
73
74 const user = request.user as any;
75
76 const instance = db
77 .prepare("SELECT * FROM instances WHERE id = ? AND user_id = ?")
78 .get(parseInt(id), user.id) as any;
79
80 if (!instance) {
81 return reply.code(404).send({ error: "Instance not found" });
82 }
83
84 const { status, ip } = parsed.data;
85 const updates: string[] = ["updated_at = datetime('now')"];
86 const values: any[] = [];
87
88 if (status) {
89 updates.push("status = ?");
90 values.push(status);
91 }
92 if (ip) {
93 updates.push("ip = ?");
94 values.push(ip);
95 }
96
97 values.push(instance.id);
98 db.prepare(
99 `UPDATE instances SET ${updates.join(", ")} WHERE id = ?`
100 ).run(...values);
101
102 const updated = db
103 .prepare("SELECT * FROM instances WHERE id = ?")
104 .get(instance.id);
105
106 return { instance: updated };
107 },
108 });
109
110 // Instance readiness callback (called by cloud-init when setup completes)
111 app.post("/:id/ready", async (request, reply) => {
112 const { id } = request.params as { id: string };
113 const body = request.body as { ip?: string; jwt_secret?: string };
114
115 const instance = db
116 .prepare("SELECT * FROM instances WHERE id = ?")
117 .get(parseInt(id)) as any;
118
119 if (!instance) {
120 return reply.code(404).send({ error: "Instance not found" });
121 }
122
123 if (instance.status !== "creating") {
124 return reply.code(400).send({ error: "Instance is not in creating state" });
125 }
126
127 const updates: string[] = [
128 "status = 'active'",
129 "updated_at = datetime('now')",
130 ];
131 const values: any[] = [];
132
133 if (body.ip) {
134 updates.push("ip = ?");
135 values.push(body.ip);
136 }
137 if (body.jwt_secret) {
138 updates.push("jwt_secret = ?");
139 values.push(body.jwt_secret);
140 }
141
142 values.push(instance.id);
143 db.prepare(
144 `UPDATE instances SET ${updates.join(", ")} WHERE id = ?`
145 ).run(...values);
146
147 const updated = db
148 .prepare("SELECT * FROM instances WHERE id = ?")
149 .get(instance.id);
150
151 return { instance: updated };
152 });
153
154 // Delete instance record
155 app.delete("/:id", {
156 preHandler: [(app as any).authenticate],
157 handler: async (request, reply) => {
158 const { id } = request.params as { id: string };
159 const user = request.user as any;
160
161 const result = db
162 .prepare("DELETE FROM instances WHERE id = ? AND user_id = ?")
163 .run(parseInt(id), user.id);
164
165 if (result.changes === 0) {
166 return reply.code(404).send({ error: "Instance not found" });
167 }
168
169 return reply.code(204).send();
170 },
171 });
172}
173