Canvas: Github repository picker
Change-Id: Icb8f2ffbef2894b2fdea4e4c13c74c0f4970506b
diff --git a/apps/canvas/back/github.ts b/apps/canvas/back/github.ts
new file mode 100644
index 0000000..f234788
--- /dev/null
+++ b/apps/canvas/back/github.ts
@@ -0,0 +1,51 @@
+import axios from 'axios';
+import { z } from 'zod';
+
+export const GithubRepositorySchema = z.object({
+ id: z.number(),
+ name: z.string(),
+ full_name: z.string(),
+ html_url: z.string(),
+ ssh_url: z.string(),
+});
+
+export type GithubRepository = z.infer<typeof GithubRepositorySchema>;
+
+export class GithubClient {
+ private token: string;
+
+ constructor(token: string) {
+ this.token = token;
+ }
+
+ private getHeaders() {
+ return {
+ "Authorization": `Bearer ${this.token}`,
+ "Accept": "application/vnd.github.v3+json",
+ };
+ }
+
+ async getRepositories(): Promise<GithubRepository[]> {
+ const response = await axios.get("https://api.github.com/user/repos", {
+ headers: this.getHeaders(),
+ });
+ return z.array(GithubRepositorySchema).parse(response.data);
+ }
+
+ async addDeployKey(repoPath: string, key: string) {
+ const sshUrl = repoPath;
+ const repoOwnerAndName = sshUrl.replace('git@github.com:', '').replace('.git', '');
+
+ await axios.post(
+ `https://api.github.com/repos/${repoOwnerAndName}/keys`,
+ {
+ title: "dodo",
+ key: key,
+ read_only: true
+ },
+ {
+ headers: this.getHeaders(),
+ }
+ );
+ }
+}
\ No newline at end of file
diff --git a/apps/canvas/back/index.ts b/apps/canvas/back/index.ts
index 258bad9..cdcf9a0 100644
--- a/apps/canvas/back/index.ts
+++ b/apps/canvas/back/index.ts
@@ -2,6 +2,7 @@
import express from "express";
import { env } from "node:process";
import axios from "axios";
+import { GithubClient } from "./github";
const db = new PrismaClient();
@@ -134,7 +135,7 @@
where: {
id: projectId,
},
- });
+ });
}
resp.status(200);
} catch (e) {
@@ -155,6 +156,8 @@
},
select: {
instanceId: true,
+ githubToken: true,
+ deployKey: true,
},
});
if (p === null) {
@@ -178,6 +181,7 @@
config: req.body.config,
},
});
+ console.log(r);
if (r.status === 200) {
await db.project.update({
where: {
@@ -190,6 +194,20 @@
deployKey: r.data.deployKey,
},
});
+
+ if (p.githubToken && r.data.deployKey) {
+ const stateObj = JSON.parse(JSON.parse(state.toString()));
+ const githubNodes = stateObj.nodes.filter((n: any) => n.type === "github" && n.data?.repository?.id);
+
+ const github = new GithubClient(p.githubToken);
+ for (const node of githubNodes) {
+ try {
+ await github.addDeployKey(node.data.repository.sshURL, r.data.deployKey);
+ } catch (error) {
+ console.error(`Failed to add deploy key to repository ${node.data.repository.sshURL}:`, error);
+ }
+ }
+ }
}
} else {
r = await axios.request({
@@ -255,6 +273,94 @@
}
};
+const handleGithubRepos: express.Handler = async (req, resp) => {
+ try {
+ const projectId = Number(req.params["projectId"]);
+ const project = await db.project.findUnique({
+ where: { id: projectId },
+ select: { githubToken: true }
+ });
+
+ if (!project?.githubToken) {
+ resp.status(400);
+ resp.write(JSON.stringify({ error: "GitHub token not configured" }));
+ return;
+ }
+
+ const github = new GithubClient(project.githubToken);
+ const repositories = await github.getRepositories();
+
+ resp.status(200);
+ resp.header("Content-Type", "application/json");
+ resp.write(JSON.stringify(repositories));
+ } catch (e) {
+ console.log(e);
+ resp.status(500);
+ resp.write(JSON.stringify({ error: "Failed to fetch repositories" }));
+ } finally {
+ resp.end();
+ }
+};
+
+const handleUpdateGithubToken: express.Handler = async (req, resp) => {
+ try {
+ const projectId = Number(req.params["projectId"]);
+ const { githubToken } = req.body;
+
+ await db.project.update({
+ where: { id: projectId },
+ data: { githubToken },
+ });
+
+ resp.status(200);
+ } catch (e) {
+ console.log(e);
+ resp.status(500);
+ } finally {
+ resp.end();
+ }
+};
+
+const handleEnv: express.Handler = async (req, resp) => {
+ const projectId = Number(req.params["projectId"]);
+ try {
+ const project = await db.project.findUnique({
+ where: { id: projectId },
+ select: {
+ deployKey: true,
+ githubToken: true
+ }
+ });
+
+ if (!project) {
+ resp.status(404);
+ resp.write(JSON.stringify({ error: "Project not found" }));
+ return;
+ }
+
+ resp.status(200);
+ resp.write(JSON.stringify({
+ deployKey: project.deployKey,
+ integrations: {
+ github: !!project.githubToken,
+ },
+ networks: [{
+ name: "Public",
+ domain: "v1.dodo.cloud",
+ }, {
+ name: "Private",
+ domain: "p.v1.dodo.cloud",
+ }]
+ }));
+ } catch (error) {
+ console.error("Error checking integrations:", error);
+ resp.status(500);
+ resp.write(JSON.stringify({ error: "Internal server error" }));
+ } finally {
+ resp.end();
+ }
+};
+
async function start() {
await db.$connect();
const app = express();
@@ -266,6 +372,9 @@
app.delete("/api/project/:projectId", handleDelete);
app.get("/api/project", handleProjectAll);
app.post("/api/project", handleProjectCreate);
+ app.get("/api/project/:projectId/repos/github", handleGithubRepos);
+ app.post("/api/project/:projectId/github-token", handleUpdateGithubToken);
+ app.get("/api/project/:projectId/env", handleEnv);
app.use("/", express.static("../front/dist"));
app.listen(env.DODO_PORT_WEB, () => {
console.log("started");
diff --git a/apps/canvas/back/package-lock.json b/apps/canvas/back/package-lock.json
index cf48f57..89b0272 100644
--- a/apps/canvas/back/package-lock.json
+++ b/apps/canvas/back/package-lock.json
@@ -12,7 +12,8 @@
"@prisma/client": "^6.6.0",
"axios": "^1.8.4",
"express": "^4.21.1",
- "sqlite3": "^5.1.7"
+ "sqlite3": "^5.1.7",
+ "zod": "^3.24.4"
},
"devDependencies": {
"@types/express": "^5.0.0",
@@ -2941,6 +2942,14 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+ },
+ "node_modules/zod": {
+ "version": "3.24.4",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.4.tgz",
+ "integrity": "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
}
}
}
diff --git a/apps/canvas/back/package.json b/apps/canvas/back/package.json
index b05fb1e..360c453 100644
--- a/apps/canvas/back/package.json
+++ b/apps/canvas/back/package.json
@@ -13,7 +13,8 @@
"@prisma/client": "^6.6.0",
"axios": "^1.8.4",
"express": "^4.21.1",
- "sqlite3": "^5.1.7"
+ "sqlite3": "^5.1.7",
+ "zod": "^3.24.4"
},
"devDependencies": {
"@types/express": "^5.0.0",
diff --git a/apps/canvas/back/prisma/migrations/20250507131125_add_github_token/migration.sql b/apps/canvas/back/prisma/migrations/20250507131125_add_github_token/migration.sql
new file mode 100644
index 0000000..e9eac71
--- /dev/null
+++ b/apps/canvas/back/prisma/migrations/20250507131125_add_github_token/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "Project" ADD COLUMN "githubToken" TEXT;
diff --git a/apps/canvas/back/prisma/schema.prisma b/apps/canvas/back/prisma/schema.prisma
index e19e84b..8dd7ceb 100644
--- a/apps/canvas/back/prisma/schema.prisma
+++ b/apps/canvas/back/prisma/schema.prisma
@@ -21,4 +21,5 @@
draft Bytes?
instanceId String?
deployKey String?
+ githubToken String?
}
\ No newline at end of file