blob: 9456d17ac6b32b09f0b49acd5f4836d0cdd7c591 [file] [log] [blame]
giod0026612025-05-08 13:00:36 +00001import { PrismaClient } from "@prisma/client";
2import express from "express";
gioa71316d2025-05-24 09:41:36 +04003import fs from "node:fs";
giod0026612025-05-08 13:00:36 +00004import { env } from "node:process";
5import axios from "axios";
gioc31bf142025-06-16 07:48:20 +00006import { GithubClient } from "./github.js";
7import { AppManager } from "./app_manager.js";
gio7d813702025-05-08 18:29:52 +00008import { z } from "zod";
gio78a22882025-07-01 18:56:01 +00009import { ProjectMonitor, WorkerSchema, LogItem } from "./project_monitor.js";
gioa71316d2025-05-24 09:41:36 +040010import tmp from "tmp";
gioc31bf142025-06-16 07:48:20 +000011import { NodeJSAnalyzer } from "./lib/nodejs.js";
gioa71316d2025-05-24 09:41:36 +040012import shell from "shelljs";
gioc31bf142025-06-16 07:48:20 +000013import { RealFileSystem } from "./lib/fs.js";
gioa71316d2025-05-24 09:41:36 +040014import path from "node:path";
gio9b7421a2025-06-18 12:31:13 +000015import {
16 Env,
17 generateDodoConfig,
18 ConfigSchema,
gio9b7421a2025-06-18 12:31:13 +000019 ConfigWithInput,
20 configToGraph,
21 Network,
22 GithubRepository,
gio8a5f12f2025-07-05 07:02:31 +000023 Graph,
gio9b7421a2025-06-18 12:31:13 +000024} from "config";
gio78a22882025-07-01 18:56:01 +000025import { Instant, DateTimeFormatter, ZoneId } from "@js-joda/core";
gio40c0c992025-07-02 13:18:05 +000026import LogStore from "./log.js";
gio10ff1342025-07-05 10:22:15 +000027import { GraphOrConfigSchema, GraphSchema } from "config/dist/graph.js";
gioa71316d2025-05-24 09:41:36 +040028
29async function generateKey(root: string): Promise<[string, string]> {
30 const privKeyPath = path.join(root, "key");
31 const pubKeyPath = path.join(root, "key.pub");
32 if (shell.exec(`ssh-keygen -t ed25519 -f ${privKeyPath} -N ""`).code !== 0) {
33 throw new Error("Failed to generate SSH key pair");
34 }
35 const publicKey = await fs.promises.readFile(pubKeyPath, "utf8");
36 const privateKey = await fs.promises.readFile(privKeyPath, "utf8");
37 return [publicKey, privateKey];
38}
giod0026612025-05-08 13:00:36 +000039
40const db = new PrismaClient();
gio40c0c992025-07-02 13:18:05 +000041const logStore = new LogStore(db);
gio3ed59592025-05-14 16:51:09 +000042const appManager = new AppManager();
giod0026612025-05-08 13:00:36 +000043
gioa1efbad2025-05-21 07:16:45 +000044const projectMonitors = new Map<number, ProjectMonitor>();
gio7d813702025-05-08 18:29:52 +000045
gio10ff1342025-07-05 10:22:15 +000046function parseGraph(data: string | null | undefined) {
47 if (data == null) {
48 return null;
49 }
50 return GraphSchema.safeParse(JSON.parse(data));
51}
52
giod0026612025-05-08 13:00:36 +000053const handleProjectCreate: express.Handler = async (req, resp) => {
54 try {
gioa71316d2025-05-24 09:41:36 +040055 const tmpDir = tmp.dirSync().name;
56 const [publicKey, privateKey] = await generateKey(tmpDir);
giod0026612025-05-08 13:00:36 +000057 const { id } = await db.project.create({
58 data: {
gio09fcab52025-05-12 14:05:07 +000059 userId: resp.locals.userId,
giod0026612025-05-08 13:00:36 +000060 name: req.body.name,
gioa71316d2025-05-24 09:41:36 +040061 deployKey: privateKey,
62 deployKeyPublic: publicKey,
giod0026612025-05-08 13:00:36 +000063 },
64 });
65 resp.status(200);
66 resp.header("Content-Type", "application/json");
67 resp.write(
68 JSON.stringify({
gio74ab7852025-05-13 13:19:31 +000069 id: id.toString(),
giod0026612025-05-08 13:00:36 +000070 }),
71 );
72 } catch (e) {
73 console.log(e);
74 resp.status(500);
75 } finally {
76 resp.end();
77 }
78};
79
80const handleProjectAll: express.Handler = async (req, resp) => {
81 try {
82 const r = await db.project.findMany({
83 where: {
gio09fcab52025-05-12 14:05:07 +000084 userId: resp.locals.userId,
giod0026612025-05-08 13:00:36 +000085 },
86 });
87 resp.status(200);
88 resp.header("Content-Type", "application/json");
89 resp.write(
90 JSON.stringify(
91 r.map((p) => ({
92 id: p.id.toString(),
93 name: p.name,
94 })),
95 ),
96 );
97 } catch (e) {
98 console.log(e);
99 resp.status(500);
100 } finally {
101 resp.end();
102 }
103};
104
gio8a5f12f2025-07-05 07:02:31 +0000105async function getState(projectId: number, userId: string, state: "deploy" | "draft"): Promise<Graph | null> {
106 const r = await db.project.findUnique({
107 where: {
108 id: projectId,
109 userId: userId,
110 },
111 select: {
112 state: true,
113 draft: true,
114 },
115 });
116 if (r == null) {
117 return null;
118 }
119 let currentState: Graph | null = null;
120 if (state === "deploy") {
121 if (r.state != null) {
gio10ff1342025-07-05 10:22:15 +0000122 currentState = parseGraph(r.state)!.data!;
gio8a5f12f2025-07-05 07:02:31 +0000123 }
124 } else {
125 if (r.draft == null) {
126 if (r.state == null) {
127 currentState = {
128 nodes: [],
129 edges: [],
130 viewport: { x: 0, y: 0, zoom: 1 },
131 };
132 } else {
gio10ff1342025-07-05 10:22:15 +0000133 currentState = parseGraph(r.state)!.data!;
gio8a5f12f2025-07-05 07:02:31 +0000134 }
135 } else {
gio10ff1342025-07-05 10:22:15 +0000136 currentState = parseGraph(r.draft)!.data!;
gio8a5f12f2025-07-05 07:02:31 +0000137 }
138 }
139 return currentState;
140}
141
gio818da4e2025-05-12 14:45:35 +0000142function handleSavedGet(state: "deploy" | "draft"): express.Handler {
143 return async (req, resp) => {
144 try {
gio8a5f12f2025-07-05 07:02:31 +0000145 const projectId = Number(req.params["projectId"]);
146 const graph = await getState(projectId, resp.locals.userId, state);
147 if (graph == null) {
gio818da4e2025-05-12 14:45:35 +0000148 resp.status(404);
149 return;
150 }
gio8a5f12f2025-07-05 07:02:31 +0000151 const env = await getEnv(projectId, resp.locals.userId, resp.locals.username);
152 const config = generateDodoConfig(projectId.toString(), graph.nodes, env);
giod0026612025-05-08 13:00:36 +0000153 resp.status(200);
154 resp.header("content-type", "application/json");
gio8a5f12f2025-07-05 07:02:31 +0000155 resp.write(
156 JSON.stringify({
157 state: graph,
gioc31bf142025-06-16 07:48:20 +0000158 config,
gio8a5f12f2025-07-05 07:02:31 +0000159 }),
160 );
gio818da4e2025-05-12 14:45:35 +0000161 } catch (e) {
162 console.log(e);
163 resp.status(500);
164 } finally {
165 resp.end();
giod0026612025-05-08 13:00:36 +0000166 }
gio818da4e2025-05-12 14:45:35 +0000167 };
168}
giod0026612025-05-08 13:00:36 +0000169
gioa71316d2025-05-24 09:41:36 +0400170const projectDeleteReqSchema = z.object({
171 state: z.optional(z.nullable(z.string())),
172});
173
174const handleProjectDelete: express.Handler = async (req, resp) => {
giod0026612025-05-08 13:00:36 +0000175 try {
176 const projectId = Number(req.params["projectId"]);
177 const p = await db.project.findUnique({
178 where: {
179 id: projectId,
gio09fcab52025-05-12 14:05:07 +0000180 userId: resp.locals.userId,
giod0026612025-05-08 13:00:36 +0000181 },
182 select: {
183 instanceId: true,
gioa71316d2025-05-24 09:41:36 +0400184 githubToken: true,
185 deployKeyPublic: true,
186 state: true,
187 draft: true,
giod0026612025-05-08 13:00:36 +0000188 },
189 });
190 if (p === null) {
191 resp.status(404);
192 return;
193 }
gioa71316d2025-05-24 09:41:36 +0400194 const parseResult = projectDeleteReqSchema.safeParse(req.body);
195 if (!parseResult.success) {
196 resp.status(400);
197 resp.write(JSON.stringify({ error: "Invalid request body", issues: parseResult.error.format() }));
198 return;
gioe440db82025-05-13 12:21:44 +0000199 }
gioa71316d2025-05-24 09:41:36 +0400200 if (p.githubToken && p.deployKeyPublic) {
201 const allRepos = [
202 ...new Set([
203 ...extractGithubRepos(p.state),
204 ...extractGithubRepos(p.draft),
205 ...extractGithubRepos(parseResult.data.state),
206 ]),
207 ];
208 if (allRepos.length > 0) {
209 const diff: RepoDiff = { toDelete: allRepos, toAdd: [] };
210 const github = new GithubClient(p.githubToken);
211 await manageGithubRepos(github, diff, p.deployKeyPublic, env.PUBLIC_ADDR);
212 console.log(
213 `Attempted to remove deploy keys for project ${projectId} from associated GitHub repositories.`,
214 );
215 }
giod0026612025-05-08 13:00:36 +0000216 }
gioa71316d2025-05-24 09:41:36 +0400217 if (p.instanceId !== null) {
218 if (!(await appManager.removeInstance(p.instanceId))) {
219 resp.status(500);
220 resp.write(JSON.stringify({ error: "Failed to remove deployment from cluster" }));
221 return;
222 }
223 }
224 await db.project.delete({
225 where: {
226 id: projectId,
227 },
228 });
giod0026612025-05-08 13:00:36 +0000229 resp.status(200);
230 } catch (e) {
231 console.log(e);
232 resp.status(500);
233 } finally {
234 resp.end();
235 }
236};
237
gioa71316d2025-05-24 09:41:36 +0400238function extractGithubRepos(serializedState: string | null | undefined): string[] {
gio3ed59592025-05-14 16:51:09 +0000239 if (!serializedState) {
240 return [];
241 }
242 try {
giobd37a2b2025-05-15 04:28:42 +0000243 const stateObj = JSON.parse(serializedState);
gio3ed59592025-05-14 16:51:09 +0000244 const githubNodes = stateObj.nodes.filter(
245 // eslint-disable-next-line @typescript-eslint/no-explicit-any
246 (n: any) => n.type === "github" && n.data?.repository?.id,
247 );
248 // eslint-disable-next-line @typescript-eslint/no-explicit-any
249 return githubNodes.map((n: any) => n.data.repository.sshURL);
250 } catch (error) {
251 console.error("Failed to parse state or extract GitHub repos:", error);
252 return [];
253 }
254}
255
256type RepoDiff = {
257 toAdd?: string[];
258 toDelete?: string[];
259};
260
261function calculateRepoDiff(oldRepos: string[], newRepos: string[]): RepoDiff {
262 const toAdd = newRepos.filter((repo) => !oldRepos.includes(repo));
263 const toDelete = oldRepos.filter((repo) => !newRepos.includes(repo));
264 return { toAdd, toDelete };
265}
266
gio76d8ae62025-05-19 15:21:54 +0000267async function manageGithubRepos(
268 github: GithubClient,
269 diff: RepoDiff,
270 deployKey: string,
271 publicAddr?: string,
272): Promise<void> {
gio3ed59592025-05-14 16:51:09 +0000273 for (const repoUrl of diff.toDelete ?? []) {
274 try {
275 await github.removeDeployKey(repoUrl, deployKey);
276 console.log(`Removed deploy key from repository ${repoUrl}`);
gio76d8ae62025-05-19 15:21:54 +0000277 if (publicAddr) {
278 const webhookCallbackUrl = `${publicAddr}/api/webhook/github/push`;
279 await github.removePushWebhook(repoUrl, webhookCallbackUrl);
280 console.log(`Removed push webhook from repository ${repoUrl}`);
281 }
gio3ed59592025-05-14 16:51:09 +0000282 } catch (error) {
283 console.error(`Failed to remove deploy key from repository ${repoUrl}:`, error);
284 }
285 }
286 for (const repoUrl of diff.toAdd ?? []) {
287 try {
288 await github.addDeployKey(repoUrl, deployKey);
289 console.log(`Added deploy key to repository ${repoUrl}`);
gio76d8ae62025-05-19 15:21:54 +0000290 if (publicAddr) {
291 const webhookCallbackUrl = `${publicAddr}/api/webhook/github/push`;
292 await github.addPushWebhook(repoUrl, webhookCallbackUrl);
293 console.log(`Added push webhook to repository ${repoUrl}`);
294 }
gio3ed59592025-05-14 16:51:09 +0000295 } catch (error) {
296 console.error(`Failed to add deploy key from repository ${repoUrl}:`, error);
297 }
298 }
299}
300
giod0026612025-05-08 13:00:36 +0000301const handleDeploy: express.Handler = async (req, resp) => {
302 try {
303 const projectId = Number(req.params["projectId"]);
giod0026612025-05-08 13:00:36 +0000304 const p = await db.project.findUnique({
305 where: {
306 id: projectId,
gioc31bf142025-06-16 07:48:20 +0000307 // userId: resp.locals.userId, TODO(gio): validate
giod0026612025-05-08 13:00:36 +0000308 },
309 select: {
310 instanceId: true,
311 githubToken: true,
312 deployKey: true,
gioa71316d2025-05-24 09:41:36 +0400313 deployKeyPublic: true,
gio3ed59592025-05-14 16:51:09 +0000314 state: true,
gio69148322025-06-19 23:16:12 +0400315 geminiApiKey: true,
gio69ff7592025-07-03 06:27:21 +0000316 anthropicApiKey: true,
giod0026612025-05-08 13:00:36 +0000317 },
318 });
319 if (p === null) {
320 resp.status(404);
321 return;
322 }
gioc31bf142025-06-16 07:48:20 +0000323 const config = ConfigSchema.safeParse(req.body.config);
324 if (!config.success) {
325 resp.status(400);
326 resp.write(JSON.stringify({ error: "Invalid configuration", issues: config.error.format() }));
327 return;
328 }
gio9b7421a2025-06-18 12:31:13 +0000329 let repos: GithubRepository[] = [];
330 if (p.githubToken) {
331 const github = new GithubClient(p.githubToken);
332 repos = await github.getRepositories();
333 }
gioc31bf142025-06-16 07:48:20 +0000334 const state = req.body.state
335 ? JSON.stringify(req.body.state)
336 : JSON.stringify(
337 configToGraph(
338 config.data,
339 getNetworks(resp.locals.username),
gio9b7421a2025-06-18 12:31:13 +0000340 repos,
gio10ff1342025-07-05 10:22:15 +0000341 p.state ? parseGraph(p.state)!.data! : undefined,
gioc31bf142025-06-16 07:48:20 +0000342 ),
343 );
giod0026612025-05-08 13:00:36 +0000344 await db.project.update({
345 where: {
346 id: projectId,
347 },
348 data: {
349 draft: state,
350 },
351 });
gioa71316d2025-05-24 09:41:36 +0400352 let deployKey: string | null = p.deployKey;
353 let deployKeyPublic: string | null = p.deployKeyPublic;
354 if (deployKeyPublic == null) {
355 [deployKeyPublic, deployKey] = await generateKey(tmp.dirSync().name);
356 await db.project.update({
357 where: { id: projectId },
358 data: { deployKeyPublic, deployKey },
359 });
360 }
gio3ed59592025-05-14 16:51:09 +0000361 let diff: RepoDiff | null = null;
gioc31bf142025-06-16 07:48:20 +0000362 const cfg: ConfigWithInput = {
363 ...config.data,
364 input: {
365 appId: projectId.toString(),
366 managerAddr: env.INTERNAL_API_ADDR!,
367 key: {
368 public: deployKeyPublic!,
369 private: deployKey!,
370 },
gio69148322025-06-19 23:16:12 +0400371 geminiApiKey: p.geminiApiKey ?? undefined,
gio69ff7592025-07-03 06:27:21 +0000372 anthropicApiKey: p.anthropicApiKey ?? undefined,
gioc31bf142025-06-16 07:48:20 +0000373 },
gioa71316d2025-05-24 09:41:36 +0400374 };
gio3ed59592025-05-14 16:51:09 +0000375 try {
376 if (p.instanceId == null) {
gioc31bf142025-06-16 07:48:20 +0000377 const deployResponse = await appManager.deploy(cfg);
giod0026612025-05-08 13:00:36 +0000378 await db.project.update({
379 where: {
380 id: projectId,
381 },
382 data: {
383 state,
384 draft: null,
gio3ed59592025-05-14 16:51:09 +0000385 instanceId: deployResponse.id,
giob77cb932025-05-19 09:37:14 +0000386 access: JSON.stringify(deployResponse.access),
giod0026612025-05-08 13:00:36 +0000387 },
388 });
gio3ed59592025-05-14 16:51:09 +0000389 diff = { toAdd: extractGithubRepos(state) };
gio3ed59592025-05-14 16:51:09 +0000390 } else {
gioc31bf142025-06-16 07:48:20 +0000391 const deployResponse = await appManager.update(p.instanceId, cfg);
giob77cb932025-05-19 09:37:14 +0000392 diff = calculateRepoDiff(extractGithubRepos(p.state), extractGithubRepos(state));
giob77cb932025-05-19 09:37:14 +0000393 await db.project.update({
394 where: {
395 id: projectId,
396 },
397 data: {
398 state,
399 draft: null,
400 access: JSON.stringify(deployResponse.access),
401 },
402 });
giod0026612025-05-08 13:00:36 +0000403 }
gio3ed59592025-05-14 16:51:09 +0000404 if (diff && p.githubToken && deployKey) {
405 const github = new GithubClient(p.githubToken);
gioa71316d2025-05-24 09:41:36 +0400406 await manageGithubRepos(github, diff, deployKeyPublic!, env.PUBLIC_ADDR);
giod0026612025-05-08 13:00:36 +0000407 }
gio3ed59592025-05-14 16:51:09 +0000408 resp.status(200);
409 } catch (error) {
410 console.error("Deployment error:", error);
411 resp.status(500);
412 resp.write(JSON.stringify({ error: "Deployment failed" }));
giod0026612025-05-08 13:00:36 +0000413 }
414 } catch (e) {
415 console.log(e);
416 resp.status(500);
417 } finally {
418 resp.end();
419 }
420};
421
gio10ff1342025-07-05 10:22:15 +0000422const handleSave: express.Handler = async (req, resp) => {
gio8a5f12f2025-07-05 07:02:31 +0000423 try {
424 const projectId = Number(req.params["projectId"]);
425 const p = await db.project.findUnique({
426 where: {
427 id: projectId,
gio10ff1342025-07-05 10:22:15 +0000428 userId: resp.locals.userId,
gio8a5f12f2025-07-05 07:02:31 +0000429 },
430 select: {
431 instanceId: true,
432 githubToken: true,
433 deployKey: true,
434 deployKeyPublic: true,
435 state: true,
436 geminiApiKey: true,
437 anthropicApiKey: true,
438 },
439 });
440 if (p === null) {
441 resp.status(404);
442 return;
443 }
gio10ff1342025-07-05 10:22:15 +0000444 const gc = GraphOrConfigSchema.safeParse(req.body);
445 if (!gc.success) {
gio8a5f12f2025-07-05 07:02:31 +0000446 resp.status(400);
gio10ff1342025-07-05 10:22:15 +0000447 resp.write(JSON.stringify({ error: "Invalid configuration", issues: gc.error.format() }));
448 return;
449 }
450 if (gc.data.type === "graph") {
451 await db.project.update({
452 where: { id: projectId },
453 data: { draft: JSON.stringify(gc.data.graph) },
454 });
455 resp.status(200);
gio8a5f12f2025-07-05 07:02:31 +0000456 return;
457 }
458 let repos: GithubRepository[] = [];
459 if (p.githubToken) {
460 const github = new GithubClient(p.githubToken);
461 repos = await github.getRepositories();
462 }
463 const state = JSON.stringify(
464 configToGraph(
gio10ff1342025-07-05 10:22:15 +0000465 gc.data.config,
gio8a5f12f2025-07-05 07:02:31 +0000466 getNetworks(resp.locals.username),
467 repos,
gio10ff1342025-07-05 10:22:15 +0000468 p.state ? parseGraph(p.state)!.data! : undefined,
gio8a5f12f2025-07-05 07:02:31 +0000469 ),
470 );
471 await db.project.update({
472 where: { id: projectId },
473 data: { draft: state },
474 });
475 resp.status(200);
476 } catch (e) {
477 console.log(e);
478 resp.status(500);
479 } finally {
480 resp.end();
481 }
482};
483
giod0026612025-05-08 13:00:36 +0000484const handleStatus: express.Handler = async (req, resp) => {
485 try {
486 const projectId = Number(req.params["projectId"]);
487 const p = await db.project.findUnique({
488 where: {
489 id: projectId,
gio09fcab52025-05-12 14:05:07 +0000490 userId: resp.locals.userId,
giod0026612025-05-08 13:00:36 +0000491 },
492 select: {
493 instanceId: true,
494 },
495 });
giod0026612025-05-08 13:00:36 +0000496 if (p === null) {
497 resp.status(404);
498 return;
499 }
500 if (p.instanceId == null) {
501 resp.status(404);
502 return;
503 }
gio3ed59592025-05-14 16:51:09 +0000504 try {
505 const status = await appManager.getStatus(p.instanceId);
506 resp.status(200);
507 resp.write(JSON.stringify(status));
508 } catch (error) {
509 console.error("Error getting status:", error);
510 resp.status(500);
giod0026612025-05-08 13:00:36 +0000511 }
512 } catch (e) {
513 console.log(e);
514 resp.status(500);
515 } finally {
516 resp.end();
517 }
518};
519
gioc31bf142025-06-16 07:48:20 +0000520const handleConfigGet: express.Handler = async (req, resp) => {
521 try {
522 const projectId = Number(req.params["projectId"]);
523 const project = await db.project.findUnique({
524 where: {
525 id: projectId,
526 },
527 select: {
528 state: true,
529 },
530 });
531
532 if (!project || !project.state) {
533 resp.status(404).send({ error: "No deployed configuration found." });
534 return;
535 }
536
gio10ff1342025-07-05 10:22:15 +0000537 const state = parseGraph(project.state)!.data!;
gioc31bf142025-06-16 07:48:20 +0000538 const env = await getEnv(projectId, resp.locals.userId, resp.locals.username);
539 const config = generateDodoConfig(projectId.toString(), state.nodes, env);
540
541 if (!config) {
542 resp.status(500).send({ error: "Failed to generate configuration." });
543 return;
544 }
545 resp.status(200).json(config);
546 } catch (e) {
547 console.log(e);
548 resp.status(500).send({ error: "Internal server error" });
549 } finally {
550 console.log("config get done");
551 resp.end();
552 }
553};
554
giobd37a2b2025-05-15 04:28:42 +0000555const handleRemoveDeployment: express.Handler = async (req, resp) => {
556 try {
557 const projectId = Number(req.params["projectId"]);
558 const p = await db.project.findUnique({
559 where: {
560 id: projectId,
561 userId: resp.locals.userId,
562 },
563 select: {
564 instanceId: true,
565 githubToken: true,
gioa71316d2025-05-24 09:41:36 +0400566 deployKeyPublic: true,
giobd37a2b2025-05-15 04:28:42 +0000567 state: true,
568 draft: true,
569 },
570 });
571 if (p === null) {
572 resp.status(404);
573 resp.write(JSON.stringify({ error: "Project not found" }));
574 return;
575 }
576 if (p.instanceId == null) {
577 resp.status(400);
578 resp.write(JSON.stringify({ error: "Project not deployed" }));
579 return;
580 }
581 const removed = await appManager.removeInstance(p.instanceId);
582 if (!removed) {
583 resp.status(500);
584 resp.write(JSON.stringify({ error: "Failed to remove deployment from cluster" }));
585 return;
586 }
gioa71316d2025-05-24 09:41:36 +0400587 if (p.githubToken && p.deployKeyPublic && p.state) {
giobd37a2b2025-05-15 04:28:42 +0000588 try {
589 const github = new GithubClient(p.githubToken);
590 const repos = extractGithubRepos(p.state);
591 const diff = { toDelete: repos, toAdd: [] };
gioa71316d2025-05-24 09:41:36 +0400592 await manageGithubRepos(github, diff, p.deployKeyPublic, env.PUBLIC_ADDR);
giobd37a2b2025-05-15 04:28:42 +0000593 } catch (error) {
594 console.error("Error removing GitHub deploy keys:", error);
595 }
596 }
597 await db.project.update({
598 where: {
599 id: projectId,
600 },
601 data: {
602 instanceId: null,
gioa71316d2025-05-24 09:41:36 +0400603 deployKeyPublic: null,
giob77cb932025-05-19 09:37:14 +0000604 access: null,
giobd37a2b2025-05-15 04:28:42 +0000605 state: null,
606 draft: p.draft ?? p.state,
607 },
608 });
609 resp.status(200);
610 resp.write(JSON.stringify({ success: true }));
611 } catch (e) {
612 console.error("Error removing deployment:", e);
613 resp.status(500);
614 resp.write(JSON.stringify({ error: "Internal server error" }));
615 } finally {
616 resp.end();
617 }
618};
619
giod0026612025-05-08 13:00:36 +0000620const handleGithubRepos: express.Handler = async (req, resp) => {
621 try {
622 const projectId = Number(req.params["projectId"]);
623 const project = await db.project.findUnique({
gio09fcab52025-05-12 14:05:07 +0000624 where: {
625 id: projectId,
626 userId: resp.locals.userId,
627 },
628 select: {
629 githubToken: true,
630 },
giod0026612025-05-08 13:00:36 +0000631 });
giod0026612025-05-08 13:00:36 +0000632 if (!project?.githubToken) {
633 resp.status(400);
634 resp.write(JSON.stringify({ error: "GitHub token not configured" }));
635 return;
636 }
giod0026612025-05-08 13:00:36 +0000637 const github = new GithubClient(project.githubToken);
638 const repositories = await github.getRepositories();
giod0026612025-05-08 13:00:36 +0000639 resp.status(200);
640 resp.header("Content-Type", "application/json");
641 resp.write(JSON.stringify(repositories));
642 } catch (e) {
643 console.log(e);
644 resp.status(500);
645 resp.write(JSON.stringify({ error: "Failed to fetch repositories" }));
646 } finally {
647 resp.end();
648 }
649};
650
651const handleUpdateGithubToken: express.Handler = async (req, resp) => {
652 try {
giod0026612025-05-08 13:00:36 +0000653 await db.project.update({
gio09fcab52025-05-12 14:05:07 +0000654 where: {
gio69148322025-06-19 23:16:12 +0400655 id: Number(req.params["projectId"]),
gio09fcab52025-05-12 14:05:07 +0000656 userId: resp.locals.userId,
657 },
gio69148322025-06-19 23:16:12 +0400658 data: {
659 githubToken: req.body.githubToken,
660 },
661 });
662 resp.status(200);
663 } catch (e) {
664 console.log(e);
665 resp.status(500);
666 } finally {
667 resp.end();
668 }
669};
670
671const handleUpdateGeminiToken: express.Handler = async (req, resp) => {
672 try {
673 await db.project.update({
674 where: {
675 id: Number(req.params["projectId"]),
676 userId: resp.locals.userId,
677 },
678 data: {
679 geminiApiKey: req.body.geminiApiKey,
680 },
giod0026612025-05-08 13:00:36 +0000681 });
giod0026612025-05-08 13:00:36 +0000682 resp.status(200);
683 } catch (e) {
684 console.log(e);
685 resp.status(500);
686 } finally {
687 resp.end();
688 }
689};
690
gio69ff7592025-07-03 06:27:21 +0000691const handleUpdateAnthropicToken: express.Handler = async (req, resp) => {
692 try {
693 await db.project.update({
694 where: {
695 id: Number(req.params["projectId"]),
696 userId: resp.locals.userId,
697 },
698 data: {
699 anthropicApiKey: req.body.anthropicApiKey,
700 },
701 });
702 resp.status(200);
703 } catch (e) {
704 console.log(e);
705 resp.status(500);
706 } finally {
707 resp.end();
708 }
709};
710
gioc31bf142025-06-16 07:48:20 +0000711const getNetworks = (username?: string | undefined): Network[] => {
712 return [
713 {
714 name: "Trial",
715 domain: "trial.dodoapp.xyz",
716 hasAuth: false,
717 },
718 // TODO(gio): Remove
719 ].concat(
720 username === "gio" || 1 == 1
721 ? [
722 {
723 name: "Public",
724 domain: "v1.dodo.cloud",
725 hasAuth: true,
726 },
727 {
728 name: "Private",
729 domain: "p.v1.dodo.cloud",
730 hasAuth: true,
731 },
732 ]
733 : [],
734 );
735};
736
737const getEnv = async (projectId: number, userId: string, username: string): Promise<Env> => {
738 const project = await db.project.findUnique({
739 where: {
740 id: projectId,
741 userId,
742 },
743 select: {
744 deployKeyPublic: true,
745 githubToken: true,
gio69148322025-06-19 23:16:12 +0400746 geminiApiKey: true,
gio69ff7592025-07-03 06:27:21 +0000747 anthropicApiKey: true,
gioc31bf142025-06-16 07:48:20 +0000748 access: true,
749 instanceId: true,
750 },
751 });
752 if (!project) {
753 throw new Error("Project not found");
754 }
755 const monitor = projectMonitors.get(projectId);
756 const serviceNames = monitor ? monitor.getAllServiceNames() : [];
757 const services = serviceNames.map((name: string) => ({
758 name,
759 workers: [...(monitor ? monitor.getWorkerStatusesForService(name) : new Map()).entries()].map(
760 ([id, status]) => ({
761 ...status,
762 id,
763 }),
764 ),
765 }));
766 return {
gioc31bf142025-06-16 07:48:20 +0000767 deployKeyPublic: project.deployKeyPublic == null ? undefined : project.deployKeyPublic,
768 instanceId: project.instanceId == null ? undefined : project.instanceId,
769 access: JSON.parse(project.access ?? "[]"),
770 integrations: {
771 github: !!project.githubToken,
gio69148322025-06-19 23:16:12 +0400772 gemini: !!project.geminiApiKey,
gio69ff7592025-07-03 06:27:21 +0000773 anthropic: !!project.anthropicApiKey,
gioc31bf142025-06-16 07:48:20 +0000774 },
775 networks: getNetworks(username),
776 services,
777 user: {
778 id: userId,
779 username: username,
780 },
781 };
782};
783
giod0026612025-05-08 13:00:36 +0000784const handleEnv: express.Handler = async (req, resp) => {
785 const projectId = Number(req.params["projectId"]);
786 try {
gioc31bf142025-06-16 07:48:20 +0000787 const env = await getEnv(projectId, resp.locals.userId, resp.locals.username);
giod0026612025-05-08 13:00:36 +0000788 resp.status(200);
gioc31bf142025-06-16 07:48:20 +0000789 resp.write(JSON.stringify(env));
giod0026612025-05-08 13:00:36 +0000790 } catch (error) {
gioc31bf142025-06-16 07:48:20 +0000791 console.error("Error getting env:", error);
giod0026612025-05-08 13:00:36 +0000792 resp.status(500);
793 resp.write(JSON.stringify({ error: "Internal server error" }));
794 } finally {
795 resp.end();
796 }
797};
798
gio3a921b82025-05-10 07:36:09 +0000799const handleServiceLogs: express.Handler = async (req, resp) => {
gio78a22882025-07-01 18:56:01 +0000800 const projectId = Number(req.params["projectId"]);
801 const service = req.params["service"];
802 const workerId = req.params["workerId"];
803
804 resp.setHeader("Content-Type", "text/event-stream");
805 resp.setHeader("Cache-Control", "no-cache");
806 resp.setHeader("Connection", "keep-alive");
807 resp.flushHeaders();
808
809 const timestampFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
810 const sendLogs = (logs: LogItem[]) => {
811 if (logs.length == 0) {
812 return;
813 }
814 const logString = logs
815 .map((l) => {
816 const t = Instant.ofEpochMilli(l.timestampMilli);
817 const formattedTimestamp = t.atZone(ZoneId.UTC).format(timestampFormat);
818 return `\x1b[38;5;240m${formattedTimestamp}\x1b[0m ${l.contents}`;
819 })
820 .join("\n");
821 resp.write("event: message\n");
822 resp.write(`data: ${JSON.stringify({ logs: logString })}\n\n`);
823 };
824
gio3a921b82025-05-10 07:36:09 +0000825 try {
gio09fcab52025-05-12 14:05:07 +0000826 const project = await db.project.findUnique({
gio78a22882025-07-01 18:56:01 +0000827 where: { id: projectId, userId: resp.locals.userId },
gio09fcab52025-05-12 14:05:07 +0000828 });
gio78a22882025-07-01 18:56:01 +0000829
830 if (!project) {
831 resp.status(404).end();
gio09fcab52025-05-12 14:05:07 +0000832 return;
833 }
gio78a22882025-07-01 18:56:01 +0000834
gioa1efbad2025-05-21 07:16:45 +0000835 const monitor = projectMonitors.get(projectId);
gio78a22882025-07-01 18:56:01 +0000836 if (!monitor) {
837 resp.status(404).end();
gio3a921b82025-05-10 07:36:09 +0000838 return;
839 }
gio78a22882025-07-01 18:56:01 +0000840
gio40c0c992025-07-02 13:18:05 +0000841 let lastLogId: number | undefined = undefined;
842 const initialLogs = (await logStore.get(projectId, service, workerId)) || [];
gio8a5f12f2025-07-05 07:02:31 +0000843 await sendLogs(initialLogs);
gio40c0c992025-07-02 13:18:05 +0000844 if (initialLogs.length > 0) {
845 lastLogId = initialLogs[initialLogs.length - 1].id;
846 }
gio78a22882025-07-01 18:56:01 +0000847 resp.flushHeaders();
848
gio40c0c992025-07-02 13:18:05 +0000849 const intervalId = setInterval(async () => {
850 const currentLogs = (await logStore.get(projectId, service, workerId, lastLogId)) || [];
851 if (currentLogs.length > 0) {
gio8a5f12f2025-07-05 07:02:31 +0000852 await sendLogs(currentLogs);
gio40c0c992025-07-02 13:18:05 +0000853 lastLogId = currentLogs[currentLogs.length - 1].id;
gio78a22882025-07-01 18:56:01 +0000854 }
855 }, 500);
856
857 req.on("close", () => {
858 clearInterval(intervalId);
859 resp.end();
860 });
gio3a921b82025-05-10 07:36:09 +0000861 } catch (e) {
862 console.log(e);
gio78a22882025-07-01 18:56:01 +0000863 resp.status(500).end();
gio3a921b82025-05-10 07:36:09 +0000864 }
865};
866
gio7d813702025-05-08 18:29:52 +0000867const handleRegisterWorker: express.Handler = async (req, resp) => {
868 try {
869 const projectId = Number(req.params["projectId"]);
gio7d813702025-05-08 18:29:52 +0000870 const result = WorkerSchema.safeParse(req.body);
gio7d813702025-05-08 18:29:52 +0000871 if (!result.success) {
gioa70535a2025-07-02 15:50:25 +0000872 console.log(JSON.stringify(result.error));
gio7d813702025-05-08 18:29:52 +0000873 resp.status(400);
874 resp.write(
875 JSON.stringify({
876 error: "Invalid request data",
877 details: result.error.format(),
878 }),
879 );
880 return;
881 }
gioa1efbad2025-05-21 07:16:45 +0000882 let monitor = projectMonitors.get(projectId);
883 if (!monitor) {
884 monitor = new ProjectMonitor();
885 projectMonitors.set(projectId, monitor);
gio7d813702025-05-08 18:29:52 +0000886 }
gioa1efbad2025-05-21 07:16:45 +0000887 monitor.registerWorker(result.data);
gio40c0c992025-07-02 13:18:05 +0000888 if (result.data.logs) {
889 await logStore.store(projectId, result.data.service, result.data.id, result.data.logs);
890 }
gio7d813702025-05-08 18:29:52 +0000891 resp.status(200);
892 resp.write(
893 JSON.stringify({
894 success: true,
gio78a22882025-07-01 18:56:01 +0000895 logItemsConsumed: result.data.logs?.length ?? 0,
gio7d813702025-05-08 18:29:52 +0000896 }),
897 );
898 } catch (e) {
899 console.log(e);
900 resp.status(500);
901 resp.write(JSON.stringify({ error: "Failed to register worker" }));
902 } finally {
903 resp.end();
904 }
905};
906
gio76d8ae62025-05-19 15:21:54 +0000907async function reloadProject(projectId: number): Promise<boolean> {
gioa1efbad2025-05-21 07:16:45 +0000908 const monitor = projectMonitors.get(projectId);
909 const projectWorkers = monitor ? monitor.getWorkerAddresses() : [];
gio76d8ae62025-05-19 15:21:54 +0000910 const workerCount = projectWorkers.length;
911 if (workerCount === 0) {
912 return true;
913 }
914 const results = await Promise.all(
gioc31bf142025-06-16 07:48:20 +0000915 projectWorkers.map(async (workerAddress: string) => {
916 try {
917 const { data } = await axios.get(`http://${workerAddress}/reload`);
918 return data.every((s: { status: string }) => s.status === "ok");
919 } catch (error) {
920 console.error(`Failed to reload worker ${workerAddress}:`, error);
921 return false;
922 }
gio76d8ae62025-05-19 15:21:54 +0000923 }),
924 );
gioc31bf142025-06-16 07:48:20 +0000925 return results.reduce((acc: boolean, curr: boolean) => acc && curr, true);
gio76d8ae62025-05-19 15:21:54 +0000926}
927
gio7d813702025-05-08 18:29:52 +0000928const handleReload: express.Handler = async (req, resp) => {
929 try {
930 const projectId = Number(req.params["projectId"]);
gio76d8ae62025-05-19 15:21:54 +0000931 const projectAuth = await db.project.findFirst({
gio09fcab52025-05-12 14:05:07 +0000932 where: {
933 id: projectId,
934 userId: resp.locals.userId,
935 },
gio76d8ae62025-05-19 15:21:54 +0000936 select: { id: true },
gio09fcab52025-05-12 14:05:07 +0000937 });
gio76d8ae62025-05-19 15:21:54 +0000938 if (!projectAuth) {
gio09fcab52025-05-12 14:05:07 +0000939 resp.status(404);
gio09fcab52025-05-12 14:05:07 +0000940 return;
941 }
gio76d8ae62025-05-19 15:21:54 +0000942 const success = await reloadProject(projectId);
943 if (success) {
944 resp.status(200);
945 } else {
946 resp.status(500);
gio7d813702025-05-08 18:29:52 +0000947 }
gio7d813702025-05-08 18:29:52 +0000948 } catch (e) {
gio76d8ae62025-05-19 15:21:54 +0000949 console.error(e);
gio7d813702025-05-08 18:29:52 +0000950 resp.status(500);
gio7d813702025-05-08 18:29:52 +0000951 }
952};
953
gio577d2342025-07-03 12:50:18 +0000954const handleQuitWorker: express.Handler = async (req, resp) => {
955 const projectId = Number(req.params["projectId"]);
956 const serviceName = req.params["serviceName"];
957 const workerId = req.params["workerId"];
958
959 const projectMonitor = projectMonitors.get(projectId);
960 if (!projectMonitor) {
961 resp.status(404).send({ error: "Project monitor not found" });
962 return;
963 }
964
965 try {
966 await projectMonitor.terminateWorker(serviceName, workerId);
967 resp.status(200).send({ message: "Worker termination initiated" });
968 } catch (error) {
969 console.error(
970 `Failed to terminate worker ${workerId} in service ${serviceName} for project ${projectId}:`,
971 error,
972 );
973 const errorMessage = error instanceof Error ? error.message : "Unknown error";
974 resp.status(500).send({ error: `Failed to terminate worker: ${errorMessage}` });
975 }
976};
977
gio918780d2025-05-22 08:24:41 +0000978const handleReloadWorker: express.Handler = async (req, resp) => {
979 const projectId = Number(req.params["projectId"]);
980 const serviceName = req.params["serviceName"];
981 const workerId = req.params["workerId"];
982
983 const projectMonitor = projectMonitors.get(projectId);
984 if (!projectMonitor) {
985 resp.status(404).send({ error: "Project monitor not found" });
986 return;
987 }
988
989 try {
990 await projectMonitor.reloadWorker(serviceName, workerId);
991 resp.status(200).send({ message: "Worker reload initiated" });
992 } catch (error) {
993 console.error(`Failed to reload worker ${workerId} in service ${serviceName} for project ${projectId}:`, error);
994 const errorMessage = error instanceof Error ? error.message : "Unknown error";
995 resp.status(500).send({ error: `Failed to reload worker: ${errorMessage}` });
996 }
997};
998
gioa71316d2025-05-24 09:41:36 +0400999const analyzeRepoReqSchema = z.object({
1000 address: z.string(),
1001});
1002
1003const handleAnalyzeRepo: express.Handler = async (req, resp) => {
1004 const projectId = Number(req.params["projectId"]);
1005 const project = await db.project.findUnique({
1006 where: {
1007 id: projectId,
1008 userId: resp.locals.userId,
1009 },
1010 select: {
1011 githubToken: true,
1012 deployKey: true,
1013 deployKeyPublic: true,
1014 },
1015 });
1016 if (!project) {
1017 resp.status(404).send({ error: "Project not found" });
1018 return;
1019 }
1020 if (!project.githubToken) {
1021 resp.status(400).send({ error: "GitHub token not configured" });
1022 return;
1023 }
gio8e74dc02025-06-13 10:19:26 +00001024 let tmpDir: tmp.DirResult | null = null;
1025 try {
1026 let deployKey: string | null = project.deployKey;
1027 let deployKeyPublic: string | null = project.deployKeyPublic;
1028 if (!deployKeyPublic) {
1029 [deployKeyPublic, deployKey] = await generateKey(tmp.dirSync().name);
1030 await db.project.update({
1031 where: { id: projectId },
1032 data: {
1033 deployKeyPublic: deployKeyPublic,
1034 deployKey: deployKey,
1035 },
1036 });
1037 }
1038 const github = new GithubClient(project.githubToken);
1039 const result = analyzeRepoReqSchema.safeParse(req.body);
1040 if (!result.success) {
1041 resp.status(400).send({ error: "Invalid request data" });
1042 return;
1043 }
1044 const { address } = result.data;
1045 tmpDir = tmp.dirSync({
1046 unsafeCleanup: true,
gioa71316d2025-05-24 09:41:36 +04001047 });
gio8e74dc02025-06-13 10:19:26 +00001048 await github.addDeployKey(address, deployKeyPublic);
1049 await fs.promises.writeFile(path.join(tmpDir.name, "key"), deployKey!, {
1050 mode: 0o600,
1051 });
1052 shell.exec(
1053 `GIT_SSH_COMMAND='ssh -i ${tmpDir.name}/key -o IdentitiesOnly=yes' git clone ${address} ${tmpDir.name}/code`,
1054 );
1055 const fsc = new RealFileSystem(`${tmpDir.name}/code`);
1056 const analyzer = new NodeJSAnalyzer();
1057 const info = await analyzer.analyze(fsc, "/");
1058 resp.status(200).send([info]);
1059 } catch (e) {
1060 console.error(e);
1061 resp.status(500).send({ error: "Failed to analyze repository" });
1062 } finally {
1063 if (tmpDir) {
1064 tmpDir.removeCallback();
1065 }
1066 resp.end();
gioa71316d2025-05-24 09:41:36 +04001067 }
gioa71316d2025-05-24 09:41:36 +04001068};
1069
gio09fcab52025-05-12 14:05:07 +00001070const auth = (req: express.Request, resp: express.Response, next: express.NextFunction) => {
gio69148322025-06-19 23:16:12 +04001071 // Hardcoded user for development
1072 resp.locals.userId = "1";
1073 resp.locals.username = "gio";
gio09fcab52025-05-12 14:05:07 +00001074 next();
1075};
1076
gio76d8ae62025-05-19 15:21:54 +00001077const handleGithubPushWebhook: express.Handler = async (req, resp) => {
1078 try {
1079 // TODO(gio): Implement GitHub signature verification for security
1080 const webhookSchema = z.object({
1081 repository: z.object({
1082 ssh_url: z.string(),
1083 }),
1084 });
1085
1086 const result = webhookSchema.safeParse(req.body);
1087 if (!result.success) {
1088 console.warn("GitHub webhook: Invalid payload:", result.error.issues);
1089 resp.status(400).json({ error: "Invalid webhook payload" });
1090 return;
1091 }
1092 const { ssh_url: addr } = result.data.repository;
1093 const allProjects = await db.project.findMany({
1094 select: {
1095 id: true,
1096 state: true,
1097 },
1098 where: {
1099 instanceId: {
1100 not: null,
1101 },
1102 },
1103 });
1104 // TODO(gio): This should run in background
1105 new Promise<boolean>((resolve, reject) => {
1106 setTimeout(() => {
1107 const projectsToReloadIds: number[] = [];
1108 for (const project of allProjects) {
1109 if (project.state && project.state.length > 0) {
1110 const projectRepos = extractGithubRepos(project.state);
1111 if (projectRepos.includes(addr)) {
1112 projectsToReloadIds.push(project.id);
1113 }
1114 }
1115 }
1116 Promise.all(projectsToReloadIds.map((id) => reloadProject(id)))
1117 .then((results) => {
1118 resolve(results.reduce((acc, curr) => acc && curr, true));
1119 })
1120 // eslint-disable-next-line @typescript-eslint/no-explicit-any
1121 .catch((reason: any) => reject(reason));
1122 }, 10);
1123 });
1124 // eslint-disable-next-line @typescript-eslint/no-explicit-any
1125 } catch (error: any) {
1126 console.error(error);
1127 resp.status(500);
1128 }
1129};
1130
gioc31bf142025-06-16 07:48:20 +00001131const handleValidateConfig: express.Handler = async (req, resp) => {
1132 try {
1133 const validationResult = ConfigSchema.safeParse(req.body);
1134 if (!validationResult.success) {
1135 resp.status(400);
1136 resp.header("Content-Type", "application/json");
1137 resp.write(JSON.stringify({ success: false, errors: validationResult.error.flatten() }));
1138 } else {
1139 resp.status(200);
1140 resp.header("Content-Type", "application/json");
1141 resp.write(JSON.stringify({ success: true }));
1142 }
1143 } catch (e) {
1144 console.log(e);
1145 resp.status(500);
1146 } finally {
1147 resp.end();
1148 }
1149};
1150
gio8a5f12f2025-07-05 07:02:31 +00001151function handleStateGetStream(state: "deploy" | "draft"): express.Handler {
1152 return async (req, resp) => {
1153 resp.setHeader("Content-Type", "text/event-stream");
1154 resp.setHeader("Cache-Control", "no-cache");
1155 resp.setHeader("Connection", "keep-alive");
1156 resp.flushHeaders();
1157
1158 try {
1159 let intervalId: NodeJS.Timeout | null = null;
1160 let lastState: Graph | null = null;
1161 const sendState = async () => {
1162 const currentState = await getState(Number(req.params["projectId"]), resp.locals.userId, state);
1163 if (currentState == null) {
1164 resp.status(404).end();
1165 return;
1166 }
1167 if (JSON.stringify(currentState) !== JSON.stringify(lastState)) {
1168 lastState = currentState;
1169 resp.write("event: message\n");
1170 resp.write(`data: ${JSON.stringify(currentState)}\n\n`);
1171 }
1172 intervalId = setTimeout(sendState, 500);
1173 };
1174
1175 await sendState();
1176
1177 req.on("close", () => {
1178 if (intervalId) {
1179 clearTimeout(intervalId);
1180 }
1181 resp.end();
1182 });
1183 } catch (e) {
1184 console.log(e);
1185 resp.end();
1186 }
1187 };
1188}
1189
giod0026612025-05-08 13:00:36 +00001190async function start() {
1191 await db.$connect();
1192 const app = express();
gioc31bf142025-06-16 07:48:20 +00001193 app.set("json spaces", 2);
gio76d8ae62025-05-19 15:21:54 +00001194 app.use(express.json()); // Global JSON parsing
1195
1196 // Public webhook route - no auth needed
1197 app.post("/api/webhook/github/push", handleGithubPushWebhook);
1198
1199 // Authenticated project routes
1200 const projectRouter = express.Router();
gioa71316d2025-05-24 09:41:36 +04001201 projectRouter.use(auth);
1202 projectRouter.post("/:projectId/analyze", handleAnalyzeRepo);
gio76d8ae62025-05-19 15:21:54 +00001203 projectRouter.post("/:projectId/saved", handleSave);
gio8a5f12f2025-07-05 07:02:31 +00001204 projectRouter.get("/:projectId/state/stream/deploy", handleStateGetStream("deploy"));
1205 projectRouter.get("/:projectId/state/stream/draft", handleStateGetStream("draft"));
gio76d8ae62025-05-19 15:21:54 +00001206 projectRouter.get("/:projectId/saved/deploy", handleSavedGet("deploy"));
1207 projectRouter.get("/:projectId/saved/draft", handleSavedGet("draft"));
1208 projectRouter.post("/:projectId/deploy", handleDeploy);
1209 projectRouter.get("/:projectId/status", handleStatus);
gioc31bf142025-06-16 07:48:20 +00001210 projectRouter.get("/:projectId/config", handleConfigGet);
gioa71316d2025-05-24 09:41:36 +04001211 projectRouter.delete("/:projectId", handleProjectDelete);
gio76d8ae62025-05-19 15:21:54 +00001212 projectRouter.get("/:projectId/repos/github", handleGithubRepos);
1213 projectRouter.post("/:projectId/github-token", handleUpdateGithubToken);
gio69148322025-06-19 23:16:12 +04001214 projectRouter.post("/:projectId/gemini-token", handleUpdateGeminiToken);
gio69ff7592025-07-03 06:27:21 +00001215 projectRouter.post("/:projectId/anthropic-token", handleUpdateAnthropicToken);
gio76d8ae62025-05-19 15:21:54 +00001216 projectRouter.get("/:projectId/env", handleEnv);
gio918780d2025-05-22 08:24:41 +00001217 projectRouter.post("/:projectId/reload/:serviceName/:workerId", handleReloadWorker);
gio577d2342025-07-03 12:50:18 +00001218 projectRouter.post("/:projectId/quitquitquit/:serviceName/:workerId", handleQuitWorker);
gio76d8ae62025-05-19 15:21:54 +00001219 projectRouter.post("/:projectId/reload", handleReload);
gioa1efbad2025-05-21 07:16:45 +00001220 projectRouter.get("/:projectId/logs/:service/:workerId", handleServiceLogs);
gio76d8ae62025-05-19 15:21:54 +00001221 projectRouter.post("/:projectId/remove-deployment", handleRemoveDeployment);
1222 projectRouter.get("/", handleProjectAll);
1223 projectRouter.post("/", handleProjectCreate);
gio918780d2025-05-22 08:24:41 +00001224
gio76d8ae62025-05-19 15:21:54 +00001225 app.use("/api/project", projectRouter); // Mount the authenticated router
1226
giod0026612025-05-08 13:00:36 +00001227 app.use("/", express.static("../front/dist"));
gio09fcab52025-05-12 14:05:07 +00001228
gio76d8ae62025-05-19 15:21:54 +00001229 const internalApi = express();
1230 internalApi.use(express.json());
1231 internalApi.post("/api/project/:projectId/workers", handleRegisterWorker);
gioc31bf142025-06-16 07:48:20 +00001232 internalApi.get("/api/project/:projectId/config", handleConfigGet);
gio10ff1342025-07-05 10:22:15 +00001233 internalApi.post("/api/project/:projectId/saved", handleSave);
gioc31bf142025-06-16 07:48:20 +00001234 internalApi.post("/api/project/:projectId/deploy", handleDeploy);
1235 internalApi.post("/api/validate-config", handleValidateConfig);
gio09fcab52025-05-12 14:05:07 +00001236
giod0026612025-05-08 13:00:36 +00001237 app.listen(env.DODO_PORT_WEB, () => {
gio09fcab52025-05-12 14:05:07 +00001238 console.log("Web server started on port", env.DODO_PORT_WEB);
1239 });
1240
gio76d8ae62025-05-19 15:21:54 +00001241 internalApi.listen(env.DODO_PORT_API, () => {
gio09fcab52025-05-12 14:05:07 +00001242 console.log("Internal API server started on port", env.DODO_PORT_API);
giod0026612025-05-08 13:00:36 +00001243 });
1244}
1245
1246start();