mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
feat: new table and crud operations
This commit is contained in:
parent
083bb7b87d
commit
f866250c25
165
components/dashboard/settings/ssh-keys/add-ssh-key.tsx
Normal file
165
components/dashboard/settings/ssh-keys/add-ssh-key.tsx
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { sshKeyCreate } from "@/server/db/validations";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { type ReactNode, useState } from "react";
|
||||||
|
import { flushSync } from "react-dom";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import type { z } from "zod";
|
||||||
|
|
||||||
|
type SSHKey = z.infer<typeof sshKeyCreate>;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddSSHKey = ({ children }: Props) => {
|
||||||
|
const utils = api.useUtils();
|
||||||
|
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
const { mutateAsync, isError, error, isLoading } =
|
||||||
|
api.sshKey.create.useMutation();
|
||||||
|
|
||||||
|
const form = useForm<SSHKey>({
|
||||||
|
resolver: zodResolver(sshKeyCreate),
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = async (data: SSHKey) => {
|
||||||
|
await mutateAsync(data)
|
||||||
|
.then(async () => {
|
||||||
|
toast.success("SSH key created successfully");
|
||||||
|
await utils.sshKey.all.invalidate();
|
||||||
|
/*
|
||||||
|
Flushsync is needed for a bug witht he react-hook-form reset method
|
||||||
|
https://github.com/orgs/react-hook-form/discussions/7589#discussioncomment-10060621
|
||||||
|
*/
|
||||||
|
flushSync(() => form.reset());
|
||||||
|
setIsOpen(false);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to create the SSH key");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger className="" asChild>
|
||||||
|
{children}
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>SSH Key</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
In this section you can add an SSH key
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
|
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
className="grid w-full gap-4 "
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder={"Personal projects"} {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="description"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Description</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder={"Used on my personal Hetzner VPS"}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="privateKey"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>Private Key</FormLabel>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea
|
||||||
|
placeholder={"-----BEGIN RSA PRIVATE KEY-----"}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="publicKey"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>Public Key</FormLabel>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea
|
||||||
|
placeholder={"ssh-rsa AAAAB3NzaC1yc2E"}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<DialogFooter className="flex w-full flex-row !justify-between pt-3">
|
||||||
|
<Button isLoading={isLoading} type="submit">
|
||||||
|
Create
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
61
components/dashboard/settings/ssh-keys/delete-ssh-key.tsx
Normal file
61
components/dashboard/settings/ssh-keys/delete-ssh-key.tsx
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
} from "@/components/ui/alert-dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { TrashIcon } from "lucide-react";
|
||||||
|
import React from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
sshKeyId: string;
|
||||||
|
}
|
||||||
|
export const DeleteSSHKey = ({ sshKeyId }: Props) => {
|
||||||
|
const { mutateAsync, isLoading } = api.sshKey.remove.useMutation();
|
||||||
|
const utils = api.useUtils();
|
||||||
|
return (
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button variant="ghost" isLoading={isLoading}>
|
||||||
|
<TrashIcon className="size-4 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
This action cannot be undone. This will permanently delete the SSH
|
||||||
|
key
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={async () => {
|
||||||
|
await mutateAsync({
|
||||||
|
sshKeyId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
utils.sshKey.all.invalidate();
|
||||||
|
toast.success("SSH Key delete successfully");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to delete SSH key");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Confirm
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
);
|
||||||
|
};
|
96
components/dashboard/settings/ssh-keys/show-ssh-keys.tsx
Normal file
96
components/dashboard/settings/ssh-keys/show-ssh-keys.tsx
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import { UpdateSSHKey } from "@/components/dashboard/settings/ssh-keys/update-ssh-key";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { formatDistanceToNow } from "date-fns";
|
||||||
|
import { KeyRound, KeyRoundIcon, PenBoxIcon } from "lucide-react";
|
||||||
|
import { AddSSHKey } from "./add-ssh-key";
|
||||||
|
import { DeleteSSHKey } from "./delete-ssh-key";
|
||||||
|
|
||||||
|
export const ShowDestinations = () => {
|
||||||
|
const { data } = api.sshKey.all.useQuery();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<Card className="h-full bg-transparent">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-xl">SSH Keys</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Use SSH to beeing able cloning from private repositories.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-2 pt-4">
|
||||||
|
{data?.length === 0 ? (
|
||||||
|
<div className="flex flex-col items-center gap-3">
|
||||||
|
<KeyRound className="size-8 self-center text-muted-foreground" />
|
||||||
|
<span className="text-base text-muted-foreground">
|
||||||
|
Add your first SSH Key
|
||||||
|
</span>
|
||||||
|
<AddSSHKey>
|
||||||
|
<Button>
|
||||||
|
<KeyRoundIcon className="size-4" /> Add SSH Key
|
||||||
|
</Button>
|
||||||
|
</AddSSHKey>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<div className="flex gap-4 text-xs px-3.5">
|
||||||
|
<div className="col-span-2 basis-4/12">Key</div>
|
||||||
|
<div className="basis-3/12">Added</div>
|
||||||
|
<div>Last Used</div>
|
||||||
|
</div>
|
||||||
|
{data?.map((sshKey) => (
|
||||||
|
<div
|
||||||
|
key={sshKey.sshKeyId}
|
||||||
|
className="flex gap-4 items-center border p-3.5 rounded-lg text-sm"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col basis-4/12">
|
||||||
|
<span>{sshKey.name}</span>
|
||||||
|
{sshKey.description && (
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{sshKey.description}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="basis-3/12">
|
||||||
|
{formatDistanceToNow(new Date(sshKey.createdAt), {
|
||||||
|
addSuffix: true,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className="grow">
|
||||||
|
{sshKey.lastUsedAt
|
||||||
|
? formatDistanceToNow(new Date(sshKey.lastUsedAt), {
|
||||||
|
addSuffix: true,
|
||||||
|
})
|
||||||
|
: "Never"}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row gap-1">
|
||||||
|
<UpdateSSHKey sshKeyId={sshKey.sshKeyId}>
|
||||||
|
<Button variant="ghost">
|
||||||
|
<PenBoxIcon className="size-4 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
</UpdateSSHKey>
|
||||||
|
<DeleteSSHKey sshKeyId={sshKey.sshKeyId} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div>
|
||||||
|
<AddSSHKey>
|
||||||
|
<Button>
|
||||||
|
<KeyRoundIcon className="size-4" /> Add SSH Key
|
||||||
|
</Button>
|
||||||
|
</AddSSHKey>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
148
components/dashboard/settings/ssh-keys/update-ssh-key.tsx
Normal file
148
components/dashboard/settings/ssh-keys/update-ssh-key.tsx
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { sshKeyUpdate } from "@/server/db/validations";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { type ReactNode, useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import type { z } from "zod";
|
||||||
|
|
||||||
|
type SSHKey = z.infer<typeof sshKeyUpdate>;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: ReactNode;
|
||||||
|
sshKeyId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UpdateSSHKey = ({ children, sshKeyId = "" }: Props) => {
|
||||||
|
const utils = api.useUtils();
|
||||||
|
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const { data } = api.sshKey.one.useQuery({
|
||||||
|
sshKeyId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutateAsync, isError, error, isLoading } =
|
||||||
|
api.sshKey.update.useMutation();
|
||||||
|
|
||||||
|
const form = useForm<SSHKey>({
|
||||||
|
resolver: zodResolver(sshKeyUpdate),
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
form.reset({
|
||||||
|
...data,
|
||||||
|
/* Convert null to undefined */
|
||||||
|
description: data.description || undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
const onSubmit = async (data: SSHKey) => {
|
||||||
|
await mutateAsync({
|
||||||
|
sshKeyId,
|
||||||
|
...data,
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
toast.success("SSH Key Updated");
|
||||||
|
await utils.sshKey.all.invalidate();
|
||||||
|
setIsOpen(false);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to update the SSH key");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger className="" asChild>
|
||||||
|
{children}
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>SSH Key</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
In this section you can edit an SSH key
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
|
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
className="grid w-full gap-4 "
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder={"Personal projects"} {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="description"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Description</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder={"Used on my personal Hetzner VPS"}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Public Key</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea rows={7} readOnly disabled value={data?.publicKey} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
|
<DialogFooter className="flex w-full flex-row !justify-between pt-3">
|
||||||
|
<Button isLoading={isLoading} type="submit">
|
||||||
|
Update
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
@ -53,6 +53,12 @@ export const SettingsLayout = ({ children }: Props) => {
|
|||||||
icon: ShieldCheck,
|
icon: ShieldCheck,
|
||||||
href: "/dashboard/settings/certificates",
|
href: "/dashboard/settings/certificates",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "SSH Keys",
|
||||||
|
label: "",
|
||||||
|
icon: KeyRound,
|
||||||
|
href: "/dashboard/settings/ssh-keys",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "Users",
|
title: "Users",
|
||||||
label: "",
|
label: "",
|
||||||
@ -86,6 +92,7 @@ import {
|
|||||||
Activity,
|
Activity,
|
||||||
Bell,
|
Bell,
|
||||||
Database,
|
Database,
|
||||||
|
KeyRound,
|
||||||
type LucideIcon,
|
type LucideIcon,
|
||||||
Route,
|
Route,
|
||||||
Server,
|
Server,
|
||||||
|
41
pages/dashboard/settings/ssh-keys.tsx
Normal file
41
pages/dashboard/settings/ssh-keys.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { ShowDestinations } from "@/components/dashboard/settings/ssh-keys/show-ssh-keys";
|
||||||
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
|
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||||
|
import { validateRequest } from "@/server/auth/auth";
|
||||||
|
import type { GetServerSidePropsContext } from "next";
|
||||||
|
import React, { type ReactElement } from "react";
|
||||||
|
|
||||||
|
const Page = () => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4 w-full">
|
||||||
|
<ShowDestinations />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Page;
|
||||||
|
|
||||||
|
Page.getLayout = (page: ReactElement) => {
|
||||||
|
return (
|
||||||
|
<DashboardLayout tab={"settings"}>
|
||||||
|
<SettingsLayout>{page}</SettingsLayout>
|
||||||
|
</DashboardLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export async function getServerSideProps(
|
||||||
|
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||||
|
) {
|
||||||
|
const { user, session } = await validateRequest(ctx.req, ctx.res);
|
||||||
|
if (!user || user.rol === "user") {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
permanent: true,
|
||||||
|
destination: "/",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {},
|
||||||
|
};
|
||||||
|
}
|
@ -23,6 +23,7 @@ import { redisRouter } from "./routers/redis";
|
|||||||
import { registryRouter } from "./routers/registry";
|
import { registryRouter } from "./routers/registry";
|
||||||
import { securityRouter } from "./routers/security";
|
import { securityRouter } from "./routers/security";
|
||||||
import { settingsRouter } from "./routers/settings";
|
import { settingsRouter } from "./routers/settings";
|
||||||
|
import { sshRouter } from "./routers/ssh-key";
|
||||||
import { userRouter } from "./routers/user";
|
import { userRouter } from "./routers/user";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -56,6 +57,7 @@ export const appRouter = createTRPCRouter({
|
|||||||
registry: registryRouter,
|
registry: registryRouter,
|
||||||
cluster: clusterRouter,
|
cluster: clusterRouter,
|
||||||
notification: notificationRouter,
|
notification: notificationRouter,
|
||||||
|
sshKey: sshRouter,
|
||||||
});
|
});
|
||||||
|
|
||||||
// export type definition of API
|
// export type definition of API
|
||||||
|
63
server/api/routers/ssh-key.ts
Normal file
63
server/api/routers/ssh-key.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import {
|
||||||
|
adminProcedure,
|
||||||
|
createTRPCRouter,
|
||||||
|
protectedProcedure,
|
||||||
|
} from "@/server/api/trpc";
|
||||||
|
import { db } from "@/server/db";
|
||||||
|
import {
|
||||||
|
apiCreateSshKey,
|
||||||
|
apiFindOneSshKey,
|
||||||
|
apiRemoveSshKey,
|
||||||
|
apiUpdateSshKey,
|
||||||
|
} from "@/server/db/schema";
|
||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import {
|
||||||
|
createSshKey,
|
||||||
|
findSSHKeyById,
|
||||||
|
removeSSHKeyById,
|
||||||
|
updateSSHKeyById,
|
||||||
|
} from "../services/ssh-key";
|
||||||
|
|
||||||
|
export const sshRouter = createTRPCRouter({
|
||||||
|
create: protectedProcedure
|
||||||
|
.input(apiCreateSshKey)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
try {
|
||||||
|
await createSshKey(input);
|
||||||
|
} catch (error) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Error to create the ssh key",
|
||||||
|
cause: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
remove: adminProcedure.input(apiRemoveSshKey).mutation(async ({ input }) => {
|
||||||
|
try {
|
||||||
|
return await removeSSHKeyById(input.sshKeyId);
|
||||||
|
} catch (error) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Error to delete this ssh key",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
one: protectedProcedure.input(apiFindOneSshKey).query(async ({ input }) => {
|
||||||
|
const sshKey = await findSSHKeyById(input.sshKeyId);
|
||||||
|
return sshKey;
|
||||||
|
}),
|
||||||
|
all: adminProcedure.query(async () => {
|
||||||
|
return await db.query.sshKeys.findMany({});
|
||||||
|
}),
|
||||||
|
update: adminProcedure.input(apiUpdateSshKey).mutation(async ({ input }) => {
|
||||||
|
try {
|
||||||
|
return await updateSSHKeyById(input);
|
||||||
|
} catch (error) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Error to update this ssh key",
|
||||||
|
cause: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
});
|
70
server/api/services/ssh-key.ts
Normal file
70
server/api/services/ssh-key.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import { db } from "@/server/db";
|
||||||
|
import {
|
||||||
|
type apiCreateSshKey,
|
||||||
|
type apiFindOneSshKey,
|
||||||
|
type apiRemoveSshKey,
|
||||||
|
type apiUpdateSshKey,
|
||||||
|
sshKeys,
|
||||||
|
} from "@/server/db/schema";
|
||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
|
export const createSshKey = async ({
|
||||||
|
privateKey,
|
||||||
|
...input
|
||||||
|
}: typeof apiCreateSshKey._type) => {
|
||||||
|
await db.transaction(async (tx) => {
|
||||||
|
const sshKey = await tx
|
||||||
|
.insert(sshKeys)
|
||||||
|
.values(input)
|
||||||
|
.returning()
|
||||||
|
.then((response) => response[0])
|
||||||
|
.catch((e) => console.error(e));
|
||||||
|
if (!sshKey) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Error to create the ssh key",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return sshKey;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const removeSSHKeyById = async (
|
||||||
|
sshKeyId: (typeof apiRemoveSshKey._type)["sshKeyId"],
|
||||||
|
) => {
|
||||||
|
const result = await db
|
||||||
|
.delete(sshKeys)
|
||||||
|
.where(eq(sshKeys.sshKeyId, sshKeyId))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return result[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateSSHKeyById = async ({
|
||||||
|
sshKeyId,
|
||||||
|
...input
|
||||||
|
}: typeof apiUpdateSshKey._type) => {
|
||||||
|
const result = await db
|
||||||
|
.update(sshKeys)
|
||||||
|
.set(input)
|
||||||
|
.where(eq(sshKeys.sshKeyId, sshKeyId))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return result[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const findSSHKeyById = async (
|
||||||
|
sshKeyId: (typeof apiFindOneSshKey._type)["sshKeyId"],
|
||||||
|
) => {
|
||||||
|
const sshKey = await db.query.sshKeys.findFirst({
|
||||||
|
where: eq(sshKeys.sshKeyId, sshKeyId),
|
||||||
|
});
|
||||||
|
if (!sshKey) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: "SSH Key not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return sshKey;
|
||||||
|
};
|
@ -22,3 +22,4 @@ export * from "./shared";
|
|||||||
export * from "./compose";
|
export * from "./compose";
|
||||||
export * from "./registry";
|
export * from "./registry";
|
||||||
export * from "./notification";
|
export * from "./notification";
|
||||||
|
export * from "./ssh-key";
|
||||||
|
57
server/db/schema/ssh-key.ts
Normal file
57
server/db/schema/ssh-key.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { sshKeyCreate } from "@/server/db/validations";
|
||||||
|
import { pgTable, text, time } from "drizzle-orm/pg-core";
|
||||||
|
import { createInsertSchema } from "drizzle-zod";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
|
||||||
|
export const sshKeys = pgTable("ssh-key", {
|
||||||
|
sshKeyId: text("sshKeyId")
|
||||||
|
.notNull()
|
||||||
|
.primaryKey()
|
||||||
|
.$defaultFn(() => nanoid()),
|
||||||
|
publicKey: text("publicKey").notNull(),
|
||||||
|
name: text("name").notNull(),
|
||||||
|
description: text("description"),
|
||||||
|
createdAt: text("createdAt")
|
||||||
|
.notNull()
|
||||||
|
.$defaultFn(() => new Date().toISOString()),
|
||||||
|
lastUsedAt: text("lastUsedAt"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const createSchema = createInsertSchema(
|
||||||
|
sshKeys,
|
||||||
|
/* Private key is not stored in the DB */
|
||||||
|
sshKeyCreate.omit({ privateKey: true }).shape,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const apiCreateSshKey = createSchema
|
||||||
|
.pick({
|
||||||
|
name: true,
|
||||||
|
description: true,
|
||||||
|
publicKey: true,
|
||||||
|
})
|
||||||
|
.merge(sshKeyCreate.pick({ privateKey: true }));
|
||||||
|
|
||||||
|
export const apiFindOneSshKey = createSchema
|
||||||
|
.pick({
|
||||||
|
sshKeyId: true,
|
||||||
|
})
|
||||||
|
.required();
|
||||||
|
|
||||||
|
export const apiRemoveSshKey = createSchema
|
||||||
|
.pick({
|
||||||
|
sshKeyId: true,
|
||||||
|
})
|
||||||
|
.required();
|
||||||
|
|
||||||
|
export const apiUpdateSshKey = createSchema
|
||||||
|
.pick({
|
||||||
|
name: true,
|
||||||
|
description: true,
|
||||||
|
})
|
||||||
|
.merge(
|
||||||
|
createSchema
|
||||||
|
.pick({
|
||||||
|
sshKeyId: true,
|
||||||
|
})
|
||||||
|
.required(),
|
||||||
|
);
|
22
server/db/validations/index.ts
Normal file
22
server/db/validations/index.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const sshKeyCreate = z.object({
|
||||||
|
name: z.string().min(1),
|
||||||
|
description: z.string().optional(),
|
||||||
|
publicKey: z.string().regex(/^ssh-rsa\s+([A-Za-z0-9+/=]+)\s*(.*)?$/, {
|
||||||
|
message: "Invalid format",
|
||||||
|
}),
|
||||||
|
privateKey: z
|
||||||
|
.string()
|
||||||
|
.regex(
|
||||||
|
/^-----BEGIN RSA PRIVATE KEY-----\n([A-Za-z0-9+/=\n]+)-----END RSA PRIVATE KEY-----$/,
|
||||||
|
{
|
||||||
|
message: "Invalid format",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const sshKeyUpdate = sshKeyCreate.pick({
|
||||||
|
name: true,
|
||||||
|
description: true,
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user