blob: aa25383ebd735e648d102b5fb5ffbb6d999e3e98 [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";
giod0026612025-05-08 13:00:36 +00008
9const db = new PrismaClient();
gio3ed59592025-05-14 16:51:09 +000010const appManager = new AppManager();
giod0026612025-05-08 13:00:36 +000011
gio7d813702025-05-08 18:29:52 +000012const workers = new Map<number, string[]>();
gio3a921b82025-05-10 07:36:09 +000013const logs = new Map<number, Map<string, string>>();
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 }
gio3a921b82025-05-10 07:36:09 +0000489 const projectLogs = logs.get(projectId) || new Map();
490 const services = Array.from(projectLogs.keys());
giod0026612025-05-08 13:00:36 +0000491 resp.status(200);
492 resp.write(
493 JSON.stringify({
gio376a81d2025-05-20 06:42:01 +0000494 managerAddr: env.INTERNAL_API_ADDR,
giod0026612025-05-08 13:00:36 +0000495 deployKey: project.deployKey,
giob77cb932025-05-19 09:37:14 +0000496 access: JSON.parse(project.access ?? "[]"),
giod0026612025-05-08 13:00:36 +0000497 integrations: {
498 github: !!project.githubToken,
499 },
500 networks: [
501 {
gio33046722025-05-16 14:49:55 +0000502 name: "Trial",
503 domain: "trial.dodoapp.xyz",
gio6d8b71c2025-05-19 12:57:35 +0000504 hasAuth: false,
giod0026612025-05-08 13:00:36 +0000505 },
giob1c5c452025-05-21 04:16:54 +0000506 // TODO(gio): Remove
507 ].concat(
508 resp.locals.username !== "gio"
509 ? []
510 : [
511 {
512 name: "Public",
513 domain: "v1.dodo.cloud",
514 hasAuth: true,
515 },
516 {
517 name: "Private",
518 domain: "p.v1.dodo.cloud",
519 hasAuth: true,
520 },
521 ],
522 ),
gio3a921b82025-05-10 07:36:09 +0000523 services,
gio3ed59592025-05-14 16:51:09 +0000524 user: {
525 id: resp.locals.userId,
526 username: resp.locals.username,
527 },
giod0026612025-05-08 13:00:36 +0000528 }),
529 );
530 } catch (error) {
531 console.error("Error checking integrations:", error);
532 resp.status(500);
533 resp.write(JSON.stringify({ error: "Internal server error" }));
534 } finally {
535 resp.end();
536 }
537};
538
gio3a921b82025-05-10 07:36:09 +0000539const handleServiceLogs: express.Handler = async (req, resp) => {
540 try {
541 const projectId = Number(req.params["projectId"]);
542 const service = req.params["service"];
gio09fcab52025-05-12 14:05:07 +0000543 const project = await db.project.findUnique({
544 where: {
545 id: projectId,
546 userId: resp.locals.userId,
547 },
548 });
549 if (project == null) {
550 resp.status(404);
551 resp.write(JSON.stringify({ error: "Project not found" }));
552 return;
553 }
gio3a921b82025-05-10 07:36:09 +0000554 const projectLogs = logs.get(projectId);
555 if (!projectLogs) {
556 resp.status(404);
557 resp.write(JSON.stringify({ error: "No logs found for this project" }));
558 return;
559 }
gio3a921b82025-05-10 07:36:09 +0000560 const serviceLog = projectLogs.get(service);
561 if (!serviceLog) {
562 resp.status(404);
563 resp.write(JSON.stringify({ error: "No logs found for this service" }));
564 return;
565 }
gio3a921b82025-05-10 07:36:09 +0000566 resp.status(200);
567 resp.write(JSON.stringify({ logs: serviceLog }));
568 } catch (e) {
569 console.log(e);
570 resp.status(500);
571 resp.write(JSON.stringify({ error: "Failed to get service logs" }));
572 } finally {
573 resp.end();
574 }
575};
576
gio7d813702025-05-08 18:29:52 +0000577const WorkerSchema = z.object({
gio3a921b82025-05-10 07:36:09 +0000578 service: z.string(),
gio7d813702025-05-08 18:29:52 +0000579 address: z.string().url(),
gio3a921b82025-05-10 07:36:09 +0000580 logs: z.optional(z.string()),
gio7d813702025-05-08 18:29:52 +0000581});
582
583const handleRegisterWorker: express.Handler = async (req, resp) => {
584 try {
585 const projectId = Number(req.params["projectId"]);
gio7d813702025-05-08 18:29:52 +0000586 const result = WorkerSchema.safeParse(req.body);
gio7d813702025-05-08 18:29:52 +0000587 if (!result.success) {
588 resp.status(400);
589 resp.write(
590 JSON.stringify({
591 error: "Invalid request data",
592 details: result.error.format(),
593 }),
594 );
595 return;
596 }
gio3a921b82025-05-10 07:36:09 +0000597 const { service, address, logs: log } = result.data;
gio7d813702025-05-08 18:29:52 +0000598 const projectWorkers = workers.get(projectId) || [];
gio7d813702025-05-08 18:29:52 +0000599 if (!projectWorkers.includes(address)) {
600 projectWorkers.push(address);
601 }
gio7d813702025-05-08 18:29:52 +0000602 workers.set(projectId, projectWorkers);
gio3a921b82025-05-10 07:36:09 +0000603 if (log) {
604 const svcLogs: Map<string, string> = logs.get(projectId) || new Map();
605 svcLogs.set(service, log);
606 logs.set(projectId, svcLogs);
607 }
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> {
624 const projectWorkers = workers.get(projectId) || [];
625 const workerCount = projectWorkers.length;
626 if (workerCount === 0) {
627 return true;
628 }
629 const results = await Promise.all(
630 projectWorkers.map(async (workerAddress) => {
631 const resp = await axios.post(`${workerAddress}/update`);
632 return resp.status === 200;
633 }),
634 );
635 return results.reduce((acc, curr) => acc && curr, true);
636}
637
gio7d813702025-05-08 18:29:52 +0000638const handleReload: express.Handler = async (req, resp) => {
639 try {
640 const projectId = Number(req.params["projectId"]);
gio76d8ae62025-05-19 15:21:54 +0000641 const projectAuth = await db.project.findFirst({
gio09fcab52025-05-12 14:05:07 +0000642 where: {
643 id: projectId,
644 userId: resp.locals.userId,
645 },
gio76d8ae62025-05-19 15:21:54 +0000646 select: { id: true },
gio09fcab52025-05-12 14:05:07 +0000647 });
gio76d8ae62025-05-19 15:21:54 +0000648 if (!projectAuth) {
gio09fcab52025-05-12 14:05:07 +0000649 resp.status(404);
gio09fcab52025-05-12 14:05:07 +0000650 return;
651 }
gio76d8ae62025-05-19 15:21:54 +0000652 const success = await reloadProject(projectId);
653 if (success) {
654 resp.status(200);
655 } else {
656 resp.status(500);
gio7d813702025-05-08 18:29:52 +0000657 }
gio7d813702025-05-08 18:29:52 +0000658 } catch (e) {
gio76d8ae62025-05-19 15:21:54 +0000659 console.error(e);
gio7d813702025-05-08 18:29:52 +0000660 resp.status(500);
gio7d813702025-05-08 18:29:52 +0000661 }
662};
663
gio09fcab52025-05-12 14:05:07 +0000664const auth = (req: express.Request, resp: express.Response, next: express.NextFunction) => {
665 const userId = req.get("x-forwarded-userid");
gio3ed59592025-05-14 16:51:09 +0000666 const username = req.get("x-forwarded-user");
667 if (userId == null || username == null) {
gio09fcab52025-05-12 14:05:07 +0000668 resp.status(401);
669 resp.write("Unauthorized");
670 resp.end();
671 return;
672 }
673 resp.locals.userId = userId;
gio3ed59592025-05-14 16:51:09 +0000674 resp.locals.username = username;
gio09fcab52025-05-12 14:05:07 +0000675 next();
676};
677
gio76d8ae62025-05-19 15:21:54 +0000678const handleGithubPushWebhook: express.Handler = async (req, resp) => {
679 try {
680 // TODO(gio): Implement GitHub signature verification for security
681 const webhookSchema = z.object({
682 repository: z.object({
683 ssh_url: z.string(),
684 }),
685 });
686
687 const result = webhookSchema.safeParse(req.body);
688 if (!result.success) {
689 console.warn("GitHub webhook: Invalid payload:", result.error.issues);
690 resp.status(400).json({ error: "Invalid webhook payload" });
691 return;
692 }
693 const { ssh_url: addr } = result.data.repository;
694 const allProjects = await db.project.findMany({
695 select: {
696 id: true,
697 state: true,
698 },
699 where: {
700 instanceId: {
701 not: null,
702 },
703 },
704 });
705 // TODO(gio): This should run in background
706 new Promise<boolean>((resolve, reject) => {
707 setTimeout(() => {
708 const projectsToReloadIds: number[] = [];
709 for (const project of allProjects) {
710 if (project.state && project.state.length > 0) {
711 const projectRepos = extractGithubRepos(project.state);
712 if (projectRepos.includes(addr)) {
713 projectsToReloadIds.push(project.id);
714 }
715 }
716 }
717 Promise.all(projectsToReloadIds.map((id) => reloadProject(id)))
718 .then((results) => {
719 resolve(results.reduce((acc, curr) => acc && curr, true));
720 })
721 // eslint-disable-next-line @typescript-eslint/no-explicit-any
722 .catch((reason: any) => reject(reason));
723 }, 10);
724 });
725 // eslint-disable-next-line @typescript-eslint/no-explicit-any
726 } catch (error: any) {
727 console.error(error);
728 resp.status(500);
729 }
730};
731
giod0026612025-05-08 13:00:36 +0000732async function start() {
733 await db.$connect();
734 const app = express();
gio76d8ae62025-05-19 15:21:54 +0000735 app.use(express.json()); // Global JSON parsing
736
737 // Public webhook route - no auth needed
738 app.post("/api/webhook/github/push", handleGithubPushWebhook);
739
740 // Authenticated project routes
741 const projectRouter = express.Router();
742 projectRouter.use(auth); // Apply auth middleware to this router
743 projectRouter.post("/:projectId/saved", handleSave);
744 projectRouter.get("/:projectId/saved/deploy", handleSavedGet("deploy"));
745 projectRouter.get("/:projectId/saved/draft", handleSavedGet("draft"));
746 projectRouter.post("/:projectId/deploy", handleDeploy);
747 projectRouter.get("/:projectId/status", handleStatus);
748 projectRouter.delete("/:projectId", handleDelete);
749 projectRouter.get("/:projectId/repos/github", handleGithubRepos);
750 projectRouter.post("/:projectId/github-token", handleUpdateGithubToken);
751 projectRouter.get("/:projectId/env", handleEnv);
752 projectRouter.post("/:projectId/reload", handleReload);
753 projectRouter.get("/:projectId/logs/:service", handleServiceLogs);
754 projectRouter.post("/:projectId/remove-deployment", handleRemoveDeployment);
755 projectRouter.get("/", handleProjectAll);
756 projectRouter.post("/", handleProjectCreate);
757 app.use("/api/project", projectRouter); // Mount the authenticated router
758
giod0026612025-05-08 13:00:36 +0000759 app.use("/", express.static("../front/dist"));
gio09fcab52025-05-12 14:05:07 +0000760
gio76d8ae62025-05-19 15:21:54 +0000761 const internalApi = express();
762 internalApi.use(express.json());
763 internalApi.post("/api/project/:projectId/workers", handleRegisterWorker);
gio09fcab52025-05-12 14:05:07 +0000764
giod0026612025-05-08 13:00:36 +0000765 app.listen(env.DODO_PORT_WEB, () => {
gio09fcab52025-05-12 14:05:07 +0000766 console.log("Web server started on port", env.DODO_PORT_WEB);
767 });
768
gio76d8ae62025-05-19 15:21:54 +0000769 internalApi.listen(env.DODO_PORT_API, () => {
gio09fcab52025-05-12 14:05:07 +0000770 console.log("Internal API server started on port", env.DODO_PORT_API);
giod0026612025-05-08 13:00:36 +0000771 });
772}
773
774start();