PKI revamp, last changes and improvements on API and UI

This commit is contained in:
Carlos Monastyrski
2025-10-16 12:29:12 -03:00
parent 42800fdfe5
commit 5684127ce0
28 changed files with 2482 additions and 2257 deletions
+1 -1
View File
@@ -1,5 +1,4 @@
export { AcmeDnsProvider, CaRenewalType, CaStatus, CaType, InternalCaType } from "./enums";
export type { TOrderCertificateDTO, TOrderCertificateResponse } from "./types";
export {
useCreateCa,
useCreateCertificate,
@@ -24,3 +23,4 @@ export {
useListCasByTypeAndProjectId,
useListExternalCasByProjectId
} from "./queries";
export type { TOrderCertificateDTO, TOrderCertificateResponse } from "./types";
@@ -123,78 +123,74 @@ export type TListCertificateTemplatesDTO = {
};
export type TCertificateTemplateV2Policy = {
attributes: Array<{
type: "common_name";
include: "mandatory" | "optional" | "prohibit";
value?: string[];
subject?: Array<{
type: "common_name" | "organization" | "country";
allowed?: string[];
required?: string[];
denied?: string[];
}>;
keyUsages: {
requiredUsages: { all: string[] };
optionalUsages: { all: string[] };
};
extendedKeyUsages: {
requiredUsages: { all: string[] };
optionalUsages: { all: string[] };
};
subjectAlternativeNames: Array<{
sans?: Array<{
type: "dns_name" | "ip_address" | "email" | "uri";
include: "mandatory" | "optional" | "prohibit";
value?: string[];
allowed?: string[];
required?: string[];
denied?: string[];
}>;
validity: {
maxDuration: { value: number; unit: "days" | "months" | "years" };
minDuration?: { value: number; unit: "days" | "months" | "years" };
keyUsages?: {
allowed?: string[];
required?: string[];
denied?: string[];
};
signatureAlgorithm: {
allowedAlgorithms: string[];
defaultAlgorithm: string;
extendedKeyUsages?: {
allowed?: string[];
required?: string[];
denied?: string[];
};
keyAlgorithm: {
allowedKeyTypes: string[];
defaultKeyType: string;
algorithms?: {
signature?: string[];
keyAlgorithm?: string[];
};
validity?: {
max?: string;
};
};
export type TCertificateTemplateV2New = {
id: string;
projectId: string;
slug: string;
name: string;
description?: string;
attributes: any;
keyUsages: any;
extendedKeyUsages: any;
subjectAlternativeNames: any;
validity: any;
signatureAlgorithm: any;
keyAlgorithm: any;
subject?: TCertificateTemplateV2Policy["subject"];
sans?: TCertificateTemplateV2Policy["sans"];
keyUsages?: TCertificateTemplateV2Policy["keyUsages"];
extendedKeyUsages?: TCertificateTemplateV2Policy["extendedKeyUsages"];
algorithms?: TCertificateTemplateV2Policy["algorithms"];
validity?: TCertificateTemplateV2Policy["validity"];
createdAt: string;
updatedAt: string;
};
export type TCreateCertificateTemplateV2NewDTO = {
projectId: string;
slug: string;
name: string;
description?: string;
attributes: TCertificateTemplateV2Policy["attributes"];
keyUsages: TCertificateTemplateV2Policy["keyUsages"];
extendedKeyUsages: TCertificateTemplateV2Policy["extendedKeyUsages"];
subjectAlternativeNames: TCertificateTemplateV2Policy["subjectAlternativeNames"];
validity: TCertificateTemplateV2Policy["validity"];
signatureAlgorithm: TCertificateTemplateV2Policy["signatureAlgorithm"];
keyAlgorithm: TCertificateTemplateV2Policy["keyAlgorithm"];
subject?: TCertificateTemplateV2Policy["subject"];
sans?: TCertificateTemplateV2Policy["sans"];
keyUsages?: TCertificateTemplateV2Policy["keyUsages"];
extendedKeyUsages?: TCertificateTemplateV2Policy["extendedKeyUsages"];
algorithms?: TCertificateTemplateV2Policy["algorithms"];
validity?: TCertificateTemplateV2Policy["validity"];
};
export type TUpdateCertificateTemplateV2NewDTO = {
templateId: string;
slug?: string;
name?: string;
description?: string;
attributes?: TCertificateTemplateV2Policy["attributes"];
subject?: TCertificateTemplateV2Policy["subject"];
sans?: TCertificateTemplateV2Policy["sans"];
keyUsages?: TCertificateTemplateV2Policy["keyUsages"];
extendedKeyUsages?: TCertificateTemplateV2Policy["extendedKeyUsages"];
subjectAlternativeNames?: TCertificateTemplateV2Policy["subjectAlternativeNames"];
algorithms?: TCertificateTemplateV2Policy["algorithms"];
validity?: TCertificateTemplateV2Policy["validity"];
signatureAlgorithm?: TCertificateTemplateV2Policy["signatureAlgorithm"];
keyAlgorithm?: TCertificateTemplateV2Policy["keyAlgorithm"];
};
export type TDeleteCertificateTemplateV2NewDTO = {
@@ -53,10 +53,10 @@ export const PkiManagerLayout = () => {
animate={{ x: 0 }}
exit={{ x: -150 }}
transition={{ duration: 0.2 }}
className="border-mineshaft-600 bg-linear-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 dark w-full border-r md:w-60"
className="dark w-full border-r border-mineshaft-600 bg-linear-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60"
>
<nav className="items-between dark:scheme-dark flex h-full flex-col overflow-y-auto">
<div className="border-mineshaft-600 flex items-center gap-3 border-b px-4 py-3.5 text-lg text-white">
<nav className="items-between flex h-full flex-col overflow-y-auto dark:scheme-dark">
<div className="flex items-center gap-3 border-b border-mineshaft-600 px-4 py-3.5 text-lg text-white">
<Lottie className="inline-block h-5 w-5 shrink-0" icon="note" />
PKI Manager
</div>
@@ -267,7 +267,7 @@ export const PkiManagerLayout = () => {
<Menu>
<Link to="/organization/projects">
<MenuItem
className="text-mineshaft-400 hover:text-mineshaft-300 relative flex items-center gap-2 overflow-hidden text-sm"
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
leftIcon={
<div className="w-6">
<FontAwesomeIcon className="mx-1 inline-block shrink-0" icon={faHome} />
@@ -281,13 +281,13 @@ export const PkiManagerLayout = () => {
</div>
</nav>
</motion.div>
<div className="bg-bunker-800 flex-1 overflow-y-auto overflow-x-hidden p-4 pt-8">
<div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 p-4 pt-8">
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
<Outlet />
</div>
</div>
</div>
<div className="z-200 bg-bunker-800 flex h-screen w-screen flex-col items-center justify-center md:hidden">
<div className="z-200 flex h-screen w-screen flex-col items-center justify-center bg-bunker-800 md:hidden">
<FontAwesomeIcon icon={faMobile} className="mb-8 text-7xl text-gray-300" />
<p className="max-w-sm px-6 text-center text-lg text-gray-200">
{` ${t("common.no-mobile")} `}
@@ -1,9 +1,6 @@
/* eslint-disable react/no-array-index-key */
/* eslint-disable no-nested-ternary */
import { useEffect, useState } from "react";
import { useCallback, useEffect, useMemo, 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 { faPlus, faQuestionCircle, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
@@ -35,13 +32,13 @@ import {
KEY_USAGES_OPTIONS,
SIGNATURE_ALGORITHMS_OPTIONS
} from "@app/hooks/api/certificates/constants";
import {
CertExtendedKeyUsage,
CertKeyAlgorithm,
CertKeyUsage
} from "@app/hooks/api/certificates/enums";
import { CertExtendedKeyUsage, CertKeyUsage } from "@app/hooks/api/certificates/enums";
import { useGetCertificateTemplateV2ById } from "@app/hooks/api/certificateTemplates/queries";
import { UsePopUpState } from "@app/hooks/usePopUp";
import {
mapTemplateKeyAlgorithmToApi,
mapTemplateSignatureAlgorithmToApi
} from "@app/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/shared/certificate-constants";
import { CertificateContent } from "./CertificateContent";
@@ -109,9 +106,12 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
const [certificateDetails, setCertificateDetails] = useState<TCertificateDetails | null>(null);
const [allowedKeyUsages, setAllowedKeyUsages] = useState<string[]>([]);
const [allowedExtendedKeyUsages, setAllowedExtendedKeyUsages] = useState<string[]>([]);
const [requiredKeyUsages, setRequiredKeyUsages] = useState<string[]>([]);
const [requiredExtendedKeyUsages, setRequiredExtendedKeyUsages] = 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 || ""
);
@@ -133,7 +133,7 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
profileId: profileId ? profileId : "",
profileId: profileId || "",
subjectAttributes: [{ type: "common_name", value: "" }],
subjectAltNames: [],
ttl: "30d",
@@ -144,276 +144,122 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
}
});
const resetAllState = useCallback(() => {
setCertificateDetails(null);
setAllowedKeyUsages([]);
setAllowedExtendedKeyUsages([]);
setRequiredKeyUsages([]);
setRequiredExtendedKeyUsages([]);
setAllowedSignatureAlgorithms([]);
setAllowedKeyAlgorithms([]);
reset();
}, [reset]);
const selectedProfileId = watch("profileId");
const selectedProfile = profilesData?.certificateProfiles?.find(
(p) => p.id === selectedProfileId
const selectedProfile = useMemo(
() => profilesData?.certificateProfiles?.find((p) => p.id === selectedProfileId),
[profilesData?.certificateProfiles, selectedProfileId]
);
const { data: templateData } = useGetCertificateTemplateV2ById({
templateId: selectedProfile?.certificateTemplateId || ""
});
const filteredKeyUsages = useMemo(() => {
if (allowedKeyUsages.length === 0) return KEY_USAGES_OPTIONS;
return KEY_USAGES_OPTIONS.filter(({ value }) => allowedKeyUsages.includes(value));
}, [allowedKeyUsages]);
const filteredExtendedKeyUsages = useMemo(() => {
if (allowedExtendedKeyUsages.length === 0) return EXTENDED_KEY_USAGES_OPTIONS;
return EXTENDED_KEY_USAGES_OPTIONS.filter(({ value }) =>
allowedExtendedKeyUsages.includes(value)
);
}, [allowedExtendedKeyUsages]);
const availableSignatureAlgorithms = useMemo(() => {
if (allowedSignatureAlgorithms.length === 0) {
return SIGNATURE_ALGORITHMS_OPTIONS;
}
return allowedSignatureAlgorithms.map((templateAlgorithm) => {
const apiAlgorithm = mapTemplateSignatureAlgorithmToApi(templateAlgorithm);
return {
value: apiAlgorithm,
label: apiAlgorithm
};
});
}, [allowedSignatureAlgorithms]);
const availableKeyAlgorithms = useMemo(() => {
if (allowedKeyAlgorithms.length === 0) {
return certKeyAlgorithms;
}
return allowedKeyAlgorithms.map((templateAlgorithm) => {
const apiAlgorithm = mapTemplateKeyAlgorithmToApi(templateAlgorithm);
return {
value: apiAlgorithm,
label: apiAlgorithm
};
});
}, [allowedKeyAlgorithms]);
useEffect(() => {
if (templateData && selectedProfile) {
if (templateData.signatureAlgorithm?.allowedAlgorithms && templateData.signatureAlgorithm.allowedAlgorithms.length > 0) {
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"
};
let defaultValue = templateData.signatureAlgorithm.defaultAlgorithm;
if (defaultValue && sigAlgMap[defaultValue]) {
defaultValue = sigAlgMap[defaultValue];
}
const allowedValues = templateData.signatureAlgorithm.allowedAlgorithms.map((alg: string) => sigAlgMap[alg] || alg);
if (defaultValue && allowedValues.includes(defaultValue)) {
setValue("signatureAlgorithm", defaultValue);
} else if (allowedValues.length > 0) {
setValue("signatureAlgorithm", allowedValues[0]);
}
}
if (templateData.keyAlgorithm?.allowedKeyTypes && templateData.keyAlgorithm.allowedKeyTypes.length > 0) {
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
};
let defaultValue = templateData.keyAlgorithm.defaultKeyType;
if (defaultValue && keyAlgMap[defaultValue]) {
defaultValue = keyAlgMap[defaultValue];
}
const allowedValues = templateData.keyAlgorithm.allowedKeyTypes.map((alg: string) => keyAlgMap[alg] || alg);
if (defaultValue && allowedValues.includes(defaultValue)) {
setValue("keyAlgorithm", defaultValue);
} else if (allowedValues.length > 0) {
setValue("keyAlgorithm", allowedValues[0]);
}
}
if (templateData.validity?.maxDuration) {
const { value, unit } = templateData.validity.maxDuration;
let ttlValue = "";
switch (unit) {
case "days":
ttlValue = `${value}d`;
break;
case "months":
ttlValue = `${value}m`;
break;
case "years":
ttlValue = `${value}y`;
break;
default:
ttlValue = `${value}d`;
}
setValue("ttl", ttlValue);
}
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);
} else {
setValue("subjectAttributes", [{ type: "common_name", value: "" }]);
}
if (templateData && selectedProfile && popUp?.certificateIssuance?.isOpen) {
if (templateData.algorithms?.signature && templateData.algorithms.signature.length > 0) {
setAllowedSignatureAlgorithms(templateData.algorithms.signature);
} else {
setAllowedSignatureAlgorithms([]);
}
if (
templateData.subjectAlternativeNames &&
Array.isArray(templateData.subjectAlternativeNames)
templateData.algorithms?.keyAlgorithm &&
templateData.algorithms.keyAlgorithm.length > 0
) {
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("subjectAltNames", templateSans);
}
setAllowedKeyAlgorithms(templateData.algorithms.keyAlgorithm);
} else {
setAllowedKeyAlgorithms([]);
}
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;
}
});
if (templateData.validity?.max) {
setValue("ttl", templateData.validity.max);
}
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;
}
});
const keyUsages: string[] = [];
if (templateData.keyUsages?.required) {
keyUsages.push(...templateData.keyUsages.required);
}
if (templateData.keyUsages?.allowed) {
keyUsages.push(...templateData.keyUsages.allowed);
}
setAllowedKeyUsages(keyUsages);
setValue("keyUsages", currentKeyUsages);
setValue("extendedKeyUsages", currentExtendedKeyUsages);
const extendedKeyUsages: string[] = [];
if (templateData.extendedKeyUsages?.required) {
extendedKeyUsages.push(...templateData.extendedKeyUsages.required);
}
if (templateData.extendedKeyUsages?.allowed) {
extendedKeyUsages.push(...templateData.extendedKeyUsages.allowed);
}
setAllowedExtendedKeyUsages(extendedKeyUsages);
setRequiredKeyUsages(templateData.keyUsages?.required || []);
setRequiredExtendedKeyUsages(templateData.extendedKeyUsages?.required || []);
const initialKeyUsages: Record<string, boolean> = {};
const initialExtendedKeyUsages: Record<string, boolean> = {};
(templateData.keyUsages?.required || []).forEach((usage: string) => {
initialKeyUsages[usage] = true;
});
(templateData.extendedKeyUsages?.required || []).forEach((usage: string) => {
initialExtendedKeyUsages[usage] = true;
});
setValue("keyUsages", initialKeyUsages);
setValue("extendedKeyUsages", initialExtendedKeyUsages);
}
}, [templateData, selectedProfile, setValue]);
}, [templateData, selectedProfile, setValue, popUp?.certificateIssuance?.isOpen]);
useEffect(() => {
if (cert) {
@@ -451,83 +297,138 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
}
}, [popUp?.certificateIssuance?.isOpen, profileId, cert, setValue]);
const onFormSubmit = async ({
profileId,
subjectAttributes,
subjectAltNames,
ttl,
signatureAlgorithm,
keyAlgorithm,
keyUsages,
extendedKeyUsages
}: FormData) => {
try {
if (!currentProject?.slug) return;
const getAttributeValue = useCallback(
(subjectAttributes: typeof schema._type.subjectAttributes, type: string) => {
const foundAttr = subjectAttributes.find((attr) => attr.type === type);
return foundAttr?.value || "";
},
[]
);
const getAttributeValue = (type: string) => {
const foundAttr = subjectAttributes.find((attr) => attr.type === type);
return foundAttr?.value || "";
};
const formatSubjectAltNames = useCallback(
(subjectAltNames: typeof schema._type.subjectAltNames) => {
return subjectAltNames
.filter((san) => san.value.trim())
.map((san) => san.value.trim())
.join(", ");
},
[]
);
const { serialNumber, certificate, certificateChain, privateKey } = await createCertificate({
profileId,
projectSlug: currentProject.slug,
commonName: getAttributeValue("common_name"),
subjectAltNames: subjectAltNames
.filter((san) => san.value.trim())
.map((san) => san.value.trim())
.join(", "),
ttl,
signatureAlgorithm: (() => {
const frontendToBackendSigAlg: Record<string, string> = {
"RSA-SHA256": "RSA-SHA256",
"RSA-SHA384": "RSA-SHA384",
"RSA-SHA512": "RSA-SHA512",
"ECDSA-SHA256": "ECDSA-SHA256",
"ECDSA-SHA384": "ECDSA-SHA384",
"ECDSA-SHA512": "ECDSA-SHA512"
};
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: "EC_prime256v1",
EC_secp384r1: "EC_secp384r1"
};
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)
});
const filterUsages = useCallback(<T extends Record<string, boolean>>(usages: T) => {
return Object.entries(usages)
.filter(([, value]) => value)
.map(([key]) => key);
}, []);
reset();
const onFormSubmit = useCallback(
async ({
profileId: formProfileId,
subjectAttributes,
subjectAltNames,
ttl,
signatureAlgorithm,
keyAlgorithm,
keyUsages,
extendedKeyUsages
}: FormData) => {
try {
if (!currentProject?.slug) {
createNotification({
text: "Project not found. Please refresh and try again.",
type: "error"
});
return;
}
setCertificateDetails({
serialNumber,
certificate,
certificateChain,
privateKey
});
if (!formProfileId) {
createNotification({
text: "Please select a certificate profile.",
type: "error"
});
return;
}
createNotification({
text: "Successfully created certificate",
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to create certificate",
type: "error"
});
const commonName = getAttributeValue(subjectAttributes, "common_name");
if (!commonName.trim()) {
createNotification({
text: "Common name is required.",
type: "error"
});
return;
}
const { serialNumber, certificate, certificateChain, privateKey } = await createCertificate(
{
profileId: formProfileId,
projectSlug: currentProject.slug,
commonName,
subjectAltNames: formatSubjectAltNames(subjectAltNames),
ttl,
signatureAlgorithm,
keyAlgorithm,
keyUsages: filterUsages(keyUsages) as CertKeyUsage[],
extendedKeyUsages: filterUsages(extendedKeyUsages) as CertExtendedKeyUsage[]
}
);
setCertificateDetails({
serialNumber,
certificate,
certificateChain,
privateKey
});
createNotification({
text: "Successfully created certificate",
type: "success"
});
} catch (err) {
console.error("Certificate creation failed:", err);
const errorMessage =
err instanceof Error
? err.message
: "An unexpected error occurred while creating the certificate";
createNotification({
text: `Failed to create certificate: ${errorMessage}`,
type: "error"
});
}
},
[
currentProject?.slug,
createCertificate,
reset,
getAttributeValue,
formatSubjectAltNames,
filterUsages
]
);
const getModalTitle = () => {
if (certificateDetails) return "Certificate Created Successfully";
if (cert) return "Certificate Details";
return "Issue New Certificate";
};
const getModalSubTitle = () => {
if (certificateDetails) return "Certificate has been successfully created and is ready for use";
if (cert) return "View certificate information";
return "Issue a new certificate using a certificate profile";
};
const getSanPlaceholder = (sanType: string) => {
switch (sanType) {
case "dns":
return "example.com or *.example.com";
case "ip":
return "192.168.1.1";
case "email":
return "[email protected]";
case "uri":
return "https://example.com";
default:
return "Enter value";
}
};
@@ -536,26 +437,12 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
isOpen={popUp?.certificateIssuance?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("certificateIssuance", isOpen);
setCertificateDetails(null);
reset();
if (!isOpen) {
resetAllState();
}
}}
>
<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"
}
>
<ModalContent title={getModalTitle()} subTitle={getModalSubTitle()}>
{certificateDetails && (
<CertificateContent
serialNumber={certificateDetails.serialNumber}
@@ -567,10 +454,10 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
{cert && (
<div className="space-y-4">
<div>
<h4 className="text-mineshaft-300 text-sm font-medium">Certificate Details</h4>
<p className="text-mineshaft-400 text-sm">Serial Number: {cert.serialNumber}</p>
<p className="text-mineshaft-400 text-sm">Common Name: {cert.commonName}</p>
<p className="text-mineshaft-400 text-sm">Status: {cert.status}</p>
<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>
)}
@@ -592,9 +479,9 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
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.
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>
}
>
@@ -629,7 +516,6 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
{(selectedProfile || profileId) && (
<>
<Controller
control={control}
name="subjectAttributes"
@@ -642,7 +528,8 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
>
<div className="space-y-2">
{value.map((attr, index) => (
<div key={`attr-${index}`} className="flex items-start gap-2">
// eslint-disable-next-line react/no-array-index-key
<div key={`subject-attr-${index}`} className="flex items-center gap-2">
<Select
value={attr.type}
onValueChange={(newType) => {
@@ -710,7 +597,11 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
>
<div className="space-y-2">
{value.map((san, index) => (
<div key={`san-${index}`} className="flex items-start gap-2">
// eslint-disable-next-line react/no-array-index-key
<div
key={`subject-alt-name-${index}`}
className="flex items-center gap-2"
>
<Select
value={san.type}
onValueChange={(newType) => {
@@ -735,15 +626,7 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
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"
}
placeholder={getSanPlaceholder(san.type)}
className="flex-1"
/>
<IconButton
@@ -807,13 +690,14 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
placeholder="Use template default"
placeholder={
availableSignatureAlgorithms.length > 0
? "Select signature algorithm"
: "No algorithms available"
}
position="popper"
>
{SIGNATURE_ALGORITHMS_OPTIONS.filter((algorithm) => {
if (allowedSignatureAlgorithms.length === 0) return true;
return allowedSignatureAlgorithms.includes(algorithm.value);
}).map((algorithm) => (
{availableSignatureAlgorithms.map((algorithm) => (
<SelectItem key={algorithm.value} value={algorithm.value}>
{algorithm.label}
</SelectItem>
@@ -839,19 +723,18 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
placeholder="Use template default"
placeholder={
availableKeyAlgorithms.length > 0
? "Select key algorithm"
: "No algorithms available"
}
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>
))}
{availableKeyAlgorithms.map((algorithm) => (
<SelectItem key={algorithm.value} value={algorithm.value}>
{algorithm.label}
</SelectItem>
))}
</Select>
</FormControl>
)}
@@ -863,88 +746,80 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
<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="text-mineshaft-300 cursor-pointer text-sm"
label={label}
/>
</div>
)}
/>
))}
</div>
<div className="grid grid-cols-2 gap-2 pl-2">
{filteredKeyUsages.map(({ label, value }) => {
const isRequired = requiredKeyUsages.includes(value);
return (
<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) => {
if (!isRequired) {
field.onChange(checked);
}
}}
isDisabled={isRequired}
/>
<div className="flex items-center gap-2">
<FormLabel
id={`key-usage-${value}`}
className={`text-sm ${isRequired ? "text-mineshaft-200" : "cursor-pointer text-mineshaft-300"}`}
label={label}
/>
{isRequired && <span className="text-xs">(Required)</span>}
</div>
</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="text-mineshaft-300 cursor-pointer text-sm"
label={label}
/>
</div>
)}
/>
))}
</div>
<div className="grid grid-cols-2 gap-2 pl-2">
{filteredExtendedKeyUsages.map(({ label, value }) => {
const isRequired = requiredExtendedKeyUsages.includes(value);
return (
<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) => {
if (!isRequired) {
field.onChange(checked);
}
}}
isDisabled={isRequired}
/>
<div className="flex items-center gap-2">
<FormLabel
id={`ext-key-usage-${value}`}
className={`text-sm ${isRequired ? "text-mineshaft-200" : "cursor-pointer text-mineshaft-300"}`}
label={label}
/>
{isRequired && <span className="text-xs">(Required)</span>}
</div>
</div>
)}
/>
);
})}
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
@@ -964,7 +839,9 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("certificateIssuance", false)}
onClick={() => {
handlePopUpToggle("certificateIssuance", false);
}}
>
Cancel
</Button>
@@ -87,16 +87,16 @@ export const PkiSubscriberSection = () => {
const subscriberName = subscriberStatusData?.subscriberName || "";
return (
<div className="border-mineshaft-600 bg-mineshaft-900 mb-6 rounded-lg border p-4">
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex justify-between">
<p className="text-mineshaft-100 text-xl font-medium">Subscribers</p>
<p className="text-xl font-medium text-mineshaft-100">Subscribers</p>
<div className="flex w-full justify-end">
<a
target="_blank"
rel="noopener noreferrer"
href="https://infisical.com/docs/documentation/platform/pki/subscribers"
>
<span className="border-mineshaft-500 bg-mineshaft-600 text-mineshaft-200 hover:border-primary/40 hover:bg-primary/10 flex w-max cursor-pointer items-center rounded-md border px-4 py-2 duration-200 hover:text-white">
<span className="flex w-max cursor-pointer items-center rounded-md border border-mineshaft-500 bg-mineshaft-600 px-4 py-2 text-mineshaft-200 duration-200 hover:border-primary/40 hover:bg-primary/10 hover:text-white">
Documentation{" "}
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
@@ -112,32 +112,30 @@ export const PkiTemplateListPage = () => {
/>
</div>
<div className="container mx-auto mb-6 max-w-7xl rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
{
subscription?.pkiLegacyTemplates && (
<div className="mb-4 flex justify-between">
<p className="text-xl font-medium text-mineshaft-100">Templates</p>
<div className="flex w-full justify-end">
<ProjectPermissionCan
I={ProjectPermissionPkiTemplateActions.Create}
a={ProjectPermissionSub.CertificateTemplates}
>
{(isAllowed) => (
<Button
colorSchema="primary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("certificateTemplate")}
isDisabled={!isAllowed}
className="ml-4"
>
Add Template
</Button>
)}
</ProjectPermissionCan>
{subscription?.pkiLegacyTemplates && (
<div className="mb-4 flex justify-between">
<p className="text-xl font-medium text-mineshaft-100">Templates</p>
<div className="flex w-full justify-end">
<ProjectPermissionCan
I={ProjectPermissionPkiTemplateActions.Create}
a={ProjectPermissionSub.CertificateTemplates}
>
{(isAllowed) => (
<Button
colorSchema="primary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("certificateTemplate")}
isDisabled={!isAllowed}
className="ml-4"
>
Add Template
</Button>
)}
</ProjectPermissionCan>
</div>
</div>
</div>
)
}
)}
<TableContainer>
<Table>
<THead>
@@ -165,7 +165,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
if (isEdit) {
const updateData: any = {
profileId: profile.id,
name: data.slug,
slug: data.slug,
description: data.description
};
@@ -240,7 +240,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="your-profile-name" isDisabled={Boolean(isEdit)} />
<Input {...field} placeholder="your-profile-name" />
</FormControl>
)}
/>
@@ -319,7 +319,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
>
{certificateTemplates.map((template) => (
<SelectItem key={template.id} value={template.id}>
{template.slug}
{template.name}
</SelectItem>
))}
</Select>
@@ -376,17 +376,17 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
name="estConfig.disableBootstrapCaValidation"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<div className="border-mineshaft-600 bg-mineshaft-900 flex items-center gap-3 rounded-md border p-4">
<div className="flex items-center gap-3 rounded-md border border-mineshaft-600 bg-mineshaft-900 p-4">
<Checkbox
id="disableBootstrapCaValidation"
isChecked={value}
onCheckedChange={onChange}
/>
<div className="space-y-1">
<span className="text-mineshaft-100 text-sm font-medium">
<span className="text-sm font-medium text-mineshaft-100">
Disable Bootstrap CA Validation
</span>
<p className="text-bunker-300 text-xs">
<p className="text-xs text-bunker-300">
Skip CA certificate validation during EST bootstrap phase
</p>
</div>
@@ -433,7 +433,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
rows={6}
className="w-full font-mono text-xs"
/>
<p className="text-bunker-400 text-xs">
<p className="text-xs text-bunker-400">
Paste the complete CA certificate chain in PEM format
</p>
</div>
@@ -1,7 +1,17 @@
/* eslint-disable no-nested-ternary */
import { faCheck, faCircleInfo, faCopy, faEdit, faEllipsis, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
import { useCallback } from "react";
import {
faCheck,
faCircleInfo,
faCopy,
faEdit,
faEllipsis,
faPlus,
faTrash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import {
Badge,
DropdownMenu,
@@ -18,12 +28,10 @@ import {
ProjectPermissionCertificateProfileActions,
ProjectPermissionSub
} from "@app/context/ProjectPermissionContext/types";
import { usePopUp, useToggle } from "@app/hooks";
import { useGetCaById } from "@app/hooks/api/ca/queries";
import { TCertificateProfile } from "@app/hooks/api/certificateProfiles";
import { useGetCertificateTemplateV2ById } from "@app/hooks/api/certificateTemplates/queries";
import { usePopUp, useToggle } from "@app/hooks";
import { createNotification } from "@app/components/notifications";
import { useCallback } from "react";
import { CertificateIssuanceModal } from "@app/pages/cert-manager/CertificatesPage/components/CertificateIssuanceModal";
interface Props {
@@ -37,9 +45,7 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
const { data: caData } = useGetCaById(profile.caId);
const { popUp, handlePopUpToggle } = usePopUp([
"certificateIssuance"
] as const);
const { popUp, handlePopUpToggle } = usePopUp(["certificateIssuance"] as const);
const [isIdCopied, setIsIdCopied] = useToggle(false);
@@ -100,15 +106,15 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
)}
</div>
</Td>
<Td className="text-center">{getEnrollmentTypeBadge(profile.enrollmentType)}</Td>
<Td className="text-center">
<Td className="text-start">{getEnrollmentTypeBadge(profile.enrollmentType)}</Td>
<Td className="text-start">
<span className="text-sm text-mineshaft-300">
{caData?.friendlyName || caData?.commonName || profile.caId}
</span>
</Td>
<Td>
<span className="text-sm text-mineshaft-300">
{templateData?.slug || profile.certificateTemplateId}
{templateData?.name || profile.certificateTemplateId}
</span>
</Td>
<Td>
@@ -176,19 +182,17 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
Edit Profile
</DropdownMenuItem>
)}
{
canIssueCertificate && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
handlePopUpToggle("certificateIssuance");
}}
icon={<FontAwesomeIcon icon={faPlus} />}
>
Issue Certificate
</DropdownMenuItem>
)
}
{canIssueCertificate && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
handlePopUpToggle("certificateIssuance");
}}
icon={<FontAwesomeIcon icon={faPlus} />}
>
Issue Certificate
</DropdownMenuItem>
)}
{canDeleteProfile && (
<DropdownMenuItem
onClick={(e) => {
@@ -202,7 +206,11 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
)}
</DropdownMenuContent>
</DropdownMenu>
<CertificateIssuanceModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} profileId={profile.id}/>
<CertificateIssuanceModal
popUp={popUp}
handlePopUpToggle={handlePopUpToggle}
profileId={profile.id}
/>
</Td>
</Tr>
);
@@ -54,7 +54,7 @@ export const CertificateTemplatesV2Tab = () => {
setIsDeleteModalOpen(false);
setSelectedTemplate(null);
createNotification({
text: `Certificate template "${selectedTemplate.slug}" deleted successfully`,
text: `Certificate template "${selectedTemplate.name}" deleted successfully`,
type: "success"
});
} catch (error: any) {
@@ -103,12 +103,12 @@ export const CertificateTemplatesV2Tab = () => {
<DeleteActionModal
isOpen={isDeleteModalOpen}
title={`Delete Certificate Template ${selectedTemplate.slug}?`}
title={`Delete Certificate Template ${selectedTemplate.name}?`}
onChange={(isOpen) => {
setIsDeleteModalOpen(isOpen);
if (!isOpen) setSelectedTemplate(null);
}}
deleteKey={selectedTemplate.slug}
deleteKey={selectedTemplate.name}
onDeleteApproved={handleDeleteConfirm}
/>
</>
@@ -75,61 +75,64 @@ export const TemplateList = ({ onEditTemplate, onDeleteTemplate }: Props) => {
</Td>
</Tr>
)}
{!isLoading && templates && templates.length > 0 && templates.map((template) => (
<Tr
key={template.id}
className="h-10 transition-colors duration-100 hover:bg-mineshaft-700"
>
<Td>
<div className="flex items-center gap-2">
<div className="font-medium">{template.slug}</div>
{template.description && (
<Tooltip content={template.description}>
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
</Tooltip>
)}
</div>
</Td>
<Td>
<span className="text-sm text-bunker-300">{formatDate(template.createdAt)}</span>
</Td>
<Td className="text-right">
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<Tooltip content="More options">
<FontAwesomeIcon size="lg" icon={faEllipsis} />
{!isLoading &&
templates &&
templates.length > 0 &&
templates.map((template) => (
<Tr
key={template.id}
className="h-10 transition-colors duration-100 hover:bg-mineshaft-700"
>
<Td>
<div className="flex items-center gap-2">
<div className="font-medium">{template.name}</div>
{template.description && (
<Tooltip content={template.description}>
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
</Tooltip>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
{canEditTemplate && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onEditTemplate(template);
}}
icon={<FontAwesomeIcon icon={faEdit} />}
>
Edit Template
</DropdownMenuItem>
)}
{canDeleteTemplate && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onDeleteTemplate(template);
}}
icon={<FontAwesomeIcon icon={faTrash} />}
>
Delete Template
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
</Td>
</Tr>
))}
</div>
</Td>
<Td>
<span className="text-sm text-bunker-300">{formatDate(template.createdAt)}</span>
</Td>
<Td className="text-right">
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<Tooltip content="More options">
<FontAwesomeIcon size="lg" icon={faEllipsis} />
</Tooltip>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
{canEditTemplate && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onEditTemplate(template);
}}
icon={<FontAwesomeIcon icon={faEdit} />}
>
Edit Template
</DropdownMenuItem>
)}
{canDeleteTemplate && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onDeleteTemplate(template);
}}
icon={<FontAwesomeIcon icon={faTrash} />}
>
Delete Template
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
</Td>
</Tr>
))}
</TBody>
</Table>
</TableContainer>
@@ -26,10 +26,16 @@ export enum CertExtendedKeyUsageType {
TIME_STAMPING = "time_stamping"
}
export enum CertIncludeType {
MANDATORY = "mandatory",
OPTIONAL = "optional",
PROHIBIT = "prohibit"
export enum CertAttributeRule {
ALLOW = "allow",
DENY = "deny",
REQUIRE = "require"
}
export enum CertSanEffect {
ALLOW = "allow",
DENY = "deny",
REQUIRE = "require"
}
export enum CertDurationUnit {
@@ -39,7 +45,9 @@ export enum CertDurationUnit {
}
export enum CertSubjectAttributeType {
COMMON_NAME = "common_name"
COMMON_NAME = "common_name",
ORGANIZATION = "organization",
COUNTRY = "country"
}
export const formatSANType = (type: CertSubjectAlternativeNameType): string => {
@@ -104,61 +112,117 @@ export const formatExtendedKeyUsage = (usage: CertExtendedKeyUsageType): string
export const formatSubjectAttributeType = (type: CertSubjectAttributeType): string => {
switch (type) {
case CertSubjectAttributeType.COMMON_NAME:
return "Common Name";
return "Common Name (CN)";
case CertSubjectAttributeType.ORGANIZATION:
return "Organization";
case CertSubjectAttributeType.COUNTRY:
return "Country";
default:
return type;
}
};
export const formatIncludeType = (include: CertIncludeType): string => {
switch (include) {
case CertIncludeType.MANDATORY:
return "Mandatory";
case CertIncludeType.OPTIONAL:
return "Optional";
case CertIncludeType.PROHIBIT:
return "Prohibit";
export const formatAttributeRule = (rule: CertAttributeRule): string => {
switch (rule) {
case CertAttributeRule.ALLOW:
return "Allow";
case CertAttributeRule.DENY:
return "Deny";
case CertAttributeRule.REQUIRE:
return "Require";
default:
return include;
return rule;
}
};
export const mapLegacySANTypeToStandard = (type: string): CertSubjectAlternativeNameType => {
switch (type) {
case "dns":
case "dns_name":
return CertSubjectAlternativeNameType.DNS_NAME;
case "ip":
case "ip_address":
return CertSubjectAlternativeNameType.IP_ADDRESS;
case "email":
return CertSubjectAlternativeNameType.EMAIL;
case "uri":
case "url":
return CertSubjectAlternativeNameType.URI;
export const formatSanEffect = (effect: CertSanEffect): string => {
switch (effect) {
case CertSanEffect.ALLOW:
return "Allow";
case CertSanEffect.DENY:
return "Deny";
case CertSanEffect.REQUIRE:
return "Require";
default:
throw new Error(`Unknown SAN type: ${type}`);
return effect;
}
};
export const mapSANTypeToLegacy = (type: CertSubjectAlternativeNameType): string => {
switch (type) {
case CertSubjectAlternativeNameType.DNS_NAME:
return "dns";
case CertSubjectAlternativeNameType.IP_ADDRESS:
return "ip";
case CertSubjectAlternativeNameType.EMAIL:
return "email";
case CertSubjectAlternativeNameType.URI:
return "uri";
default:
return type;
}
};
export const SAN_TYPE_OPTIONS = Object.values(CertSubjectAlternativeNameType);
export const KEY_USAGE_OPTIONS = Object.values(CertKeyUsageType);
export const EXTENDED_KEY_USAGE_OPTIONS = Object.values(CertExtendedKeyUsageType);
export const INCLUDE_TYPE_OPTIONS = Object.values(CertIncludeType);
export const DURATION_UNIT_OPTIONS = Object.values(CertDurationUnit);
export const SUBJECT_ATTRIBUTE_TYPE_OPTIONS = Object.values(CertSubjectAttributeType);
export const SUBJECT_ATTRIBUTE_TYPE_OPTIONS = Object.values(CertSubjectAttributeType);
export const ATTRIBUTE_RULE_OPTIONS = Object.values(CertAttributeRule);
export const SAN_EFFECT_OPTIONS = Object.values(CertSanEffect);
export const SUBJECT_ATTRIBUTE_INCLUDE_OPTIONS = ["optional", "prohibit"] as const;
export const SAN_INCLUDE_OPTIONS = ["mandatory", "optional", "prohibit"] as const;
export const USAGE_STATES = {
REQUIRED: "required",
OPTIONAL: "optional"
} as const;
export type UsageState = typeof USAGE_STATES[keyof typeof USAGE_STATES] | undefined;
export const TEMPLATE_SIGNATURE_ALGORITHMS = [
"SHA256-RSA",
"SHA384-RSA",
"SHA512-RSA",
"SHA256-ECDSA",
"SHA384-ECDSA",
"SHA512-ECDSA"
] as const;
export const TEMPLATE_KEY_ALGORITHMS = [
"RSA-2048",
"RSA-3072",
"RSA-4096",
"ECDSA-P256",
"ECDSA-P384"
] as const;
// API format algorithm constants
export const API_SIGNATURE_ALGORITHMS = [
"RSA-SHA256",
"RSA-SHA384",
"RSA-SHA512",
"ECDSA-SHA256",
"ECDSA-SHA384",
"ECDSA-SHA512"
] as const;
export const API_KEY_ALGORITHMS = [
"RSA_2048",
"RSA_3072",
"RSA_4096",
"EC_prime256v1",
"EC_secp384r1"
] as const;
// Mapping functions between template and API formats
export const mapTemplateSignatureAlgorithmToApi = (templateFormat: string): string => {
const mapping: 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 mapping[templateFormat] || templateFormat;
};
export const mapTemplateKeyAlgorithmToApi = (templateFormat: string): string => {
const mapping: Record<string, string> = {
"RSA-2048": "RSA_2048",
"RSA-3072": "RSA_3072",
"RSA-4096": "RSA_4096",
"ECDSA-P256": "EC_prime256v1",
"ECDSA-P384": "EC_secp384r1"
};
return mapping[templateFormat] || templateFormat;
};
@@ -1,152 +1,159 @@
import { Checkbox } from "@app/components/v2";
import React from "react";
import { Select, SelectItem } from "@app/components/v2";
import {
CertExtendedKeyUsageType,
CertKeyUsageType,
EXTENDED_KEY_USAGE_OPTIONS,
formatExtendedKeyUsage,
formatKeyUsage,
EXTENDED_KEY_USAGE_OPTIONS,
KEY_USAGE_OPTIONS
} from "./certificate-constants";
type UsageState = "mandatory" | "optional" | undefined;
type KeyUsagePolicy = "allow" | "require" | "deny" | "none";
type ThreeStateCheckboxProps = {
value: UsageState;
onChange: (newValue: UsageState) => void;
label: string;
id: string;
};
const ThreeStateCheckbox = ({ value, onChange, label, id }: ThreeStateCheckboxProps) => {
const handleClick = () => {
if (value === undefined) {
onChange("optional");
} else if (value === "optional") {
onChange("mandatory");
} else {
onChange(undefined);
}
interface KeyUsagesSectionProps {
watchedKeyUsages: {
requiredUsages: CertKeyUsageType[];
optionalUsages: CertKeyUsageType[];
};
const getCheckboxState = () => {
if (value) return true;
return false;
watchedExtendedKeyUsages: {
requiredUsages: CertExtendedKeyUsageType[];
optionalUsages: CertExtendedKeyUsageType[];
};
onKeyUsagesChange: (usages: {
requiredUsages: CertKeyUsageType[];
optionalUsages: CertKeyUsageType[];
}) => void;
onExtendedKeyUsagesChange: (usages: {
requiredUsages: CertExtendedKeyUsageType[];
optionalUsages: CertExtendedKeyUsageType[];
}) => void;
}
const getIndeterminateState = () => {
return value === "optional";
};
const getStateLabel = () => {
if (value === "mandatory") return " (Mandatory)";
if (value === "optional") return " (Optional)";
return "";
};
return (
<div className="flex items-center space-x-3">
<Checkbox
id={id}
isChecked={getCheckboxState()}
isIndeterminate={getIndeterminateState()}
onCheckedChange={handleClick}
/>
<label
htmlFor={id}
className="text-mineshaft-200 cursor-pointer text-sm font-medium"
>
{label}
{value && (
<span className="text-mineshaft-400 text-xs ml-1">
{getStateLabel()}
</span>
)}
</label>
</div>
);
};
type KeyUsagesSectionProps = {
watchedKeyUsages?: { requiredUsages?: string[]; optionalUsages?: string[] };
watchedExtendedKeyUsages?: { requiredUsages?: string[]; optionalUsages?: string[] };
onKeyUsagesChange: (usages: { requiredUsages: string[]; optionalUsages: string[] }) => void;
onExtendedKeyUsagesChange: (usages: { requiredUsages: string[]; optionalUsages: string[] }) => void;
};
export const KeyUsagesSection = ({
watchedKeyUsages = { requiredUsages: [], optionalUsages: [] },
watchedExtendedKeyUsages = { requiredUsages: [], optionalUsages: [] },
export const KeyUsagesSection: React.FC<KeyUsagesSectionProps> = ({
watchedKeyUsages,
watchedExtendedKeyUsages,
onKeyUsagesChange,
onExtendedKeyUsagesChange
}: KeyUsagesSectionProps) => {
const getUsageState = (usage: string, data: { requiredUsages?: string[]; optionalUsages?: string[] }): UsageState => {
if (data.requiredUsages?.includes(usage)) return "mandatory";
if (data.optionalUsages?.includes(usage)) return "optional";
return undefined;
}) => {
const getKeyUsagePolicy = (usage: CertKeyUsageType): KeyUsagePolicy => {
if (watchedKeyUsages.requiredUsages.includes(usage)) return "require";
if (watchedKeyUsages.optionalUsages.includes(usage)) return "allow";
return "deny";
};
const handleKeyUsageChange = (usage: CertKeyUsageType, newState: UsageState) => {
const currentRequired = watchedKeyUsages.requiredUsages || [];
const currentOptional = watchedKeyUsages.optionalUsages || [];
const getExtendedKeyUsagePolicy = (usage: CertExtendedKeyUsageType): KeyUsagePolicy => {
if (watchedExtendedKeyUsages.requiredUsages.includes(usage)) return "require";
if (watchedExtendedKeyUsages.optionalUsages.includes(usage)) return "allow";
return "deny";
};
let newRequired = currentRequired.filter(u => u !== usage);
let newOptional = currentOptional.filter(u => u !== usage);
const handleKeyUsagePolicyChange = (usage: CertKeyUsageType, policy: KeyUsagePolicy) => {
const newRequired = watchedKeyUsages.requiredUsages.filter((u) => u !== usage);
const newOptional = watchedKeyUsages.optionalUsages.filter((u) => u !== usage);
if (newState === "mandatory") {
newRequired = [...newRequired, usage];
} else if (newState === "optional") {
newOptional = [...newOptional, usage];
if (policy === "require") {
newRequired.push(usage);
} else if (policy === "allow") {
newOptional.push(usage);
}
onKeyUsagesChange({ requiredUsages: newRequired, optionalUsages: newOptional });
onKeyUsagesChange({
requiredUsages: newRequired,
optionalUsages: newOptional
});
};
const handleExtendedKeyUsageChange = (usage: CertExtendedKeyUsageType, newState: UsageState) => {
const currentRequired = watchedExtendedKeyUsages.requiredUsages || [];
const currentOptional = watchedExtendedKeyUsages.optionalUsages || [];
const handleExtendedKeyUsagePolicyChange = (
usage: CertExtendedKeyUsageType,
policy: KeyUsagePolicy
) => {
const newRequired = watchedExtendedKeyUsages.requiredUsages.filter((u) => u !== usage);
const newOptional = watchedExtendedKeyUsages.optionalUsages.filter((u) => u !== usage);
let newRequired = currentRequired.filter(u => u !== usage);
let newOptional = currentOptional.filter(u => u !== usage);
if (newState === "mandatory") {
newRequired = [...newRequired, usage];
} else if (newState === "optional") {
newOptional = [...newOptional, usage];
if (policy === "require") {
newRequired.push(usage);
} else if (policy === "allow") {
newOptional.push(usage);
}
onExtendedKeyUsagesChange({ requiredUsages: newRequired, optionalUsages: newOptional });
onExtendedKeyUsagesChange({
requiredUsages: newRequired,
optionalUsages: newOptional
});
};
const keyUsagePolicyOptions = [
{ value: "deny", label: "Deny" },
{ value: "allow", label: "Allow" },
{ value: "require", label: "Require" }
];
const extendedKeyUsagePolicyOptions = [
{ value: "deny", label: "Deny" },
{ value: "allow", label: "Allow" },
{ value: "require", label: "Require" }
];
return (
<div className="space-y-6">
<div className="space-y-3">
<h3 className="text-mineshaft-200 text-sm font-medium">Key Usages</h3>
<div className="grid grid-cols-2 gap-2 pl-2">
{KEY_USAGE_OPTIONS.map((usage) => (
<ThreeStateCheckbox
key={usage}
id={`key-usage-${usage}`}
label={formatKeyUsage(usage)}
value={getUsageState(usage, watchedKeyUsages)}
onChange={(newState) => handleKeyUsageChange(usage, newState)}
/>
))}
<div className="space-y-8">
{/* Key Usages */}
<div className="space-y-4">
<h4 className="text-sm font-medium text-bunker-200">Key Usages</h4>
<div className="grid grid-cols-2 gap-4">
{KEY_USAGE_OPTIONS.map((usage) => {
const policy = getKeyUsagePolicy(usage);
return (
<div key={usage} className="flex items-center justify-between">
<span className="text-sm text-bunker-200">{formatKeyUsage(usage)}</span>
<Select
value={policy}
onValueChange={(value) =>
handleKeyUsagePolicyChange(usage, value as KeyUsagePolicy)
}
className="w-32"
>
{keyUsagePolicyOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</Select>
</div>
);
})}
</div>
</div>
<div className="space-y-3">
<h3 className="text-mineshaft-200 text-sm font-medium">Extended Key Usages</h3>
<div className="grid grid-cols-2 gap-2 pl-2">
{EXTENDED_KEY_USAGE_OPTIONS.map((usage) => (
<ThreeStateCheckbox
key={usage}
id={`ext-key-usage-${usage}`}
label={formatExtendedKeyUsage(usage)}
value={getUsageState(usage, watchedExtendedKeyUsages)}
onChange={(newState) => handleExtendedKeyUsageChange(usage, newState)}
/>
))}
{/* Extended Key Usages */}
<div className="space-y-4">
<h4 className="text-sm font-medium text-bunker-200">Extended Key Usages</h4>
<div className="grid grid-cols-2 gap-4">
{EXTENDED_KEY_USAGE_OPTIONS.map((usage) => {
const policy = getExtendedKeyUsagePolicy(usage);
return (
<div key={usage} className="flex items-center justify-between">
<span className="text-sm text-bunker-200">{formatExtendedKeyUsage(usage)}</span>
<Select
value={policy}
onValueChange={(value) =>
handleExtendedKeyUsagePolicyChange(usage, value as KeyUsagePolicy)
}
className="w-32"
>
{extendedKeyUsagePolicyOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</Select>
</div>
);
})}
</div>
</div>
</div>
@@ -3,93 +3,121 @@ import { z } from "zod";
import {
CertDurationUnit,
CertExtendedKeyUsageType,
CertIncludeType,
CertKeyUsageType,
CertSubjectAlternativeNameType,
CertSubjectAttributeType
CertSubjectAttributeType,
SUBJECT_ATTRIBUTE_INCLUDE_OPTIONS,
SAN_INCLUDE_OPTIONS
} from "./certificate-constants";
export const attributeSchema = z.object({
export const uiAttributeSchema = z.object({
type: z.nativeEnum(CertSubjectAttributeType),
include: z.nativeEnum(CertIncludeType),
value: z.array(z.string().min(1, "Value cannot be empty")).optional()
include: z.enum(SUBJECT_ATTRIBUTE_INCLUDE_OPTIONS),
value: z.array(z.string().min(1, "Value cannot be empty"))
});
export const sanSchema = z.object({
export const uiSanSchema = z.object({
type: z.nativeEnum(CertSubjectAlternativeNameType),
include: z.nativeEnum(CertIncludeType),
value: z.array(z.string().min(1, "Value cannot be empty")).optional()
include: z.enum(SAN_INCLUDE_OPTIONS),
value: z.array(z.string().min(1, "Value cannot be empty"))
});
export const uiKeyUsagesSchema = z.object({
requiredUsages: z.array(z.nativeEnum(CertKeyUsageType)),
optionalUsages: z.array(z.nativeEnum(CertKeyUsageType))
});
export const uiExtendedKeyUsagesSchema = z.object({
requiredUsages: z.array(z.nativeEnum(CertExtendedKeyUsageType)),
optionalUsages: z.array(z.nativeEnum(CertExtendedKeyUsageType))
});
export const uiValiditySchema = z.object({
maxDuration: z.object({
value: z.number().min(1, "Duration must be at least 1"),
unit: z.nativeEnum(CertDurationUnit)
})
});
export const uiSignatureAlgorithmSchema = z.object({
allowedAlgorithms: z.array(z.string()).optional(),
defaultAlgorithm: z.string().optional()
});
export const uiKeyAlgorithmSchema = z.object({
allowedKeyTypes: z.array(z.string()).optional(),
defaultKeyType: z.string().optional()
});
export const templateSchema = z.object({
slug: z.string().trim().min(1, "Template name is required"),
description: z.string().optional(),
attributes: z.array(attributeSchema).optional().refine((attributes) => {
if (!attributes) return true;
attributes: z.array(uiAttributeSchema).optional(),
subjectAlternativeNames: z.array(uiSanSchema).optional(),
keyUsages: uiKeyUsagesSchema.optional(),
extendedKeyUsages: uiExtendedKeyUsagesSchema.optional(),
validity: uiValiditySchema.optional(),
signatureAlgorithm: uiSignatureAlgorithmSchema.optional(),
keyAlgorithm: uiKeyAlgorithmSchema.optional()
});
const attributesByType = attributes.reduce((acc, attr) => {
if (!acc[attr.type]) acc[attr.type] = [];
acc[attr.type].push(attr);
return acc;
}, {} as Record<string, typeof attributes>);
export type TemplateFormData = z.infer<typeof templateSchema>;
for (const [, attrs] of Object.entries(attributesByType)) {
const mandatoryAttrs = attrs.filter(attr => attr.include === 'mandatory');
export const apiSubjectSchema = z
.object({
type: z.nativeEnum(CertSubjectAttributeType),
allowed: z.array(z.string().min(1, "Value cannot be empty")).optional(),
required: z.array(z.string().min(1, "Value cannot be empty")).optional(),
denied: z.array(z.string().min(1, "Value cannot be empty")).optional()
})
.refine((data) => data.allowed || data.required || data.denied, {
message: "At least one allowed, required, or denied value must be provided"
});
if (mandatoryAttrs.length > 1) {
return false;
}
export const apiSanSchema = z
.object({
type: z.nativeEnum(CertSubjectAlternativeNameType),
allowed: z.array(z.string().min(1, "Value cannot be empty")).optional(),
required: z.array(z.string().min(1, "Value cannot be empty")).optional(),
denied: z.array(z.string().min(1, "Value cannot be empty")).optional()
})
.refine((data) => data.allowed || data.required || data.denied, {
message: "At least one allowed, required, or denied value must be provided"
});
if (mandatoryAttrs.length === 1 && attrs.length > 1) {
return false;
}
}
return true;
}, {
message: "Attribute validation failed: when a mandatory value exists, no other values are allowed for that attribute type"
}),
export const apiTemplateSchema = z.object({
name: z.string().trim().min(1, "Template name is required"),
description: z.string().optional(),
subject: z.array(apiSubjectSchema).optional(),
sans: z.array(apiSanSchema).optional(),
keyUsages: z
.object({
requiredUsages: z.array(z.nativeEnum(CertKeyUsageType)).optional(),
optionalUsages: z.array(z.nativeEnum(CertKeyUsageType)).optional()
allowed: z.array(z.nativeEnum(CertKeyUsageType)).optional(),
required: z.array(z.nativeEnum(CertKeyUsageType)).optional(),
denied: z.array(z.nativeEnum(CertKeyUsageType)).optional()
})
.optional(),
extendedKeyUsages: z
.object({
requiredUsages: z.array(z.nativeEnum(CertExtendedKeyUsageType)).optional(),
optionalUsages: z.array(z.nativeEnum(CertExtendedKeyUsageType)).optional()
allowed: z.array(z.nativeEnum(CertExtendedKeyUsageType)).optional(),
required: z.array(z.nativeEnum(CertExtendedKeyUsageType)).optional(),
denied: z.array(z.nativeEnum(CertExtendedKeyUsageType)).optional()
})
.optional(),
algorithms: z
.object({
signature: z.array(z.string()).optional(),
keyAlgorithm: z.array(z.string()).optional()
})
.optional(),
subjectAlternativeNames: z.array(sanSchema).optional(),
validity: z
.object({
maxDuration: z
.object({
value: z.number().min(1, "Duration must be at least 1"),
unit: z.nativeEnum(CertDurationUnit)
})
.optional(),
minDuration: z
.object({
value: z.number().min(1, "Duration must be at least 1"),
unit: z.nativeEnum(CertDurationUnit)
})
max: z
.string()
.regex(/^\d+[dhmy]$/, "Must be in format like '365d', '12m', '1y', or '24h'")
.optional()
})
.optional(),
signatureAlgorithm: z
.object({
allowedAlgorithms: z.array(z.string()).optional(),
defaultAlgorithm: z.string().optional()
})
.optional(),
keyAlgorithm: z
.object({
allowedKeyTypes: z.array(z.string()).optional(),
defaultKeyType: z.string().optional()
})
.optional()
});
export type TemplateFormData = z.infer<typeof templateSchema>;
export type ApiTemplateFormData = z.infer<typeof apiTemplateSchema>;
@@ -2,7 +2,9 @@ import {
CertExtendedKeyUsageType,
CertKeyUsageType,
formatExtendedKeyUsage,
formatKeyUsage
formatKeyUsage,
USAGE_STATES,
UsageState
} from "./certificate-constants";
export const formatUsageName = (usage: string): string => {
@@ -14,6 +16,7 @@ export const formatUsageName = (usage: string): string => {
return formatExtendedKeyUsage(usage as CertExtendedKeyUsageType);
}
} catch {
// Handle any errors in type checking
}
return usage.replace(/_/g, " ");
};
@@ -22,15 +25,15 @@ export const getUsageState = (
usage: CertKeyUsageType | CertExtendedKeyUsageType,
requiredUsages: (CertKeyUsageType | CertExtendedKeyUsageType)[],
optionalUsages: (CertKeyUsageType | CertExtendedKeyUsageType)[]
): "required" | "optional" | undefined => {
if (requiredUsages.includes(usage)) return "required";
if (optionalUsages.includes(usage)) return "optional";
): UsageState => {
if (requiredUsages.includes(usage)) return USAGE_STATES.REQUIRED;
if (optionalUsages.includes(usage)) return USAGE_STATES.OPTIONAL;
return undefined;
};
export const toggleUsageState = (
usage: CertKeyUsageType | CertExtendedKeyUsageType,
newState: "required" | "optional" | undefined,
newState: UsageState,
currentRequiredUsages: (CertKeyUsageType | CertExtendedKeyUsageType)[],
currentOptionalUsages: (CertKeyUsageType | CertExtendedKeyUsageType)[],
toggleRequired: (usage: CertKeyUsageType | CertExtendedKeyUsageType) => void,
@@ -39,10 +42,10 @@ export const toggleUsageState = (
const isRequired = currentRequiredUsages.includes(usage);
const isOptional = currentOptionalUsages.includes(usage);
if (newState === "required") {
if (newState === USAGE_STATES.REQUIRED) {
if (isOptional) toggleOptional(usage);
if (!isRequired) toggleRequired(usage);
} else if (newState === "optional") {
} else if (newState === USAGE_STATES.OPTIONAL) {
if (isRequired) toggleRequired(usage);
if (!isOptional) toggleOptional(usage);
} else {