mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
feat: ssh key type and whitespaces
This commit is contained in:
@@ -19,7 +19,7 @@ import {
|
|||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { sshKeyCreate } from "@/server/db/validations";
|
import { sshKeyCreate, type sshKeyType } from "@/server/db/validations";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { type ReactNode, useState } from "react";
|
import { type ReactNode, useState } from "react";
|
||||||
@@ -65,6 +65,18 @@ export const AddSSHKey = ({ children }: Props) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onGenerateSSHKey = (type: z.infer<typeof sshKeyType>) =>
|
||||||
|
generateMutation
|
||||||
|
.mutateAsync(type)
|
||||||
|
.then(async (data) => {
|
||||||
|
toast.success("SSH Key Generated");
|
||||||
|
form.setValue("privateKey", data.privateKey);
|
||||||
|
form.setValue("publicKey", data.publicKey);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to generate the SSH Key");
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger className="" asChild>
|
<DialogTrigger className="" asChild>
|
||||||
@@ -78,26 +90,26 @@ export const AddSSHKey = ({ children }: Props) => {
|
|||||||
In this section you can add one of your keys or generate a new
|
In this section you can add one of your keys or generate a new
|
||||||
one.
|
one.
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<div className="flex gap-4">
|
||||||
variant={"secondary"}
|
<Button
|
||||||
isLoading={generateMutation.isLoading}
|
variant={"secondary"}
|
||||||
className="max-sm:w-full"
|
disabled={generateMutation.isLoading}
|
||||||
onClick={async () => {
|
className="max-sm:w-full"
|
||||||
await generateMutation
|
onClick={() => onGenerateSSHKey("rsa")}
|
||||||
.mutateAsync()
|
type="button"
|
||||||
.then(async (data) => {
|
>
|
||||||
toast.success("SSH Key Generated");
|
Generate RSA SSH Key
|
||||||
form.setValue("privateKey", data.privateKey);
|
</Button>
|
||||||
form.setValue("publicKey", data.publicKey);
|
<Button
|
||||||
})
|
variant={"secondary"}
|
||||||
.catch(() => {
|
disabled={generateMutation.isLoading}
|
||||||
toast.error("Error to generate the SSH Key");
|
className="max-sm:w-full"
|
||||||
});
|
onClick={() => onGenerateSSHKey("ed25519")}
|
||||||
}}
|
type="button"
|
||||||
type="button"
|
>
|
||||||
>
|
Generate ED25519 SSH Key
|
||||||
Generate SSH Key
|
</Button>
|
||||||
</Button>
|
</div>
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ import { Textarea } from "@/components/ui/textarea";
|
|||||||
import { sshKeyUpdate } from "@/server/db/validations";
|
import { sshKeyUpdate } from "@/server/db/validations";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import copy from "copy-to-clipboard";
|
||||||
|
import { CopyIcon } from "lucide-react";
|
||||||
import { type ReactNode, useEffect, useState } from "react";
|
import { type ReactNode, useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -131,7 +133,24 @@ export const UpdateSSHKey = ({ children, sshKeyId = "" }: Props) => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Public Key</FormLabel>
|
<FormLabel>Public Key</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Textarea rows={7} readOnly disabled value={data?.publicKey} />
|
<div className="relative">
|
||||||
|
<Textarea
|
||||||
|
rows={7}
|
||||||
|
readOnly
|
||||||
|
disabled
|
||||||
|
value={data?.publicKey}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="absolute right-2 top-2"
|
||||||
|
onClick={() => {
|
||||||
|
copy(data?.publicKey || "Generate a SSH Key");
|
||||||
|
toast.success("SSH Copied to clipboard");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CopyIcon className="size-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { db } from "@/server/db";
|
|||||||
import {
|
import {
|
||||||
apiCreateSshKey,
|
apiCreateSshKey,
|
||||||
apiFindOneSshKey,
|
apiFindOneSshKey,
|
||||||
|
apiGenerateSSHKey,
|
||||||
apiRemoveSshKey,
|
apiRemoveSshKey,
|
||||||
apiUpdateSshKey,
|
apiUpdateSshKey,
|
||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
@@ -50,9 +51,11 @@ export const sshRouter = createTRPCRouter({
|
|||||||
all: adminProcedure.query(async () => {
|
all: adminProcedure.query(async () => {
|
||||||
return await db.query.sshKeys.findMany({});
|
return await db.query.sshKeys.findMany({});
|
||||||
}),
|
}),
|
||||||
generate: protectedProcedure.mutation(async () => {
|
generate: protectedProcedure
|
||||||
return await generateSSHKey();
|
.input(apiGenerateSSHKey)
|
||||||
}),
|
.mutation(async ({ input }) => {
|
||||||
|
return await generateSSHKey(input);
|
||||||
|
}),
|
||||||
update: adminProcedure.input(apiUpdateSshKey).mutation(async ({ input }) => {
|
update: adminProcedure.input(apiUpdateSshKey).mutation(async ({ input }) => {
|
||||||
try {
|
try {
|
||||||
return await updateSSHKeyById(input);
|
return await updateSSHKeyById(input);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { applications } from "@/server/db/schema/application";
|
import { applications } from "@/server/db/schema/application";
|
||||||
import { compose } from "@/server/db/schema/compose";
|
import { compose } from "@/server/db/schema/compose";
|
||||||
import { sshKeyCreate } from "@/server/db/validations";
|
import { sshKeyCreate, sshKeyType } from "@/server/db/validations";
|
||||||
import { relations } from "drizzle-orm";
|
import { relations } from "drizzle-orm";
|
||||||
import { pgTable, text, time } from "drizzle-orm/pg-core";
|
import { pgTable, text, time } from "drizzle-orm/pg-core";
|
||||||
import { createInsertSchema } from "drizzle-zod";
|
import { createInsertSchema } from "drizzle-zod";
|
||||||
@@ -45,6 +45,8 @@ export const apiFindOneSshKey = createSchema
|
|||||||
})
|
})
|
||||||
.required();
|
.required();
|
||||||
|
|
||||||
|
export const apiGenerateSSHKey = sshKeyType;
|
||||||
|
|
||||||
export const apiRemoveSshKey = createSchema
|
export const apiRemoveSshKey = createSchema
|
||||||
.pick({
|
.pick({
|
||||||
sshKeyId: true,
|
sshKeyId: true,
|
||||||
|
|||||||
@@ -3,20 +3,33 @@ import { z } from "zod";
|
|||||||
export const sshKeyCreate = z.object({
|
export const sshKeyCreate = z.object({
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
publicKey: z.string().regex(/^ssh-rsa\s+([A-Za-z0-9+/=]+)\s*(.*)?$/, {
|
publicKey: z.string().refine(
|
||||||
message: "Invalid format",
|
(key) => {
|
||||||
}),
|
const rsaPubPattern = /^ssh-rsa\s+([A-Za-z0-9+/=]+)\s*(.*)?\s*$/;
|
||||||
privateKey: z
|
const ed25519PubPattern = /^ssh-ed25519\s+([A-Za-z0-9+/=]+)\s*(.*)?\s*$/;
|
||||||
.string()
|
return rsaPubPattern.test(key) || ed25519PubPattern.test(key);
|
||||||
.regex(
|
},
|
||||||
/^-----BEGIN RSA PRIVATE KEY-----\n([A-Za-z0-9+/=\n]+)-----END RSA PRIVATE KEY-----$/,
|
{
|
||||||
{
|
message: "Invalid public key format",
|
||||||
message: "Invalid format",
|
},
|
||||||
},
|
),
|
||||||
),
|
privateKey: z.string().refine(
|
||||||
|
(key) => {
|
||||||
|
const rsaPrivPattern =
|
||||||
|
/^-----BEGIN RSA PRIVATE KEY-----\n([A-Za-z0-9+/=\n]+)-----END RSA PRIVATE KEY-----\s*$/;
|
||||||
|
const ed25519PrivPattern =
|
||||||
|
/^-----BEGIN OPENSSH PRIVATE KEY-----\n([A-Za-z0-9+/=\n]+)-----END OPENSSH PRIVATE KEY-----\s*$/;
|
||||||
|
return rsaPrivPattern.test(key) || ed25519PrivPattern.test(key);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "Invalid private key format",
|
||||||
|
},
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const sshKeyUpdate = sshKeyCreate.pick({
|
export const sshKeyUpdate = sshKeyCreate.pick({
|
||||||
name: true,
|
name: true,
|
||||||
description: true,
|
description: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const sshKeyType = z.enum(["rsa", "ed25519"]).optional();
|
||||||
|
|||||||
@@ -10,16 +10,12 @@ const readSSHKey = async (id: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
privateKey: fs
|
privateKey: fs.readFileSync(path.join(SSH_PATH, `${id}_rsa`), {
|
||||||
.readFileSync(path.join(SSH_PATH, `${id}_rsa`), {
|
encoding: "utf-8",
|
||||||
encoding: "utf-8",
|
}),
|
||||||
})
|
publicKey: fs.readFileSync(path.join(SSH_PATH, `${id}_rsa.pub`), {
|
||||||
.trim(),
|
encoding: "utf-8",
|
||||||
publicKey: fs
|
}),
|
||||||
.readFileSync(path.join(SSH_PATH, `${id}_rsa.pub`), {
|
|
||||||
encoding: "utf-8",
|
|
||||||
})
|
|
||||||
.trim(),
|
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
@@ -47,7 +43,7 @@ export const saveSSHKey = async (
|
|||||||
publicKeyStream.end();
|
publicKeyStream.end();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const generateSSHKey = async () => {
|
export const generateSSHKey = async (type: "rsa" | "ed25519" = "rsa") => {
|
||||||
const applicationDirectory = SSH_PATH;
|
const applicationDirectory = SSH_PATH;
|
||||||
|
|
||||||
if (!fs.existsSync(applicationDirectory)) {
|
if (!fs.existsSync(applicationDirectory)) {
|
||||||
@@ -66,7 +62,7 @@ export const generateSSHKey = async () => {
|
|||||||
|
|
||||||
const args = [
|
const args = [
|
||||||
"-t",
|
"-t",
|
||||||
"rsa",
|
type,
|
||||||
"-b",
|
"-b",
|
||||||
"4096",
|
"4096",
|
||||||
"-C",
|
"-C",
|
||||||
|
|||||||
Reference in New Issue
Block a user