mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Remove cert secret structure, show cert sk once upon issuance
This commit is contained in:
@@ -5,7 +5,7 @@ import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasTable(TableName.CertificateAuthority))) {
|
||||
// TODO: consider adding algo details
|
||||
// TODO: add algo deets
|
||||
await knex.schema.createTable(TableName.CertificateAuthority, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.timestamps(true, true, true);
|
||||
@@ -77,30 +77,15 @@ export async function up(knex: Knex): Promise<void> {
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.CertificateSecret))) {
|
||||
await knex.schema.createTable(TableName.CertificateSecret, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.timestamps(true, true, true);
|
||||
t.uuid("certId").notNullable().unique();
|
||||
t.foreign("certId").references("id").inTable(TableName.Certificate).onDelete("CASCADE");
|
||||
t.text("pk").notNullable(); // TODO: encrypt
|
||||
t.text("sk").notNullable(); // TODO: encrypt
|
||||
});
|
||||
}
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.CertificateAuthority);
|
||||
await createOnUpdateTrigger(knex, TableName.CertificateAuthorityCert);
|
||||
await createOnUpdateTrigger(knex, TableName.CertificateAuthoritySk);
|
||||
await createOnUpdateTrigger(knex, TableName.Certificate);
|
||||
await createOnUpdateTrigger(knex, TableName.CertificateCert);
|
||||
await createOnUpdateTrigger(knex, TableName.CertificateSecret);
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
// certificates
|
||||
await knex.schema.dropTableIfExists(TableName.CertificateSecret);
|
||||
await dropOnUpdateTrigger(knex, TableName.CertificateSecret);
|
||||
|
||||
await knex.schema.dropTableIfExists(TableName.CertificateCert);
|
||||
await dropOnUpdateTrigger(knex, TableName.CertificateCert);
|
||||
|
||||
|
||||
@@ -73,7 +73,6 @@ import { tokenDALFactory } from "@app/services/auth-token/auth-token-dal";
|
||||
import { tokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
||||
import { certificateCertDALFactory } from "@app/services/certificate/certificate-cert-dal";
|
||||
import { certificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||
import { certificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
|
||||
import { certificateServiceFactory } from "@app/services/certificate/certificate-service";
|
||||
import { certificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
|
||||
import { certificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
||||
@@ -502,14 +501,11 @@ export const registerRoutes = async (
|
||||
|
||||
const certificateDAL = certificateDALFactory(db);
|
||||
const certificateCertDAL = certificateCertDALFactory(db);
|
||||
const certificateSecretDAL = certificateSecretDALFactory(db);
|
||||
|
||||
const certificateService = certificateServiceFactory({
|
||||
certificateDAL,
|
||||
certificateCertDAL,
|
||||
certificateSecretDAL,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
permissionService
|
||||
});
|
||||
|
||||
@@ -519,7 +515,6 @@ export const registerRoutes = async (
|
||||
certificateAuthoritySkDAL,
|
||||
certificateDAL,
|
||||
certificateCertDAL,
|
||||
certificateSecretDAL,
|
||||
projectDAL,
|
||||
permissionService
|
||||
});
|
||||
|
||||
@@ -86,26 +86,21 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
200: z.object({
|
||||
certificate: z.string().trim(),
|
||||
certificateChain: z.string().trim(),
|
||||
issuingCaCertificate: z.string().trim(),
|
||||
privateKey: z.string().trim(),
|
||||
serialNumber: z.string().trim()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { certificate, certificateChain, issuingCaCertificate, privateKey, serialNumber } =
|
||||
await server.services.certificate.getCertCert({
|
||||
certId: req.params.certId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
const { certificate, certificateChain, serialNumber } = await server.services.certificate.getCertCert({
|
||||
certId: req.params.certId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
return {
|
||||
certificate,
|
||||
certificateChain,
|
||||
issuingCaCertificate,
|
||||
privateKey,
|
||||
serialNumber
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { TCertificateCertDALFactory } from "@app/services/certificate/certificate-cert-dal";
|
||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
|
||||
import { TCertificateAuthorityCertDALFactory } from "./certificate-authority-cert-dal";
|
||||
@@ -29,15 +28,16 @@ import {
|
||||
} from "./certificate-authority-types";
|
||||
|
||||
type TCertificateAuthorityServiceFactoryDep = {
|
||||
// TODO: Pick
|
||||
certificateAuthorityDAL: TCertificateAuthorityDALFactory;
|
||||
certificateAuthorityCertDAL: TCertificateAuthorityCertDALFactory;
|
||||
certificateAuthoritySkDAL: TCertificateAuthoritySkDALFactory;
|
||||
certificateDAL: TCertificateDALFactory;
|
||||
certificateCertDAL: TCertificateCertDALFactory;
|
||||
certificateSecretDAL: TCertificateSecretDALFactory;
|
||||
projectDAL: TProjectDALFactory;
|
||||
permissionService: TPermissionServiceFactory;
|
||||
certificateAuthorityDAL: Pick<
|
||||
TCertificateAuthorityDALFactory,
|
||||
"transaction" | "create" | "findById" | "updateById" | "deleteById" | "findOne" | "buildCertificateChain"
|
||||
>;
|
||||
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "create" | "findOne" | "transaction">;
|
||||
certificateAuthoritySkDAL: Pick<TCertificateAuthoritySkDALFactory, "create" | "findOne">;
|
||||
certificateDAL: Pick<TCertificateDALFactory, "transaction" | "create">;
|
||||
certificateCertDAL: Pick<TCertificateCertDALFactory, "create">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
};
|
||||
|
||||
export type TCertificateAuthorityServiceFactory = ReturnType<typeof certificateAuthorityServiceFactory>;
|
||||
@@ -48,7 +48,6 @@ export const certificateAuthorityServiceFactory = ({
|
||||
certificateAuthoritySkDAL,
|
||||
certificateDAL,
|
||||
certificateCertDAL,
|
||||
certificateSecretDAL,
|
||||
projectDAL,
|
||||
permissionService
|
||||
}: TCertificateAuthorityServiceFactoryDep) => {
|
||||
@@ -624,12 +623,6 @@ export const certificateAuthorityServiceFactory = ({
|
||||
|
||||
const chain = await certificateAuthorityDAL.buildCertificateChain(caId);
|
||||
|
||||
// https://nodejs.org/api/crypto.html#static-method-keyobjectfromkey
|
||||
const skObj = KeyObject.from(leafKeys.privateKey);
|
||||
const sk = skObj.export({ format: "pem", type: "pkcs8" }) as string;
|
||||
const pkObj = KeyObject.from(leafKeys.publicKey);
|
||||
const pk = pkObj.export({ format: "pem", type: "spki" }) as string;
|
||||
|
||||
await certificateDAL.transaction(async (tx) => {
|
||||
const cert = await certificateDAL.create(
|
||||
{
|
||||
@@ -650,15 +643,6 @@ export const certificateAuthorityServiceFactory = ({
|
||||
tx
|
||||
);
|
||||
|
||||
await certificateSecretDAL.create(
|
||||
{
|
||||
certId: cert.id,
|
||||
pk, // TODO: encrypt
|
||||
sk // TODO: encrypt
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
return cert;
|
||||
});
|
||||
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TCertificateSecretDALFactory = ReturnType<typeof certificateSecretDALFactory>;
|
||||
|
||||
export const certificateSecretDALFactory = (db: TDbClient) => {
|
||||
const certificateSecretOrm = ormify(db, TableName.CertificateSecret);
|
||||
return certificateSecretOrm;
|
||||
};
|
||||
@@ -5,20 +5,15 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { TCertificateCertDALFactory } from "@app/services/certificate/certificate-cert-dal";
|
||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
|
||||
import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
|
||||
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
||||
|
||||
import { TDeleteCertDTO, TGetCertCertDTO, TGetCertDTO } from "./certificate-types";
|
||||
|
||||
type TCertificateServiceFactoryDep = {
|
||||
// TODO: Pick
|
||||
certificateDAL: TCertificateDALFactory;
|
||||
certificateCertDAL: TCertificateCertDALFactory;
|
||||
certificateSecretDAL: TCertificateSecretDALFactory;
|
||||
certificateAuthorityDAL: TCertificateAuthorityDALFactory;
|
||||
certificateAuthorityCertDAL: TCertificateAuthorityCertDALFactory;
|
||||
permissionService: TPermissionServiceFactory;
|
||||
certificateDAL: Pick<TCertificateDALFactory, "findById" | "deleteById">;
|
||||
certificateCertDAL: Pick<TCertificateCertDALFactory, "findOne">;
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
};
|
||||
|
||||
export type TCertificateServiceFactory = ReturnType<typeof certificateServiceFactory>;
|
||||
@@ -26,9 +21,7 @@ export type TCertificateServiceFactory = ReturnType<typeof certificateServiceFac
|
||||
export const certificateServiceFactory = ({
|
||||
certificateDAL,
|
||||
certificateCertDAL,
|
||||
certificateSecretDAL,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
permissionService
|
||||
}: TCertificateServiceFactoryDep) => {
|
||||
const getCertById = async ({ certId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertDTO) => {
|
||||
@@ -77,19 +70,15 @@ export const certificateServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
// TODO: re-evaluate this permission
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates);
|
||||
|
||||
const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
|
||||
|
||||
const certCert = await certificateCertDAL.findOne({ certId });
|
||||
const certSecret = await certificateSecretDAL.findOne({ certId });
|
||||
const certObj = new x509.X509Certificate(certCert.certificate);
|
||||
|
||||
return {
|
||||
certificate: certCert.certificate,
|
||||
certificateChain: certCert.certificateChain,
|
||||
issuingCaCertificate: caCert.certificate,
|
||||
privateKey: certSecret.sk,
|
||||
serialNumber: certObj.serialNumber
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CaStatus,CaType } from "./enums";
|
||||
import { CaStatus, CaType } from "./enums";
|
||||
|
||||
export type TCertificateAuthority = {
|
||||
id: string;
|
||||
@@ -85,6 +85,6 @@ export type TCreateCertificateResponse = {
|
||||
certificate: string;
|
||||
issuingCertificate: string;
|
||||
certificateChain: string;
|
||||
sk: string;
|
||||
privateKey: string;
|
||||
serialNumber: string;
|
||||
};
|
||||
|
||||
@@ -29,8 +29,6 @@ export const useGetCertCert = (certId: string) => {
|
||||
const { data } = await apiRequest.get<{
|
||||
certificate: string;
|
||||
certificateChain: string;
|
||||
issuingCaCertificate: string;
|
||||
privateKey: string;
|
||||
serialNumber: string;
|
||||
}>(`/api/v1/certificates/${certId}/certificate`);
|
||||
return data;
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { useEffect } from "react";
|
||||
import { faCheck, faCopy, faDownload } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { IconButton, Modal, ModalContent } from "@app/components/v2";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { Modal, ModalContent } from "@app/components/v2";
|
||||
import { useGetCaCert } from "@app/hooks/api";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
import { CertificateContent } from "../../CertificatesTab/components/CertificateContent";
|
||||
|
||||
type Props = {
|
||||
popUp: UsePopUpState<["caCert"]>;
|
||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<["caCert"]>, state?: boolean) => void;
|
||||
@@ -14,39 +11,6 @@ type Props = {
|
||||
|
||||
export const CaCertModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const { data } = useGetCaCert((popUp?.caCert?.data as { caId: string })?.caId || "");
|
||||
const [isSerialNumberCopied, setIsSerialNumberCopied] = useToggle(false);
|
||||
const [isCertificateCopied, setIsCertificateCopied] = useToggle(false);
|
||||
const [isCertificateChainCopied, setIsCertificateChainCopied] = useToggle(false);
|
||||
|
||||
useEffect(() => {
|
||||
let timer: NodeJS.Timeout;
|
||||
if (isSerialNumberCopied) {
|
||||
timer = setTimeout(() => setIsSerialNumberCopied.off(), 2000);
|
||||
}
|
||||
|
||||
if (isCertificateCopied) {
|
||||
timer = setTimeout(() => setIsCertificateCopied.off(), 2000);
|
||||
}
|
||||
|
||||
if (isCertificateChainCopied) {
|
||||
timer = setTimeout(() => setIsCertificateChainCopied.off(), 2000);
|
||||
}
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [isSerialNumberCopied, isCertificateCopied, isCertificateChainCopied]);
|
||||
|
||||
const downloadTxtFile = (filename: string, content: string) => {
|
||||
const blob = new Blob([content], { type: "text/plain" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp?.caCert?.isOpen}
|
||||
@@ -56,101 +20,11 @@ export const CaCertModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
>
|
||||
<ModalContent title="CA Certificate">
|
||||
{data ? (
|
||||
<div>
|
||||
<h2 className="mb-4">Serial Number</h2>
|
||||
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||
<p className="mr-4 break-all">{data.serialNumber}</p>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(data.serialNumber);
|
||||
setIsSerialNumberCopied.on();
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={isSerialNumberCopied ? faCheck : faCopy} />
|
||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||
Click to copy
|
||||
</span>
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h2>Certificate Body</h2>
|
||||
<div className="flex">
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(data.certificate);
|
||||
setIsCertificateCopied.on();
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={isCertificateCopied ? faCheck : faCopy} />
|
||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||
Copy
|
||||
</span>
|
||||
</IconButton>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative ml-2"
|
||||
onClick={() => {
|
||||
downloadTxtFile("certificate.txt", data.certificate);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faDownload} />
|
||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||
Download
|
||||
</span>
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||
<p className="mr-4 whitespace-pre-wrap break-all">{data.certificate}</p>
|
||||
</div>
|
||||
{data.certificateChain && (
|
||||
<>
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h2>Certificate Chain</h2>
|
||||
<div className="flex">
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(data.certificateChain);
|
||||
setIsCertificateChainCopied.on();
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={isCertificateChainCopied ? faCheck : faCopy} />
|
||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||
Copy
|
||||
</span>
|
||||
</IconButton>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative ml-2"
|
||||
onClick={() => {
|
||||
downloadTxtFile("certificate_chain.txt", data.certificate);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faDownload} />
|
||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||
Download
|
||||
</span>
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||
<p className="mr-4 whitespace-pre-wrap break-all">{data.certificateChain}</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<CertificateContent
|
||||
serialNumber={data.serialNumber}
|
||||
certificate={data.certificate}
|
||||
certificateChain={data.certificateChain}
|
||||
/>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { useEffect } from "react";
|
||||
import { faCheck, faCopy, faDownload } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { IconButton, Modal, ModalContent } from "@app/components/v2";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { Modal, ModalContent } from "@app/components/v2";
|
||||
import { useGetCertCert } from "@app/hooks/api";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
import { CertificateContent } from "./CertificateContent";
|
||||
|
||||
type Props = {
|
||||
popUp: UsePopUpState<["certificateCert"]>;
|
||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<["certificateCert"]>, state?: boolean) => void;
|
||||
@@ -17,44 +14,6 @@ export const CertificateCertModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
(popUp?.certificateCert?.data as { certId: string })?.certId || ""
|
||||
);
|
||||
|
||||
const [isSerialNumberCopied, setIsSerialNumberCopied] = useToggle(false);
|
||||
const [isCertificateCopied, setIsCertificateCopied] = useToggle(false);
|
||||
const [isCertificateChainCopied, setIsCertificateChainCopied] = useToggle(false);
|
||||
const [isCertificateSkCopied, setIsCertificateSkCopied] = useToggle(false);
|
||||
|
||||
useEffect(() => {
|
||||
let timer: NodeJS.Timeout;
|
||||
if (isSerialNumberCopied) {
|
||||
timer = setTimeout(() => setIsSerialNumberCopied.off(), 2000);
|
||||
}
|
||||
|
||||
if (isCertificateCopied) {
|
||||
timer = setTimeout(() => setIsCertificateCopied.off(), 2000);
|
||||
}
|
||||
|
||||
if (isCertificateChainCopied) {
|
||||
timer = setTimeout(() => setIsCertificateChainCopied.off(), 2000);
|
||||
}
|
||||
|
||||
if (isCertificateSkCopied) {
|
||||
timer = setTimeout(() => setIsCertificateSkCopied.off(), 2000);
|
||||
}
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [isSerialNumberCopied, isCertificateCopied, isCertificateChainCopied, isCertificateSkCopied]);
|
||||
|
||||
const downloadTxtFile = (filename: string, content: string) => {
|
||||
const blob = new Blob([content], { type: "text/plain" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp?.certificateCert?.isOpen}
|
||||
@@ -64,131 +23,11 @@ export const CertificateCertModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
>
|
||||
<ModalContent title="Export Certificate">
|
||||
{data ? (
|
||||
<div>
|
||||
<h2 className="mb-4">Serial Number</h2>
|
||||
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||
<p className="mr-4 break-all">{data.serialNumber}</p>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(data.serialNumber);
|
||||
setIsSerialNumberCopied.on();
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={isSerialNumberCopied ? faCheck : faCopy} />
|
||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||
Click to copy
|
||||
</span>
|
||||
</IconButton>
|
||||
</div>
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h2>Certificate Body</h2>
|
||||
<div className="flex">
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(data.certificate);
|
||||
setIsCertificateCopied.on();
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={isCertificateCopied ? faCheck : faCopy} />
|
||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||
Copy
|
||||
</span>
|
||||
</IconButton>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative ml-2"
|
||||
onClick={() => {
|
||||
downloadTxtFile("certificate.txt", data.certificate);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faDownload} />
|
||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||
Download
|
||||
</span>
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||
<p className="mr-4 whitespace-pre-wrap break-all">{data.certificate}</p>
|
||||
</div>
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h2>Certificate Chain</h2>
|
||||
<div className="flex">
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(data.certificateChain);
|
||||
setIsCertificateChainCopied.on();
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={isCertificateChainCopied ? faCheck : faCopy} />
|
||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||
Copy
|
||||
</span>
|
||||
</IconButton>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative ml-2"
|
||||
onClick={() => {
|
||||
downloadTxtFile("certificate_chain.txt", data.certificate);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faDownload} />
|
||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||
Download
|
||||
</span>
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||
<p className="mr-4 whitespace-pre-wrap break-all">{data.certificateChain}</p>
|
||||
</div>
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h2>Certificate Private Key</h2>
|
||||
<div className="flex">
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(data.privateKey);
|
||||
setIsCertificateSkCopied.on();
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={isCertificateSkCopied ? faCheck : faCopy} />
|
||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||
Copy
|
||||
</span>
|
||||
</IconButton>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative ml-2"
|
||||
onClick={() => {
|
||||
downloadTxtFile("private_key.txt", data.privateKey);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faDownload} />
|
||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||
Download
|
||||
</span>
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||
<p className="mr-4 whitespace-pre-wrap break-all">{data.privateKey}</p>
|
||||
</div>
|
||||
</div>
|
||||
<CertificateContent
|
||||
serialNumber={data.serialNumber}
|
||||
certificate={data.certificate}
|
||||
certificateChain={data.certificateChain}
|
||||
/>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
import { useEffect } from "react";
|
||||
import { faCheck, faCopy, faDownload } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { IconButton } from "@app/components/v2";
|
||||
import { useToggle } from "@app/hooks";
|
||||
|
||||
type Props = {
|
||||
serialNumber: string;
|
||||
certificate: string;
|
||||
certificateChain: string;
|
||||
privateKey?: string;
|
||||
};
|
||||
|
||||
export const CertificateContent = ({
|
||||
serialNumber,
|
||||
certificate,
|
||||
certificateChain,
|
||||
privateKey
|
||||
}: Props) => {
|
||||
const [isSerialNumberCopied, setIsSerialNumberCopied] = useToggle(false);
|
||||
const [isCertificateCopied, setIsCertificateCopied] = useToggle(false);
|
||||
const [isCertificateChainCopied, setIsCertificateChainCopied] = useToggle(false);
|
||||
const [isCertificateSkCopied, setIsCertificateSkCopied] = useToggle(false);
|
||||
|
||||
useEffect(() => {
|
||||
let timer: NodeJS.Timeout;
|
||||
if (isSerialNumberCopied) {
|
||||
timer = setTimeout(() => setIsSerialNumberCopied.off(), 2000);
|
||||
}
|
||||
|
||||
if (isCertificateCopied) {
|
||||
timer = setTimeout(() => setIsCertificateCopied.off(), 2000);
|
||||
}
|
||||
|
||||
if (isCertificateChainCopied) {
|
||||
timer = setTimeout(() => setIsCertificateChainCopied.off(), 2000);
|
||||
}
|
||||
|
||||
if (isCertificateSkCopied) {
|
||||
timer = setTimeout(() => setIsCertificateSkCopied.off(), 2000);
|
||||
}
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [isSerialNumberCopied, isCertificateCopied, isCertificateChainCopied, isCertificateSkCopied]);
|
||||
|
||||
const downloadTxtFile = (filename: string, content: string) => {
|
||||
const blob = new Blob([content], { type: "text/plain" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="mb-4">Serial Number</h2>
|
||||
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||
<p className="mr-4 break-all">{serialNumber}</p>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(serialNumber);
|
||||
setIsSerialNumberCopied.on();
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={isSerialNumberCopied ? faCheck : faCopy} />
|
||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||
Click to copy
|
||||
</span>
|
||||
</IconButton>
|
||||
</div>
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h2>Certificate Body</h2>
|
||||
<div className="flex">
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(certificate);
|
||||
setIsCertificateCopied.on();
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={isCertificateCopied ? faCheck : faCopy} />
|
||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||
Copy
|
||||
</span>
|
||||
</IconButton>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative ml-2"
|
||||
onClick={() => {
|
||||
downloadTxtFile("certificate.txt", certificate);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faDownload} />
|
||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||
Download
|
||||
</span>
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||
<p className="mr-4 whitespace-pre-wrap break-all">{certificate}</p>
|
||||
</div>
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h2>Certificate Chain</h2>
|
||||
<div className="flex">
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(certificateChain);
|
||||
setIsCertificateChainCopied.on();
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={isCertificateChainCopied ? faCheck : faCopy} />
|
||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||
Copy
|
||||
</span>
|
||||
</IconButton>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative ml-2"
|
||||
onClick={() => {
|
||||
downloadTxtFile("certificate_chain.txt", certificate);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faDownload} />
|
||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||
Download
|
||||
</span>
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||
<p className="mr-4 whitespace-pre-wrap break-all">{certificateChain}</p>
|
||||
</div>
|
||||
{privateKey && (
|
||||
<>
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h2>Certificate Private Key</h2>
|
||||
<div className="flex">
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(privateKey);
|
||||
setIsCertificateSkCopied.on();
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={isCertificateSkCopied ? faCheck : faCopy} />
|
||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||
Copy
|
||||
</span>
|
||||
</IconButton>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative ml-2"
|
||||
onClick={() => {
|
||||
downloadTxtFile("private_key.txt", privateKey);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faDownload} />
|
||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||
Download
|
||||
</span>
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||
<p className="mr-4 whitespace-pre-wrap break-all">{privateKey}</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { format } from "date-fns";
|
||||
@@ -24,6 +24,8 @@ import {
|
||||
import { caTypeToNameMap } from "@app/hooks/api/ca/constants";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
import { CertificateContent } from "./CertificateContent";
|
||||
|
||||
const isValidDate = (dateString: string) => {
|
||||
if (dateString === "") return true;
|
||||
const date = new Date(dateString);
|
||||
@@ -48,7 +50,15 @@ type Props = {
|
||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<["certificate"]>, state?: boolean) => void;
|
||||
};
|
||||
|
||||
type TCertificateDetails = {
|
||||
serialNumber: string;
|
||||
certificate: string;
|
||||
certificateChain: string;
|
||||
privateKey: string;
|
||||
};
|
||||
|
||||
export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const [certificateDetails, setCertificateDetails] = useState<TCertificateDetails | null>(null);
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { data: cert } = useGetCertById(
|
||||
(popUp?.certificate?.data as { certId: string })?.certId || ""
|
||||
@@ -93,7 +103,7 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
try {
|
||||
if (!currentWorkspace?.slug) return;
|
||||
|
||||
await createCertificate({
|
||||
const { serialNumber, certificate, certificateChain, privateKey } = await createCertificate({
|
||||
projectSlug: currentWorkspace.slug,
|
||||
caId,
|
||||
commonName,
|
||||
@@ -103,7 +113,13 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
});
|
||||
|
||||
reset();
|
||||
handlePopUpToggle("certificate", false);
|
||||
|
||||
setCertificateDetails({
|
||||
serialNumber,
|
||||
certificate,
|
||||
certificateChain,
|
||||
privateKey
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully created certificate",
|
||||
@@ -122,7 +138,7 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
if (cas?.length) {
|
||||
setValue("caId", cas[0].id);
|
||||
}
|
||||
}, [cas, setValue]);
|
||||
}, [cas]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -130,93 +146,107 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("certificate", isOpen);
|
||||
reset();
|
||||
setCertificateDetails(null);
|
||||
}}
|
||||
>
|
||||
<ModalContent title={`${cert ? "View" : "Issue"} Certificate`}>
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="caId"
|
||||
defaultValue=""
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Issuing CA"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="mt-4"
|
||||
isRequired
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
isDisabled={Boolean(cert)}
|
||||
{!certificateDetails ? (
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="caId"
|
||||
defaultValue=""
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Issuing CA"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="mt-4"
|
||||
isRequired
|
||||
>
|
||||
{(cas || []).map(({ id, type, dn }) => (
|
||||
<SelectItem value={id} key={`ca-${id}`}>
|
||||
{`${caTypeToNameMap[type]}: ${dn}`}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
isDisabled={Boolean(cert)}
|
||||
>
|
||||
{(cas || []).map(({ id, type, dn }) => (
|
||||
<SelectItem value={id} key={`ca-${id}`}>
|
||||
{`${caTypeToNameMap[type]}: ${dn}`}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="commonName"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Common Name (CN)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<Input {...field} placeholder="Acme Corp" isDisabled={Boolean(cert)} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="ttl"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="86400" isDisabled={Boolean(cert)} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="notAfter"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Valid Until"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="YYYY-MM-DD" isDisabled={Boolean(cert)} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{!cert && (
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
) : (
|
||||
<CertificateContent
|
||||
serialNumber={certificateDetails.serialNumber}
|
||||
certificate={certificateDetails.certificate}
|
||||
certificateChain={certificateDetails.certificateChain}
|
||||
privateKey={certificateDetails.privateKey}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="commonName"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Common Name (CN)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<Input {...field} placeholder="Acme Corp" isDisabled={Boolean(cert)} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="ttl"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="86400" isDisabled={Boolean(cert)} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="notAfter"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="Valid Until" isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input {...field} placeholder="YYYY-MM-DD" isDisabled={Boolean(cert)} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{!cert && (
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user