feat: ssh key type and whitespaces

This commit is contained in:
Lorenzo Migliorero
2024-07-25 23:32:52 +02:00
parent 12bd017d07
commit 2724336cad
6 changed files with 94 additions and 49 deletions

View File

@@ -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>}

View File

@@ -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>

View File

@@ -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);

View File

@@ -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,

View File

@@ -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();

View File

@@ -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",