From 1f81ebd4fe36e55bb644e853a772873e951905be Mon Sep 17 00:00:00 2001 From: Lorenzo Migliorero Date: Thu, 25 Jul 2024 20:16:49 +0200 Subject: [PATCH] feat: ssh keys filesystel --- .../general/generic/save-git-provider.tsx | 9 ++--- server/api/routers/application.ts | 10 +++--- server/api/routers/compose.ts | 10 +++--- server/api/services/ssh-key.ts | 8 +++++ server/utils/filesystem/ssh.ts | 35 +++++++++++++++---- server/utils/providers/git.ts | 10 +++--- 6 files changed, 56 insertions(+), 26 deletions(-) diff --git a/components/dashboard/application/general/generic/save-git-provider.tsx b/components/dashboard/application/general/generic/save-git-provider.tsx index bcce4c83..baafa20a 100644 --- a/components/dashboard/application/general/generic/save-git-provider.tsx +++ b/components/dashboard/application/general/generic/save-git-provider.tsx @@ -37,7 +37,7 @@ const GitProviderSchema = z.object({ }), branch: z.string().min(1, "Branch required"), buildPath: z.string().min(1, "Build Path required"), - sshKey: z.string(), + sshKey: z.string().optional(), }); type GitProvider = z.infer; @@ -63,7 +63,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => { branch: "", buildPath: "/", repositoryURL: "", - sshKey: "", + sshKey: undefined, }, resolver: zodResolver(GitProviderSchema), }); @@ -71,7 +71,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => { useEffect(() => { if (data) { form.reset({ - sshKey: data.customGitSSHKeyId || "", + sshKey: data.customGitSSHKeyId || undefined, branch: data.customGitBranch || "", buildPath: data.customGitBuildPath || "/", repositoryURL: data.customGitUrl || "", @@ -84,7 +84,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => { customGitBranch: values.branch, customGitBuildPath: values.buildPath, customGitUrl: values.repositoryURL, - customGitSSHKeyId: values.sshKey, + customGitSSHKeyId: values.sshKey === "none" ? null : values.sshKey, applicationId, }) .then(async () => { @@ -149,6 +149,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => { {sshKey.name} ))} + None Keys ({sshKeys?.length}) diff --git a/server/api/routers/application.ts b/server/api/routers/application.ts index b3f66cf5..1b991176 100644 --- a/server/api/routers/application.ts +++ b/server/api/routers/application.ts @@ -33,8 +33,8 @@ import { } from "@/server/utils/filesystem/directory"; import { generateSSHKey, - readRSAFile, - removeRSAFiles, + readSSHPublicKey, + removeSSHKey, } from "@/server/utils/filesystem/ssh"; import { readConfig, @@ -130,7 +130,7 @@ export const applicationRouter = createTRPCRouter({ async () => await removeMonitoringDirectory(application?.appName), async () => await removeTraefikConfig(application?.appName), async () => await removeService(application?.appName), - async () => await removeRSAFiles(application?.appName), + async () => await removeSSHKey(application?.appName), ]; for (const operation of cleanupOperations) { @@ -248,7 +248,7 @@ export const applicationRouter = createTRPCRouter({ const application = await findApplicationById(input.applicationId); try { await generateSSHKey(application.appName); - const file = await readRSAFile(application.appName); + const file = await readSSHPublicKey(application.appName); // await updateApplication(input.applicationId, { // customGitSSHKey: file, @@ -261,7 +261,7 @@ export const applicationRouter = createTRPCRouter({ .input(apiFindOneApplication) .mutation(async ({ input }) => { const application = await findApplicationById(input.applicationId); - await removeRSAFiles(application.appName); + await removeSSHKey(application.appName); // await updateApplication(input.applicationId, { // customGitSSHKey: null, // }); diff --git a/server/api/routers/compose.ts b/server/api/routers/compose.ts index dba4c476..2400b16d 100644 --- a/server/api/routers/compose.ts +++ b/server/api/routers/compose.ts @@ -18,8 +18,8 @@ import { randomizeComposeFile } from "@/server/utils/docker/compose"; import { removeComposeDirectory } from "@/server/utils/filesystem/directory"; import { generateSSHKey, - readRSAFile, - removeRSAFiles, + readSSHPublicKey, + removeSSHKey, } from "@/server/utils/filesystem/ssh"; import { templates } from "@/templates/templates"; import type { TemplatesKeys } from "@/templates/types/templates-data.type"; @@ -102,7 +102,7 @@ export const composeRouter = createTRPCRouter({ async () => await removeCompose(composeResult), async () => await removeDeploymentsByComposeId(composeResult), async () => await removeComposeDirectory(composeResult.appName), - async () => await removeRSAFiles(composeResult.appName), + async () => await removeSSHKey(composeResult.appName), ]; for (const operation of cleanupOperations) { @@ -187,7 +187,7 @@ export const composeRouter = createTRPCRouter({ const compose = await findComposeById(input.composeId); try { await generateSSHKey(compose.appName); - const file = await readRSAFile(compose.appName); + const file = await readSSHPublicKey(compose.appName); await updateCompose(input.composeId, { customGitSSHKey: file, @@ -208,7 +208,7 @@ export const composeRouter = createTRPCRouter({ .input(apiFindCompose) .mutation(async ({ input }) => { const compose = await findComposeById(input.composeId); - await removeRSAFiles(compose.appName); + await removeSSHKey(compose.appName); await updateCompose(input.composeId, { customGitSSHKey: null, }); diff --git a/server/api/services/ssh-key.ts b/server/api/services/ssh-key.ts index 5ae04d68..9d6f0070 100644 --- a/server/api/services/ssh-key.ts +++ b/server/api/services/ssh-key.ts @@ -6,6 +6,7 @@ import { type apiUpdateSshKey, sshKeys, } from "@/server/db/schema"; +import { removeSSHKey, saveSSHKey } from "@/server/utils/filesystem/ssh"; import { TRPCError } from "@trpc/server"; import { eq } from "drizzle-orm"; @@ -20,6 +21,11 @@ export const createSshKey = async ({ .returning() .then((response) => response[0]) .catch((e) => console.error(e)); + + if (sshKey) { + saveSSHKey(sshKey.sshKeyId, sshKey.publicKey, privateKey); + } + if (!sshKey) { throw new TRPCError({ code: "BAD_REQUEST", @@ -38,6 +44,8 @@ export const removeSSHKeyById = async ( .where(eq(sshKeys.sshKeyId, sshKeyId)) .returning(); + removeSSHKey(sshKeyId); + return result[0]; }; diff --git a/server/utils/filesystem/ssh.ts b/server/utils/filesystem/ssh.ts index 452bafaf..09a3a73d 100644 --- a/server/utils/filesystem/ssh.ts +++ b/server/utils/filesystem/ssh.ts @@ -3,14 +3,35 @@ import * as path from "node:path"; import { SSH_PATH } from "@/server/constants"; import { spawnAsync } from "../process/spawnAsync"; -export const generateSSHKey = async (appName: string) => { +export const saveSSHKey = async ( + id: string, + publicKey: string, + privateKey: string, +) => { + const applicationDirectory = SSH_PATH; + + const privateKeyPath = path.join(applicationDirectory, `${id}_rsa`); + const publicKeyPath = path.join(applicationDirectory, `${id}_rsa.pub`); + + const privateKeyStream = fs.createWriteStream(privateKeyPath, { + mode: 0o400, + }); + privateKeyStream.write(privateKey); + privateKeyStream.end(); + + const publicKeyStream = fs.createWriteStream(publicKeyPath, { mode: 0o400 }); + publicKeyStream.write(publicKey); + publicKeyStream.end(); +}; + +export const generateSSHKey = async (id: string) => { const applicationDirectory = SSH_PATH; if (!fs.existsSync(applicationDirectory)) { fs.mkdirSync(applicationDirectory, { recursive: true }); } - const keyPath = path.join(applicationDirectory, `${appName}_rsa`); + const keyPath = path.join(applicationDirectory, `${id}_rsa`); if (fs.existsSync(`${keyPath}`)) { fs.unlinkSync(`${keyPath}`); @@ -37,12 +58,12 @@ export const generateSSHKey = async (appName: string) => { throw error; } }; -export const readRSAFile = async (appName: string) => { +export const readSSHPublicKey = async (id: string) => { try { if (!fs.existsSync(SSH_PATH)) { fs.mkdirSync(SSH_PATH, { recursive: true }); } - const keyPath = path.join(SSH_PATH, `${appName}_rsa.pub`); + const keyPath = path.join(SSH_PATH, `${id}_rsa.pub`); const data = fs.readFileSync(keyPath, { encoding: "utf-8" }); return data; } catch (error) { @@ -50,10 +71,10 @@ export const readRSAFile = async (appName: string) => { } }; -export const removeRSAFiles = async (appName: string) => { +export const removeSSHKey = async (id: string) => { try { - const publicKeyPath = path.join(SSH_PATH, `${appName}_rsa.pub`); - const privateKeyPath = path.join(SSH_PATH, `${appName}_rsa`); + const publicKeyPath = path.join(SSH_PATH, `${id}_rsa.pub`); + const privateKeyPath = path.join(SSH_PATH, `${id}_rsa`); await fs.promises.unlink(publicKeyPath); await fs.promises.unlink(privateKeyPath); } catch (error) { diff --git a/server/utils/providers/git.ts b/server/utils/providers/git.ts index 48d9e1ca..53859a69 100644 --- a/server/utils/providers/git.ts +++ b/server/utils/providers/git.ts @@ -11,12 +11,12 @@ export const cloneGitRepository = async ( appName: string; customGitUrl?: string | null; customGitBranch?: string | null; - customGitSSHKey?: string | null; + customGitSSHKeyId?: string | null; }, logPath: string, isCompose = false, ) => { - const { appName, customGitUrl, customGitBranch, customGitSSHKey } = entity; + const { appName, customGitUrl, customGitBranch, customGitSSHKeyId } = entity; if (!customGitUrl || !customGitBranch) { throw new TRPCError({ @@ -26,7 +26,7 @@ export const cloneGitRepository = async ( } const writeStream = createWriteStream(logPath, { flags: "a" }); - const keyPath = path.join(SSH_PATH, `${appName}_rsa`); + const keyPath = path.join(SSH_PATH, `${customGitSSHKeyId}_rsa`); const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH; const outputPath = join(basePath, appName, "code"); const knownHostsPath = path.join(SSH_PATH, "known_hosts"); @@ -39,7 +39,7 @@ export const cloneGitRepository = async ( writeStream.write( `\nCloning Repo Custom ${customGitUrl} to ${outputPath}: ✅\n`, ); - + console.log(customGitSSHKeyId); await spawnAsync( "git", [ @@ -60,7 +60,7 @@ export const cloneGitRepository = async ( { env: { ...process.env, - ...(customGitSSHKey && { + ...(customGitSSHKeyId && { GIT_SSH_COMMAND: `ssh -i ${keyPath} -o UserKnownHostsFile=${knownHostsPath}`, }), },