import { useEffect, useState } from "react"; import { Controller, useForm } from "react-hook-form"; import { faQuestionCircle } from "@fortawesome/free-regular-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, Button, Checkbox, FormControl, FormLabel, Input, Modal, ModalContent, Select, SelectItem, Tooltip } from "@app/components/v2"; import { useProject } from "@app/context"; import { CaStatus, useCreateCertificate, useGetCert, useGetCertTemplate, useListWorkspaceCas, useListWorkspaceCertificateTemplates, useListWorkspacePkiCollections } from "@app/hooks/api"; import { caTypeToNameMap } from "@app/hooks/api/ca/constants"; import { EXTENDED_KEY_USAGES_OPTIONS, KEY_USAGES_OPTIONS } from "@app/hooks/api/certificates/constants"; import { CertExtendedKeyUsage, CertKeyUsage } from "@app/hooks/api/certificates/enums"; import { UsePopUpState } from "@app/hooks/usePopUp"; import { CertificateContent } from "./CertificateContent"; const schema = z.object({ certificateTemplateId: z.string().optional(), caId: z.string(), collectionId: z.string().optional(), friendlyName: z.string(), commonName: z.string().trim().min(1), altNames: z.string(), ttl: z.string().trim(), keyUsages: z.object({ [CertKeyUsage.DIGITAL_SIGNATURE]: z.boolean().optional(), [CertKeyUsage.KEY_ENCIPHERMENT]: z.boolean().optional(), [CertKeyUsage.NON_REPUDIATION]: z.boolean().optional(), [CertKeyUsage.DATA_ENCIPHERMENT]: z.boolean().optional(), [CertKeyUsage.KEY_AGREEMENT]: z.boolean().optional(), [CertKeyUsage.KEY_CERT_SIGN]: z.boolean().optional(), [CertKeyUsage.CRL_SIGN]: z.boolean().optional(), [CertKeyUsage.ENCIPHER_ONLY]: z.boolean().optional(), [CertKeyUsage.DECIPHER_ONLY]: z.boolean().optional() }), extendedKeyUsages: z.object({ [CertExtendedKeyUsage.CLIENT_AUTH]: z.boolean().optional(), [CertExtendedKeyUsage.CODE_SIGNING]: z.boolean().optional(), [CertExtendedKeyUsage.EMAIL_PROTECTION]: z.boolean().optional(), [CertExtendedKeyUsage.OCSP_SIGNING]: z.boolean().optional(), [CertExtendedKeyUsage.SERVER_AUTH]: z.boolean().optional(), [CertExtendedKeyUsage.TIMESTAMPING]: z.boolean().optional() }) }); export type FormData = z.infer; type Props = { popUp: UsePopUpState<["certificate"]>; handlePopUpToggle: (popUpName: keyof UsePopUpState<["certificate"]>, state?: boolean) => void; }; type TCertificateDetails = { serialNumber: string; certificate: string; certificateChain: string; privateKey: string; }; const CERT_TEMPLATE_NONE_VALUE = "none"; export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => { const [certificateDetails, setCertificateDetails] = useState(null); const { currentProject } = useProject(); const { data: cert } = useGetCert( (popUp?.certificate?.data as { serialNumber: string })?.serialNumber || "" ); const { data: cas } = useListWorkspaceCas({ projectId: currentProject.id, status: CaStatus.ACTIVE }); const { data } = useListWorkspacePkiCollections({ projectId: currentProject?.id || "" }); const { data: templatesData } = useListWorkspaceCertificateTemplates({ projectId: currentProject?.id || "" }); const { mutateAsync: createCertificate } = useCreateCertificate(); const { control, handleSubmit, reset, formState: { isSubmitting }, setValue, watch } = useForm({ resolver: zodResolver(schema), defaultValues: { keyUsages: { [CertKeyUsage.DIGITAL_SIGNATURE]: true, [CertKeyUsage.KEY_ENCIPHERMENT]: true }, extendedKeyUsages: {} } }); const selectedCertTemplateId = watch("certificateTemplateId"); const hasCertTemplateSelected = selectedCertTemplateId !== "" && selectedCertTemplateId !== CERT_TEMPLATE_NONE_VALUE; const { data: selectedCertTemplate } = useGetCertTemplate( hasCertTemplateSelected ? (selectedCertTemplateId as string) : "" ); useEffect(() => { if (cert) { reset({ caId: cert.caId, friendlyName: cert.friendlyName, commonName: cert.commonName, altNames: cert.altNames, certificateTemplateId: cert.certificateTemplateId ?? CERT_TEMPLATE_NONE_VALUE, ttl: "", keyUsages: Object.fromEntries((cert.keyUsages || []).map((name) => [name, true])), extendedKeyUsages: Object.fromEntries( (cert.extendedKeyUsages || []).map((name) => [name, true]) ) }); } else { reset({ caId: "", friendlyName: "", commonName: "", altNames: "", ttl: "", certificateTemplateId: CERT_TEMPLATE_NONE_VALUE, keyUsages: { [CertKeyUsage.DIGITAL_SIGNATURE]: true, [CertKeyUsage.KEY_ENCIPHERMENT]: true }, extendedKeyUsages: {} }); } }, [cert]); useEffect(() => { if (!cert && selectedCertTemplate) { setValue("ttl", selectedCertTemplate.ttl); setValue( "keyUsages", Object.fromEntries(selectedCertTemplate.keyUsages.map((name) => [name, true])) ); setValue( "extendedKeyUsages", Object.fromEntries(selectedCertTemplate.extendedKeyUsages.map((name) => [name, true])) ); } }, [selectedCertTemplate, cert]); const onFormSubmit = async ({ caId, friendlyName, collectionId, commonName, altNames, ttl, keyUsages, extendedKeyUsages }: FormData) => { try { if (!currentProject?.slug) return; const { serialNumber, certificate, certificateChain, privateKey } = await createCertificate({ caId: !selectedCertTemplate ? caId : undefined, certificateTemplateId: selectedCertTemplate ? selectedCertTemplateId : undefined, projectSlug: currentProject.slug, pkiCollectionId: collectionId, friendlyName, commonName, altNames, ttl, keyUsages: Object.entries(keyUsages) .filter(([, value]) => value) .map(([key]) => key as CertKeyUsage), extendedKeyUsages: Object.entries(extendedKeyUsages) .filter(([, value]) => value) .map(([key]) => key as CertExtendedKeyUsage) }); reset(); setCertificateDetails({ serialNumber, certificate, certificateChain, privateKey }); createNotification({ text: "Successfully created certificate", type: "success" }); } catch (err) { console.error(err); createNotification({ text: "Failed to create certificate", type: "error" }); } }; useEffect(() => { if (cas?.length) { setValue("caId", cas[0].id); } }, [cas]); return ( { handlePopUpToggle("certificate", isOpen); reset(); setCertificateDetails(null); }} > {!certificateDetails ? (
( When a template is selected, the details provided are validated against the template policies. } > } /> } errorText={error?.message} isError={Boolean(error)} isRequired > )} /> {(!selectedCertTemplateId || selectedCertTemplateId === CERT_TEMPLATE_NONE_VALUE || cert) && ( <> ( )} /> ( )} /> )} ( )} /> ( )} /> ( )} /> ( )} />
Key Usage
{ return (
{KEY_USAGES_OPTIONS.map(({ label, value: optionValue }) => { return ( { onChange({ ...value, [optionValue]: state }); }} > {label} ); })}
); }} /> { return (
{EXTENDED_KEY_USAGES_OPTIONS.map(({ label, value: optionValue }) => { return ( { onChange({ ...value, [optionValue]: state }); }} > {label} ); })}
); }} />
{!cert && (
)} ) : ( )}
); };