Canvas: Remove deployment action

Change-Id: I5887f130f5d11880271c943f58284d62f7d07a23
diff --git a/apps/canvas/back/src/index.ts b/apps/canvas/back/src/index.ts
index 7c0fffe..cd8249d 100644
--- a/apps/canvas/back/src/index.ts
+++ b/apps/canvas/back/src/index.ts
@@ -68,7 +68,7 @@
 				userId: resp.locals.userId,
 			},
 			data: {
-				draft: Buffer.from(JSON.stringify(req.body)),
+				draft: JSON.stringify(req.body),
 			},
 		});
 		resp.status(200);
@@ -171,12 +171,12 @@
 	}
 };
 
-function extractGithubRepos(serializedState: Buffer | Uint8Array | null): string[] {
+function extractGithubRepos(serializedState: string | null): string[] {
 	if (!serializedState) {
 		return [];
 	}
 	try {
-		const stateObj = JSON.parse(serializedState.toString());
+		const stateObj = JSON.parse(serializedState);
 		const githubNodes = stateObj.nodes.filter(
 			// eslint-disable-next-line @typescript-eslint/no-explicit-any
 			(n: any) => n.type === "github" && n.data?.repository?.id,
@@ -222,7 +222,7 @@
 const handleDeploy: express.Handler = async (req, resp) => {
 	try {
 		const projectId = Number(req.params["projectId"]);
-		const state = Buffer.from(JSON.stringify(req.body.state));
+		const state = JSON.stringify(req.body.state);
 		const p = await db.project.findUnique({
 			where: {
 				id: projectId,
@@ -339,6 +339,70 @@
 	}
 };
 
+const handleRemoveDeployment: express.Handler = async (req, resp) => {
+	try {
+		const projectId = Number(req.params["projectId"]);
+		const p = await db.project.findUnique({
+			where: {
+				id: projectId,
+				userId: resp.locals.userId,
+			},
+			select: {
+				instanceId: true,
+				githubToken: true,
+				deployKey: true,
+				state: true,
+				draft: true,
+			},
+		});
+		if (p === null) {
+			resp.status(404);
+			resp.write(JSON.stringify({ error: "Project not found" }));
+			return;
+		}
+		if (p.instanceId == null) {
+			resp.status(400);
+			resp.write(JSON.stringify({ error: "Project not deployed" }));
+			return;
+		}
+		const removed = await appManager.removeInstance(p.instanceId);
+		if (!removed) {
+			resp.status(500);
+			resp.write(JSON.stringify({ error: "Failed to remove deployment from cluster" }));
+			return;
+		}
+		if (p.githubToken && p.deployKey && p.state) {
+			try {
+				const github = new GithubClient(p.githubToken);
+				const repos = extractGithubRepos(p.state);
+				const diff = { toDelete: repos, toAdd: [] };
+				await manageGithubKeys(github, p.deployKey, diff);
+			} catch (error) {
+				console.error("Error removing GitHub deploy keys:", error);
+			}
+		}
+		await db.project.update({
+			where: {
+				id: projectId,
+			},
+			data: {
+				instanceId: null,
+				deployKey: null,
+				state: null,
+				draft: p.draft ?? p.state,
+			},
+		});
+		resp.status(200);
+		resp.write(JSON.stringify({ success: true }));
+	} catch (e) {
+		console.error("Error removing deployment:", e);
+		resp.status(500);
+		resp.write(JSON.stringify({ error: "Internal server error" }));
+	} finally {
+		resp.end();
+	}
+};
+
 const handleGithubRepos: express.Handler = async (req, resp) => {
 	try {
 		const projectId = Number(req.params["projectId"]);
@@ -603,6 +667,7 @@
 	app.get("/api/project/:projectId/env", handleEnv);
 	app.post("/api/project/:projectId/reload", handleReload);
 	app.get("/api/project/:projectId/logs/:service", handleServiceLogs);
+	app.post("/api/project/:projectId/remove-deployment", handleRemoveDeployment);
 	app.use("/", express.static("../front/dist"));
 
 	const api = express();