Files
infisical/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateIssuanceModal.tsx
T

986 lines
38 KiB
TypeScript

/* eslint-disable react/no-array-index-key */
/* eslint-disable no-nested-ternary */
import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { faQuestionCircle } from "@fortawesome/free-regular-svg-icons";
import { faPlus, faTrash } from "@fortawesome/free-solid-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,
IconButton,
Input,
Modal,
ModalContent,
Select,
SelectItem,
Tooltip
} from "@app/components/v2";
import { useProject } from "@app/context";
import { useCreateCertificateV3, useGetCert } from "@app/hooks/api";
import { useListCertificateProfiles } from "@app/hooks/api/certificateProfiles";
import {
certKeyAlgorithms,
EXTENDED_KEY_USAGES_OPTIONS,
KEY_USAGES_OPTIONS,
SIGNATURE_ALGORITHMS_OPTIONS
} from "@app/hooks/api/certificates/constants";
import {
CertExtendedKeyUsage,
CertKeyAlgorithm,
CertKeyUsage
} from "@app/hooks/api/certificates/enums";
import { useGetCertificateTemplateV2ById } from "@app/hooks/api/certificateTemplates/queries";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { CertificateContent } from "./CertificateContent";
const schema = z.object({
profileId: z.string().min(1, "Profile is required"),
friendlyName: z.string(),
subjectAttributes: z
.array(
z.object({
type: z.enum(["common_name"]),
value: z.string().min(1, "Value is required"),
include: z.enum(["mandatory", "optional", "prohibit"]).optional()
})
)
.min(1, "At least one subject attribute is required"),
altNames: z
.array(
z.object({
type: z.enum(["dns", "ip", "email", "uri"]),
value: z.string().min(1, "Value is required"),
include: z.enum(["mandatory", "optional", "prohibit"]).optional()
})
)
.default([]),
ttl: z.string().trim().min(1, "TTL is required"),
signatureAlgorithm: z.string().optional(),
keyAlgorithm: z.string().optional(),
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<typeof schema>;
type Props = {
popUp: UsePopUpState<["certificateIssuance"]>;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["certificateIssuance"]>,
state?: boolean
) => void;
};
type TCertificateDetails = {
serialNumber: string;
certificate: string;
certificateChain: string;
privateKey: string;
};
export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle }: Props) => {
const [certificateDetails, setCertificateDetails] = useState<TCertificateDetails | null>(null);
const [allowedKeyUsages, setAllowedKeyUsages] = useState<string[]>([]);
const [allowedExtendedKeyUsages, setAllowedExtendedKeyUsages] = useState<string[]>([]);
const [allowedSignatureAlgorithms, setAllowedSignatureAlgorithms] = useState<string[]>([]);
const [allowedKeyAlgorithms, setAllowedKeyAlgorithms] = useState<string[]>([]);
const { currentProject } = useProject();
const { data: cert } = useGetCert(
(popUp?.certificateIssuance?.data as { serialNumber: string })?.serialNumber || ""
);
const { data: profilesData } = useListCertificateProfiles({
projectId: currentProject?.id || "",
includeMetrics: false
});
const { mutateAsync: createCertificate } = useCreateCertificateV3();
const {
control,
handleSubmit,
reset,
watch,
setValue,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
profileId: "",
friendlyName: "",
subjectAttributes: [{ type: "common_name", value: "" }],
altNames: [],
ttl: "30d",
signatureAlgorithm: "",
keyAlgorithm: "",
keyUsages: {},
extendedKeyUsages: {}
}
});
const selectedProfileId = watch("profileId");
const selectedProfile = profilesData?.certificateProfiles?.find(
(p) => p.id === selectedProfileId
);
const { data: templateData } = useGetCertificateTemplateV2ById({
templateId: selectedProfile?.certificateTemplateId || ""
});
useEffect(() => {
if (templateData && selectedProfile) {
if (templateData.signatureAlgorithm?.defaultAlgorithm) {
let sigAlgValue = templateData.signatureAlgorithm.defaultAlgorithm;
const sigAlgMap: Record<string, string> = {
"SHA256-RSA": "RSA-SHA256",
"SHA384-RSA": "RSA-SHA384",
"SHA512-RSA": "RSA-SHA512",
"SHA256-ECDSA": "ECDSA-SHA256",
"SHA384-ECDSA": "ECDSA-SHA384",
"SHA512-ECDSA": "ECDSA-SHA512"
};
if (sigAlgMap[sigAlgValue]) {
sigAlgValue = sigAlgMap[sigAlgValue];
}
setValue("signatureAlgorithm", sigAlgValue);
}
if (templateData.keyAlgorithm?.defaultKeyType) {
let keyAlgValue = templateData.keyAlgorithm.defaultKeyType;
const keyAlgMap: Record<string, string> = {
"RSA-2048": CertKeyAlgorithm.RSA_2048,
"RSA-3072": CertKeyAlgorithm.RSA_3072,
"RSA-4096": CertKeyAlgorithm.RSA_4096,
"ECDSA-P256": CertKeyAlgorithm.ECDSA_P256,
"ECDSA-P384": CertKeyAlgorithm.ECDSA_P384,
[CertKeyAlgorithm.ECDSA_P256]: CertKeyAlgorithm.ECDSA_P256,
[CertKeyAlgorithm.ECDSA_P384]: CertKeyAlgorithm.ECDSA_P384
};
if (keyAlgMap[keyAlgValue]) {
keyAlgValue = keyAlgMap[keyAlgValue];
} else {
keyAlgValue = templateData.keyAlgorithm.defaultKeyType;
}
setValue("keyAlgorithm", keyAlgValue);
}
if (templateData.signatureAlgorithm?.allowedAlgorithms) {
const mappedSigAlgs = templateData.signatureAlgorithm.allowedAlgorithms.map(
(alg: string) => {
const sigAlgMap: Record<string, string> = {
"SHA256-RSA": "RSA-SHA256",
"SHA384-RSA": "RSA-SHA384",
"SHA512-RSA": "RSA-SHA512",
"SHA256-ECDSA": "ECDSA-SHA256",
"SHA384-ECDSA": "ECDSA-SHA384",
"SHA512-ECDSA": "ECDSA-SHA512"
};
return sigAlgMap[alg] || alg;
}
);
setAllowedSignatureAlgorithms(mappedSigAlgs);
}
if (templateData.keyAlgorithm?.allowedKeyTypes) {
const mappedKeyAlgs = templateData.keyAlgorithm.allowedKeyTypes.map((alg: string) => {
const keyAlgMap: Record<string, string> = {
"RSA-2048": CertKeyAlgorithm.RSA_2048,
"RSA-3072": CertKeyAlgorithm.RSA_3072,
"RSA-4096": CertKeyAlgorithm.RSA_4096,
"ECDSA-P256": CertKeyAlgorithm.ECDSA_P256,
"ECDSA-P384": CertKeyAlgorithm.ECDSA_P384
};
return keyAlgMap[alg] || alg;
});
setAllowedKeyAlgorithms(mappedKeyAlgs);
}
const allAllowedKeyUsages: string[] = [];
if (templateData.keyUsages?.requiredUsages?.all) {
allAllowedKeyUsages.push(...templateData.keyUsages.requiredUsages.all);
}
if (templateData.keyUsages?.optionalUsages?.all) {
allAllowedKeyUsages.push(...templateData.keyUsages.optionalUsages.all);
}
setAllowedKeyUsages([...new Set(allAllowedKeyUsages)]);
const allAllowedExtendedKeyUsages: string[] = [];
if (templateData.extendedKeyUsages?.requiredUsages?.all) {
allAllowedExtendedKeyUsages.push(...templateData.extendedKeyUsages.requiredUsages.all);
}
if (templateData.extendedKeyUsages?.optionalUsages?.all) {
allAllowedExtendedKeyUsages.push(...templateData.extendedKeyUsages.optionalUsages.all);
}
setAllowedExtendedKeyUsages([...new Set(allAllowedExtendedKeyUsages)]);
if (templateData.attributes && Array.isArray(templateData.attributes)) {
const subjectAttrs: Array<{
type: "common_name";
value: string;
}> = [];
templateData.attributes.forEach((attr) => {
if (
(attr.include === "mandatory" ||
attr.include === "optional" ||
attr.include === "prohibit") &&
attr.value &&
attr.value.length > 0
) {
attr.value.forEach((val: string) => {
subjectAttrs.push({ type: attr.type as any, value: val });
});
}
});
if (subjectAttrs.length > 0) {
setValue("subjectAttributes", subjectAttrs);
}
}
if (
templateData.subjectAlternativeNames &&
Array.isArray(templateData.subjectAlternativeNames)
) {
const templateSans: Array<{ type: "dns" | "ip" | "email" | "uri"; value: string }> = [];
templateData.subjectAlternativeNames.forEach((sanPolicy) => {
if (
(sanPolicy.include === "mandatory" ||
sanPolicy.include === "optional" ||
sanPolicy.include === "prohibit") &&
sanPolicy.value &&
sanPolicy.value.length > 0
) {
const typeMapping: Record<string, "dns" | "ip" | "email" | "uri"> = {
dns_name: "dns",
ip_address: "ip",
email: "email",
uri: "uri"
};
const mappedType = typeMapping[sanPolicy.type];
if (mappedType) {
sanPolicy.value.forEach((val: string) => {
templateSans.push({ type: mappedType, value: val });
});
}
}
});
if (templateSans.length > 0) {
setValue("altNames", templateSans);
}
}
const resetKeyUsages = {
[CertKeyUsage.DIGITAL_SIGNATURE]: false,
[CertKeyUsage.KEY_ENCIPHERMENT]: false,
[CertKeyUsage.NON_REPUDIATION]: false,
[CertKeyUsage.DATA_ENCIPHERMENT]: false,
[CertKeyUsage.KEY_AGREEMENT]: false,
[CertKeyUsage.KEY_CERT_SIGN]: false,
[CertKeyUsage.CRL_SIGN]: false,
[CertKeyUsage.ENCIPHER_ONLY]: false,
[CertKeyUsage.DECIPHER_ONLY]: false
};
const resetExtendedKeyUsages = {
[CertExtendedKeyUsage.CLIENT_AUTH]: false,
[CertExtendedKeyUsage.CODE_SIGNING]: false,
[CertExtendedKeyUsage.EMAIL_PROTECTION]: false,
[CertExtendedKeyUsage.OCSP_SIGNING]: false,
[CertExtendedKeyUsage.SERVER_AUTH]: false,
[CertExtendedKeyUsage.TIMESTAMPING]: false
};
const templateToEnumMap = {
digital_signature: CertKeyUsage.DIGITAL_SIGNATURE,
digitalSignature: CertKeyUsage.DIGITAL_SIGNATURE,
key_encipherment: CertKeyUsage.KEY_ENCIPHERMENT,
keyEncipherment: CertKeyUsage.KEY_ENCIPHERMENT,
non_repudiation: CertKeyUsage.NON_REPUDIATION,
nonRepudiation: CertKeyUsage.NON_REPUDIATION,
data_encipherment: CertKeyUsage.DATA_ENCIPHERMENT,
dataEncipherment: CertKeyUsage.DATA_ENCIPHERMENT,
key_agreement: CertKeyUsage.KEY_AGREEMENT,
keyAgreement: CertKeyUsage.KEY_AGREEMENT,
key_cert_sign: CertKeyUsage.KEY_CERT_SIGN,
keyCertSign: CertKeyUsage.KEY_CERT_SIGN,
crl_sign: CertKeyUsage.CRL_SIGN,
cRLSign: CertKeyUsage.CRL_SIGN,
encipher_only: CertKeyUsage.ENCIPHER_ONLY,
encipherOnly: CertKeyUsage.ENCIPHER_ONLY,
decipher_only: CertKeyUsage.DECIPHER_ONLY,
decipherOnly: CertKeyUsage.DECIPHER_ONLY,
client_auth: CertExtendedKeyUsage.CLIENT_AUTH,
clientAuth: CertExtendedKeyUsage.CLIENT_AUTH,
server_auth: CertExtendedKeyUsage.SERVER_AUTH,
serverAuth: CertExtendedKeyUsage.SERVER_AUTH,
code_signing: CertExtendedKeyUsage.CODE_SIGNING,
codeSigning: CertExtendedKeyUsage.CODE_SIGNING,
email_protection: CertExtendedKeyUsage.EMAIL_PROTECTION,
emailProtection: CertExtendedKeyUsage.EMAIL_PROTECTION,
ocsp_signing: CertExtendedKeyUsage.OCSP_SIGNING,
ocspSigning: CertExtendedKeyUsage.OCSP_SIGNING,
time_stamping: CertExtendedKeyUsage.TIMESTAMPING,
timestamping: CertExtendedKeyUsage.TIMESTAMPING,
timeStamping: CertExtendedKeyUsage.TIMESTAMPING
};
const currentKeyUsages = { ...resetKeyUsages };
if (templateData.keyUsages?.requiredUsages?.all) {
templateData.keyUsages.requiredUsages.all.forEach((usage: string) => {
const enumValue = (templateToEnumMap as any)[usage];
if (enumValue && enumValue in currentKeyUsages) {
(currentKeyUsages as any)[enumValue] = true;
}
});
}
const currentExtendedKeyUsages = { ...resetExtendedKeyUsages };
if (templateData.extendedKeyUsages?.requiredUsages?.all) {
templateData.extendedKeyUsages.requiredUsages.all.forEach((usage: string) => {
const enumValue = (templateToEnumMap as any)[usage];
if (enumValue && enumValue in currentExtendedKeyUsages) {
(currentExtendedKeyUsages as any)[enumValue] = true;
}
});
}
setValue("keyUsages", currentKeyUsages);
setValue("extendedKeyUsages", currentExtendedKeyUsages);
}
}, [templateData, selectedProfile, setValue]);
useEffect(() => {
if (cert) {
const subjectAttrs: Array<{ type: string; value: string }> = [];
if (cert.commonName) subjectAttrs.push({ type: "common_name", value: cert.commonName });
reset({
profileId: "",
friendlyName: cert.friendlyName,
subjectAttributes:
subjectAttrs.length > 0
? (subjectAttrs as any)
: [{ type: "common_name" as const, value: "" }],
altNames: cert.altNames
? cert.altNames.split(",").map((name) => {
const trimmed = name.trim();
if (trimmed.includes("@")) return { type: "email" as const, value: trimmed };
if (trimmed.match(/^\d+\.\d+\.\d+\.\d+$/))
return { type: "ip" as const, value: trimmed };
if (trimmed.startsWith("http")) return { type: "uri" as const, value: trimmed };
return { type: "dns" as const, value: trimmed };
})
: [],
ttl: "",
keyUsages: Object.fromEntries((cert.keyUsages || []).map((name) => [name, true])),
extendedKeyUsages: Object.fromEntries(
(cert.extendedKeyUsages || []).map((name) => [name, true])
)
});
}
}, [cert, reset]);
const onFormSubmit = async ({
profileId,
friendlyName,
subjectAttributes,
altNames,
ttl,
signatureAlgorithm,
keyAlgorithm,
keyUsages,
extendedKeyUsages
}: FormData) => {
try {
if (!currentProject?.slug) return;
const getAttributeValue = (type: string) => {
const foundAttr = subjectAttributes.find((attr) => attr.type === type);
return foundAttr?.value || "";
};
const { serialNumber, certificate, certificateChain, privateKey } = await createCertificate({
profileId,
projectSlug: currentProject.slug,
friendlyName,
commonName: getAttributeValue("common_name"),
altNames: altNames
.filter((san) => san.value.trim())
.map((san) => san.value.trim())
.join(", "),
ttl,
signatureAlgorithm: (() => {
const frontendToBackendSigAlg: Record<string, string> = {
"RSA-SHA256": "SHA256-RSA",
"RSA-SHA384": "SHA384-RSA",
"RSA-SHA512": "SHA512-RSA",
"ECDSA-SHA256": "SHA256-ECDSA",
"ECDSA-SHA384": "SHA384-ECDSA",
"ECDSA-SHA512": "SHA512-ECDSA"
};
return signatureAlgorithm
? frontendToBackendSigAlg[signatureAlgorithm] || signatureAlgorithm
: undefined;
})(),
keyAlgorithm: (() => {
const frontendToBackendKeyAlg: Record<string, string> = {
RSA_2048: "RSA-2048",
RSA_3072: "RSA-3072",
RSA_4096: "RSA-4096",
EC_prime256v1: "ECDSA-P256",
EC_secp384r1: "ECDSA-P384"
};
return keyAlgorithm ? frontendToBackendKeyAlg[keyAlgorithm] || keyAlgorithm : undefined;
})(),
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"
});
}
};
return (
<Modal
isOpen={popUp?.certificateIssuance?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("certificateIssuance", isOpen);
setCertificateDetails(null);
reset();
}}
>
<ModalContent
title={
certificateDetails
? "Certificate Created Successfully"
: cert
? "Certificate Details"
: "Issue New Certificate"
}
subTitle={
certificateDetails
? "Certificate has been successfully created and is ready for use"
: cert
? "View certificate information"
: "Issue a new certificate using a certificate profile"
}
>
{certificateDetails && (
<CertificateContent
serialNumber={certificateDetails.serialNumber}
certificate={certificateDetails.certificate}
certificateChain={certificateDetails.certificateChain}
privateKey={certificateDetails.privateKey}
/>
)}
{cert && (
<div className="space-y-4">
<div>
<h4 className="text-sm font-medium text-mineshaft-300">Certificate Details</h4>
<p className="text-sm text-mineshaft-400">Serial Number: {cert.serialNumber}</p>
<p className="text-sm text-mineshaft-400">Common Name: {cert.commonName}</p>
<p className="text-sm text-mineshaft-400">Status: {cert.status}</p>
</div>
</div>
)}
{!cert && !certificateDetails && (
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
name="profileId"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label={
<div>
<FormLabel
isRequired
label="Certificate Profile"
icon={
<Tooltip
className="text-center"
content={
<span>
Certificate profiles define the policies and enrollment methods for
certificate issuance. The selected profile will enforce validation
rules and determine the CA used for signing.
</span>
}
>
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
</Tooltip>
}
/>
</div>
}
errorText={error?.message}
isError={Boolean(error)}
isRequired
>
<Select
defaultValue=""
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
placeholder="Select a certificate profile"
position="popper"
>
{profilesData?.certificateProfiles?.map((profile) => (
<SelectItem key={profile.id} value={profile.id}>
{profile.slug}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
name="friendlyName"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Friendly Name"
errorText={error?.message}
isError={Boolean(error)}
>
<Input {...field} placeholder="Enter a friendly name for the certificate" />
</FormControl>
)}
/>
<Controller
control={control}
name="subjectAttributes"
render={({ field: { onChange, value }, fieldState: { error } }) => (
<FormControl
label="Subject Attributes"
isRequired
errorText={error?.message}
isError={Boolean(error)}
>
<div className="space-y-2">
{value.map((attr, index) => (
<div key={`attr-${index}`} className="flex items-start gap-2">
<Select
value={attr.type}
onValueChange={(newType) => {
const newValue = [...value];
newValue[index] = {
...attr,
type: newType as typeof attr.type
};
onChange(newValue);
}}
className="w-48"
>
<SelectItem value="common_name">Common Name</SelectItem>
</Select>
<Select
value={attr.include || "optional"}
onValueChange={(newInclude) => {
const newValue = [...value];
newValue[index] = {
...attr,
include: newInclude as "mandatory" | "optional" | "prohibit"
};
onChange(newValue);
}}
className="w-32"
>
<SelectItem value="mandatory">Mandatory</SelectItem>
<SelectItem value="optional">Optional</SelectItem>
<SelectItem value="prohibit">Prohibited</SelectItem>
</Select>
<Input
value={attr.value}
onChange={(e) => {
const newValue = [...value];
newValue[index] = { ...attr, value: e.target.value };
onChange(newValue);
}}
placeholder="example.com"
className="flex-1"
/>
{value.length > 1 && (
<IconButton
ariaLabel="Remove Subject Attribute"
variant="plain"
size="sm"
onClick={() => {
const newValue = value.filter((_, i) => i !== index);
onChange(newValue);
}}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
)}
</div>
))}
<Button
type="button"
variant="outline_bg"
size="xs"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
onChange([...value, { type: "common_name", value: "" }]);
}}
className="w-full"
>
Add Subject Attribute
</Button>
</div>
</FormControl>
)}
/>
<Controller
control={control}
name="altNames"
render={({ field: { onChange, value }, fieldState: { error } }) => (
<FormControl
label="Subject Alternative Names (SANs)"
errorText={error?.message}
isError={Boolean(error)}
>
<div className="space-y-2">
{value.map((san, index) => (
<div key={`san-${index}`} className="flex items-start gap-2">
<Select
value={san.type}
onValueChange={(newType) => {
const newValue = [...value];
newValue[index] = {
...san,
type: newType as "dns" | "ip" | "email" | "uri"
};
onChange(newValue);
}}
className="w-24"
>
<SelectItem value="dns">DNS</SelectItem>
<SelectItem value="ip">IP</SelectItem>
<SelectItem value="email">Email</SelectItem>
<SelectItem value="uri">URI</SelectItem>
</Select>
<Select
value={san.include || "optional"}
onValueChange={(newInclude) => {
const newValue = [...value];
newValue[index] = {
...san,
include: newInclude as "mandatory" | "optional" | "prohibit"
};
onChange(newValue);
}}
className="w-32"
>
<SelectItem value="mandatory">Mandatory</SelectItem>
<SelectItem value="optional">Optional</SelectItem>
<SelectItem value="prohibit">Prohibited</SelectItem>
</Select>
<Input
value={san.value}
onChange={(e) => {
const newValue = [...value];
newValue[index] = { ...san, value: e.target.value };
onChange(newValue);
}}
placeholder={
san.type === "dns"
? "example.com or *.example.com"
: san.type === "ip"
? "192.168.1.1"
: san.type === "email"
? "[email protected]"
: "https://example.com"
}
className="flex-1"
/>
<IconButton
ariaLabel="Remove SAN"
variant="plain"
size="sm"
onClick={() => {
const newValue = value.filter((_, i) => i !== index);
onChange(newValue);
}}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
))}
<Button
type="button"
variant="outline_bg"
size="xs"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
onChange([...value, { type: "dns", value: "" }]);
}}
className="w-full"
>
Add Subject Alternative Name
</Button>
</div>
</FormControl>
)}
/>
<Controller
control={control}
name="ttl"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Time to Live (TTL)"
isRequired
errorText={error?.message}
isError={Boolean(error)}
>
<Input {...field} placeholder="30d, 1y, 8760h" />
</FormControl>
)}
/>
<div className="grid grid-cols-2 gap-4">
<div>
<Controller
control={control}
name="signatureAlgorithm"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Signature Algorithm"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
defaultValue=""
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
placeholder="Use template default"
position="popper"
>
{SIGNATURE_ALGORITHMS_OPTIONS.filter((algorithm) => {
if (allowedSignatureAlgorithms.length === 0) return true;
return allowedSignatureAlgorithms.includes(algorithm.value);
}).map((algorithm) => (
<SelectItem key={algorithm.value} value={algorithm.value}>
{algorithm.label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
</div>
<div>
<Controller
control={control}
name="keyAlgorithm"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Key Algorithm"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
defaultValue=""
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
placeholder="Use template default"
position="popper"
>
{certKeyAlgorithms
.filter((algorithm) => {
if (allowedKeyAlgorithms.length === 0) return true;
return allowedKeyAlgorithms.includes(algorithm.value);
})
.map((algorithm) => (
<SelectItem key={algorithm.value} value={algorithm.value}>
{algorithm.label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
</div>
</div>
<Accordion type="single" collapsible className="w-full">
<AccordionItem value="key-usages">
<AccordionTrigger>Key Usages</AccordionTrigger>
<AccordionContent>
<div className="grid grid-cols-2 gap-2 pl-2">
{KEY_USAGES_OPTIONS.filter(({ value }) => {
if (allowedKeyUsages.length === 0) return true;
const templateToEnumMap = {
digital_signature: CertKeyUsage.DIGITAL_SIGNATURE,
key_encipherment: CertKeyUsage.KEY_ENCIPHERMENT,
non_repudiation: CertKeyUsage.NON_REPUDIATION,
data_encipherment: CertKeyUsage.DATA_ENCIPHERMENT,
key_agreement: CertKeyUsage.KEY_AGREEMENT,
key_cert_sign: CertKeyUsage.KEY_CERT_SIGN,
crl_sign: CertKeyUsage.CRL_SIGN,
encipher_only: CertKeyUsage.ENCIPHER_ONLY,
decipher_only: CertKeyUsage.DECIPHER_ONLY
};
return allowedKeyUsages.some(
(allowedUsage) => (templateToEnumMap as any)[allowedUsage] === value
);
}).map(({ label, value }) => (
<Controller
key={label}
control={control}
name={`keyUsages.${value}` as any}
render={({ field }) => (
<div className="flex items-center space-x-3">
<Checkbox
id={`key-usage-${value}`}
isChecked={field.value || false}
onCheckedChange={(checked) => field.onChange(checked)}
/>
<FormLabel
id={`key-usage-${value}`}
className="cursor-pointer text-sm text-mineshaft-300"
label={label}
/>
</div>
)}
/>
))}
</div>
</AccordionContent>
</AccordionItem>
<AccordionItem value="extended-key-usages">
<AccordionTrigger>Extended Key Usages</AccordionTrigger>
<AccordionContent>
<div className="grid grid-cols-2 gap-2 pl-2">
{EXTENDED_KEY_USAGES_OPTIONS.filter(({ value }) => {
if (allowedExtendedKeyUsages.length === 0) return true;
const templateToEnumMap = {
client_auth: CertExtendedKeyUsage.CLIENT_AUTH,
server_auth: CertExtendedKeyUsage.SERVER_AUTH,
code_signing: CertExtendedKeyUsage.CODE_SIGNING,
email_protection: CertExtendedKeyUsage.EMAIL_PROTECTION,
ocsp_signing: CertExtendedKeyUsage.OCSP_SIGNING,
time_stamping: CertExtendedKeyUsage.TIMESTAMPING,
timestamping: CertExtendedKeyUsage.TIMESTAMPING
};
return allowedExtendedKeyUsages.some(
(allowedUsage) => (templateToEnumMap as any)[allowedUsage] === value
);
}).map(({ label, value }) => (
<Controller
key={label}
control={control}
name={`extendedKeyUsages.${value}` as any}
render={({ field }) => (
<div className="flex items-center space-x-3">
<Checkbox
id={`ext-key-usage-${value}`}
isChecked={field.value || false}
onCheckedChange={(checked) => field.onChange(checked)}
/>
<FormLabel
id={`ext-key-usage-${value}`}
className="cursor-pointer text-sm text-mineshaft-300"
label={label}
/>
</div>
)}
/>
))}
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
<div className="mt-7 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{cert ? "Update" : "Issue Certificate"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("certificateIssuance", false)}
>
Cancel
</Button>
</div>
</form>
)}
</ModalContent>
</Modal>
);
};