mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-06 22:45:24 +02:00
feat: be able to edit certificate
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { HelpCircle, PlusIcon } from "lucide-react";
|
||||
import { HelpCircle, PlusIcon, SquarePen } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -47,7 +47,7 @@ const certificateDataHolder =
|
||||
const privateKeyDataHolder =
|
||||
"-----BEGIN PRIVATE KEY-----\nMIIFRDCCAyygAwIBAgIUEPOR47ys6VDwMVB9tYoeEka83uQwDQYJKoZIhvcNAQELBQAwGTEXMBUGA1UEAwwObWktZG9taW5pby5jb20wHhcNMjQwMzExMDQyNzU3WhcN\n-----END PRIVATE KEY-----";
|
||||
|
||||
const addCertificate = z.object({
|
||||
const handleCertificateSchema = z.object({
|
||||
name: z.string().min(1, "Name is required"),
|
||||
certificateData: z.string().min(1, "Certificate data is required"),
|
||||
privateKey: z.string().min(1, "Private key is required"),
|
||||
@@ -55,100 +55,143 @@ const addCertificate = z.object({
|
||||
serverId: z.string().optional(),
|
||||
});
|
||||
|
||||
type AddCertificate = z.infer<typeof addCertificate>;
|
||||
type HandleCertificateForm = z.infer<typeof handleCertificateSchema>;
|
||||
|
||||
export const AddCertificate = () => {
|
||||
interface Props {
|
||||
certificateId?: string;
|
||||
}
|
||||
|
||||
export const HandleCertificate = ({ certificateId }: Props) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const utils = api.useUtils();
|
||||
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const { mutateAsync, isError, error, isLoading } =
|
||||
api.certificates.create.useMutation();
|
||||
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||
const hasServers = servers && servers.length > 0;
|
||||
// Show dropdown logic based on cloud environment
|
||||
// Cloud: show only if there are remote servers (no Dokploy option)
|
||||
// Self-hosted: show only if there are remote servers (Dokploy is default, hide if no remote servers)
|
||||
const shouldShowServerDropdown = hasServers;
|
||||
const shouldShowServerDropdown = hasServers && !certificateId; // Hide on edit
|
||||
|
||||
const form = useForm<AddCertificate>({
|
||||
const { data: existingCert, refetch } = api.certificates.one.useQuery(
|
||||
{ certificateId: certificateId || "" },
|
||||
{ enabled: !!certificateId },
|
||||
);
|
||||
|
||||
const { mutateAsync, isError, error, isLoading } = certificateId
|
||||
? api.certificates.update.useMutation()
|
||||
: api.certificates.create.useMutation();
|
||||
|
||||
const form = useForm<HandleCertificateForm>({
|
||||
defaultValues: {
|
||||
name: "",
|
||||
certificateData: "",
|
||||
privateKey: "",
|
||||
autoRenew: false,
|
||||
},
|
||||
resolver: zodResolver(addCertificate),
|
||||
resolver: zodResolver(handleCertificateSchema),
|
||||
});
|
||||
useEffect(() => {
|
||||
form.reset();
|
||||
}, [form, form.formState.isSubmitSuccessful, form.reset]);
|
||||
|
||||
const onSubmit = async (data: AddCertificate) => {
|
||||
useEffect(() => {
|
||||
if (existingCert) {
|
||||
form.reset({
|
||||
name: existingCert.name,
|
||||
certificateData: existingCert.certificateData,
|
||||
privateKey: existingCert.privateKey,
|
||||
autoRenew: existingCert.autoRenew ?? false,
|
||||
});
|
||||
} else {
|
||||
form.reset({
|
||||
name: "",
|
||||
certificateData: "",
|
||||
privateKey: "",
|
||||
autoRenew: false,
|
||||
});
|
||||
}
|
||||
}, [existingCert, form, open]);
|
||||
|
||||
const onSubmit = async (data: HandleCertificateForm) => {
|
||||
await mutateAsync({
|
||||
...(certificateId ? { certificateId } : {}),
|
||||
name: data.name,
|
||||
certificateData: data.certificateData,
|
||||
privateKey: data.privateKey,
|
||||
autoRenew: data.autoRenew,
|
||||
serverId: data.serverId === "dokploy" ? undefined : data.serverId,
|
||||
organizationId: "",
|
||||
})
|
||||
} as any)
|
||||
.then(async () => {
|
||||
toast.success("Certificate Created");
|
||||
toast.success(
|
||||
certificateId ? "Certificate Updated" : "Certificate Created",
|
||||
);
|
||||
await utils.certificates.all.invalidate();
|
||||
if (certificateId) {
|
||||
refetch();
|
||||
}
|
||||
setOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error creating the Certificate");
|
||||
toast.error(
|
||||
certificateId
|
||||
? "Error updating the Certificate"
|
||||
: "Error creating the Certificate",
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger className="" asChild>
|
||||
<Button>
|
||||
{" "}
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
Add Certificate
|
||||
</Button>
|
||||
<DialogTrigger asChild>
|
||||
{certificateId ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="group hover:bg-blue-500/10"
|
||||
>
|
||||
<SquarePen className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||
</Button>
|
||||
) : (
|
||||
<Button>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
Add Certificate
|
||||
</Button>
|
||||
)}
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add New Certificate</DialogTitle>
|
||||
<DialogTitle>
|
||||
{certificateId ? "Update" : "Add New"} Certificate
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Upload or generate a certificate to secure your application
|
||||
{certificateId
|
||||
? "Modify the certificate details"
|
||||
: "Upload or generate a certificate to secure your application"}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form-add-certificate"
|
||||
id="hook-form-handle-certificate"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-4 "
|
||||
className="grid w-full gap-4"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Certificate Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={"My Certificate"} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Certificate Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="My Certificate" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="certificateData"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Certificate Data</FormLabel>
|
||||
</div>
|
||||
<FormLabel>Certificate Data</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
className="h-32"
|
||||
@@ -165,9 +208,7 @@ export const AddCertificate = () => {
|
||||
name="privateKey"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Private Key</FormLabel>
|
||||
</div>
|
||||
<FormLabel>Private Key</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
className="h-32"
|
||||
@@ -179,6 +220,23 @@ export const AddCertificate = () => {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="autoRenew"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center gap-2">
|
||||
<FormControl>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={field.value}
|
||||
onChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="!mt-0">Auto-renew</FormLabel>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{shouldShowServerDropdown && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
@@ -248,10 +306,10 @@ export const AddCertificate = () => {
|
||||
<DialogFooter className="flex w-full flex-row !justify-end">
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
form="hook-form-add-certificate"
|
||||
form="hook-form-handle-certificate"
|
||||
type="submit"
|
||||
>
|
||||
Create
|
||||
{certificateId ? "Update" : "Create"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Form>
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { api } from "@/utils/api";
|
||||
import { AddCertificate } from "./add-certificate";
|
||||
import { HandleCertificate } from "./handle-certificate";
|
||||
import { getCertificateChainInfo, getExpirationStatus } from "./utils";
|
||||
|
||||
export const ShowCertificates = () => {
|
||||
@@ -53,7 +53,7 @@ export const ShowCertificates = () => {
|
||||
<span className="text-base text-muted-foreground text-center">
|
||||
You don't have any certificates created
|
||||
</span>
|
||||
<AddCertificate />
|
||||
<HandleCertificate />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-4 min-h-[25vh]">
|
||||
@@ -102,6 +102,10 @@ export const ShowCertificates = () => {
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row gap-1">
|
||||
<HandleCertificate
|
||||
certificateId={certificate.certificateId}
|
||||
/>
|
||||
|
||||
<DialogAction
|
||||
title="Delete Certificate"
|
||||
description="Are you sure you want to delete this certificate?"
|
||||
@@ -126,7 +130,7 @@ export const ShowCertificates = () => {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="group hover:bg-red-500/10 "
|
||||
className="group hover:bg-red-500/10"
|
||||
isLoading={isRemoving}
|
||||
>
|
||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||
@@ -140,7 +144,7 @@ export const ShowCertificates = () => {
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
||||
<AddCertificate />
|
||||
<HandleCertificate />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
findCertificateById,
|
||||
IS_CLOUD,
|
||||
removeCertificateById,
|
||||
updateCertificate,
|
||||
} from "@dokploy/server";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
@@ -11,6 +12,7 @@ import { db } from "@/server/db";
|
||||
import {
|
||||
apiCreateCertificate,
|
||||
apiFindCertificate,
|
||||
apiUpdateCertificate,
|
||||
certificates,
|
||||
} from "@/server/db/schema";
|
||||
|
||||
@@ -57,4 +59,21 @@ export const certificateRouter = createTRPCRouter({
|
||||
where: eq(certificates.organizationId, ctx.session.activeOrganizationId),
|
||||
});
|
||||
}),
|
||||
update: adminProcedure
|
||||
.input(apiUpdateCertificate)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const certificate = await findCertificateById(input.certificateId);
|
||||
if (certificate.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not allowed to update this certificate",
|
||||
});
|
||||
}
|
||||
return await updateCertificate(input.certificateId, {
|
||||
name: input.name,
|
||||
certificateData: input.certificateData,
|
||||
privateKey: input.privateKey,
|
||||
autoRenew: input.autoRenew,
|
||||
});
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -126,3 +126,39 @@ const createCertificateFiles = async (certificate: Certificate) => {
|
||||
fs.writeFileSync(configFile, yamlConfig);
|
||||
}
|
||||
};
|
||||
|
||||
export const updateCertificate = async (
|
||||
certificateId: string,
|
||||
updates: {
|
||||
name?: string;
|
||||
certificateData?: string;
|
||||
privateKey?: string;
|
||||
autoRenew?: boolean;
|
||||
},
|
||||
) => {
|
||||
const existing = await findCertificateById(certificateId);
|
||||
|
||||
const updated = await db
|
||||
.update(certificates)
|
||||
.set({
|
||||
...updates,
|
||||
})
|
||||
.where(eq(certificates.certificateId, certificateId))
|
||||
.returning();
|
||||
|
||||
if (!updated || updated[0] === undefined) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Failed to update the certificate",
|
||||
});
|
||||
}
|
||||
|
||||
const cert = updated[0];
|
||||
|
||||
// If cert data or private key changed, rewrite files
|
||||
if (updates.certificateData || updates.privateKey) {
|
||||
await createCertificateFiles(cert);
|
||||
}
|
||||
|
||||
return cert;
|
||||
};
|
||||
Reference in New Issue
Block a user