Add external CA support, certificate requests and profile external configurations

This commit is contained in:
Carlos Monastyrski
2025-11-30 20:42:34 -03:00
parent e719de8710
commit dbd4a85fc8
42 changed files with 3947 additions and 314 deletions
@@ -22,14 +22,25 @@ export type TCertificateProfile = {
apiConfigId?: string;
createdAt: string;
updatedAt: string;
externalConfigs?: Record<string, unknown> | null;
certificateAuthority?: {
id: string;
projectId?: string;
status: string;
name: string;
isExternal?: boolean;
externalType?: string | null;
};
};
export type TCertificateProfileWithDetails = TCertificateProfile & {
certificateAuthority?: {
id: string;
projectId: string;
projectId?: string;
status: string;
name: string;
isExternal?: boolean;
externalType?: string | null;
};
certificateTemplate?: {
id: string;
@@ -72,6 +83,7 @@ export type TCreateCertificateProfileDTO = {
renewBeforeDays?: number;
};
acmeConfig?: unknown;
externalConfigs?: Record<string, unknown> | null;
};
export type TUpdateCertificateProfileDTO = {
@@ -90,6 +102,7 @@ export type TUpdateCertificateProfileDTO = {
renewBeforeDays?: number;
};
acmeConfig?: unknown;
externalConfigs?: Record<string, unknown> | null;
};
export type TDeleteCertificateProfileDTO = {
@@ -7,4 +7,4 @@ export {
useRevokeCert,
useUpdateRenewalConfig
} from "./mutations";
export { useGetCert, useGetCertBody } from "./queries";
export { useGetCert, useGetCertBody, useGetCertificateRequest } from "./queries";
@@ -13,6 +13,8 @@ import {
TRenewCertificateDTO,
TRenewCertificateResponse,
TRevokeCertDTO,
TUnifiedCertificateIssuanceDTO,
TUnifiedCertificateIssuanceResponse,
TUpdateRenewalConfigDTO
} from "./types";
@@ -185,3 +187,34 @@ export const useDownloadCertPkcs12 = () => {
}
});
};
export const useUnifiedCertificateIssuance = () => {
const queryClient = useQueryClient();
return useMutation<TUnifiedCertificateIssuanceResponse, object, TUnifiedCertificateIssuanceDTO>({
mutationFn: async (body) => {
const { projectSlug, ...requestData } = body;
const { data } = await apiRequest.post<TUnifiedCertificateIssuanceResponse>(
"/api/v3/pki/certificates",
requestData,
{
params: { projectSlug }
}
);
return data;
},
onSuccess: (_, { projectSlug }) => {
queryClient.invalidateQueries({
queryKey: ["certificate-profiles", "list"]
});
queryClient.invalidateQueries({
queryKey: pkiSubscriberKeys.allPkiSubscriberCertificates()
});
queryClient.invalidateQueries({
queryKey: projectKeys.allProjectCertificates()
});
queryClient.invalidateQueries({
queryKey: projectKeys.forProjectCertificates(projectSlug)
});
}
});
};
@@ -2,12 +2,16 @@ import { useQuery } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { TCertificate } from "./types";
import { TCertificate, TCertificateRequestDetails } from "./types";
export const certKeys = {
getCertById: (serialNumber: string) => [{ serialNumber }, "cert"],
getCertBody: (serialNumber: string) => [{ serialNumber }, "certBody"],
getCertBundle: (serialNumber: string) => [{ serialNumber }, "certBundle"]
getCertBundle: (serialNumber: string) => [{ serialNumber }, "certBundle"],
getCertificateRequest: (requestId: string, projectSlug: string) => [
{ requestId, projectSlug },
"certificateRequest"
]
};
export const useGetCert = (serialNumber: string) => {
@@ -55,3 +59,23 @@ export const useGetCertBundle = (serialNumber: string) => {
enabled: Boolean(serialNumber)
});
};
export const useGetCertificateRequest = (requestId: string, projectSlug: string) => {
return useQuery({
queryKey: certKeys.getCertificateRequest(requestId, projectSlug),
queryFn: async () => {
const { data } = await apiRequest.get<TCertificateRequestDetails>(
`/api/v3/pki/certificates/requests/${requestId}`,
{
params: { projectSlug }
}
);
return data;
},
enabled: Boolean(requestId) && Boolean(projectSlug),
refetchInterval: (query) => {
// Only refetch if status is pending
return query.state.data?.status === "pending" ? 5000 : false;
}
});
};
@@ -64,6 +64,7 @@ export type TRenewCertificateResponse = {
serialNumber: string;
certificateId: string;
projectId: string;
certificateRequestId?: string;
};
export type TUpdateRenewalConfigDTO = {
@@ -79,3 +80,51 @@ export type TDownloadPkcs12DTO = {
password: string;
alias: string;
};
export type TUnifiedCertificateIssuanceDTO = {
projectSlug: string;
projectId: string;
profileId?: string;
caId?: string;
csr?: string;
commonName?: string;
altNames?: string;
keyUsages?: string[];
extendedKeyUsages?: string[];
notBefore?: Date;
notAfter?: Date;
keyAlgorithm?: string;
signatureAlgorithm?: string;
ttl?: string;
friendlyName?: string;
pkiCollectionId?: string;
issuerType?: string;
};
export type TUnifiedCertificateResponse = {
certificate: string;
issuingCaCertificate: string;
certificateChain: string;
privateKey?: string;
serialNumber: string;
certificateId: string;
projectId: string;
};
export type TCertificateRequestResponse = {
certificateRequestId: string;
status: "pending" | "issued" | "failed";
projectId: string;
};
export type TUnifiedCertificateIssuanceResponse =
| TUnifiedCertificateResponse
| TCertificateRequestResponse;
export type TCertificateRequestDetails = {
status: "pending" | "issued" | "failed";
certificate: TCertificate | null;
errorMessage: string | null;
createdAt: string;
updatedAt: string;
};
@@ -20,9 +20,9 @@ import {
} from "@app/components/v2";
import { useProject } from "@app/context";
import { useGetCert } from "@app/hooks/api";
import { useCreateCertificateV3 } from "@app/hooks/api/ca";
import { EnrollmentType, useListCertificateProfiles } from "@app/hooks/api/certificateProfiles";
import { CertExtendedKeyUsage, CertKeyUsage } from "@app/hooks/api/certificates/enums";
import { useUnifiedCertificateIssuance } from "@app/hooks/api/certificates/mutations";
import { useGetCertificateTemplateV2ById } from "@app/hooks/api/certificateTemplates/queries";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { CertSubjectAlternativeNameType } from "@app/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/shared/certificate-constants";
@@ -103,10 +103,10 @@ type Props = {
};
type TCertificateDetails = {
serialNumber: string;
certificate: string;
certificateChain: string;
privateKey: string;
serialNumber?: string;
certificate?: string;
certificateChain?: string;
privateKey?: string;
};
export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }: Props) => {
@@ -122,12 +122,11 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
const { data: profilesData } = useListCertificateProfiles({
projectId: currentProject?.id || "",
enrollmentType: EnrollmentType.API
enrollmentType: EnrollmentType.API,
includeConfigs: true
});
const { mutateAsync: createCertificate } = useCreateCertificateV3({
projectId: currentProject?.id
});
const { mutateAsync: issueCertificate } = useUnifiedCertificateIssuance();
const formResolver = useMemo(() => {
return zodResolver(createSchema(shouldShowSubjectSection));
@@ -243,7 +242,7 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
keyUsages,
extendedKeyUsages
}: FormData) => {
if (!currentProject?.slug) {
if (!currentProject?.slug || !currentProject?.id) {
createNotification({
text: "Project not found. Please refresh and try again.",
type: "error"
@@ -275,44 +274,70 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
}
}
const certificateRequest: any = {
profileId: formProfileId,
projectSlug: currentProject.slug,
ttl,
signatureAlgorithm,
keyAlgorithm,
keyUsages: filterUsages(keyUsages) as CertKeyUsage[],
extendedKeyUsages: filterUsages(extendedKeyUsages) as CertExtendedKeyUsage[]
};
try {
// Prepare unified request
const request: any = {
profileId: formProfileId,
projectSlug: currentProject.slug,
projectId: currentProject.id,
ttl,
keyUsages: filterUsages(keyUsages) as CertKeyUsage[],
extendedKeyUsages: filterUsages(extendedKeyUsages) as CertExtendedKeyUsage[]
};
if (constraints.shouldShowSubjectSection && commonName) {
certificateRequest.commonName = commonName;
}
if (constraints.shouldShowSanSection && subjectAltNames && subjectAltNames.length > 0) {
const formattedSans = formatSubjectAltNames(subjectAltNames);
if (formattedSans && formattedSans.length > 0) {
certificateRequest.altNames = formattedSans;
if (constraints.shouldShowSubjectSection && commonName) {
request.commonName = commonName;
}
if (signatureAlgorithm) {
request.signatureAlgorithm = signatureAlgorithm;
}
if (keyAlgorithm) {
request.keyAlgorithm = keyAlgorithm;
}
if (constraints.shouldShowSanSection && subjectAltNames && subjectAltNames.length > 0) {
const formattedSans = formatSubjectAltNames(subjectAltNames);
if (formattedSans && formattedSans.length > 0) {
request.altNames = formattedSans;
}
}
const response = await issueCertificate(request);
// Handle certificate issuance response
if ("certificate" in response) {
// Immediate certificate issuance
setCertificateDetails({
serialNumber: response.serialNumber,
certificate: response.certificate,
certificateChain: response.certificateChain,
privateKey: response.privateKey
});
createNotification({
text: "Successfully created certificate",
type: "success"
});
} else {
// Certificate request - async processing
createNotification({
text: `Certificate request submitted successfully. This may take a few minutes to process. Request ID: ${response.certificateRequestId}`,
type: "success"
});
handlePopUpToggle("issueCertificate", false);
}
} catch (error) {
createNotification({
text: `Failed to request certificate: ${(error as Error)?.message || "Unknown error"}`,
type: "error"
});
}
const { serialNumber, certificate, certificateChain, privateKey } =
await createCertificate(certificateRequest);
setCertificateDetails({
serialNumber,
certificate,
certificateChain,
privateKey
});
createNotification({
text: "Successfully created certificate",
type: "success"
});
},
[
currentProject?.slug,
createCertificate,
issueCertificate,
constraints.shouldShowSubjectSection,
constraints.shouldShowSanSection
]
@@ -321,13 +346,13 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
const getModalTitle = () => {
if (certificateDetails) return "Certificate Created Successfully";
if (cert) return "Certificate Details";
return "Issue New Certificate";
return "Request 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";
return "Request a new certificate using a certificate profile";
};
return (
@@ -343,10 +368,10 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
<ModalContent title={getModalTitle()} subTitle={getModalSubTitle()}>
{certificateDetails && (
<CertificateContent
serialNumber={certificateDetails.serialNumber}
certificate={certificateDetails.certificate}
certificateChain={certificateDetails.certificateChain}
privateKey={certificateDetails.privateKey}
serialNumber={certificateDetails.serialNumber!}
certificate={certificateDetails.certificate!}
certificateChain={certificateDetails.certificateChain!}
privateKey={certificateDetails.privateKey!}
/>
)}
{cert && (
@@ -498,7 +523,7 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
isLoading={isSubmitting}
isDisabled={isSubmitting || (!actualSelectedProfile && !profileId)}
>
{cert ? "Update" : "Issue Certificate"}
{cert ? "Update" : "Request Certificate"}
</Button>
<Button
colorSchema="secondary"
@@ -20,12 +20,16 @@ export const CertificateRenewalModal = ({ popUp, handlePopUpToggle }: Props) =>
const onRenewConfirm = async () => {
const { certificateId } = popUp.renewCertificate.data as { certificateId: string };
await renewCertificate({
const result = await renewCertificate({
certificateId
});
const notificationText = result.certificateRequestId
? `Certificate renewal initiated successfully. Request ID: ${result.certificateRequestId}`
: "Certificate renewed successfully";
createNotification({
text: "Certificate renewed successfully",
text: notificationText,
type: "success"
});
@@ -0,0 +1,108 @@
import { useEffect } from "react";
import { faCheck, faExclamationTriangle, faSpinner } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useProject } from "@app/context";
import { useGetCertificateRequest } from "@app/hooks/api/certificates";
type CertificateInfo = {
id: string;
serialNumber: string;
commonName: string;
notAfter: string;
[key: string]: unknown;
};
type Props = {
requestId: string;
onCertificateIssued?: (certificate: CertificateInfo) => void;
};
export const CertificateRequestTracker = ({ requestId, onCertificateIssued }: Props) => {
const { currentProject } = useProject();
const { data: requestData, isLoading } = useGetCertificateRequest(
requestId,
currentProject?.slug || ""
);
useEffect(() => {
if (requestData?.status === "issued" && requestData.certificate && onCertificateIssued) {
onCertificateIssued(requestData.certificate);
}
}, [requestData, onCertificateIssued]);
if (isLoading) {
return (
<div className="flex items-center space-x-2">
<FontAwesomeIcon icon={faSpinner} className="animate-spin text-primary" />
<span className="text-sm text-mineshaft-400">Loading request status...</span>
</div>
);
}
const getStatusIcon = () => {
switch (requestData?.status) {
case "pending":
return <FontAwesomeIcon icon={faSpinner} className="animate-spin text-yellow-500" />;
case "issued":
return <FontAwesomeIcon icon={faCheck} className="text-green-500" />;
case "failed":
return <FontAwesomeIcon icon={faExclamationTriangle} className="text-red-500" />;
default:
return null;
}
};
const getStatusMessage = () => {
switch (requestData?.status) {
case "pending":
return "Certificate request is being processed...";
case "issued":
return "Certificate has been issued successfully!";
case "failed":
return `Certificate request failed: ${requestData.errorMessage || "Unknown error"}`;
default:
return "Unknown status";
}
};
return (
<div className="space-y-4">
<div className="flex items-center space-x-2">
{getStatusIcon()}
<span className="text-sm font-medium text-mineshaft-300">Request ID: {requestId}</span>
</div>
<div className="text-sm text-mineshaft-400">
Status: <span className="font-medium capitalize">{requestData?.status || "Unknown"}</span>
</div>
<div className="text-sm text-mineshaft-400">{getStatusMessage()}</div>
{requestData?.status === "issued" && requestData.certificate && (
<div className="mt-4 rounded-md border border-green-500/30 bg-green-900/20 p-3">
<div className="text-sm text-green-400">
<strong>Certificate Details:</strong>
<br />
Serial Number: {requestData.certificate.serialNumber}
<br />
Common Name: {requestData.certificate.commonName}
<br />
Valid Until: {new Date(requestData.certificate.notAfter).toLocaleDateString()}
</div>
</div>
)}
{requestData?.status === "failed" && requestData.errorMessage && (
<div className="mt-4 rounded-md border border-red-500/30 bg-red-900/20 p-3">
<div className="text-sm text-red-400">
<strong>Error Details:</strong>
<br />
{requestData.errorMessage}
</div>
</div>
)}
</div>
);
};
@@ -119,7 +119,7 @@ export const CertificatesSection = () => {
onClick={() => handlePopUpOpen("issueCertificate")}
isDisabled={!isAllowed}
>
Issue
Request
</Button>
</div>
)}
@@ -9,6 +9,7 @@ import { createNotification } from "@app/components/notifications";
import {
Button,
Checkbox,
FilterableSelect,
FormControl,
Input,
Modal,
@@ -19,7 +20,8 @@ import {
Tooltip
} from "@app/components/v2";
import { useProject, useSubscription } from "@app/context";
import { useListCasByProjectId } from "@app/hooks/api/ca/queries";
import { CaType } from "@app/hooks/api/ca/enums";
import { useGetAzureAdcsTemplates, useListCasByProjectId } from "@app/hooks/api/ca/queries";
import {
EnrollmentType,
IssuerType,
@@ -77,7 +79,12 @@ const createSchema = z
renewBeforeDays: z.number().min(1).max(365).optional()
})
.optional(),
acmeConfig: z.object({}).optional()
acmeConfig: z.object({}).optional(),
externalConfigs: z
.object({
template: z.string().min(1, "Azure ADCS template is required")
})
.optional()
})
.refine(
(data) => {
@@ -212,7 +219,12 @@ const editSchema = z
renewBeforeDays: z.number().min(1).max(365).optional()
})
.optional(),
acmeConfig: z.object({}).optional()
acmeConfig: z.object({}).optional(),
externalConfigs: z
.object({
template: z.string().optional()
})
.optional()
})
.refine(
(data) => {
@@ -339,7 +351,7 @@ export const CreateProfileModal = ({
const { currentProject } = useProject();
const { subscription } = useSubscription();
const { data: caData } = useListCasByProjectId(currentProject?.id || "");
const { data: allCaData } = useListCasByProjectId(currentProject?.id || "");
const { data: templateData } = useListCertificateTemplatesV2({
projectId: currentProject?.id || "",
limit: 100,
@@ -351,9 +363,23 @@ export const CreateProfileModal = ({
const isEdit = mode === "edit" && profile;
const certificateAuthorities = caData || [];
const certificateAuthorities = (allCaData || []).map((ca) => ({
...ca,
groupType: ca.type === "internal" ? "internal" : "external"
}));
const certificateTemplates = templateData?.certificateTemplates || [];
const getGroupHeaderLabel = (groupType: "internal" | "external") => {
switch (groupType) {
case "internal":
return "Internal CAs";
case "external":
return "External CAs";
default:
return "";
}
};
const { control, handleSubmit, reset, watch, setValue, formState } = useForm<FormData>({
resolver: zodResolver(isEdit ? editSchema : createSchema),
defaultValues: isEdit
@@ -380,7 +406,17 @@ export const CreateProfileModal = ({
renewBeforeDays: profile.apiConfig?.renewBeforeDays || 30
}
: undefined,
acmeConfig: profile.enrollmentType === EnrollmentType.ACME ? {} : undefined
acmeConfig: profile.enrollmentType === EnrollmentType.ACME ? {} : undefined,
externalConfigs: profile.externalConfigs
? {
template:
typeof profile.externalConfigs === "object" &&
profile.externalConfigs !== null &&
typeof profile.externalConfigs.template === "string"
? profile.externalConfigs.template
: ""
}
: undefined
}
: {
slug: "",
@@ -393,15 +429,27 @@ export const CreateProfileModal = ({
autoRenew: false,
renewBeforeDays: 30
},
acmeConfig: {}
acmeConfig: {},
externalConfigs: undefined
}
});
const watchedEnrollmentType = watch("enrollmentType");
const watchedIssuerType = watch("issuerType");
const watchedCertificateAuthorityId = watch("certificateAuthorityId");
const watchedDisableBootstrapValidation = watch("estConfig.disableBootstrapCaValidation");
const watchedAutoRenew = watch("apiConfig.autoRenew");
// Get the selected CA to check if it's Azure ADCS
const selectedCa = certificateAuthorities.find((ca) => ca.id === watchedCertificateAuthorityId);
const isAzureAdcsCa = selectedCa?.type === CaType.AZURE_AD_CS;
// Fetch Azure ADCS templates if needed
const { data: azureAdcsTemplatesData } = useGetAzureAdcsTemplates({
caId: watchedCertificateAuthorityId || "",
projectId: currentProject?.id || ""
});
useEffect(() => {
if (isEdit && profile) {
reset({
@@ -427,10 +475,38 @@ export const CreateProfileModal = ({
renewBeforeDays: profile.apiConfig?.renewBeforeDays || 30
}
: undefined,
acmeConfig: profile.enrollmentType === EnrollmentType.ACME ? {} : undefined
acmeConfig: profile.enrollmentType === EnrollmentType.ACME ? {} : undefined,
externalConfigs: profile.externalConfigs
? {
template:
typeof profile.externalConfigs === "object" &&
profile.externalConfigs !== null &&
typeof profile.externalConfigs.template === "string"
? profile.externalConfigs.template
: ""
}
: undefined
});
}
}, [isEdit, profile, reset]);
}, [isEdit, profile, reset, allCaData]);
// Additional effect to reset external configs when Azure ADCS templates are loaded
useEffect(() => {
if (
isEdit &&
profile &&
isAzureAdcsCa &&
azureAdcsTemplatesData?.templates &&
profile.externalConfigs &&
typeof profile.externalConfigs === "object" &&
profile.externalConfigs !== null &&
typeof profile.externalConfigs.template === "string"
) {
// Re-set the external configs to ensure the template value is properly set
// after the Azure ADCS templates have been loaded
setValue("externalConfigs.template", profile.externalConfigs.template);
}
}, [isEdit, profile, isAzureAdcsCa, azureAdcsTemplatesData, setValue]);
const onFormSubmit = async (data: FormData) => {
if (!isEdit && !subscription?.pkiAcme && data.enrollmentType === EnrollmentType.ACME) {
@@ -444,6 +520,18 @@ export const CreateProfileModal = ({
if (!currentProject?.id && !isEdit) return;
// Validate Azure ADCS template requirement
if (
isAzureAdcsCa &&
(!data.externalConfigs?.template || data.externalConfigs.template.trim() === "")
) {
createNotification({
text: "Azure ADCS Certificate Authority requires a template to be specified",
type: "error"
});
return;
}
if (isEdit) {
const updateData: TUpdateCertificateProfileDTO = {
profileId: profile.id,
@@ -460,6 +548,11 @@ export const CreateProfileModal = ({
updateData.acmeConfig = data.acmeConfig;
}
// Add external configs if present
if (data.externalConfigs) {
updateData.externalConfigs = data.externalConfigs;
}
await updateProfile.mutateAsync(updateData);
} else {
if (!currentProject?.id) {
@@ -491,6 +584,11 @@ export const CreateProfileModal = ({
createData.acmeConfig = data.acmeConfig;
}
// Add external configs if present
if (data.externalConfigs) {
createData.externalConfigs = data.externalConfigs;
}
await createProfile.mutateAsync(createData);
}
@@ -587,30 +685,82 @@ export const CreateProfileModal = ({
<Controller
control={control}
name="certificateAuthorityId"
render={({ field: { onChange, value, ...field }, fieldState: { error } }) => (
render={({ field: { onChange, value }, fieldState: { error } }) => (
<FormControl
label="Issuing CA"
isRequired
isError={Boolean(error)}
errorText={error?.message}
>
<Select
{...field}
value={value || undefined}
onValueChange={onChange}
<FilterableSelect
value={certificateAuthorities.find((ca) => ca.id === value) || null}
onChange={(selectedCaValue) => {
if (Array.isArray(selectedCaValue)) {
onChange(selectedCaValue[0]?.id || "");
} else if (
selectedCaValue &&
typeof selectedCaValue === "object" &&
"id" in selectedCaValue
) {
onChange(selectedCaValue.id || "");
} else {
onChange("");
}
}}
getOptionLabel={(ca) =>
ca.type === "internal" && ca.configuration.friendlyName
? ca.configuration.friendlyName
: ca.name
}
getOptionValue={(ca) => ca.id}
options={certificateAuthorities}
groupBy="groupType"
getGroupHeaderLabel={getGroupHeaderLabel}
placeholder="Select a certificate authority"
className="w-full"
position="popper"
isDisabled={Boolean(isEdit)}
>
{certificateAuthorities.map((ca) => (
<SelectItem key={ca.id} value={ca.id}>
{ca.type === "internal" && ca.configuration.friendlyName
? ca.configuration.friendlyName
: ca.name}
</SelectItem>
))}
</Select>
className="w-full"
/>
</FormControl>
)}
/>
)}
{/* Azure ADCS Template Selection */}
{isAzureAdcsCa && (
<Controller
control={control}
name="externalConfigs.template"
render={({ field: { onChange, value }, fieldState: { error } }) => (
<FormControl
label="Azure ADCS Template"
isRequired
isError={Boolean(error)}
errorText={error?.message}
>
<FilterableSelect
value={
azureAdcsTemplatesData?.templates.find((template) => template.id === value) ||
null
}
onChange={(selectedTemplate) => {
if (Array.isArray(selectedTemplate)) {
onChange(selectedTemplate[0]?.id || "");
} else if (
selectedTemplate &&
typeof selectedTemplate === "object" &&
"id" in selectedTemplate
) {
onChange(selectedTemplate.id || "");
} else {
onChange("");
}
}}
getOptionLabel={(template) => template.name}
getOptionValue={(template) => template.id}
options={azureAdcsTemplatesData?.templates || []}
placeholder="Select an Azure ADCS certificate template"
className="w-full"
/>
</FormControl>
)}
/>
@@ -1,3 +1,4 @@
/* eslint-disable no-nested-ternary */
import { useCallback } from "react";
import {
faCheck,
@@ -49,7 +50,9 @@ export const ProfileRow = ({
}: Props) => {
const { permission } = useProjectPermission();
const { data: caData } = useGetCaById(profile.caId ?? "");
const { data: caData } = useGetCaById(
profile.certificateAuthority?.isExternal ? "" : (profile.caId ?? "")
);
const { popUp, handlePopUpToggle } = usePopUp(["issueCertificate"] as const);
@@ -120,11 +123,18 @@ export const ProfileRow = ({
</Td>
<Td className="text-start">{getEnrollmentTypeBadge(profile.enrollmentType)}</Td>
<Td className="text-start">
<span className="text-sm text-mineshaft-300">
{profile.issuerType === IssuerType.SELF_SIGNED
? "Self-signed"
: caData?.friendlyName || caData?.commonName || profile.caId}
</span>
<div className="flex items-center gap-2">
<span className="text-sm text-mineshaft-300">
{profile.issuerType === IssuerType.SELF_SIGNED
? "Self-signed"
: profile.certificateAuthority?.isExternal
? profile.certificateAuthority.name
: caData?.friendlyName ||
caData?.commonName ||
profile.certificateAuthority?.name ||
profile.caId}
</span>
</div>
</Td>
<Td>
<span className="text-sm text-mineshaft-300">
@@ -177,7 +187,7 @@ export const ProfileRow = ({
}}
icon={<FontAwesomeIcon icon={faPlus} className="w-3" />}
>
Issue Certificate
Request Certificate
</DropdownMenuItem>
)}
{canDeleteProfile && (