Merge pull request #547 from Dokploy/536-implement-custom-certificates-in-external-server

feat(certificates): create certificates in a remote server
This commit is contained in:
Mauricio Siu
2024-10-13 11:01:16 -06:00
committed by GitHub
10 changed files with 4105 additions and 43 deletions

View File

@@ -18,10 +18,25 @@ import {
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle } from "lucide-react";
import { AlertTriangle, HelpCircle } from "lucide-react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -35,6 +50,7 @@ const addCertificate = z.object({
certificateData: z.string().min(1, "Certificate data is required"),
privateKey: z.string().min(1, "Private key is required"),
autoRenew: z.boolean().optional(),
serverId: z.string().optional(),
});
type AddCertificate = z.infer<typeof addCertificate>;
@@ -44,6 +60,7 @@ export const AddCertificate = () => {
const { mutateAsync, isError, error, isLoading } =
api.certificates.create.useMutation();
const { data: servers } = api.server.withSSHKey.useQuery();
const form = useForm<AddCertificate>({
defaultValues: {
@@ -64,6 +81,7 @@ export const AddCertificate = () => {
certificateData: data.certificateData,
privateKey: data.privateKey,
autoRenew: data.autoRenew,
serverId: data.serverId,
})
.then(async () => {
toast.success("Certificate Created");
@@ -144,6 +162,47 @@ export const AddCertificate = () => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="serverId"
render={({ field }) => (
<FormItem>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
Select a Server (Optional)
<HelpCircle className="size-4 text-muted-foreground" />
</FormLabel>
</TooltipTrigger>
</Tooltip>
</TooltipProvider>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<SelectTrigger>
<SelectValue placeholder="Select a Server" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{servers?.map((server) => (
<SelectItem
key={server.serverId}
value={server.serverId}
>
{server.name}
</SelectItem>
))}
<SelectLabel>Servers ({servers?.length})</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</form>
<DialogFooter className="flex w-full flex-row !justify-between pt-3">

View File

@@ -48,16 +48,13 @@ export const SettingsLayout = ({ children }: Props) => {
icon: Database,
href: "/dashboard/settings/destinations",
},
...(!isCloud
? [
{
title: "Certificates",
label: "",
icon: ShieldCheck,
href: "/dashboard/settings/certificates",
},
]
: []),
{
title: "Certificates",
label: "",
icon: ShieldCheck,
href: "/dashboard/settings/certificates",
},
{
title: "SSH Keys",
label: "",

View File

@@ -0,0 +1,13 @@
ALTER TABLE "certificate" ADD COLUMN "adminId" text;--> statement-breakpoint
ALTER TABLE "certificate" ADD COLUMN "serverId" text;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "certificate" ADD CONSTRAINT "certificate_adminId_admin_adminId_fk" FOREIGN KEY ("adminId") REFERENCES "public"."admin"("adminId") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "certificate" ADD CONSTRAINT "certificate_serverId_server_serverId_fk" FOREIGN KEY ("serverId") REFERENCES "public"."server"("serverId") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

File diff suppressed because it is too large Load Diff

View File

@@ -281,6 +281,13 @@
"when": 1728021127765,
"tag": "0039_many_tiger_shark",
"breakpoints": true
},
{
"idx": 40,
"version": "6",
"when": 1728780577084,
"tag": "0040_graceful_wolfsbane",
"breakpoints": true
}
]
}

View File

@@ -1,30 +1,62 @@
import { adminProcedure, createTRPCRouter } from "@/server/api/trpc";
import { apiCreateCertificate, apiFindCertificate } from "@/server/db/schema";
import {
apiCreateCertificate,
apiFindCertificate,
certificates,
} from "@/server/db/schema";
import { db } from "@/server/db";
import {
IS_CLOUD,
createCertificate,
findCertificateById,
findCertificates,
removeCertificateById,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
export const certificateRouter = createTRPCRouter({
create: adminProcedure
.input(apiCreateCertificate)
.mutation(async ({ input }) => {
return await createCertificate(input);
.mutation(async ({ input, ctx }) => {
if (IS_CLOUD && !input.serverId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Please set a server to create a certificate",
});
}
return await createCertificate(input, ctx.user.adminId);
}),
one: adminProcedure.input(apiFindCertificate).query(async ({ input }) => {
return await findCertificateById(input.certificateId);
}),
one: adminProcedure
.input(apiFindCertificate)
.query(async ({ input, ctx }) => {
const certificates = await findCertificateById(input.certificateId);
if (IS_CLOUD && certificates.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this certificate",
});
}
return certificates;
}),
remove: adminProcedure
.input(apiFindCertificate)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
const certificates = await findCertificateById(input.certificateId);
if (IS_CLOUD && certificates.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to delete this certificate",
});
}
await removeCertificateById(input.certificateId);
return true;
}),
all: adminProcedure.query(async () => {
return findCertificates();
all: adminProcedure.query(async ({ ctx }) => {
return await db.query.certificates.findMany({
// TODO: Remove this line when the cloud version is ready
...(IS_CLOUD && { where: eq(certificates.adminId, ctx.user.adminId) }),
});
}),
});