blob: 5ab90ef9697ee5271fa882a51124a120dc50734e [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 },
506 ],
gio3a921b82025-05-10 07:36:09 +0000507 services,
gio3ed59592025-05-14 16:51:09 +0000508 user: {
509 id: resp.locals.userId,
510 username: resp.locals.username,
511 },
giod0026612025-05-08 13:00:36 +0000512 }),
513 );
514 } catch (error) {
515 console.error("Error checking integrations:", error);
516 resp.status(500);
517 resp.write(JSON.stringify({ error: "Internal server error" }));
518 } finally {
519 resp.end();
520 }
521};
522
gio3a921b82025-05-10 07:36:09 +0000523const handleServiceLogs: express.Handler = async (req, resp) => {
524 try {
525 const projectId = Number(req.params["projectId"]);
526 const service = req.params["service"];
gio09fcab52025-05-12 14:05:07 +0000527 const project = await db.project.findUnique({
528 where: {
529 id: projectId,
530 userId: resp.locals.userId,
531 },
532 });
533 if (project == null) {
534 resp.status(404);
535 resp.write(JSON.stringify({ error: "Project not found" }));
536 return;
537 }
gio3a921b82025-05-10 07:36:09 +0000538 const projectLogs = logs.get(projectId);
539 if (!projectLogs) {
540 resp.status(404);
541 resp.write(JSON.stringify({ error: "No logs found for this project" }));
542 return;
543 }
gio3a921b82025-05-10 07:36:09 +0000544 const serviceLog = projectLogs.get(service);
545 if (!serviceLog) {
546 resp.status(404);
547 resp.write(JSON.stringify({ error: "No logs found for this service" }));
548 return;
549 }
gio3a921b82025-05-10 07:36:09 +0000550 resp.status(200);
551 resp.write(JSON.stringify({ logs: serviceLog }));
552 } catch (e) {
553 console.log(e);
554 resp.status(500);
555 resp.write(JSON.stringify({ error: "Failed to get service logs" }));
556 } finally {
557 resp.end();
558 }
559};
560
gio7d813702025-05-08 18:29:52 +0000561const WorkerSchema = z.object({
gio3a921b82025-05-10 07:36:09 +0000562 service: z.string(),
gio7d813702025-05-08 18:29:52 +0000563 address: z.string().url(),
gio3a921b82025-05-10 07:36:09 +0000564 logs: z.optional(z.string()),
gio7d813702025-05-08 18:29:52 +0000565});
566
567const handleRegisterWorker: express.Handler = async (req, resp) => {
568 try {
569 const projectId = Number(req.params["projectId"]);
gio7d813702025-05-08 18:29:52 +0000570 const result = WorkerSchema.safeParse(req.body);
gio7d813702025-05-08 18:29:52 +0000571 if (!result.success) {
572 resp.status(400);
573 resp.write(
574 JSON.stringify({
575 error: "Invalid request data",
576 details: result.error.format(),
577 }),
578 );
579 return;
580 }
gio3a921b82025-05-10 07:36:09 +0000581 const { service, address, logs: log } = result.data;
gio7d813702025-05-08 18:29:52 +0000582 const projectWorkers = workers.get(projectId) || [];
gio7d813702025-05-08 18:29:52 +0000583 if (!projectWorkers.includes(address)) {
584 projectWorkers.push(address);
585 }
gio7d813702025-05-08 18:29:52 +0000586 workers.set(projectId, projectWorkers);
gio3a921b82025-05-10 07:36:09 +0000587 if (log) {
588 const svcLogs: Map<string, string> = logs.get(projectId) || new Map();
589 svcLogs.set(service, log);
590 logs.set(projectId, svcLogs);
591 }
gio7d813702025-05-08 18:29:52 +0000592 resp.status(200);
593 resp.write(
594 JSON.stringify({
595 success: true,
gio7d813702025-05-08 18:29:52 +0000596 }),
597 );
598 } catch (e) {
599 console.log(e);
600 resp.status(500);
601 resp.write(JSON.stringify({ error: "Failed to register worker" }));
602 } finally {
603 resp.end();
604 }
605};
606
gio76d8ae62025-05-19 15:21:54 +0000607async function reloadProject(projectId: number): Promise<boolean> {
608 const projectWorkers = workers.get(projectId) || [];
609 const workerCount = projectWorkers.length;
610 if (workerCount === 0) {
611 return true;
612 }
613 const results = await Promise.all(
614 projectWorkers.map(async (workerAddress) => {
615 const resp = await axios.post(`${workerAddress}/update`);
616 return resp.status === 200;
617 }),
618 );
619 return results.reduce((acc, curr) => acc && curr, true);
620}
621
gio7d813702025-05-08 18:29:52 +0000622const handleReload: express.Handler = async (req, resp) => {
623 try {
624 const projectId = Number(req.params["projectId"]);
gio76d8ae62025-05-19 15:21:54 +0000625 const projectAuth = await db.project.findFirst({
gio09fcab52025-05-12 14:05:07 +0000626 where: {
627 id: projectId,
628 userId: resp.locals.userId,
629 },
gio76d8ae62025-05-19 15:21:54 +0000630 select: { id: true },
gio09fcab52025-05-12 14:05:07 +0000631 });
gio76d8ae62025-05-19 15:21:54 +0000632 if (!projectAuth) {
gio09fcab52025-05-12 14:05:07 +0000633 resp.status(404);
gio09fcab52025-05-12 14:05:07 +0000634 return;
635 }
gio76d8ae62025-05-19 15:21:54 +0000636 const success = await reloadProject(projectId);
637 if (success) {
638 resp.status(200);
639 } else {
640 resp.status(500);
gio7d813702025-05-08 18:29:52 +0000641 }
gio7d813702025-05-08 18:29:52 +0000642 } catch (e) {
gio76d8ae62025-05-19 15:21:54 +0000643 console.error(e);
gio7d813702025-05-08 18:29:52 +0000644 resp.status(500);
gio7d813702025-05-08 18:29:52 +0000645 }
646};
647
gio09fcab52025-05-12 14:05:07 +0000648const auth = (req: express.Request, resp: express.Response, next: express.NextFunction) => {
649 const userId = req.get("x-forwarded-userid");
gio3ed59592025-05-14 16:51:09 +0000650 const username = req.get("x-forwarded-user");
651 if (userId == null || username == null) {
gio09fcab52025-05-12 14:05:07 +0000652 resp.status(401);
653 resp.write("Unauthorized");
654 resp.end();
655 return;
656 }
657 resp.locals.userId = userId;
gio3ed59592025-05-14 16:51:09 +0000658 resp.locals.username = username;
gio09fcab52025-05-12 14:05:07 +0000659 next();
660};
661
gio76d8ae62025-05-19 15:21:54 +0000662const handleGithubPushWebhook: express.Handler = async (req, resp) => {
663 try {
664 // TODO(gio): Implement GitHub signature verification for security
665 const webhookSchema = z.object({
666 repository: z.object({
667 ssh_url: z.string(),
668 }),
669 });
670
671 const result = webhookSchema.safeParse(req.body);
672 if (!result.success) {
673 console.warn("GitHub webhook: Invalid payload:", result.error.issues);
674 resp.status(400).json({ error: "Invalid webhook payload" });
675 return;
676 }
677 const { ssh_url: addr } = result.data.repository;
678 const allProjects = await db.project.findMany({
679 select: {
680 id: true,
681 state: true,
682 },
683 where: {
684 instanceId: {
685 not: null,
686 },
687 },
688 });
689 // TODO(gio): This should run in background
690 new Promise<boolean>((resolve, reject) => {
691 setTimeout(() => {
692 const projectsToReloadIds: number[] = [];
693 for (const project of allProjects) {
694 if (project.state && project.state.length > 0) {
695 const projectRepos = extractGithubRepos(project.state);
696 if (projectRepos.includes(addr)) {
697 projectsToReloadIds.push(project.id);
698 }
699 }
700 }
701 Promise.all(projectsToReloadIds.map((id) => reloadProject(id)))
702 .then((results) => {
703 resolve(results.reduce((acc, curr) => acc && curr, true));
704 })
705 // eslint-disable-next-line @typescript-eslint/no-explicit-any
706 .catch((reason: any) => reject(reason));
707 }, 10);
708 });
709 // eslint-disable-next-line @typescript-eslint/no-explicit-any
710 } catch (error: any) {
711 console.error(error);
712 resp.status(500);
713 }
714};
715
giod0026612025-05-08 13:00:36 +0000716async function start() {
717 await db.$connect();
718 const app = express();
gio76d8ae62025-05-19 15:21:54 +0000719 app.use(express.json()); // Global JSON parsing
720
721 // Public webhook route - no auth needed
722 app.post("/api/webhook/github/push", handleGithubPushWebhook);
723
724 // Authenticated project routes
725 const projectRouter = express.Router();
726 projectRouter.use(auth); // Apply auth middleware to this router
727 projectRouter.post("/:projectId/saved", handleSave);
728 projectRouter.get("/:projectId/saved/deploy", handleSavedGet("deploy"));
729 projectRouter.get("/:projectId/saved/draft", handleSavedGet("draft"));
730 projectRouter.post("/:projectId/deploy", handleDeploy);
731 projectRouter.get("/:projectId/status", handleStatus);
732 projectRouter.delete("/:projectId", handleDelete);
733 projectRouter.get("/:projectId/repos/github", handleGithubRepos);
734 projectRouter.post("/:projectId/github-token", handleUpdateGithubToken);
735 projectRouter.get("/:projectId/env", handleEnv);
736 projectRouter.post("/:projectId/reload", handleReload);
737 projectRouter.get("/:projectId/logs/:service", handleServiceLogs);
738 projectRouter.post("/:projectId/remove-deployment", handleRemoveDeployment);
739 projectRouter.get("/", handleProjectAll);
740 projectRouter.post("/", handleProjectCreate);
741 app.use("/api/project", projectRouter); // Mount the authenticated router
742
giod0026612025-05-08 13:00:36 +0000743 app.use("/", express.static("../front/dist"));
gio09fcab52025-05-12 14:05:07 +0000744
gio76d8ae62025-05-19 15:21:54 +0000745 const internalApi = express();
746 internalApi.use(express.json());
747 internalApi.post("/api/project/:projectId/workers", handleRegisterWorker);
gio09fcab52025-05-12 14:05:07 +0000748
giod0026612025-05-08 13:00:36 +0000749 app.listen(env.DODO_PORT_WEB, () => {
gio09fcab52025-05-12 14:05:07 +0000750 console.log("Web server started on port", env.DODO_PORT_WEB);
751 });
752
gio76d8ae62025-05-19 15:21:54 +0000753 internalApi.listen(env.DODO_PORT_API, () => {
gio09fcab52025-05-12 14:05:07 +0000754 console.log("Internal API server started on port", env.DODO_PORT_API);
giod0026612025-05-08 13:00:36 +0000755 });
756}
757
758start();