blob: 38122f8bab1a64b6c800efc3e16b790642652d7d [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";
gioa71316d2025-05-24 09:41:36 +040027
28async function generateKey(root: string): Promise<[string, string]> {
29 const privKeyPath = path.join(root, "key");
30 const pubKeyPath = path.join(root, "key.pub");
31 if (shell.exec(`ssh-keygen -t ed25519 -f ${privKeyPath} -N ""`).code !== 0) {
32 throw new Error("Failed to generate SSH key pair");
33 }
34 const publicKey = await fs.promises.readFile(pubKeyPath, "utf8");
35 const privateKey = await fs.promises.readFile(privKeyPath, "utf8");
36 return [publicKey, privateKey];
37}
giod0026612025-05-08 13:00:36 +000038
39const db = new PrismaClient();
gio40c0c992025-07-02 13:18:05 +000040const logStore = new LogStore(db);
gio3ed59592025-05-14 16:51:09 +000041const appManager = new AppManager();
giod0026612025-05-08 13:00:36 +000042
gioa1efbad2025-05-21 07:16:45 +000043const projectMonitors = new Map<number, ProjectMonitor>();
gio7d813702025-05-08 18:29:52 +000044
giod0026612025-05-08 13:00:36 +000045const handleProjectCreate: express.Handler = async (req, resp) => {
46 try {
gioa71316d2025-05-24 09:41:36 +040047 const tmpDir = tmp.dirSync().name;
48 const [publicKey, privateKey] = await generateKey(tmpDir);
giod0026612025-05-08 13:00:36 +000049 const { id } = await db.project.create({
50 data: {
gio09fcab52025-05-12 14:05:07 +000051 userId: resp.locals.userId,
giod0026612025-05-08 13:00:36 +000052 name: req.body.name,
gioa71316d2025-05-24 09:41:36 +040053 deployKey: privateKey,
54 deployKeyPublic: publicKey,
giod0026612025-05-08 13:00:36 +000055 },
56 });
57 resp.status(200);
58 resp.header("Content-Type", "application/json");
59 resp.write(
60 JSON.stringify({
gio74ab7852025-05-13 13:19:31 +000061 id: id.toString(),
giod0026612025-05-08 13:00:36 +000062 }),
63 );
64 } catch (e) {
65 console.log(e);
66 resp.status(500);
67 } finally {
68 resp.end();
69 }
70};
71
72const handleProjectAll: express.Handler = async (req, resp) => {
73 try {
74 const r = await db.project.findMany({
75 where: {
gio09fcab52025-05-12 14:05:07 +000076 userId: resp.locals.userId,
giod0026612025-05-08 13:00:36 +000077 },
78 });
79 resp.status(200);
80 resp.header("Content-Type", "application/json");
81 resp.write(
82 JSON.stringify(
83 r.map((p) => ({
84 id: p.id.toString(),
85 name: p.name,
86 })),
87 ),
88 );
89 } catch (e) {
90 console.log(e);
91 resp.status(500);
92 } finally {
93 resp.end();
94 }
95};
96
97const handleSave: express.Handler = async (req, resp) => {
98 try {
99 await db.project.update({
100 where: {
101 id: Number(req.params["projectId"]),
gio09fcab52025-05-12 14:05:07 +0000102 userId: resp.locals.userId,
giod0026612025-05-08 13:00:36 +0000103 },
104 data: {
giobd37a2b2025-05-15 04:28:42 +0000105 draft: JSON.stringify(req.body),
giod0026612025-05-08 13:00:36 +0000106 },
107 });
108 resp.status(200);
109 } catch (e) {
110 console.log(e);
111 resp.status(500);
112 } finally {
113 resp.end();
114 }
115};
116
gio8a5f12f2025-07-05 07:02:31 +0000117async function getState(projectId: number, userId: string, state: "deploy" | "draft"): Promise<Graph | null> {
118 const r = await db.project.findUnique({
119 where: {
120 id: projectId,
121 userId: userId,
122 },
123 select: {
124 state: true,
125 draft: true,
126 },
127 });
128 if (r == null) {
129 return null;
130 }
131 let currentState: Graph | null = null;
132 if (state === "deploy") {
133 if (r.state != null) {
134 currentState = JSON.parse(Buffer.from(r.state).toString("utf8"));
135 }
136 } else {
137 if (r.draft == null) {
138 if (r.state == null) {
139 currentState = {
140 nodes: [],
141 edges: [],
142 viewport: { x: 0, y: 0, zoom: 1 },
143 };
144 } else {
145 currentState = JSON.parse(Buffer.from(r.state).toString("utf8"));
146 }
147 } else {
148 currentState = JSON.parse(Buffer.from(r.draft).toString("utf8"));
149 }
150 }
151 return currentState;
152}
153
gio818da4e2025-05-12 14:45:35 +0000154function handleSavedGet(state: "deploy" | "draft"): express.Handler {
155 return async (req, resp) => {
156 try {
gio8a5f12f2025-07-05 07:02:31 +0000157 const projectId = Number(req.params["projectId"]);
158 const graph = await getState(projectId, resp.locals.userId, state);
159 if (graph == null) {
gio818da4e2025-05-12 14:45:35 +0000160 resp.status(404);
161 return;
162 }
gio8a5f12f2025-07-05 07:02:31 +0000163 const env = await getEnv(projectId, resp.locals.userId, resp.locals.username);
164 const config = generateDodoConfig(projectId.toString(), graph.nodes, env);
giod0026612025-05-08 13:00:36 +0000165 resp.status(200);
166 resp.header("content-type", "application/json");
gio8a5f12f2025-07-05 07:02:31 +0000167 resp.write(
168 JSON.stringify({
169 state: graph,
gioc31bf142025-06-16 07:48:20 +0000170 config,
gio8a5f12f2025-07-05 07:02:31 +0000171 }),
172 );
gio818da4e2025-05-12 14:45:35 +0000173 } catch (e) {
174 console.log(e);
175 resp.status(500);
176 } finally {
177 resp.end();
giod0026612025-05-08 13:00:36 +0000178 }
gio818da4e2025-05-12 14:45:35 +0000179 };
180}
giod0026612025-05-08 13:00:36 +0000181
gioa71316d2025-05-24 09:41:36 +0400182const projectDeleteReqSchema = z.object({
183 state: z.optional(z.nullable(z.string())),
184});
185
186const handleProjectDelete: express.Handler = async (req, resp) => {
giod0026612025-05-08 13:00:36 +0000187 try {
188 const projectId = Number(req.params["projectId"]);
189 const p = await db.project.findUnique({
190 where: {
191 id: projectId,
gio09fcab52025-05-12 14:05:07 +0000192 userId: resp.locals.userId,
giod0026612025-05-08 13:00:36 +0000193 },
194 select: {
195 instanceId: true,
gioa71316d2025-05-24 09:41:36 +0400196 githubToken: true,
197 deployKeyPublic: true,
198 state: true,
199 draft: true,
giod0026612025-05-08 13:00:36 +0000200 },
201 });
202 if (p === null) {
203 resp.status(404);
204 return;
205 }
gioa71316d2025-05-24 09:41:36 +0400206 const parseResult = projectDeleteReqSchema.safeParse(req.body);
207 if (!parseResult.success) {
208 resp.status(400);
209 resp.write(JSON.stringify({ error: "Invalid request body", issues: parseResult.error.format() }));
210 return;
gioe440db82025-05-13 12:21:44 +0000211 }
gioa71316d2025-05-24 09:41:36 +0400212 if (p.githubToken && p.deployKeyPublic) {
213 const allRepos = [
214 ...new Set([
215 ...extractGithubRepos(p.state),
216 ...extractGithubRepos(p.draft),
217 ...extractGithubRepos(parseResult.data.state),
218 ]),
219 ];
220 if (allRepos.length > 0) {
221 const diff: RepoDiff = { toDelete: allRepos, toAdd: [] };
222 const github = new GithubClient(p.githubToken);
223 await manageGithubRepos(github, diff, p.deployKeyPublic, env.PUBLIC_ADDR);
224 console.log(
225 `Attempted to remove deploy keys for project ${projectId} from associated GitHub repositories.`,
226 );
227 }
giod0026612025-05-08 13:00:36 +0000228 }
gioa71316d2025-05-24 09:41:36 +0400229 if (p.instanceId !== null) {
230 if (!(await appManager.removeInstance(p.instanceId))) {
231 resp.status(500);
232 resp.write(JSON.stringify({ error: "Failed to remove deployment from cluster" }));
233 return;
234 }
235 }
236 await db.project.delete({
237 where: {
238 id: projectId,
239 },
240 });
giod0026612025-05-08 13:00:36 +0000241 resp.status(200);
242 } catch (e) {
243 console.log(e);
244 resp.status(500);
245 } finally {
246 resp.end();
247 }
248};
249
gioa71316d2025-05-24 09:41:36 +0400250function extractGithubRepos(serializedState: string | null | undefined): string[] {
gio3ed59592025-05-14 16:51:09 +0000251 if (!serializedState) {
252 return [];
253 }
254 try {
giobd37a2b2025-05-15 04:28:42 +0000255 const stateObj = JSON.parse(serializedState);
gio3ed59592025-05-14 16:51:09 +0000256 const githubNodes = stateObj.nodes.filter(
257 // eslint-disable-next-line @typescript-eslint/no-explicit-any
258 (n: any) => n.type === "github" && n.data?.repository?.id,
259 );
260 // eslint-disable-next-line @typescript-eslint/no-explicit-any
261 return githubNodes.map((n: any) => n.data.repository.sshURL);
262 } catch (error) {
263 console.error("Failed to parse state or extract GitHub repos:", error);
264 return [];
265 }
266}
267
268type RepoDiff = {
269 toAdd?: string[];
270 toDelete?: string[];
271};
272
273function calculateRepoDiff(oldRepos: string[], newRepos: string[]): RepoDiff {
274 const toAdd = newRepos.filter((repo) => !oldRepos.includes(repo));
275 const toDelete = oldRepos.filter((repo) => !newRepos.includes(repo));
276 return { toAdd, toDelete };
277}
278
gio76d8ae62025-05-19 15:21:54 +0000279async function manageGithubRepos(
280 github: GithubClient,
281 diff: RepoDiff,
282 deployKey: string,
283 publicAddr?: string,
284): Promise<void> {
gio3ed59592025-05-14 16:51:09 +0000285 for (const repoUrl of diff.toDelete ?? []) {
286 try {
287 await github.removeDeployKey(repoUrl, deployKey);
288 console.log(`Removed deploy key from repository ${repoUrl}`);
gio76d8ae62025-05-19 15:21:54 +0000289 if (publicAddr) {
290 const webhookCallbackUrl = `${publicAddr}/api/webhook/github/push`;
291 await github.removePushWebhook(repoUrl, webhookCallbackUrl);
292 console.log(`Removed push webhook from repository ${repoUrl}`);
293 }
gio3ed59592025-05-14 16:51:09 +0000294 } catch (error) {
295 console.error(`Failed to remove deploy key from repository ${repoUrl}:`, error);
296 }
297 }
298 for (const repoUrl of diff.toAdd ?? []) {
299 try {
300 await github.addDeployKey(repoUrl, deployKey);
301 console.log(`Added deploy key to repository ${repoUrl}`);
gio76d8ae62025-05-19 15:21:54 +0000302 if (publicAddr) {
303 const webhookCallbackUrl = `${publicAddr}/api/webhook/github/push`;
304 await github.addPushWebhook(repoUrl, webhookCallbackUrl);
305 console.log(`Added push webhook to repository ${repoUrl}`);
306 }
gio3ed59592025-05-14 16:51:09 +0000307 } catch (error) {
308 console.error(`Failed to add deploy key from repository ${repoUrl}:`, error);
309 }
310 }
311}
312
giod0026612025-05-08 13:00:36 +0000313const handleDeploy: express.Handler = async (req, resp) => {
314 try {
315 const projectId = Number(req.params["projectId"]);
giod0026612025-05-08 13:00:36 +0000316 const p = await db.project.findUnique({
317 where: {
318 id: projectId,
gioc31bf142025-06-16 07:48:20 +0000319 // userId: resp.locals.userId, TODO(gio): validate
giod0026612025-05-08 13:00:36 +0000320 },
321 select: {
322 instanceId: true,
323 githubToken: true,
324 deployKey: true,
gioa71316d2025-05-24 09:41:36 +0400325 deployKeyPublic: true,
gio3ed59592025-05-14 16:51:09 +0000326 state: true,
gio69148322025-06-19 23:16:12 +0400327 geminiApiKey: true,
gio69ff7592025-07-03 06:27:21 +0000328 anthropicApiKey: true,
giod0026612025-05-08 13:00:36 +0000329 },
330 });
331 if (p === null) {
332 resp.status(404);
333 return;
334 }
gioc31bf142025-06-16 07:48:20 +0000335 const config = ConfigSchema.safeParse(req.body.config);
336 if (!config.success) {
337 resp.status(400);
338 resp.write(JSON.stringify({ error: "Invalid configuration", issues: config.error.format() }));
339 return;
340 }
gio9b7421a2025-06-18 12:31:13 +0000341 let repos: GithubRepository[] = [];
342 if (p.githubToken) {
343 const github = new GithubClient(p.githubToken);
344 repos = await github.getRepositories();
345 }
gioc31bf142025-06-16 07:48:20 +0000346 const state = req.body.state
347 ? JSON.stringify(req.body.state)
348 : JSON.stringify(
349 configToGraph(
350 config.data,
351 getNetworks(resp.locals.username),
gio9b7421a2025-06-18 12:31:13 +0000352 repos,
gioc31bf142025-06-16 07:48:20 +0000353 p.state ? JSON.parse(Buffer.from(p.state).toString("utf8")) : null,
354 ),
355 );
giod0026612025-05-08 13:00:36 +0000356 await db.project.update({
357 where: {
358 id: projectId,
359 },
360 data: {
361 draft: state,
362 },
363 });
gioa71316d2025-05-24 09:41:36 +0400364 let deployKey: string | null = p.deployKey;
365 let deployKeyPublic: string | null = p.deployKeyPublic;
366 if (deployKeyPublic == null) {
367 [deployKeyPublic, deployKey] = await generateKey(tmp.dirSync().name);
368 await db.project.update({
369 where: { id: projectId },
370 data: { deployKeyPublic, deployKey },
371 });
372 }
gio3ed59592025-05-14 16:51:09 +0000373 let diff: RepoDiff | null = null;
gioc31bf142025-06-16 07:48:20 +0000374 const cfg: ConfigWithInput = {
375 ...config.data,
376 input: {
377 appId: projectId.toString(),
378 managerAddr: env.INTERNAL_API_ADDR!,
379 key: {
380 public: deployKeyPublic!,
381 private: deployKey!,
382 },
gio69148322025-06-19 23:16:12 +0400383 geminiApiKey: p.geminiApiKey ?? undefined,
gio69ff7592025-07-03 06:27:21 +0000384 anthropicApiKey: p.anthropicApiKey ?? undefined,
gioc31bf142025-06-16 07:48:20 +0000385 },
gioa71316d2025-05-24 09:41:36 +0400386 };
gio3ed59592025-05-14 16:51:09 +0000387 try {
388 if (p.instanceId == null) {
gioc31bf142025-06-16 07:48:20 +0000389 const deployResponse = await appManager.deploy(cfg);
giod0026612025-05-08 13:00:36 +0000390 await db.project.update({
391 where: {
392 id: projectId,
393 },
394 data: {
395 state,
396 draft: null,
gio3ed59592025-05-14 16:51:09 +0000397 instanceId: deployResponse.id,
giob77cb932025-05-19 09:37:14 +0000398 access: JSON.stringify(deployResponse.access),
giod0026612025-05-08 13:00:36 +0000399 },
400 });
gio3ed59592025-05-14 16:51:09 +0000401 diff = { toAdd: extractGithubRepos(state) };
gio3ed59592025-05-14 16:51:09 +0000402 } else {
gioc31bf142025-06-16 07:48:20 +0000403 const deployResponse = await appManager.update(p.instanceId, cfg);
giob77cb932025-05-19 09:37:14 +0000404 diff = calculateRepoDiff(extractGithubRepos(p.state), extractGithubRepos(state));
giob77cb932025-05-19 09:37:14 +0000405 await db.project.update({
406 where: {
407 id: projectId,
408 },
409 data: {
410 state,
411 draft: null,
412 access: JSON.stringify(deployResponse.access),
413 },
414 });
giod0026612025-05-08 13:00:36 +0000415 }
gio3ed59592025-05-14 16:51:09 +0000416 if (diff && p.githubToken && deployKey) {
417 const github = new GithubClient(p.githubToken);
gioa71316d2025-05-24 09:41:36 +0400418 await manageGithubRepos(github, diff, deployKeyPublic!, env.PUBLIC_ADDR);
giod0026612025-05-08 13:00:36 +0000419 }
gio3ed59592025-05-14 16:51:09 +0000420 resp.status(200);
421 } catch (error) {
422 console.error("Deployment error:", error);
423 resp.status(500);
424 resp.write(JSON.stringify({ error: "Deployment failed" }));
giod0026612025-05-08 13:00:36 +0000425 }
426 } catch (e) {
427 console.log(e);
428 resp.status(500);
429 } finally {
430 resp.end();
431 }
432};
433
gio8a5f12f2025-07-05 07:02:31 +0000434const handleSaveFromConfig: express.Handler = async (req, resp) => {
435 try {
436 const projectId = Number(req.params["projectId"]);
437 const p = await db.project.findUnique({
438 where: {
439 id: projectId,
440 // userId: resp.locals.userId, TODO(gio): validate
441 },
442 select: {
443 instanceId: true,
444 githubToken: true,
445 deployKey: true,
446 deployKeyPublic: true,
447 state: true,
448 geminiApiKey: true,
449 anthropicApiKey: true,
450 },
451 });
452 if (p === null) {
453 resp.status(404);
454 return;
455 }
456 const config = ConfigSchema.safeParse(req.body.config);
457 if (!config.success) {
458 resp.status(400);
459 resp.write(JSON.stringify({ error: "Invalid configuration", issues: config.error.format() }));
460 return;
461 }
462 let repos: GithubRepository[] = [];
463 if (p.githubToken) {
464 const github = new GithubClient(p.githubToken);
465 repos = await github.getRepositories();
466 }
467 const state = JSON.stringify(
468 configToGraph(
469 config.data,
470 getNetworks(resp.locals.username),
471 repos,
472 p.state ? JSON.parse(Buffer.from(p.state).toString("utf8")) : null,
473 ),
474 );
475 await db.project.update({
476 where: { id: projectId },
477 data: { draft: state },
478 });
479 resp.status(200);
480 } catch (e) {
481 console.log(e);
482 resp.status(500);
483 } finally {
484 resp.end();
485 }
486};
487
giod0026612025-05-08 13:00:36 +0000488const handleStatus: express.Handler = async (req, resp) => {
489 try {
490 const projectId = Number(req.params["projectId"]);
491 const p = await db.project.findUnique({
492 where: {
493 id: projectId,
gio09fcab52025-05-12 14:05:07 +0000494 userId: resp.locals.userId,
giod0026612025-05-08 13:00:36 +0000495 },
496 select: {
497 instanceId: true,
498 },
499 });
giod0026612025-05-08 13:00:36 +0000500 if (p === null) {
501 resp.status(404);
502 return;
503 }
504 if (p.instanceId == null) {
505 resp.status(404);
506 return;
507 }
gio3ed59592025-05-14 16:51:09 +0000508 try {
509 const status = await appManager.getStatus(p.instanceId);
510 resp.status(200);
511 resp.write(JSON.stringify(status));
512 } catch (error) {
513 console.error("Error getting status:", error);
514 resp.status(500);
giod0026612025-05-08 13:00:36 +0000515 }
516 } catch (e) {
517 console.log(e);
518 resp.status(500);
519 } finally {
520 resp.end();
521 }
522};
523
gioc31bf142025-06-16 07:48:20 +0000524const handleConfigGet: express.Handler = async (req, resp) => {
525 try {
526 const projectId = Number(req.params["projectId"]);
527 const project = await db.project.findUnique({
528 where: {
529 id: projectId,
530 },
531 select: {
532 state: true,
533 },
534 });
535
536 if (!project || !project.state) {
537 resp.status(404).send({ error: "No deployed configuration found." });
538 return;
539 }
540
541 const state = JSON.parse(Buffer.from(project.state).toString("utf8"));
542 const env = await getEnv(projectId, resp.locals.userId, resp.locals.username);
543 const config = generateDodoConfig(projectId.toString(), state.nodes, env);
544
545 if (!config) {
546 resp.status(500).send({ error: "Failed to generate configuration." });
547 return;
548 }
549 resp.status(200).json(config);
550 } catch (e) {
551 console.log(e);
552 resp.status(500).send({ error: "Internal server error" });
553 } finally {
554 console.log("config get done");
555 resp.end();
556 }
557};
558
giobd37a2b2025-05-15 04:28:42 +0000559const handleRemoveDeployment: express.Handler = async (req, resp) => {
560 try {
561 const projectId = Number(req.params["projectId"]);
562 const p = await db.project.findUnique({
563 where: {
564 id: projectId,
565 userId: resp.locals.userId,
566 },
567 select: {
568 instanceId: true,
569 githubToken: true,
gioa71316d2025-05-24 09:41:36 +0400570 deployKeyPublic: true,
giobd37a2b2025-05-15 04:28:42 +0000571 state: true,
572 draft: true,
573 },
574 });
575 if (p === null) {
576 resp.status(404);
577 resp.write(JSON.stringify({ error: "Project not found" }));
578 return;
579 }
580 if (p.instanceId == null) {
581 resp.status(400);
582 resp.write(JSON.stringify({ error: "Project not deployed" }));
583 return;
584 }
585 const removed = await appManager.removeInstance(p.instanceId);
586 if (!removed) {
587 resp.status(500);
588 resp.write(JSON.stringify({ error: "Failed to remove deployment from cluster" }));
589 return;
590 }
gioa71316d2025-05-24 09:41:36 +0400591 if (p.githubToken && p.deployKeyPublic && p.state) {
giobd37a2b2025-05-15 04:28:42 +0000592 try {
593 const github = new GithubClient(p.githubToken);
594 const repos = extractGithubRepos(p.state);
595 const diff = { toDelete: repos, toAdd: [] };
gioa71316d2025-05-24 09:41:36 +0400596 await manageGithubRepos(github, diff, p.deployKeyPublic, env.PUBLIC_ADDR);
giobd37a2b2025-05-15 04:28:42 +0000597 } catch (error) {
598 console.error("Error removing GitHub deploy keys:", error);
599 }
600 }
601 await db.project.update({
602 where: {
603 id: projectId,
604 },
605 data: {
606 instanceId: null,
gioa71316d2025-05-24 09:41:36 +0400607 deployKeyPublic: null,
giob77cb932025-05-19 09:37:14 +0000608 access: null,
giobd37a2b2025-05-15 04:28:42 +0000609 state: null,
610 draft: p.draft ?? p.state,
611 },
612 });
613 resp.status(200);
614 resp.write(JSON.stringify({ success: true }));
615 } catch (e) {
616 console.error("Error removing deployment:", e);
617 resp.status(500);
618 resp.write(JSON.stringify({ error: "Internal server error" }));
619 } finally {
620 resp.end();
621 }
622};
623
giod0026612025-05-08 13:00:36 +0000624const handleGithubRepos: express.Handler = async (req, resp) => {
625 try {
626 const projectId = Number(req.params["projectId"]);
627 const project = await db.project.findUnique({
gio09fcab52025-05-12 14:05:07 +0000628 where: {
629 id: projectId,
630 userId: resp.locals.userId,
631 },
632 select: {
633 githubToken: true,
634 },
giod0026612025-05-08 13:00:36 +0000635 });
giod0026612025-05-08 13:00:36 +0000636 if (!project?.githubToken) {
637 resp.status(400);
638 resp.write(JSON.stringify({ error: "GitHub token not configured" }));
639 return;
640 }
giod0026612025-05-08 13:00:36 +0000641 const github = new GithubClient(project.githubToken);
642 const repositories = await github.getRepositories();
giod0026612025-05-08 13:00:36 +0000643 resp.status(200);
644 resp.header("Content-Type", "application/json");
645 resp.write(JSON.stringify(repositories));
646 } catch (e) {
647 console.log(e);
648 resp.status(500);
649 resp.write(JSON.stringify({ error: "Failed to fetch repositories" }));
650 } finally {
651 resp.end();
652 }
653};
654
655const handleUpdateGithubToken: express.Handler = async (req, resp) => {
656 try {
giod0026612025-05-08 13:00:36 +0000657 await db.project.update({
gio09fcab52025-05-12 14:05:07 +0000658 where: {
gio69148322025-06-19 23:16:12 +0400659 id: Number(req.params["projectId"]),
gio09fcab52025-05-12 14:05:07 +0000660 userId: resp.locals.userId,
661 },
gio69148322025-06-19 23:16:12 +0400662 data: {
663 githubToken: req.body.githubToken,
664 },
665 });
666 resp.status(200);
667 } catch (e) {
668 console.log(e);
669 resp.status(500);
670 } finally {
671 resp.end();
672 }
673};
674
675const handleUpdateGeminiToken: express.Handler = async (req, resp) => {
676 try {
677 await db.project.update({
678 where: {
679 id: Number(req.params["projectId"]),
680 userId: resp.locals.userId,
681 },
682 data: {
683 geminiApiKey: req.body.geminiApiKey,
684 },
giod0026612025-05-08 13:00:36 +0000685 });
giod0026612025-05-08 13:00:36 +0000686 resp.status(200);
687 } catch (e) {
688 console.log(e);
689 resp.status(500);
690 } finally {
691 resp.end();
692 }
693};
694
gio69ff7592025-07-03 06:27:21 +0000695const handleUpdateAnthropicToken: express.Handler = async (req, resp) => {
696 try {
697 await db.project.update({
698 where: {
699 id: Number(req.params["projectId"]),
700 userId: resp.locals.userId,
701 },
702 data: {
703 anthropicApiKey: req.body.anthropicApiKey,
704 },
705 });
706 resp.status(200);
707 } catch (e) {
708 console.log(e);
709 resp.status(500);
710 } finally {
711 resp.end();
712 }
713};
714
gioc31bf142025-06-16 07:48:20 +0000715const getNetworks = (username?: string | undefined): Network[] => {
716 return [
717 {
718 name: "Trial",
719 domain: "trial.dodoapp.xyz",
720 hasAuth: false,
721 },
722 // TODO(gio): Remove
723 ].concat(
724 username === "gio" || 1 == 1
725 ? [
726 {
727 name: "Public",
728 domain: "v1.dodo.cloud",
729 hasAuth: true,
730 },
731 {
732 name: "Private",
733 domain: "p.v1.dodo.cloud",
734 hasAuth: true,
735 },
736 ]
737 : [],
738 );
739};
740
741const getEnv = async (projectId: number, userId: string, username: string): Promise<Env> => {
742 const project = await db.project.findUnique({
743 where: {
744 id: projectId,
745 userId,
746 },
747 select: {
748 deployKeyPublic: true,
749 githubToken: true,
gio69148322025-06-19 23:16:12 +0400750 geminiApiKey: true,
gio69ff7592025-07-03 06:27:21 +0000751 anthropicApiKey: true,
gioc31bf142025-06-16 07:48:20 +0000752 access: true,
753 instanceId: true,
754 },
755 });
756 if (!project) {
757 throw new Error("Project not found");
758 }
759 const monitor = projectMonitors.get(projectId);
760 const serviceNames = monitor ? monitor.getAllServiceNames() : [];
761 const services = serviceNames.map((name: string) => ({
762 name,
763 workers: [...(monitor ? monitor.getWorkerStatusesForService(name) : new Map()).entries()].map(
764 ([id, status]) => ({
765 ...status,
766 id,
767 }),
768 ),
769 }));
770 return {
gioc31bf142025-06-16 07:48:20 +0000771 deployKeyPublic: project.deployKeyPublic == null ? undefined : project.deployKeyPublic,
772 instanceId: project.instanceId == null ? undefined : project.instanceId,
773 access: JSON.parse(project.access ?? "[]"),
774 integrations: {
775 github: !!project.githubToken,
gio69148322025-06-19 23:16:12 +0400776 gemini: !!project.geminiApiKey,
gio69ff7592025-07-03 06:27:21 +0000777 anthropic: !!project.anthropicApiKey,
gioc31bf142025-06-16 07:48:20 +0000778 },
779 networks: getNetworks(username),
780 services,
781 user: {
782 id: userId,
783 username: username,
784 },
785 };
786};
787
giod0026612025-05-08 13:00:36 +0000788const handleEnv: express.Handler = async (req, resp) => {
789 const projectId = Number(req.params["projectId"]);
790 try {
gioc31bf142025-06-16 07:48:20 +0000791 const env = await getEnv(projectId, resp.locals.userId, resp.locals.username);
giod0026612025-05-08 13:00:36 +0000792 resp.status(200);
gioc31bf142025-06-16 07:48:20 +0000793 resp.write(JSON.stringify(env));
giod0026612025-05-08 13:00:36 +0000794 } catch (error) {
gioc31bf142025-06-16 07:48:20 +0000795 console.error("Error getting env:", error);
giod0026612025-05-08 13:00:36 +0000796 resp.status(500);
797 resp.write(JSON.stringify({ error: "Internal server error" }));
798 } finally {
799 resp.end();
800 }
801};
802
gio3a921b82025-05-10 07:36:09 +0000803const handleServiceLogs: express.Handler = async (req, resp) => {
gio78a22882025-07-01 18:56:01 +0000804 const projectId = Number(req.params["projectId"]);
805 const service = req.params["service"];
806 const workerId = req.params["workerId"];
807
808 resp.setHeader("Content-Type", "text/event-stream");
809 resp.setHeader("Cache-Control", "no-cache");
810 resp.setHeader("Connection", "keep-alive");
811 resp.flushHeaders();
812
813 const timestampFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
814 const sendLogs = (logs: LogItem[]) => {
815 if (logs.length == 0) {
816 return;
817 }
818 const logString = logs
819 .map((l) => {
820 const t = Instant.ofEpochMilli(l.timestampMilli);
821 const formattedTimestamp = t.atZone(ZoneId.UTC).format(timestampFormat);
822 return `\x1b[38;5;240m${formattedTimestamp}\x1b[0m ${l.contents}`;
823 })
824 .join("\n");
825 resp.write("event: message\n");
826 resp.write(`data: ${JSON.stringify({ logs: logString })}\n\n`);
827 };
828
gio3a921b82025-05-10 07:36:09 +0000829 try {
gio09fcab52025-05-12 14:05:07 +0000830 const project = await db.project.findUnique({
gio78a22882025-07-01 18:56:01 +0000831 where: { id: projectId, userId: resp.locals.userId },
gio09fcab52025-05-12 14:05:07 +0000832 });
gio78a22882025-07-01 18:56:01 +0000833
834 if (!project) {
835 resp.status(404).end();
gio09fcab52025-05-12 14:05:07 +0000836 return;
837 }
gio78a22882025-07-01 18:56:01 +0000838
gioa1efbad2025-05-21 07:16:45 +0000839 const monitor = projectMonitors.get(projectId);
gio78a22882025-07-01 18:56:01 +0000840 if (!monitor) {
841 resp.status(404).end();
gio3a921b82025-05-10 07:36:09 +0000842 return;
843 }
gio78a22882025-07-01 18:56:01 +0000844
gio40c0c992025-07-02 13:18:05 +0000845 let lastLogId: number | undefined = undefined;
846 const initialLogs = (await logStore.get(projectId, service, workerId)) || [];
gio8a5f12f2025-07-05 07:02:31 +0000847 await sendLogs(initialLogs);
gio40c0c992025-07-02 13:18:05 +0000848 if (initialLogs.length > 0) {
849 lastLogId = initialLogs[initialLogs.length - 1].id;
850 }
gio78a22882025-07-01 18:56:01 +0000851 resp.flushHeaders();
852
gio40c0c992025-07-02 13:18:05 +0000853 const intervalId = setInterval(async () => {
854 const currentLogs = (await logStore.get(projectId, service, workerId, lastLogId)) || [];
855 if (currentLogs.length > 0) {
gio8a5f12f2025-07-05 07:02:31 +0000856 await sendLogs(currentLogs);
gio40c0c992025-07-02 13:18:05 +0000857 lastLogId = currentLogs[currentLogs.length - 1].id;
gio78a22882025-07-01 18:56:01 +0000858 }
859 }, 500);
860
861 req.on("close", () => {
862 clearInterval(intervalId);
863 resp.end();
864 });
gio3a921b82025-05-10 07:36:09 +0000865 } catch (e) {
866 console.log(e);
gio78a22882025-07-01 18:56:01 +0000867 resp.status(500).end();
gio3a921b82025-05-10 07:36:09 +0000868 }
869};
870
gio7d813702025-05-08 18:29:52 +0000871const handleRegisterWorker: express.Handler = async (req, resp) => {
872 try {
873 const projectId = Number(req.params["projectId"]);
gio7d813702025-05-08 18:29:52 +0000874 const result = WorkerSchema.safeParse(req.body);
gio7d813702025-05-08 18:29:52 +0000875 if (!result.success) {
gioa70535a2025-07-02 15:50:25 +0000876 console.log(JSON.stringify(result.error));
gio7d813702025-05-08 18:29:52 +0000877 resp.status(400);
878 resp.write(
879 JSON.stringify({
880 error: "Invalid request data",
881 details: result.error.format(),
882 }),
883 );
884 return;
885 }
gioa1efbad2025-05-21 07:16:45 +0000886 let monitor = projectMonitors.get(projectId);
887 if (!monitor) {
888 monitor = new ProjectMonitor();
889 projectMonitors.set(projectId, monitor);
gio7d813702025-05-08 18:29:52 +0000890 }
gioa1efbad2025-05-21 07:16:45 +0000891 monitor.registerWorker(result.data);
gio40c0c992025-07-02 13:18:05 +0000892 if (result.data.logs) {
893 await logStore.store(projectId, result.data.service, result.data.id, result.data.logs);
894 }
gio7d813702025-05-08 18:29:52 +0000895 resp.status(200);
896 resp.write(
897 JSON.stringify({
898 success: true,
gio78a22882025-07-01 18:56:01 +0000899 logItemsConsumed: result.data.logs?.length ?? 0,
gio7d813702025-05-08 18:29:52 +0000900 }),
901 );
902 } catch (e) {
903 console.log(e);
904 resp.status(500);
905 resp.write(JSON.stringify({ error: "Failed to register worker" }));
906 } finally {
907 resp.end();
908 }
909};
910
gio76d8ae62025-05-19 15:21:54 +0000911async function reloadProject(projectId: number): Promise<boolean> {
gioa1efbad2025-05-21 07:16:45 +0000912 const monitor = projectMonitors.get(projectId);
913 const projectWorkers = monitor ? monitor.getWorkerAddresses() : [];
gio76d8ae62025-05-19 15:21:54 +0000914 const workerCount = projectWorkers.length;
915 if (workerCount === 0) {
916 return true;
917 }
918 const results = await Promise.all(
gioc31bf142025-06-16 07:48:20 +0000919 projectWorkers.map(async (workerAddress: string) => {
920 try {
921 const { data } = await axios.get(`http://${workerAddress}/reload`);
922 return data.every((s: { status: string }) => s.status === "ok");
923 } catch (error) {
924 console.error(`Failed to reload worker ${workerAddress}:`, error);
925 return false;
926 }
gio76d8ae62025-05-19 15:21:54 +0000927 }),
928 );
gioc31bf142025-06-16 07:48:20 +0000929 return results.reduce((acc: boolean, curr: boolean) => acc && curr, true);
gio76d8ae62025-05-19 15:21:54 +0000930}
931
gio7d813702025-05-08 18:29:52 +0000932const handleReload: express.Handler = async (req, resp) => {
933 try {
934 const projectId = Number(req.params["projectId"]);
gio76d8ae62025-05-19 15:21:54 +0000935 const projectAuth = await db.project.findFirst({
gio09fcab52025-05-12 14:05:07 +0000936 where: {
937 id: projectId,
938 userId: resp.locals.userId,
939 },
gio76d8ae62025-05-19 15:21:54 +0000940 select: { id: true },
gio09fcab52025-05-12 14:05:07 +0000941 });
gio76d8ae62025-05-19 15:21:54 +0000942 if (!projectAuth) {
gio09fcab52025-05-12 14:05:07 +0000943 resp.status(404);
gio09fcab52025-05-12 14:05:07 +0000944 return;
945 }
gio76d8ae62025-05-19 15:21:54 +0000946 const success = await reloadProject(projectId);
947 if (success) {
948 resp.status(200);
949 } else {
950 resp.status(500);
gio7d813702025-05-08 18:29:52 +0000951 }
gio7d813702025-05-08 18:29:52 +0000952 } catch (e) {
gio76d8ae62025-05-19 15:21:54 +0000953 console.error(e);
gio7d813702025-05-08 18:29:52 +0000954 resp.status(500);
gio7d813702025-05-08 18:29:52 +0000955 }
956};
957
gio577d2342025-07-03 12:50:18 +0000958const handleQuitWorker: express.Handler = async (req, resp) => {
959 const projectId = Number(req.params["projectId"]);
960 const serviceName = req.params["serviceName"];
961 const workerId = req.params["workerId"];
962
963 const projectMonitor = projectMonitors.get(projectId);
964 if (!projectMonitor) {
965 resp.status(404).send({ error: "Project monitor not found" });
966 return;
967 }
968
969 try {
970 await projectMonitor.terminateWorker(serviceName, workerId);
971 resp.status(200).send({ message: "Worker termination initiated" });
972 } catch (error) {
973 console.error(
974 `Failed to terminate worker ${workerId} in service ${serviceName} for project ${projectId}:`,
975 error,
976 );
977 const errorMessage = error instanceof Error ? error.message : "Unknown error";
978 resp.status(500).send({ error: `Failed to terminate worker: ${errorMessage}` });
979 }
980};
981
gio918780d2025-05-22 08:24:41 +0000982const handleReloadWorker: express.Handler = async (req, resp) => {
983 const projectId = Number(req.params["projectId"]);
984 const serviceName = req.params["serviceName"];
985 const workerId = req.params["workerId"];
986
987 const projectMonitor = projectMonitors.get(projectId);
988 if (!projectMonitor) {
989 resp.status(404).send({ error: "Project monitor not found" });
990 return;
991 }
992
993 try {
994 await projectMonitor.reloadWorker(serviceName, workerId);
995 resp.status(200).send({ message: "Worker reload initiated" });
996 } catch (error) {
997 console.error(`Failed to reload worker ${workerId} in service ${serviceName} for project ${projectId}:`, error);
998 const errorMessage = error instanceof Error ? error.message : "Unknown error";
999 resp.status(500).send({ error: `Failed to reload worker: ${errorMessage}` });
1000 }
1001};
1002
gioa71316d2025-05-24 09:41:36 +04001003const analyzeRepoReqSchema = z.object({
1004 address: z.string(),
1005});
1006
1007const handleAnalyzeRepo: express.Handler = async (req, resp) => {
1008 const projectId = Number(req.params["projectId"]);
1009 const project = await db.project.findUnique({
1010 where: {
1011 id: projectId,
1012 userId: resp.locals.userId,
1013 },
1014 select: {
1015 githubToken: true,
1016 deployKey: true,
1017 deployKeyPublic: true,
1018 },
1019 });
1020 if (!project) {
1021 resp.status(404).send({ error: "Project not found" });
1022 return;
1023 }
1024 if (!project.githubToken) {
1025 resp.status(400).send({ error: "GitHub token not configured" });
1026 return;
1027 }
gio8e74dc02025-06-13 10:19:26 +00001028 let tmpDir: tmp.DirResult | null = null;
1029 try {
1030 let deployKey: string | null = project.deployKey;
1031 let deployKeyPublic: string | null = project.deployKeyPublic;
1032 if (!deployKeyPublic) {
1033 [deployKeyPublic, deployKey] = await generateKey(tmp.dirSync().name);
1034 await db.project.update({
1035 where: { id: projectId },
1036 data: {
1037 deployKeyPublic: deployKeyPublic,
1038 deployKey: deployKey,
1039 },
1040 });
1041 }
1042 const github = new GithubClient(project.githubToken);
1043 const result = analyzeRepoReqSchema.safeParse(req.body);
1044 if (!result.success) {
1045 resp.status(400).send({ error: "Invalid request data" });
1046 return;
1047 }
1048 const { address } = result.data;
1049 tmpDir = tmp.dirSync({
1050 unsafeCleanup: true,
gioa71316d2025-05-24 09:41:36 +04001051 });
gio8e74dc02025-06-13 10:19:26 +00001052 await github.addDeployKey(address, deployKeyPublic);
1053 await fs.promises.writeFile(path.join(tmpDir.name, "key"), deployKey!, {
1054 mode: 0o600,
1055 });
1056 shell.exec(
1057 `GIT_SSH_COMMAND='ssh -i ${tmpDir.name}/key -o IdentitiesOnly=yes' git clone ${address} ${tmpDir.name}/code`,
1058 );
1059 const fsc = new RealFileSystem(`${tmpDir.name}/code`);
1060 const analyzer = new NodeJSAnalyzer();
1061 const info = await analyzer.analyze(fsc, "/");
1062 resp.status(200).send([info]);
1063 } catch (e) {
1064 console.error(e);
1065 resp.status(500).send({ error: "Failed to analyze repository" });
1066 } finally {
1067 if (tmpDir) {
1068 tmpDir.removeCallback();
1069 }
1070 resp.end();
gioa71316d2025-05-24 09:41:36 +04001071 }
gioa71316d2025-05-24 09:41:36 +04001072};
1073
gio09fcab52025-05-12 14:05:07 +00001074const auth = (req: express.Request, resp: express.Response, next: express.NextFunction) => {
gio69148322025-06-19 23:16:12 +04001075 // Hardcoded user for development
1076 resp.locals.userId = "1";
1077 resp.locals.username = "gio";
gio09fcab52025-05-12 14:05:07 +00001078 next();
1079};
1080
gio76d8ae62025-05-19 15:21:54 +00001081const handleGithubPushWebhook: express.Handler = async (req, resp) => {
1082 try {
1083 // TODO(gio): Implement GitHub signature verification for security
1084 const webhookSchema = z.object({
1085 repository: z.object({
1086 ssh_url: z.string(),
1087 }),
1088 });
1089
1090 const result = webhookSchema.safeParse(req.body);
1091 if (!result.success) {
1092 console.warn("GitHub webhook: Invalid payload:", result.error.issues);
1093 resp.status(400).json({ error: "Invalid webhook payload" });
1094 return;
1095 }
1096 const { ssh_url: addr } = result.data.repository;
1097 const allProjects = await db.project.findMany({
1098 select: {
1099 id: true,
1100 state: true,
1101 },
1102 where: {
1103 instanceId: {
1104 not: null,
1105 },
1106 },
1107 });
1108 // TODO(gio): This should run in background
1109 new Promise<boolean>((resolve, reject) => {
1110 setTimeout(() => {
1111 const projectsToReloadIds: number[] = [];
1112 for (const project of allProjects) {
1113 if (project.state && project.state.length > 0) {
1114 const projectRepos = extractGithubRepos(project.state);
1115 if (projectRepos.includes(addr)) {
1116 projectsToReloadIds.push(project.id);
1117 }
1118 }
1119 }
1120 Promise.all(projectsToReloadIds.map((id) => reloadProject(id)))
1121 .then((results) => {
1122 resolve(results.reduce((acc, curr) => acc && curr, true));
1123 })
1124 // eslint-disable-next-line @typescript-eslint/no-explicit-any
1125 .catch((reason: any) => reject(reason));
1126 }, 10);
1127 });
1128 // eslint-disable-next-line @typescript-eslint/no-explicit-any
1129 } catch (error: any) {
1130 console.error(error);
1131 resp.status(500);
1132 }
1133};
1134
gioc31bf142025-06-16 07:48:20 +00001135const handleValidateConfig: express.Handler = async (req, resp) => {
1136 try {
1137 const validationResult = ConfigSchema.safeParse(req.body);
1138 if (!validationResult.success) {
1139 resp.status(400);
1140 resp.header("Content-Type", "application/json");
1141 resp.write(JSON.stringify({ success: false, errors: validationResult.error.flatten() }));
1142 } else {
1143 resp.status(200);
1144 resp.header("Content-Type", "application/json");
1145 resp.write(JSON.stringify({ success: true }));
1146 }
1147 } catch (e) {
1148 console.log(e);
1149 resp.status(500);
1150 } finally {
1151 resp.end();
1152 }
1153};
1154
gio8a5f12f2025-07-05 07:02:31 +00001155function handleStateGetStream(state: "deploy" | "draft"): express.Handler {
1156 return async (req, resp) => {
1157 resp.setHeader("Content-Type", "text/event-stream");
1158 resp.setHeader("Cache-Control", "no-cache");
1159 resp.setHeader("Connection", "keep-alive");
1160 resp.flushHeaders();
1161
1162 try {
1163 let intervalId: NodeJS.Timeout | null = null;
1164 let lastState: Graph | null = null;
1165 const sendState = async () => {
1166 const currentState = await getState(Number(req.params["projectId"]), resp.locals.userId, state);
1167 if (currentState == null) {
1168 resp.status(404).end();
1169 return;
1170 }
1171 if (JSON.stringify(currentState) !== JSON.stringify(lastState)) {
1172 lastState = currentState;
1173 resp.write("event: message\n");
1174 resp.write(`data: ${JSON.stringify(currentState)}\n\n`);
1175 }
1176 intervalId = setTimeout(sendState, 500);
1177 };
1178
1179 await sendState();
1180
1181 req.on("close", () => {
1182 if (intervalId) {
1183 clearTimeout(intervalId);
1184 }
1185 resp.end();
1186 });
1187 } catch (e) {
1188 console.log(e);
1189 resp.end();
1190 }
1191 };
1192}
1193
giod0026612025-05-08 13:00:36 +00001194async function start() {
1195 await db.$connect();
1196 const app = express();
gioc31bf142025-06-16 07:48:20 +00001197 app.set("json spaces", 2);
gio76d8ae62025-05-19 15:21:54 +00001198 app.use(express.json()); // Global JSON parsing
1199
1200 // Public webhook route - no auth needed
1201 app.post("/api/webhook/github/push", handleGithubPushWebhook);
1202
1203 // Authenticated project routes
1204 const projectRouter = express.Router();
gioa71316d2025-05-24 09:41:36 +04001205 projectRouter.use(auth);
1206 projectRouter.post("/:projectId/analyze", handleAnalyzeRepo);
gio8a5f12f2025-07-05 07:02:31 +00001207 projectRouter.post("/:projectId/saved/config", handleSaveFromConfig);
gio76d8ae62025-05-19 15:21:54 +00001208 projectRouter.post("/:projectId/saved", handleSave);
gio8a5f12f2025-07-05 07:02:31 +00001209 projectRouter.get("/:projectId/state/stream/deploy", handleStateGetStream("deploy"));
1210 projectRouter.get("/:projectId/state/stream/draft", handleStateGetStream("draft"));
gio76d8ae62025-05-19 15:21:54 +00001211 projectRouter.get("/:projectId/saved/deploy", handleSavedGet("deploy"));
1212 projectRouter.get("/:projectId/saved/draft", handleSavedGet("draft"));
1213 projectRouter.post("/:projectId/deploy", handleDeploy);
1214 projectRouter.get("/:projectId/status", handleStatus);
gioc31bf142025-06-16 07:48:20 +00001215 projectRouter.get("/:projectId/config", handleConfigGet);
gioa71316d2025-05-24 09:41:36 +04001216 projectRouter.delete("/:projectId", handleProjectDelete);
gio76d8ae62025-05-19 15:21:54 +00001217 projectRouter.get("/:projectId/repos/github", handleGithubRepos);
1218 projectRouter.post("/:projectId/github-token", handleUpdateGithubToken);
gio69148322025-06-19 23:16:12 +04001219 projectRouter.post("/:projectId/gemini-token", handleUpdateGeminiToken);
gio69ff7592025-07-03 06:27:21 +00001220 projectRouter.post("/:projectId/anthropic-token", handleUpdateAnthropicToken);
gio76d8ae62025-05-19 15:21:54 +00001221 projectRouter.get("/:projectId/env", handleEnv);
gio918780d2025-05-22 08:24:41 +00001222 projectRouter.post("/:projectId/reload/:serviceName/:workerId", handleReloadWorker);
gio577d2342025-07-03 12:50:18 +00001223 projectRouter.post("/:projectId/quitquitquit/:serviceName/:workerId", handleQuitWorker);
gio76d8ae62025-05-19 15:21:54 +00001224 projectRouter.post("/:projectId/reload", handleReload);
gioa1efbad2025-05-21 07:16:45 +00001225 projectRouter.get("/:projectId/logs/:service/:workerId", handleServiceLogs);
gio76d8ae62025-05-19 15:21:54 +00001226 projectRouter.post("/:projectId/remove-deployment", handleRemoveDeployment);
1227 projectRouter.get("/", handleProjectAll);
1228 projectRouter.post("/", handleProjectCreate);
gio918780d2025-05-22 08:24:41 +00001229
gio76d8ae62025-05-19 15:21:54 +00001230 app.use("/api/project", projectRouter); // Mount the authenticated router
1231
giod0026612025-05-08 13:00:36 +00001232 app.use("/", express.static("../front/dist"));
gio09fcab52025-05-12 14:05:07 +00001233
gio76d8ae62025-05-19 15:21:54 +00001234 const internalApi = express();
1235 internalApi.use(express.json());
1236 internalApi.post("/api/project/:projectId/workers", handleRegisterWorker);
gioc31bf142025-06-16 07:48:20 +00001237 internalApi.get("/api/project/:projectId/config", handleConfigGet);
1238 internalApi.post("/api/project/:projectId/deploy", handleDeploy);
1239 internalApi.post("/api/validate-config", handleValidateConfig);
gio09fcab52025-05-12 14:05:07 +00001240
giod0026612025-05-08 13:00:36 +00001241 app.listen(env.DODO_PORT_WEB, () => {
gio09fcab52025-05-12 14:05:07 +00001242 console.log("Web server started on port", env.DODO_PORT_WEB);
1243 });
1244
gio76d8ae62025-05-19 15:21:54 +00001245 internalApi.listen(env.DODO_PORT_API, () => {
gio09fcab52025-05-12 14:05:07 +00001246 console.log("Internal API server started on port", env.DODO_PORT_API);
giod0026612025-05-08 13:00:36 +00001247 });
1248}
1249
1250start();