blob: 653eeaa1b8ed21f571c01778830ea632864e76e8 [file] [log] [blame]
giod0026612025-05-08 13:00:36 +00001import { PrismaClient } from "@prisma/client";
2import express from "express";
3import { env } from "node:process";
4import axios from "axios";
5import { GithubClient } from "./github";
gio7d813702025-05-08 18:29:52 +00006import { z } from "zod";
giod0026612025-05-08 13:00:36 +00007
8const db = new PrismaClient();
9
gio7d813702025-05-08 18:29:52 +000010// Map to store worker addresses by project ID
11const workers = new Map<number, string[]>();
gio3a921b82025-05-10 07:36:09 +000012const logs = new Map<number, Map<string, string>>();
gio7d813702025-05-08 18:29:52 +000013
giod0026612025-05-08 13:00:36 +000014const handleProjectCreate: express.Handler = async (req, resp) => {
15 try {
16 const { id } = await db.project.create({
17 data: {
gio09fcab52025-05-12 14:05:07 +000018 userId: resp.locals.userId,
giod0026612025-05-08 13:00:36 +000019 name: req.body.name,
20 },
21 });
22 resp.status(200);
23 resp.header("Content-Type", "application/json");
24 resp.write(
25 JSON.stringify({
26 id,
27 }),
28 );
29 } catch (e) {
30 console.log(e);
31 resp.status(500);
32 } finally {
33 resp.end();
34 }
35};
36
37const handleProjectAll: express.Handler = async (req, resp) => {
38 try {
39 const r = await db.project.findMany({
40 where: {
gio09fcab52025-05-12 14:05:07 +000041 userId: resp.locals.userId,
giod0026612025-05-08 13:00:36 +000042 },
43 });
44 resp.status(200);
45 resp.header("Content-Type", "application/json");
46 resp.write(
47 JSON.stringify(
48 r.map((p) => ({
49 id: p.id.toString(),
50 name: p.name,
51 })),
52 ),
53 );
54 } catch (e) {
55 console.log(e);
56 resp.status(500);
57 } finally {
58 resp.end();
59 }
60};
61
62const handleSave: express.Handler = async (req, resp) => {
63 try {
64 await db.project.update({
65 where: {
66 id: Number(req.params["projectId"]),
gio09fcab52025-05-12 14:05:07 +000067 userId: resp.locals.userId,
giod0026612025-05-08 13:00:36 +000068 },
69 data: {
70 draft: Buffer.from(JSON.stringify(req.body)),
71 },
72 });
73 resp.status(200);
74 } catch (e) {
75 console.log(e);
76 resp.status(500);
77 } finally {
78 resp.end();
79 }
80};
81
82const handleSavedGet: express.Handler = async (req, resp) => {
83 try {
84 const r = await db.project.findUnique({
85 where: {
86 id: Number(req.params["projectId"]),
gio09fcab52025-05-12 14:05:07 +000087 userId: resp.locals.userId,
giod0026612025-05-08 13:00:36 +000088 },
89 select: {
90 state: true,
91 draft: true,
92 },
93 });
94 if (r == null) {
95 resp.status(404);
96 } else {
97 resp.status(200);
98 resp.header("content-type", "application/json");
99 if (r.draft == null) {
100 if (r.state == null) {
101 resp.send({
102 nodes: [],
103 edges: [],
104 viewport: { x: 0, y: 0, zoom: 1 },
105 });
106 } else {
107 resp.send(JSON.parse(Buffer.from(r.state).toString("utf8")));
108 }
109 } else {
110 resp.send(JSON.parse(Buffer.from(r.draft).toString("utf8")));
111 }
112 }
113 } catch (e) {
114 console.log(e);
115 resp.status(500);
116 } finally {
117 resp.end();
118 }
119};
120
121const handleDelete: express.Handler = async (req, resp) => {
122 try {
123 const projectId = Number(req.params["projectId"]);
124 const p = await db.project.findUnique({
125 where: {
126 id: projectId,
gio09fcab52025-05-12 14:05:07 +0000127 userId: resp.locals.userId,
giod0026612025-05-08 13:00:36 +0000128 },
129 select: {
130 instanceId: true,
131 },
132 });
133 if (p === null) {
134 resp.status(404);
135 return;
136 }
137 const r = await axios.request({
138 url: `http://appmanager.hgrz-appmanager.svc.cluster.local/api/instance/${p.instanceId}/remove`,
139 method: "post",
140 });
141 if (r.status === 200) {
142 await db.project.delete({
143 where: {
144 id: projectId,
145 },
146 });
147 }
148 resp.status(200);
149 } catch (e) {
150 console.log(e);
151 resp.status(500);
152 } finally {
153 resp.end();
154 }
155};
156
157const handleDeploy: express.Handler = async (req, resp) => {
158 try {
159 const projectId = Number(req.params["projectId"]);
160 const state = Buffer.from(JSON.stringify(req.body.state));
161 const p = await db.project.findUnique({
162 where: {
163 id: projectId,
gio09fcab52025-05-12 14:05:07 +0000164 userId: resp.locals.userId,
giod0026612025-05-08 13:00:36 +0000165 },
166 select: {
167 instanceId: true,
168 githubToken: true,
169 deployKey: true,
170 },
171 });
172 if (p === null) {
173 resp.status(404);
174 return;
175 }
176 await db.project.update({
177 where: {
178 id: projectId,
179 },
180 data: {
181 draft: state,
182 },
183 });
184 let r: { status: number; data: { id: string; deployKey: string } };
185 if (p.instanceId == null) {
186 r = await axios.request({
187 url: "http://appmanager.hgrz-appmanager.svc.cluster.local/api/dodo-app",
188 method: "post",
189 data: {
190 config: req.body.config,
191 },
192 });
giod0026612025-05-08 13:00:36 +0000193 if (r.status === 200) {
194 await db.project.update({
195 where: {
196 id: projectId,
197 },
198 data: {
199 state,
200 draft: null,
201 instanceId: r.data.id,
202 deployKey: r.data.deployKey,
203 },
204 });
205
206 if (p.githubToken && r.data.deployKey) {
207 const stateObj = JSON.parse(JSON.parse(state.toString()));
gio0b4002c2025-05-11 15:48:51 +0000208 const githubNodes = stateObj.nodes.filter(
209 // eslint-disable-next-line @typescript-eslint/no-explicit-any
210 (n: any) => n.type === "github" && n.data?.repository?.id,
211 );
giod0026612025-05-08 13:00:36 +0000212
213 const github = new GithubClient(p.githubToken);
214 for (const node of githubNodes) {
215 try {
216 await github.addDeployKey(node.data.repository.sshURL, r.data.deployKey);
217 } catch (error) {
218 console.error(
219 `Failed to add deploy key to repository ${node.data.repository.sshURL}:`,
220 error,
221 );
222 }
223 }
224 }
225 }
226 } else {
227 r = await axios.request({
228 url: `http://appmanager.hgrz-appmanager.svc.cluster.local/api/dodo-app/${p.instanceId}`,
229 method: "put",
230 data: {
231 config: req.body.config,
232 },
233 });
234 if (r.status === 200) {
235 await db.project.update({
236 where: {
237 id: projectId,
238 },
239 data: {
240 state,
241 draft: null,
242 },
243 });
244 }
245 }
246 } catch (e) {
247 console.log(e);
248 resp.status(500);
249 } finally {
250 resp.end();
251 }
252};
253
254const handleStatus: express.Handler = async (req, resp) => {
255 try {
256 const projectId = Number(req.params["projectId"]);
257 const p = await db.project.findUnique({
258 where: {
259 id: projectId,
gio09fcab52025-05-12 14:05:07 +0000260 userId: resp.locals.userId,
giod0026612025-05-08 13:00:36 +0000261 },
262 select: {
263 instanceId: true,
264 },
265 });
giod0026612025-05-08 13:00:36 +0000266 if (p === null) {
267 resp.status(404);
268 return;
269 }
270 if (p.instanceId == null) {
271 resp.status(404);
272 return;
273 }
274 const r = await axios.request({
275 url: `http://appmanager.hgrz-appmanager.svc.cluster.local/api/instance/${p.instanceId}/status`,
276 method: "get",
277 });
278 resp.status(r.status);
279 if (r.status === 200) {
280 resp.write(JSON.stringify(r.data));
281 }
282 } catch (e) {
283 console.log(e);
284 resp.status(500);
285 } finally {
286 resp.end();
287 }
288};
289
290const handleGithubRepos: express.Handler = async (req, resp) => {
291 try {
292 const projectId = Number(req.params["projectId"]);
293 const project = await db.project.findUnique({
gio09fcab52025-05-12 14:05:07 +0000294 where: {
295 id: projectId,
296 userId: resp.locals.userId,
297 },
298 select: {
299 githubToken: true,
300 },
giod0026612025-05-08 13:00:36 +0000301 });
302
303 if (!project?.githubToken) {
304 resp.status(400);
305 resp.write(JSON.stringify({ error: "GitHub token not configured" }));
306 return;
307 }
308
309 const github = new GithubClient(project.githubToken);
310 const repositories = await github.getRepositories();
311
312 resp.status(200);
313 resp.header("Content-Type", "application/json");
314 resp.write(JSON.stringify(repositories));
315 } catch (e) {
316 console.log(e);
317 resp.status(500);
318 resp.write(JSON.stringify({ error: "Failed to fetch repositories" }));
319 } finally {
320 resp.end();
321 }
322};
323
324const handleUpdateGithubToken: express.Handler = async (req, resp) => {
325 try {
326 const projectId = Number(req.params["projectId"]);
327 const { githubToken } = req.body;
328
329 await db.project.update({
gio09fcab52025-05-12 14:05:07 +0000330 where: {
331 id: projectId,
332 userId: resp.locals.userId,
333 },
giod0026612025-05-08 13:00:36 +0000334 data: { githubToken },
335 });
336
337 resp.status(200);
338 } catch (e) {
339 console.log(e);
340 resp.status(500);
341 } finally {
342 resp.end();
343 }
344};
345
346const handleEnv: express.Handler = async (req, resp) => {
347 const projectId = Number(req.params["projectId"]);
348 try {
349 const project = await db.project.findUnique({
gio09fcab52025-05-12 14:05:07 +0000350 where: {
351 id: projectId,
352 userId: resp.locals.userId,
353 },
giod0026612025-05-08 13:00:36 +0000354 select: {
355 deployKey: true,
356 githubToken: true,
357 },
358 });
359
360 if (!project) {
361 resp.status(404);
362 resp.write(JSON.stringify({ error: "Project not found" }));
363 return;
364 }
365
gio3a921b82025-05-10 07:36:09 +0000366 const projectLogs = logs.get(projectId) || new Map();
367 const services = Array.from(projectLogs.keys());
368
giod0026612025-05-08 13:00:36 +0000369 resp.status(200);
370 resp.write(
371 JSON.stringify({
gio7d813702025-05-08 18:29:52 +0000372 // TODO(gio): get from env or command line flags
gio09fcab52025-05-12 14:05:07 +0000373 managerAddr: "http://10.42.0.211:8081",
giod0026612025-05-08 13:00:36 +0000374 deployKey: project.deployKey,
375 integrations: {
376 github: !!project.githubToken,
377 },
378 networks: [
379 {
380 name: "Public",
381 domain: "v1.dodo.cloud",
382 },
383 {
384 name: "Private",
385 domain: "p.v1.dodo.cloud",
386 },
387 ],
gio3a921b82025-05-10 07:36:09 +0000388 services,
giod0026612025-05-08 13:00:36 +0000389 }),
390 );
391 } catch (error) {
392 console.error("Error checking integrations:", error);
393 resp.status(500);
394 resp.write(JSON.stringify({ error: "Internal server error" }));
395 } finally {
396 resp.end();
397 }
398};
399
gio3a921b82025-05-10 07:36:09 +0000400const handleServiceLogs: express.Handler = async (req, resp) => {
401 try {
402 const projectId = Number(req.params["projectId"]);
403 const service = req.params["service"];
gio09fcab52025-05-12 14:05:07 +0000404 const project = await db.project.findUnique({
405 where: {
406 id: projectId,
407 userId: resp.locals.userId,
408 },
409 });
410 if (project == null) {
411 resp.status(404);
412 resp.write(JSON.stringify({ error: "Project not found" }));
413 return;
414 }
gio3a921b82025-05-10 07:36:09 +0000415
416 const projectLogs = logs.get(projectId);
417 if (!projectLogs) {
418 resp.status(404);
419 resp.write(JSON.stringify({ error: "No logs found for this project" }));
420 return;
421 }
422
423 const serviceLog = projectLogs.get(service);
424 if (!serviceLog) {
425 resp.status(404);
426 resp.write(JSON.stringify({ error: "No logs found for this service" }));
427 return;
428 }
429
430 resp.status(200);
431 resp.write(JSON.stringify({ logs: serviceLog }));
432 } catch (e) {
433 console.log(e);
434 resp.status(500);
435 resp.write(JSON.stringify({ error: "Failed to get service logs" }));
436 } finally {
437 resp.end();
438 }
439};
440
gio7d813702025-05-08 18:29:52 +0000441const WorkerSchema = z.object({
gio3a921b82025-05-10 07:36:09 +0000442 service: z.string(),
gio7d813702025-05-08 18:29:52 +0000443 address: z.string().url(),
gio3a921b82025-05-10 07:36:09 +0000444 logs: z.optional(z.string()),
gio7d813702025-05-08 18:29:52 +0000445});
446
447const handleRegisterWorker: express.Handler = async (req, resp) => {
448 try {
449 const projectId = Number(req.params["projectId"]);
450
451 const result = WorkerSchema.safeParse(req.body);
gio7d813702025-05-08 18:29:52 +0000452 if (!result.success) {
453 resp.status(400);
454 resp.write(
455 JSON.stringify({
456 error: "Invalid request data",
457 details: result.error.format(),
458 }),
459 );
460 return;
461 }
462
gio3a921b82025-05-10 07:36:09 +0000463 const { service, address, logs: log } = result.data;
gio7d813702025-05-08 18:29:52 +0000464
465 // Get existing workers or initialize empty array
466 const projectWorkers = workers.get(projectId) || [];
467
468 // Add new worker if not already present
469 if (!projectWorkers.includes(address)) {
470 projectWorkers.push(address);
471 }
472
473 workers.set(projectId, projectWorkers);
gio3a921b82025-05-10 07:36:09 +0000474 if (log) {
475 const svcLogs: Map<string, string> = logs.get(projectId) || new Map();
476 svcLogs.set(service, log);
477 logs.set(projectId, svcLogs);
478 }
gio7d813702025-05-08 18:29:52 +0000479 resp.status(200);
480 resp.write(
481 JSON.stringify({
482 success: true,
gio7d813702025-05-08 18:29:52 +0000483 }),
484 );
485 } catch (e) {
486 console.log(e);
487 resp.status(500);
488 resp.write(JSON.stringify({ error: "Failed to register worker" }));
489 } finally {
490 resp.end();
491 }
492};
493
494const handleReload: express.Handler = async (req, resp) => {
495 try {
496 const projectId = Number(req.params["projectId"]);
497 const projectWorkers = workers.get(projectId) || [];
gio09fcab52025-05-12 14:05:07 +0000498 const project = await db.project.findUnique({
499 where: {
500 id: projectId,
501 userId: resp.locals.userId,
502 },
503 });
504 if (project == null) {
505 resp.status(404);
506 resp.write(JSON.stringify({ error: "Project not found" }));
507 return;
508 }
gio7d813702025-05-08 18:29:52 +0000509
510 if (projectWorkers.length === 0) {
511 resp.status(404);
512 resp.write(JSON.stringify({ error: "No workers registered for this project" }));
513 return;
514 }
515
516 await Promise.all(
517 projectWorkers.map(async (workerAddress) => {
518 try {
519 const updateEndpoint = `${workerAddress}/update`;
520 await axios.post(updateEndpoint);
gio0b4002c2025-05-11 15:48:51 +0000521 // eslint-disable-next-line @typescript-eslint/no-explicit-any
522 } catch (error: any) {
gio7d813702025-05-08 18:29:52 +0000523 console.log(`Failed to update worker ${workerAddress}: ${error.message || "Unknown error"}`);
524 }
525 }),
526 );
527
528 resp.status(200);
529 resp.write(JSON.stringify({ success: true }));
530 } catch (e) {
531 console.log(e);
532 resp.status(500);
533 resp.write(JSON.stringify({ error: "Failed to reload workers" }));
534 } finally {
535 resp.end();
536 }
537};
538
gio09fcab52025-05-12 14:05:07 +0000539const auth = (req: express.Request, resp: express.Response, next: express.NextFunction) => {
540 const userId = req.get("x-forwarded-userid");
541 if (userId === undefined) {
542 resp.status(401);
543 resp.write("Unauthorized");
544 resp.end();
545 return;
546 }
547 resp.locals.userId = userId;
548 next();
549};
550
giod0026612025-05-08 13:00:36 +0000551async function start() {
552 await db.$connect();
553 const app = express();
554 app.use(express.json());
gio09fcab52025-05-12 14:05:07 +0000555 app.use(auth);
giod0026612025-05-08 13:00:36 +0000556 app.post("/api/project/:projectId/saved", handleSave);
557 app.get("/api/project/:projectId/saved", handleSavedGet);
558 app.post("/api/project/:projectId/deploy", handleDeploy);
559 app.get("/api/project/:projectId/status", handleStatus);
560 app.delete("/api/project/:projectId", handleDelete);
561 app.get("/api/project", handleProjectAll);
562 app.post("/api/project", handleProjectCreate);
563 app.get("/api/project/:projectId/repos/github", handleGithubRepos);
564 app.post("/api/project/:projectId/github-token", handleUpdateGithubToken);
565 app.get("/api/project/:projectId/env", handleEnv);
gio7d813702025-05-08 18:29:52 +0000566 app.post("/api/project/:projectId/reload", handleReload);
gio3a921b82025-05-10 07:36:09 +0000567 app.get("/api/project/:projectId/logs/:service", handleServiceLogs);
giod0026612025-05-08 13:00:36 +0000568 app.use("/", express.static("../front/dist"));
gio09fcab52025-05-12 14:05:07 +0000569
570 const api = express();
571 api.use(express.json());
572 api.post("/api/project/:projectId/workers", handleRegisterWorker);
573
574 // Start both servers
giod0026612025-05-08 13:00:36 +0000575 app.listen(env.DODO_PORT_WEB, () => {
gio09fcab52025-05-12 14:05:07 +0000576 console.log("Web server started on port", env.DODO_PORT_WEB);
577 });
578
579 api.listen(env.DODO_PORT_API, () => {
580 console.log("Internal API server started on port", env.DODO_PORT_API);
giod0026612025-05-08 13:00:36 +0000581 });
582}
583
584start();