blob: 76e250a85765496d83e7b51aa7b713541e681bbd [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({
gio7d813702025-05-08 18:29:52 +0000494 // TODO(gio): get from env or command line flags
gio33046722025-05-16 14:49:55 +0000495 managerAddr: "http://10.42.0.95:8081",
giod0026612025-05-08 13:00:36 +0000496 deployKey: project.deployKey,
giob77cb932025-05-19 09:37:14 +0000497 access: JSON.parse(project.access ?? "[]"),
giod0026612025-05-08 13:00:36 +0000498 integrations: {
499 github: !!project.githubToken,
500 },
501 networks: [
502 {
gio33046722025-05-16 14:49:55 +0000503 name: "Trial",
504 domain: "trial.dodoapp.xyz",
gio6d8b71c2025-05-19 12:57:35 +0000505 hasAuth: false,
giod0026612025-05-08 13:00:36 +0000506 },
507 ],
gio3a921b82025-05-10 07:36:09 +0000508 services,
gio3ed59592025-05-14 16:51:09 +0000509 user: {
510 id: resp.locals.userId,
511 username: resp.locals.username,
512 },
giod0026612025-05-08 13:00:36 +0000513 }),
514 );
515 } catch (error) {
516 console.error("Error checking integrations:", error);
517 resp.status(500);
518 resp.write(JSON.stringify({ error: "Internal server error" }));
519 } finally {
520 resp.end();
521 }
522};
523
gio3a921b82025-05-10 07:36:09 +0000524const handleServiceLogs: express.Handler = async (req, resp) => {
525 try {
526 const projectId = Number(req.params["projectId"]);
527 const service = req.params["service"];
gio09fcab52025-05-12 14:05:07 +0000528 const project = await db.project.findUnique({
529 where: {
530 id: projectId,
531 userId: resp.locals.userId,
532 },
533 });
534 if (project == null) {
535 resp.status(404);
536 resp.write(JSON.stringify({ error: "Project not found" }));
537 return;
538 }
gio3a921b82025-05-10 07:36:09 +0000539 const projectLogs = logs.get(projectId);
540 if (!projectLogs) {
541 resp.status(404);
542 resp.write(JSON.stringify({ error: "No logs found for this project" }));
543 return;
544 }
gio3a921b82025-05-10 07:36:09 +0000545 const serviceLog = projectLogs.get(service);
546 if (!serviceLog) {
547 resp.status(404);
548 resp.write(JSON.stringify({ error: "No logs found for this service" }));
549 return;
550 }
gio3a921b82025-05-10 07:36:09 +0000551 resp.status(200);
552 resp.write(JSON.stringify({ logs: serviceLog }));
553 } catch (e) {
554 console.log(e);
555 resp.status(500);
556 resp.write(JSON.stringify({ error: "Failed to get service logs" }));
557 } finally {
558 resp.end();
559 }
560};
561
gio7d813702025-05-08 18:29:52 +0000562const WorkerSchema = z.object({
gio3a921b82025-05-10 07:36:09 +0000563 service: z.string(),
gio7d813702025-05-08 18:29:52 +0000564 address: z.string().url(),
gio3a921b82025-05-10 07:36:09 +0000565 logs: z.optional(z.string()),
gio7d813702025-05-08 18:29:52 +0000566});
567
568const handleRegisterWorker: express.Handler = async (req, resp) => {
569 try {
570 const projectId = Number(req.params["projectId"]);
gio7d813702025-05-08 18:29:52 +0000571 const result = WorkerSchema.safeParse(req.body);
gio7d813702025-05-08 18:29:52 +0000572 if (!result.success) {
573 resp.status(400);
574 resp.write(
575 JSON.stringify({
576 error: "Invalid request data",
577 details: result.error.format(),
578 }),
579 );
580 return;
581 }
gio3a921b82025-05-10 07:36:09 +0000582 const { service, address, logs: log } = result.data;
gio7d813702025-05-08 18:29:52 +0000583 const projectWorkers = workers.get(projectId) || [];
gio7d813702025-05-08 18:29:52 +0000584 if (!projectWorkers.includes(address)) {
585 projectWorkers.push(address);
586 }
gio7d813702025-05-08 18:29:52 +0000587 workers.set(projectId, projectWorkers);
gio3a921b82025-05-10 07:36:09 +0000588 if (log) {
589 const svcLogs: Map<string, string> = logs.get(projectId) || new Map();
590 svcLogs.set(service, log);
591 logs.set(projectId, svcLogs);
592 }
gio7d813702025-05-08 18:29:52 +0000593 resp.status(200);
594 resp.write(
595 JSON.stringify({
596 success: true,
gio7d813702025-05-08 18:29:52 +0000597 }),
598 );
599 } catch (e) {
600 console.log(e);
601 resp.status(500);
602 resp.write(JSON.stringify({ error: "Failed to register worker" }));
603 } finally {
604 resp.end();
605 }
606};
607
gio76d8ae62025-05-19 15:21:54 +0000608async function reloadProject(projectId: number): Promise<boolean> {
609 const projectWorkers = workers.get(projectId) || [];
610 const workerCount = projectWorkers.length;
611 if (workerCount === 0) {
612 return true;
613 }
614 const results = await Promise.all(
615 projectWorkers.map(async (workerAddress) => {
616 const resp = await axios.post(`${workerAddress}/update`);
617 return resp.status === 200;
618 }),
619 );
620 return results.reduce((acc, curr) => acc && curr, true);
621}
622
gio7d813702025-05-08 18:29:52 +0000623const handleReload: express.Handler = async (req, resp) => {
624 try {
625 const projectId = Number(req.params["projectId"]);
gio76d8ae62025-05-19 15:21:54 +0000626 const projectAuth = await db.project.findFirst({
gio09fcab52025-05-12 14:05:07 +0000627 where: {
628 id: projectId,
629 userId: resp.locals.userId,
630 },
gio76d8ae62025-05-19 15:21:54 +0000631 select: { id: true },
gio09fcab52025-05-12 14:05:07 +0000632 });
gio76d8ae62025-05-19 15:21:54 +0000633 if (!projectAuth) {
gio09fcab52025-05-12 14:05:07 +0000634 resp.status(404);
gio09fcab52025-05-12 14:05:07 +0000635 return;
636 }
gio76d8ae62025-05-19 15:21:54 +0000637 const success = await reloadProject(projectId);
638 if (success) {
639 resp.status(200);
640 } else {
641 resp.status(500);
gio7d813702025-05-08 18:29:52 +0000642 }
gio7d813702025-05-08 18:29:52 +0000643 } catch (e) {
gio76d8ae62025-05-19 15:21:54 +0000644 console.error(e);
gio7d813702025-05-08 18:29:52 +0000645 resp.status(500);
gio7d813702025-05-08 18:29:52 +0000646 }
647};
648
gio09fcab52025-05-12 14:05:07 +0000649const auth = (req: express.Request, resp: express.Response, next: express.NextFunction) => {
650 const userId = req.get("x-forwarded-userid");
gio3ed59592025-05-14 16:51:09 +0000651 const username = req.get("x-forwarded-user");
652 if (userId == null || username == null) {
gio09fcab52025-05-12 14:05:07 +0000653 resp.status(401);
654 resp.write("Unauthorized");
655 resp.end();
656 return;
657 }
658 resp.locals.userId = userId;
gio3ed59592025-05-14 16:51:09 +0000659 resp.locals.username = username;
gio09fcab52025-05-12 14:05:07 +0000660 next();
661};
662
gio76d8ae62025-05-19 15:21:54 +0000663const handleGithubPushWebhook: express.Handler = async (req, resp) => {
664 try {
665 // TODO(gio): Implement GitHub signature verification for security
666 const webhookSchema = z.object({
667 repository: z.object({
668 ssh_url: z.string(),
669 }),
670 });
671
672 const result = webhookSchema.safeParse(req.body);
673 if (!result.success) {
674 console.warn("GitHub webhook: Invalid payload:", result.error.issues);
675 resp.status(400).json({ error: "Invalid webhook payload" });
676 return;
677 }
678 const { ssh_url: addr } = result.data.repository;
679 const allProjects = await db.project.findMany({
680 select: {
681 id: true,
682 state: true,
683 },
684 where: {
685 instanceId: {
686 not: null,
687 },
688 },
689 });
690 // TODO(gio): This should run in background
691 new Promise<boolean>((resolve, reject) => {
692 setTimeout(() => {
693 const projectsToReloadIds: number[] = [];
694 for (const project of allProjects) {
695 if (project.state && project.state.length > 0) {
696 const projectRepos = extractGithubRepos(project.state);
697 if (projectRepos.includes(addr)) {
698 projectsToReloadIds.push(project.id);
699 }
700 }
701 }
702 Promise.all(projectsToReloadIds.map((id) => reloadProject(id)))
703 .then((results) => {
704 resolve(results.reduce((acc, curr) => acc && curr, true));
705 })
706 // eslint-disable-next-line @typescript-eslint/no-explicit-any
707 .catch((reason: any) => reject(reason));
708 }, 10);
709 });
710 // eslint-disable-next-line @typescript-eslint/no-explicit-any
711 } catch (error: any) {
712 console.error(error);
713 resp.status(500);
714 }
715};
716
giod0026612025-05-08 13:00:36 +0000717async function start() {
718 await db.$connect();
719 const app = express();
gio76d8ae62025-05-19 15:21:54 +0000720 app.use(express.json()); // Global JSON parsing
721
722 // Public webhook route - no auth needed
723 app.post("/api/webhook/github/push", handleGithubPushWebhook);
724
725 // Authenticated project routes
726 const projectRouter = express.Router();
727 projectRouter.use(auth); // Apply auth middleware to this router
728 projectRouter.post("/:projectId/saved", handleSave);
729 projectRouter.get("/:projectId/saved/deploy", handleSavedGet("deploy"));
730 projectRouter.get("/:projectId/saved/draft", handleSavedGet("draft"));
731 projectRouter.post("/:projectId/deploy", handleDeploy);
732 projectRouter.get("/:projectId/status", handleStatus);
733 projectRouter.delete("/:projectId", handleDelete);
734 projectRouter.get("/:projectId/repos/github", handleGithubRepos);
735 projectRouter.post("/:projectId/github-token", handleUpdateGithubToken);
736 projectRouter.get("/:projectId/env", handleEnv);
737 projectRouter.post("/:projectId/reload", handleReload);
738 projectRouter.get("/:projectId/logs/:service", handleServiceLogs);
739 projectRouter.post("/:projectId/remove-deployment", handleRemoveDeployment);
740 projectRouter.get("/", handleProjectAll);
741 projectRouter.post("/", handleProjectCreate);
742 app.use("/api/project", projectRouter); // Mount the authenticated router
743
giod0026612025-05-08 13:00:36 +0000744 app.use("/", express.static("../front/dist"));
gio09fcab52025-05-12 14:05:07 +0000745
gio76d8ae62025-05-19 15:21:54 +0000746 const internalApi = express();
747 internalApi.use(express.json());
748 internalApi.post("/api/project/:projectId/workers", handleRegisterWorker);
gio09fcab52025-05-12 14:05:07 +0000749
giod0026612025-05-08 13:00:36 +0000750 app.listen(env.DODO_PORT_WEB, () => {
gio09fcab52025-05-12 14:05:07 +0000751 console.log("Web server started on port", env.DODO_PORT_WEB);
752 });
753
gio76d8ae62025-05-19 15:21:54 +0000754 internalApi.listen(env.DODO_PORT_API, () => {
gio09fcab52025-05-12 14:05:07 +0000755 console.log("Internal API server started on port", env.DODO_PORT_API);
giod0026612025-05-08 13:00:36 +0000756 });
757}
758
759start();