blob: de185824d65f6225af52d725cf71f04df8e798fc [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";
gio3ed59592025-05-14 16:51:09 +00006import { AppManager } from "./app_manager";
gio7d813702025-05-08 18:29:52 +00007import { z } from "zod";
gioa1efbad2025-05-21 07:16:45 +00008import { ProjectMonitor, WorkerSchema } from "./project_monitor";
giod0026612025-05-08 13:00:36 +00009
10const db = new PrismaClient();
gio3ed59592025-05-14 16:51:09 +000011const appManager = new AppManager();
giod0026612025-05-08 13:00:36 +000012
gioa1efbad2025-05-21 07:16:45 +000013const projectMonitors = new Map<number, ProjectMonitor>();
gio7d813702025-05-08 18:29:52 +000014
giod0026612025-05-08 13:00:36 +000015const handleProjectCreate: express.Handler = async (req, resp) => {
16 try {
17 const { id } = await db.project.create({
18 data: {
gio09fcab52025-05-12 14:05:07 +000019 userId: resp.locals.userId,
giod0026612025-05-08 13:00:36 +000020 name: req.body.name,
21 },
22 });
23 resp.status(200);
24 resp.header("Content-Type", "application/json");
25 resp.write(
26 JSON.stringify({
gio74ab7852025-05-13 13:19:31 +000027 id: id.toString(),
giod0026612025-05-08 13:00:36 +000028 }),
29 );
30 } catch (e) {
31 console.log(e);
32 resp.status(500);
33 } finally {
34 resp.end();
35 }
36};
37
38const handleProjectAll: express.Handler = async (req, resp) => {
39 try {
40 const r = await db.project.findMany({
41 where: {
gio09fcab52025-05-12 14:05:07 +000042 userId: resp.locals.userId,
giod0026612025-05-08 13:00:36 +000043 },
44 });
45 resp.status(200);
46 resp.header("Content-Type", "application/json");
47 resp.write(
48 JSON.stringify(
49 r.map((p) => ({
50 id: p.id.toString(),
51 name: p.name,
52 })),
53 ),
54 );
55 } catch (e) {
56 console.log(e);
57 resp.status(500);
58 } finally {
59 resp.end();
60 }
61};
62
63const handleSave: express.Handler = async (req, resp) => {
64 try {
65 await db.project.update({
66 where: {
67 id: Number(req.params["projectId"]),
gio09fcab52025-05-12 14:05:07 +000068 userId: resp.locals.userId,
giod0026612025-05-08 13:00:36 +000069 },
70 data: {
giobd37a2b2025-05-15 04:28:42 +000071 draft: JSON.stringify(req.body),
giod0026612025-05-08 13:00:36 +000072 },
73 });
74 resp.status(200);
75 } catch (e) {
76 console.log(e);
77 resp.status(500);
78 } finally {
79 resp.end();
80 }
81};
82
gio818da4e2025-05-12 14:45:35 +000083function handleSavedGet(state: "deploy" | "draft"): express.Handler {
84 return async (req, resp) => {
85 try {
86 const r = await db.project.findUnique({
87 where: {
88 id: Number(req.params["projectId"]),
89 userId: resp.locals.userId,
90 },
91 select: {
92 state: true,
93 draft: true,
94 },
95 });
96 if (r == null) {
97 resp.status(404);
98 return;
99 }
giod0026612025-05-08 13:00:36 +0000100 resp.status(200);
101 resp.header("content-type", "application/json");
gio818da4e2025-05-12 14:45:35 +0000102 if (state === "deploy") {
giod0026612025-05-08 13:00:36 +0000103 if (r.state == null) {
104 resp.send({
105 nodes: [],
106 edges: [],
107 viewport: { x: 0, y: 0, zoom: 1 },
108 });
109 } else {
110 resp.send(JSON.parse(Buffer.from(r.state).toString("utf8")));
111 }
112 } else {
gio818da4e2025-05-12 14:45:35 +0000113 if (r.draft == null) {
114 if (r.state == null) {
115 resp.send({
116 nodes: [],
117 edges: [],
118 viewport: { x: 0, y: 0, zoom: 1 },
119 });
120 } else {
121 resp.send(JSON.parse(Buffer.from(r.state).toString("utf8")));
122 }
123 } else {
124 resp.send(JSON.parse(Buffer.from(r.draft).toString("utf8")));
125 }
giod0026612025-05-08 13:00:36 +0000126 }
gio818da4e2025-05-12 14:45:35 +0000127 } catch (e) {
128 console.log(e);
129 resp.status(500);
130 } finally {
131 resp.end();
giod0026612025-05-08 13:00:36 +0000132 }
gio818da4e2025-05-12 14:45:35 +0000133 };
134}
giod0026612025-05-08 13:00:36 +0000135
136const handleDelete: express.Handler = async (req, resp) => {
137 try {
138 const projectId = Number(req.params["projectId"]);
139 const p = await db.project.findUnique({
140 where: {
141 id: projectId,
gio09fcab52025-05-12 14:05:07 +0000142 userId: resp.locals.userId,
giod0026612025-05-08 13:00:36 +0000143 },
144 select: {
145 instanceId: true,
146 },
147 });
148 if (p === null) {
149 resp.status(404);
150 return;
151 }
gioe440db82025-05-13 12:21:44 +0000152 let ok = false;
153 if (p.instanceId === null) {
154 ok = true;
155 } else {
gio3ed59592025-05-14 16:51:09 +0000156 ok = await appManager.removeInstance(p.instanceId);
gioe440db82025-05-13 12:21:44 +0000157 }
158 if (ok) {
giod0026612025-05-08 13:00:36 +0000159 await db.project.delete({
160 where: {
161 id: projectId,
162 },
163 });
164 }
165 resp.status(200);
166 } catch (e) {
167 console.log(e);
168 resp.status(500);
169 } finally {
170 resp.end();
171 }
172};
173
giobd37a2b2025-05-15 04:28:42 +0000174function extractGithubRepos(serializedState: string | null): string[] {
gio3ed59592025-05-14 16:51:09 +0000175 if (!serializedState) {
176 return [];
177 }
178 try {
giobd37a2b2025-05-15 04:28:42 +0000179 const stateObj = JSON.parse(serializedState);
gio3ed59592025-05-14 16:51:09 +0000180 const githubNodes = stateObj.nodes.filter(
181 // eslint-disable-next-line @typescript-eslint/no-explicit-any
182 (n: any) => n.type === "github" && n.data?.repository?.id,
183 );
184 // eslint-disable-next-line @typescript-eslint/no-explicit-any
185 return githubNodes.map((n: any) => n.data.repository.sshURL);
186 } catch (error) {
187 console.error("Failed to parse state or extract GitHub repos:", error);
188 return [];
189 }
190}
191
192type RepoDiff = {
193 toAdd?: string[];
194 toDelete?: string[];
195};
196
197function calculateRepoDiff(oldRepos: string[], newRepos: string[]): RepoDiff {
198 const toAdd = newRepos.filter((repo) => !oldRepos.includes(repo));
199 const toDelete = oldRepos.filter((repo) => !newRepos.includes(repo));
200 return { toAdd, toDelete };
201}
202
gio76d8ae62025-05-19 15:21:54 +0000203async function manageGithubRepos(
204 github: GithubClient,
205 diff: RepoDiff,
206 deployKey: string,
207 publicAddr?: string,
208): Promise<void> {
209 console.log(publicAddr);
gio3ed59592025-05-14 16:51:09 +0000210 for (const repoUrl of diff.toDelete ?? []) {
211 try {
212 await github.removeDeployKey(repoUrl, deployKey);
213 console.log(`Removed deploy key from repository ${repoUrl}`);
gio76d8ae62025-05-19 15:21:54 +0000214 if (publicAddr) {
215 const webhookCallbackUrl = `${publicAddr}/api/webhook/github/push`;
216 await github.removePushWebhook(repoUrl, webhookCallbackUrl);
217 console.log(`Removed push webhook from repository ${repoUrl}`);
218 }
gio3ed59592025-05-14 16:51:09 +0000219 } catch (error) {
220 console.error(`Failed to remove deploy key from repository ${repoUrl}:`, error);
221 }
222 }
223 for (const repoUrl of diff.toAdd ?? []) {
224 try {
225 await github.addDeployKey(repoUrl, deployKey);
226 console.log(`Added deploy key to repository ${repoUrl}`);
gio76d8ae62025-05-19 15:21:54 +0000227 if (publicAddr) {
228 const webhookCallbackUrl = `${publicAddr}/api/webhook/github/push`;
229 await github.addPushWebhook(repoUrl, webhookCallbackUrl);
230 console.log(`Added push webhook to repository ${repoUrl}`);
231 }
gio3ed59592025-05-14 16:51:09 +0000232 } catch (error) {
233 console.error(`Failed to add deploy key from repository ${repoUrl}:`, error);
234 }
235 }
236}
237
giod0026612025-05-08 13:00:36 +0000238const handleDeploy: express.Handler = async (req, resp) => {
239 try {
240 const projectId = Number(req.params["projectId"]);
giobd37a2b2025-05-15 04:28:42 +0000241 const state = JSON.stringify(req.body.state);
giod0026612025-05-08 13:00:36 +0000242 const p = await db.project.findUnique({
243 where: {
244 id: projectId,
gio09fcab52025-05-12 14:05:07 +0000245 userId: resp.locals.userId,
giod0026612025-05-08 13:00:36 +0000246 },
247 select: {
248 instanceId: true,
249 githubToken: true,
250 deployKey: true,
gio3ed59592025-05-14 16:51:09 +0000251 state: true,
giod0026612025-05-08 13:00:36 +0000252 },
253 });
254 if (p === null) {
255 resp.status(404);
256 return;
257 }
258 await db.project.update({
259 where: {
260 id: projectId,
261 },
262 data: {
263 draft: state,
264 },
265 });
gio3ed59592025-05-14 16:51:09 +0000266 let diff: RepoDiff | null = null;
267 let deployKey: string | null = null;
268 try {
269 if (p.instanceId == null) {
270 const deployResponse = await appManager.deploy(req.body.config);
giod0026612025-05-08 13:00:36 +0000271 await db.project.update({
272 where: {
273 id: projectId,
274 },
275 data: {
276 state,
277 draft: null,
gio3ed59592025-05-14 16:51:09 +0000278 instanceId: deployResponse.id,
279 deployKey: deployResponse.deployKey,
giob77cb932025-05-19 09:37:14 +0000280 access: JSON.stringify(deployResponse.access),
giod0026612025-05-08 13:00:36 +0000281 },
282 });
gio3ed59592025-05-14 16:51:09 +0000283 diff = { toAdd: extractGithubRepos(state) };
284 deployKey = deployResponse.deployKey;
285 } else {
giob77cb932025-05-19 09:37:14 +0000286 const deployResponse = await appManager.update(p.instanceId, req.body.config);
287 diff = calculateRepoDiff(extractGithubRepos(p.state), extractGithubRepos(state));
288 deployKey = p.deployKey;
289 await db.project.update({
290 where: {
291 id: projectId,
292 },
293 data: {
294 state,
295 draft: null,
296 access: JSON.stringify(deployResponse.access),
297 },
298 });
giod0026612025-05-08 13:00:36 +0000299 }
gio3ed59592025-05-14 16:51:09 +0000300 if (diff && p.githubToken && deployKey) {
301 const github = new GithubClient(p.githubToken);
gio76d8ae62025-05-19 15:21:54 +0000302 await manageGithubRepos(github, diff, deployKey, env.PUBLIC_ADDR);
giod0026612025-05-08 13:00:36 +0000303 }
gio3ed59592025-05-14 16:51:09 +0000304 resp.status(200);
305 } catch (error) {
306 console.error("Deployment error:", error);
307 resp.status(500);
308 resp.write(JSON.stringify({ error: "Deployment failed" }));
giod0026612025-05-08 13:00:36 +0000309 }
310 } catch (e) {
311 console.log(e);
312 resp.status(500);
313 } finally {
314 resp.end();
315 }
316};
317
318const handleStatus: express.Handler = async (req, resp) => {
319 try {
320 const projectId = Number(req.params["projectId"]);
321 const p = await db.project.findUnique({
322 where: {
323 id: projectId,
gio09fcab52025-05-12 14:05:07 +0000324 userId: resp.locals.userId,
giod0026612025-05-08 13:00:36 +0000325 },
326 select: {
327 instanceId: true,
328 },
329 });
giod0026612025-05-08 13:00:36 +0000330 if (p === null) {
331 resp.status(404);
332 return;
333 }
334 if (p.instanceId == null) {
335 resp.status(404);
336 return;
337 }
gio3ed59592025-05-14 16:51:09 +0000338 try {
339 const status = await appManager.getStatus(p.instanceId);
340 resp.status(200);
341 resp.write(JSON.stringify(status));
342 } catch (error) {
343 console.error("Error getting status:", error);
344 resp.status(500);
giod0026612025-05-08 13:00:36 +0000345 }
346 } catch (e) {
347 console.log(e);
348 resp.status(500);
349 } finally {
350 resp.end();
351 }
352};
353
giobd37a2b2025-05-15 04:28:42 +0000354const handleRemoveDeployment: express.Handler = async (req, resp) => {
355 try {
356 const projectId = Number(req.params["projectId"]);
357 const p = await db.project.findUnique({
358 where: {
359 id: projectId,
360 userId: resp.locals.userId,
361 },
362 select: {
363 instanceId: true,
364 githubToken: true,
365 deployKey: true,
366 state: true,
367 draft: true,
368 },
369 });
370 if (p === null) {
371 resp.status(404);
372 resp.write(JSON.stringify({ error: "Project not found" }));
373 return;
374 }
375 if (p.instanceId == null) {
376 resp.status(400);
377 resp.write(JSON.stringify({ error: "Project not deployed" }));
378 return;
379 }
380 const removed = await appManager.removeInstance(p.instanceId);
381 if (!removed) {
382 resp.status(500);
383 resp.write(JSON.stringify({ error: "Failed to remove deployment from cluster" }));
384 return;
385 }
386 if (p.githubToken && p.deployKey && p.state) {
387 try {
388 const github = new GithubClient(p.githubToken);
389 const repos = extractGithubRepos(p.state);
390 const diff = { toDelete: repos, toAdd: [] };
gio76d8ae62025-05-19 15:21:54 +0000391 await manageGithubRepos(github, diff, p.deployKey, env.PUBLIC_ADDR);
giobd37a2b2025-05-15 04:28:42 +0000392 } catch (error) {
393 console.error("Error removing GitHub deploy keys:", error);
394 }
395 }
396 await db.project.update({
397 where: {
398 id: projectId,
399 },
400 data: {
401 instanceId: null,
402 deployKey: null,
giob77cb932025-05-19 09:37:14 +0000403 access: null,
giobd37a2b2025-05-15 04:28:42 +0000404 state: null,
405 draft: p.draft ?? p.state,
406 },
407 });
408 resp.status(200);
409 resp.write(JSON.stringify({ success: true }));
410 } catch (e) {
411 console.error("Error removing deployment:", e);
412 resp.status(500);
413 resp.write(JSON.stringify({ error: "Internal server error" }));
414 } finally {
415 resp.end();
416 }
417};
418
giod0026612025-05-08 13:00:36 +0000419const handleGithubRepos: express.Handler = async (req, resp) => {
420 try {
421 const projectId = Number(req.params["projectId"]);
422 const project = await db.project.findUnique({
gio09fcab52025-05-12 14:05:07 +0000423 where: {
424 id: projectId,
425 userId: resp.locals.userId,
426 },
427 select: {
428 githubToken: true,
429 },
giod0026612025-05-08 13:00:36 +0000430 });
giod0026612025-05-08 13:00:36 +0000431 if (!project?.githubToken) {
432 resp.status(400);
433 resp.write(JSON.stringify({ error: "GitHub token not configured" }));
434 return;
435 }
giod0026612025-05-08 13:00:36 +0000436 const github = new GithubClient(project.githubToken);
437 const repositories = await github.getRepositories();
giod0026612025-05-08 13:00:36 +0000438 resp.status(200);
439 resp.header("Content-Type", "application/json");
440 resp.write(JSON.stringify(repositories));
441 } catch (e) {
442 console.log(e);
443 resp.status(500);
444 resp.write(JSON.stringify({ error: "Failed to fetch repositories" }));
445 } finally {
446 resp.end();
447 }
448};
449
450const handleUpdateGithubToken: express.Handler = async (req, resp) => {
451 try {
452 const projectId = Number(req.params["projectId"]);
453 const { githubToken } = req.body;
giod0026612025-05-08 13:00:36 +0000454 await db.project.update({
gio09fcab52025-05-12 14:05:07 +0000455 where: {
456 id: projectId,
457 userId: resp.locals.userId,
458 },
giod0026612025-05-08 13:00:36 +0000459 data: { githubToken },
460 });
giod0026612025-05-08 13:00:36 +0000461 resp.status(200);
462 } catch (e) {
463 console.log(e);
464 resp.status(500);
465 } finally {
466 resp.end();
467 }
468};
469
470const handleEnv: express.Handler = async (req, resp) => {
471 const projectId = Number(req.params["projectId"]);
472 try {
473 const project = await db.project.findUnique({
gio09fcab52025-05-12 14:05:07 +0000474 where: {
475 id: projectId,
476 userId: resp.locals.userId,
477 },
giod0026612025-05-08 13:00:36 +0000478 select: {
479 deployKey: true,
480 githubToken: true,
giob77cb932025-05-19 09:37:14 +0000481 access: true,
giod0026612025-05-08 13:00:36 +0000482 },
483 });
giod0026612025-05-08 13:00:36 +0000484 if (!project) {
485 resp.status(404);
486 resp.write(JSON.stringify({ error: "Project not found" }));
487 return;
488 }
gioa1efbad2025-05-21 07:16:45 +0000489 const monitor = projectMonitors.get(projectId);
490 const serviceNames = monitor ? monitor.getAllServiceNames() : [];
491 const services = serviceNames.map((name) => ({
492 name,
493 workers: [...(monitor ? monitor.getWorkerStatusesForService(name) : new Map()).entries()].map(
494 ([id, status]) => ({
495 ...status,
496 id,
497 }),
498 ),
499 }));
500
giod0026612025-05-08 13:00:36 +0000501 resp.status(200);
502 resp.write(
503 JSON.stringify({
gio376a81d2025-05-20 06:42:01 +0000504 managerAddr: env.INTERNAL_API_ADDR,
giod0026612025-05-08 13:00:36 +0000505 deployKey: project.deployKey,
giob77cb932025-05-19 09:37:14 +0000506 access: JSON.parse(project.access ?? "[]"),
giod0026612025-05-08 13:00:36 +0000507 integrations: {
508 github: !!project.githubToken,
509 },
510 networks: [
511 {
gio33046722025-05-16 14:49:55 +0000512 name: "Trial",
513 domain: "trial.dodoapp.xyz",
gio6d8b71c2025-05-19 12:57:35 +0000514 hasAuth: false,
giod0026612025-05-08 13:00:36 +0000515 },
giob1c5c452025-05-21 04:16:54 +0000516 // TODO(gio): Remove
517 ].concat(
518 resp.locals.username !== "gio"
519 ? []
520 : [
521 {
522 name: "Public",
523 domain: "v1.dodo.cloud",
524 hasAuth: true,
525 },
526 {
527 name: "Private",
528 domain: "p.v1.dodo.cloud",
529 hasAuth: true,
530 },
531 ],
532 ),
gio3a921b82025-05-10 07:36:09 +0000533 services,
gio3ed59592025-05-14 16:51:09 +0000534 user: {
535 id: resp.locals.userId,
536 username: resp.locals.username,
537 },
giod0026612025-05-08 13:00:36 +0000538 }),
539 );
540 } catch (error) {
541 console.error("Error checking integrations:", error);
542 resp.status(500);
543 resp.write(JSON.stringify({ error: "Internal server error" }));
544 } finally {
545 resp.end();
546 }
547};
548
gio3a921b82025-05-10 07:36:09 +0000549const handleServiceLogs: express.Handler = async (req, resp) => {
550 try {
551 const projectId = Number(req.params["projectId"]);
552 const service = req.params["service"];
gioa1efbad2025-05-21 07:16:45 +0000553 const workerId = req.params["workerId"];
gio09fcab52025-05-12 14:05:07 +0000554 const project = await db.project.findUnique({
555 where: {
556 id: projectId,
557 userId: resp.locals.userId,
558 },
559 });
560 if (project == null) {
561 resp.status(404);
562 resp.write(JSON.stringify({ error: "Project not found" }));
563 return;
564 }
gioa1efbad2025-05-21 07:16:45 +0000565 const monitor = projectMonitors.get(projectId);
566 if (!monitor || !monitor.hasLogs()) {
gio3a921b82025-05-10 07:36:09 +0000567 resp.status(404);
568 resp.write(JSON.stringify({ error: "No logs found for this project" }));
569 return;
570 }
gioa1efbad2025-05-21 07:16:45 +0000571 const serviceLog = monitor.getWorkerLog(service, workerId);
gio3a921b82025-05-10 07:36:09 +0000572 if (!serviceLog) {
573 resp.status(404);
gioa1efbad2025-05-21 07:16:45 +0000574 resp.write(JSON.stringify({ error: "No logs found for this service/worker" }));
gio3a921b82025-05-10 07:36:09 +0000575 return;
576 }
gio3a921b82025-05-10 07:36:09 +0000577 resp.status(200);
578 resp.write(JSON.stringify({ logs: serviceLog }));
579 } catch (e) {
580 console.log(e);
581 resp.status(500);
582 resp.write(JSON.stringify({ error: "Failed to get service logs" }));
583 } finally {
584 resp.end();
585 }
586};
587
gio7d813702025-05-08 18:29:52 +0000588const handleRegisterWorker: express.Handler = async (req, resp) => {
589 try {
590 const projectId = Number(req.params["projectId"]);
gio7d813702025-05-08 18:29:52 +0000591 const result = WorkerSchema.safeParse(req.body);
gio7d813702025-05-08 18:29:52 +0000592 if (!result.success) {
593 resp.status(400);
594 resp.write(
595 JSON.stringify({
596 error: "Invalid request data",
597 details: result.error.format(),
598 }),
599 );
600 return;
601 }
gioa1efbad2025-05-21 07:16:45 +0000602 let monitor = projectMonitors.get(projectId);
603 if (!monitor) {
604 monitor = new ProjectMonitor();
605 projectMonitors.set(projectId, monitor);
gio7d813702025-05-08 18:29:52 +0000606 }
gioa1efbad2025-05-21 07:16:45 +0000607 monitor.registerWorker(result.data);
gio7d813702025-05-08 18:29:52 +0000608 resp.status(200);
609 resp.write(
610 JSON.stringify({
611 success: true,
gio7d813702025-05-08 18:29:52 +0000612 }),
613 );
614 } catch (e) {
615 console.log(e);
616 resp.status(500);
617 resp.write(JSON.stringify({ error: "Failed to register worker" }));
618 } finally {
619 resp.end();
620 }
621};
622
gio76d8ae62025-05-19 15:21:54 +0000623async function reloadProject(projectId: number): Promise<boolean> {
gioa1efbad2025-05-21 07:16:45 +0000624 const monitor = projectMonitors.get(projectId);
625 const projectWorkers = monitor ? monitor.getWorkerAddresses() : [];
gio76d8ae62025-05-19 15:21:54 +0000626 const workerCount = projectWorkers.length;
627 if (workerCount === 0) {
628 return true;
629 }
630 const results = await Promise.all(
631 projectWorkers.map(async (workerAddress) => {
632 const resp = await axios.post(`${workerAddress}/update`);
633 return resp.status === 200;
634 }),
635 );
636 return results.reduce((acc, curr) => acc && curr, true);
637}
638
gio7d813702025-05-08 18:29:52 +0000639const handleReload: express.Handler = async (req, resp) => {
640 try {
641 const projectId = Number(req.params["projectId"]);
gio76d8ae62025-05-19 15:21:54 +0000642 const projectAuth = await db.project.findFirst({
gio09fcab52025-05-12 14:05:07 +0000643 where: {
644 id: projectId,
645 userId: resp.locals.userId,
646 },
gio76d8ae62025-05-19 15:21:54 +0000647 select: { id: true },
gio09fcab52025-05-12 14:05:07 +0000648 });
gio76d8ae62025-05-19 15:21:54 +0000649 if (!projectAuth) {
gio09fcab52025-05-12 14:05:07 +0000650 resp.status(404);
gio09fcab52025-05-12 14:05:07 +0000651 return;
652 }
gio76d8ae62025-05-19 15:21:54 +0000653 const success = await reloadProject(projectId);
654 if (success) {
655 resp.status(200);
656 } else {
657 resp.status(500);
gio7d813702025-05-08 18:29:52 +0000658 }
gio7d813702025-05-08 18:29:52 +0000659 } catch (e) {
gio76d8ae62025-05-19 15:21:54 +0000660 console.error(e);
gio7d813702025-05-08 18:29:52 +0000661 resp.status(500);
gio7d813702025-05-08 18:29:52 +0000662 }
663};
664
gio09fcab52025-05-12 14:05:07 +0000665const auth = (req: express.Request, resp: express.Response, next: express.NextFunction) => {
666 const userId = req.get("x-forwarded-userid");
gio3ed59592025-05-14 16:51:09 +0000667 const username = req.get("x-forwarded-user");
668 if (userId == null || username == null) {
gio09fcab52025-05-12 14:05:07 +0000669 resp.status(401);
670 resp.write("Unauthorized");
671 resp.end();
672 return;
673 }
674 resp.locals.userId = userId;
gio3ed59592025-05-14 16:51:09 +0000675 resp.locals.username = username;
gio09fcab52025-05-12 14:05:07 +0000676 next();
677};
678
gio76d8ae62025-05-19 15:21:54 +0000679const handleGithubPushWebhook: express.Handler = async (req, resp) => {
680 try {
681 // TODO(gio): Implement GitHub signature verification for security
682 const webhookSchema = z.object({
683 repository: z.object({
684 ssh_url: z.string(),
685 }),
686 });
687
688 const result = webhookSchema.safeParse(req.body);
689 if (!result.success) {
690 console.warn("GitHub webhook: Invalid payload:", result.error.issues);
691 resp.status(400).json({ error: "Invalid webhook payload" });
692 return;
693 }
694 const { ssh_url: addr } = result.data.repository;
695 const allProjects = await db.project.findMany({
696 select: {
697 id: true,
698 state: true,
699 },
700 where: {
701 instanceId: {
702 not: null,
703 },
704 },
705 });
706 // TODO(gio): This should run in background
707 new Promise<boolean>((resolve, reject) => {
708 setTimeout(() => {
709 const projectsToReloadIds: number[] = [];
710 for (const project of allProjects) {
711 if (project.state && project.state.length > 0) {
712 const projectRepos = extractGithubRepos(project.state);
713 if (projectRepos.includes(addr)) {
714 projectsToReloadIds.push(project.id);
715 }
716 }
717 }
718 Promise.all(projectsToReloadIds.map((id) => reloadProject(id)))
719 .then((results) => {
720 resolve(results.reduce((acc, curr) => acc && curr, true));
721 })
722 // eslint-disable-next-line @typescript-eslint/no-explicit-any
723 .catch((reason: any) => reject(reason));
724 }, 10);
725 });
726 // eslint-disable-next-line @typescript-eslint/no-explicit-any
727 } catch (error: any) {
728 console.error(error);
729 resp.status(500);
730 }
731};
732
giod0026612025-05-08 13:00:36 +0000733async function start() {
734 await db.$connect();
735 const app = express();
gio76d8ae62025-05-19 15:21:54 +0000736 app.use(express.json()); // Global JSON parsing
737
738 // Public webhook route - no auth needed
739 app.post("/api/webhook/github/push", handleGithubPushWebhook);
740
741 // Authenticated project routes
742 const projectRouter = express.Router();
743 projectRouter.use(auth); // Apply auth middleware to this router
744 projectRouter.post("/:projectId/saved", handleSave);
745 projectRouter.get("/:projectId/saved/deploy", handleSavedGet("deploy"));
746 projectRouter.get("/:projectId/saved/draft", handleSavedGet("draft"));
747 projectRouter.post("/:projectId/deploy", handleDeploy);
748 projectRouter.get("/:projectId/status", handleStatus);
749 projectRouter.delete("/:projectId", handleDelete);
750 projectRouter.get("/:projectId/repos/github", handleGithubRepos);
751 projectRouter.post("/:projectId/github-token", handleUpdateGithubToken);
752 projectRouter.get("/:projectId/env", handleEnv);
753 projectRouter.post("/:projectId/reload", handleReload);
gioa1efbad2025-05-21 07:16:45 +0000754 projectRouter.get("/:projectId/logs/:service/:workerId", handleServiceLogs);
gio76d8ae62025-05-19 15:21:54 +0000755 projectRouter.post("/:projectId/remove-deployment", handleRemoveDeployment);
756 projectRouter.get("/", handleProjectAll);
757 projectRouter.post("/", handleProjectCreate);
758 app.use("/api/project", projectRouter); // Mount the authenticated router
759
giod0026612025-05-08 13:00:36 +0000760 app.use("/", express.static("../front/dist"));
gio09fcab52025-05-12 14:05:07 +0000761
gio76d8ae62025-05-19 15:21:54 +0000762 const internalApi = express();
763 internalApi.use(express.json());
764 internalApi.post("/api/project/:projectId/workers", handleRegisterWorker);
gio09fcab52025-05-12 14:05:07 +0000765
giod0026612025-05-08 13:00:36 +0000766 app.listen(env.DODO_PORT_WEB, () => {
gio09fcab52025-05-12 14:05:07 +0000767 console.log("Web server started on port", env.DODO_PORT_WEB);
768 });
769
gio76d8ae62025-05-19 15:21:54 +0000770 internalApi.listen(env.DODO_PORT_API, () => {
gio09fcab52025-05-12 14:05:07 +0000771 console.log("Internal API server started on port", env.DODO_PORT_API);
giod0026612025-05-08 13:00:36 +0000772 });
773}
774
775start();