Remove cert secret structure, show cert sk once upon issuance

This commit is contained in:
Tuan Dang
2024-05-28 22:04:13 -07:00
parent 2f06168b29
commit f9847f48b0
12 changed files with 347 additions and 478 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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