mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-11 06:29:04 +00:00
PKI revamp EST improvements and fixes
This commit is contained in:
@@ -48,8 +48,8 @@ export type TCreateCertificateProfileDTO = {
|
||||
enrollmentType: "api" | "est";
|
||||
estConfig?: {
|
||||
disableBootstrapCaValidation?: boolean;
|
||||
passphrase: string;
|
||||
caChain: string;
|
||||
passphraseInput: string;
|
||||
caChain?: string;
|
||||
};
|
||||
apiConfig?: {
|
||||
autoRenew?: boolean;
|
||||
|
||||
@@ -7,17 +7,17 @@ import { projectKeys } from "../projects";
|
||||
import { certTemplateKeys } from "./queries";
|
||||
import {
|
||||
TCertificateTemplate,
|
||||
TCertificateTemplateV2New,
|
||||
TCertificateTemplateV2WithPolicies,
|
||||
TCreateCertificateTemplateDTO,
|
||||
TCreateCertificateTemplateV2DTO,
|
||||
TCreateCertificateTemplateV2NewDTO,
|
||||
TCreateCertificateTemplateV2WithPoliciesDTO,
|
||||
TCreateEstConfigDTO,
|
||||
TDeleteCertificateTemplateDTO,
|
||||
TDeleteCertificateTemplateV2DTO,
|
||||
TDeleteCertificateTemplateV2NewDTO,
|
||||
TDeleteCertificateTemplateV2WithPoliciesDTO,
|
||||
TUpdateCertificateTemplateDTO,
|
||||
TUpdateCertificateTemplateV2DTO,
|
||||
TUpdateCertificateTemplateV2NewDTO,
|
||||
TUpdateCertificateTemplateV2WithPoliciesDTO,
|
||||
TUpdateEstConfigDTO
|
||||
} from "./types";
|
||||
|
||||
@@ -168,12 +168,16 @@ export const useUpdateEstConfig = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useCreateCertificateTemplateV2New = () => {
|
||||
export const useCreateCertificateTemplateV2WithPolicies = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TCertificateTemplateV2New, object, TCreateCertificateTemplateV2NewDTO>({
|
||||
return useMutation<
|
||||
TCertificateTemplateV2WithPolicies,
|
||||
object,
|
||||
TCreateCertificateTemplateV2WithPoliciesDTO
|
||||
>({
|
||||
mutationFn: async (data) => {
|
||||
const { data: response } = await apiRequest.post<{
|
||||
certificateTemplate: TCertificateTemplateV2New;
|
||||
certificateTemplate: TCertificateTemplateV2WithPolicies;
|
||||
}>("/api/v2/certificate-templates", data);
|
||||
return response.certificateTemplate;
|
||||
},
|
||||
@@ -185,12 +189,16 @@ export const useCreateCertificateTemplateV2New = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateCertificateTemplateV2New = () => {
|
||||
export const useUpdateCertificateTemplateV2WithPolicies = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TCertificateTemplateV2New, object, TUpdateCertificateTemplateV2NewDTO>({
|
||||
return useMutation<
|
||||
TCertificateTemplateV2WithPolicies,
|
||||
object,
|
||||
TUpdateCertificateTemplateV2WithPoliciesDTO
|
||||
>({
|
||||
mutationFn: async ({ templateId, ...data }) => {
|
||||
const { data: response } = await apiRequest.patch<{
|
||||
certificateTemplate: TCertificateTemplateV2New;
|
||||
certificateTemplate: TCertificateTemplateV2WithPolicies;
|
||||
}>(`/api/v2/certificate-templates/${templateId}`, data);
|
||||
return response.certificateTemplate;
|
||||
},
|
||||
@@ -205,12 +213,16 @@ export const useUpdateCertificateTemplateV2New = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteCertificateTemplateV2New = () => {
|
||||
export const useDeleteCertificateTemplateV2WithPolicies = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TCertificateTemplateV2New, object, TDeleteCertificateTemplateV2NewDTO>({
|
||||
return useMutation<
|
||||
TCertificateTemplateV2WithPolicies,
|
||||
object,
|
||||
TDeleteCertificateTemplateV2WithPoliciesDTO
|
||||
>({
|
||||
mutationFn: async ({ templateId }) => {
|
||||
const { data: response } = await apiRequest.delete<{
|
||||
certificateTemplate: TCertificateTemplateV2New;
|
||||
certificateTemplate: TCertificateTemplateV2WithPolicies;
|
||||
}>(`/api/v2/certificate-templates/${templateId}`);
|
||||
return response.certificateTemplate;
|
||||
},
|
||||
|
||||
@@ -5,7 +5,7 @@ import { apiRequest } from "@app/config/request";
|
||||
import {
|
||||
TCertificateTemplate,
|
||||
TCertificateTemplateV2,
|
||||
TCertificateTemplateV2New,
|
||||
TCertificateTemplateV2WithPolicies,
|
||||
TEstConfig,
|
||||
TGetCertificateTemplateV2ByIdDTO,
|
||||
TListCertificateTemplatesDTO,
|
||||
@@ -90,7 +90,7 @@ export const useListCertificateTemplatesV2 = ({
|
||||
queryKey: certTemplateKeys.listTemplatesV2({ projectId, limit, offset }),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{
|
||||
certificateTemplates: TCertificateTemplateV2New[];
|
||||
certificateTemplates: TCertificateTemplateV2WithPolicies[];
|
||||
totalCount: number;
|
||||
}>("/api/v2/certificate-templates", {
|
||||
params: {
|
||||
@@ -112,7 +112,7 @@ export const useGetCertificateTemplateV2ById = ({
|
||||
queryKey: certTemplateKeys.getTemplateV2ById(templateId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{
|
||||
certificateTemplate: TCertificateTemplateV2New;
|
||||
certificateTemplate: TCertificateTemplateV2WithPolicies;
|
||||
}>(`/api/v2/certificate-templates/${templateId}`);
|
||||
return data.certificateTemplate;
|
||||
},
|
||||
|
||||
@@ -156,7 +156,7 @@ export type TCertificateTemplateV2Policy = {
|
||||
};
|
||||
};
|
||||
|
||||
export type TCertificateTemplateV2New = {
|
||||
export type TCertificateTemplateV2WithPolicies = {
|
||||
id: string;
|
||||
projectId: string;
|
||||
name: string;
|
||||
@@ -171,7 +171,7 @@ export type TCertificateTemplateV2New = {
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type TCreateCertificateTemplateV2NewDTO = {
|
||||
export type TCreateCertificateTemplateV2WithPoliciesDTO = {
|
||||
projectId: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
@@ -183,7 +183,7 @@ export type TCreateCertificateTemplateV2NewDTO = {
|
||||
validity?: TCertificateTemplateV2Policy["validity"];
|
||||
};
|
||||
|
||||
export type TUpdateCertificateTemplateV2NewDTO = {
|
||||
export type TUpdateCertificateTemplateV2WithPoliciesDTO = {
|
||||
templateId: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
@@ -195,7 +195,7 @@ export type TUpdateCertificateTemplateV2NewDTO = {
|
||||
validity?: TCertificateTemplateV2Policy["validity"];
|
||||
};
|
||||
|
||||
export type TDeleteCertificateTemplateV2NewDTO = {
|
||||
export type TDeleteCertificateTemplateV2WithPoliciesDTO = {
|
||||
templateId: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { Control, Controller } from "react-hook-form";
|
||||
|
||||
import { FormControl, Select, SelectItem } from "@app/components/v2";
|
||||
|
||||
type AlgorithmOption = {
|
||||
value: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
type AlgorithmSelectorsProps = {
|
||||
control: Control<any>;
|
||||
availableSignatureAlgorithms: AlgorithmOption[];
|
||||
availableKeyAlgorithms: AlgorithmOption[];
|
||||
signatureError?: string;
|
||||
keyError?: string;
|
||||
};
|
||||
|
||||
export const AlgorithmSelectors = ({
|
||||
control,
|
||||
availableSignatureAlgorithms,
|
||||
availableKeyAlgorithms,
|
||||
signatureError,
|
||||
keyError
|
||||
}: AlgorithmSelectorsProps) => {
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="signatureAlgorithm"
|
||||
render={({ field: { onChange, ...field } }) => (
|
||||
<FormControl
|
||||
label="Signature Algorithm"
|
||||
errorText={signatureError}
|
||||
isError={Boolean(signatureError)}
|
||||
>
|
||||
<Select
|
||||
defaultValue=""
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
placeholder={
|
||||
availableSignatureAlgorithms.length > 0
|
||||
? "Select signature algorithm"
|
||||
: "No algorithms available"
|
||||
}
|
||||
position="popper"
|
||||
>
|
||||
{availableSignatureAlgorithms.map((algorithm) => (
|
||||
<SelectItem key={algorithm.value} value={algorithm.value}>
|
||||
{algorithm.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="keyAlgorithm"
|
||||
render={({ field: { onChange, ...field } }) => (
|
||||
<FormControl label="Key Algorithm" errorText={keyError} isError={Boolean(keyError)}>
|
||||
<Select
|
||||
defaultValue=""
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
placeholder={
|
||||
availableKeyAlgorithms.length > 0
|
||||
? "Select key algorithm"
|
||||
: "No algorithms available"
|
||||
}
|
||||
position="popper"
|
||||
>
|
||||
{availableKeyAlgorithms.map((algorithm) => (
|
||||
<SelectItem key={algorithm.value} value={algorithm.value}>
|
||||
{algorithm.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+140
-550
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { faPlus, faQuestionCircle, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
@@ -8,14 +8,9 @@ import { z } from "zod";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
Button,
|
||||
Checkbox,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
IconButton,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
@@ -24,21 +19,24 @@ import {
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { useProject } from "@app/context";
|
||||
import { useCreateCertificateV3, useGetCert } from "@app/hooks/api";
|
||||
import { useGetCert } from "@app/hooks/api";
|
||||
import { useCreateCertificateV3 } from "@app/hooks/api/ca";
|
||||
import { useListCertificateProfiles } from "@app/hooks/api/certificateProfiles";
|
||||
import {
|
||||
EXTENDED_KEY_USAGES_OPTIONS,
|
||||
KEY_USAGES_OPTIONS
|
||||
} from "@app/hooks/api/certificates/constants";
|
||||
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 { AlgorithmSelectors } from "./AlgorithmSelectors";
|
||||
import { CertificateContent } from "./CertificateContent";
|
||||
import {
|
||||
filterUsages,
|
||||
formatSubjectAltNames,
|
||||
FrontendSanType,
|
||||
getAttributeValue
|
||||
} from "./certificateUtils";
|
||||
import { KeyUsageSection } from "./KeyUsageSection";
|
||||
import { SubjectAltNamesField } from "./SubjectAltNamesField";
|
||||
import { useCertificateTemplate } from "./useCertificateTemplate";
|
||||
|
||||
const createSchema = (shouldShowSubjectSection: boolean) => {
|
||||
return z.object({
|
||||
@@ -63,7 +61,7 @@ const createSchema = (shouldShowSubjectSection: boolean) => {
|
||||
subjectAltNames: z
|
||||
.array(
|
||||
z.object({
|
||||
type: z.enum(["dns", "ip", "email", "uri"]),
|
||||
type: z.nativeEnum(FrontendSanType),
|
||||
value: z.string().min(1, "Value is required")
|
||||
})
|
||||
)
|
||||
@@ -71,25 +69,29 @@ const createSchema = (shouldShowSubjectSection: boolean) => {
|
||||
ttl: z.string().trim().min(1, "TTL is required"),
|
||||
signatureAlgorithm: z.string().min(1, "Signature algorithm is required"),
|
||||
keyAlgorithm: z.string().min(1, "Key algorithm is required"),
|
||||
keyUsages: z.object({
|
||||
[CertKeyUsage.DIGITAL_SIGNATURE]: z.boolean().optional(),
|
||||
[CertKeyUsage.KEY_ENCIPHERMENT]: z.boolean().optional(),
|
||||
[CertKeyUsage.NON_REPUDIATION]: z.boolean().optional(),
|
||||
[CertKeyUsage.DATA_ENCIPHERMENT]: z.boolean().optional(),
|
||||
[CertKeyUsage.KEY_AGREEMENT]: z.boolean().optional(),
|
||||
[CertKeyUsage.KEY_CERT_SIGN]: z.boolean().optional(),
|
||||
[CertKeyUsage.CRL_SIGN]: z.boolean().optional(),
|
||||
[CertKeyUsage.ENCIPHER_ONLY]: z.boolean().optional(),
|
||||
[CertKeyUsage.DECIPHER_ONLY]: z.boolean().optional()
|
||||
}),
|
||||
extendedKeyUsages: z.object({
|
||||
[CertExtendedKeyUsage.CLIENT_AUTH]: z.boolean().optional(),
|
||||
[CertExtendedKeyUsage.CODE_SIGNING]: z.boolean().optional(),
|
||||
[CertExtendedKeyUsage.EMAIL_PROTECTION]: z.boolean().optional(),
|
||||
[CertExtendedKeyUsage.OCSP_SIGNING]: z.boolean().optional(),
|
||||
[CertExtendedKeyUsage.SERVER_AUTH]: z.boolean().optional(),
|
||||
[CertExtendedKeyUsage.TIMESTAMPING]: z.boolean().optional()
|
||||
})
|
||||
keyUsages: z
|
||||
.object({
|
||||
[CertKeyUsage.DIGITAL_SIGNATURE]: z.boolean().optional(),
|
||||
[CertKeyUsage.KEY_ENCIPHERMENT]: z.boolean().optional(),
|
||||
[CertKeyUsage.NON_REPUDIATION]: z.boolean().optional(),
|
||||
[CertKeyUsage.DATA_ENCIPHERMENT]: z.boolean().optional(),
|
||||
[CertKeyUsage.KEY_AGREEMENT]: z.boolean().optional(),
|
||||
[CertKeyUsage.KEY_CERT_SIGN]: z.boolean().optional(),
|
||||
[CertKeyUsage.CRL_SIGN]: z.boolean().optional(),
|
||||
[CertKeyUsage.ENCIPHER_ONLY]: z.boolean().optional(),
|
||||
[CertKeyUsage.DECIPHER_ONLY]: z.boolean().optional()
|
||||
})
|
||||
.default({}),
|
||||
extendedKeyUsages: z
|
||||
.object({
|
||||
[CertExtendedKeyUsage.CLIENT_AUTH]: z.boolean().optional(),
|
||||
[CertExtendedKeyUsage.CODE_SIGNING]: z.boolean().optional(),
|
||||
[CertExtendedKeyUsage.EMAIL_PROTECTION]: z.boolean().optional(),
|
||||
[CertExtendedKeyUsage.OCSP_SIGNING]: z.boolean().optional(),
|
||||
[CertExtendedKeyUsage.SERVER_AUTH]: z.boolean().optional(),
|
||||
[CertExtendedKeyUsage.TIMESTAMPING]: z.boolean().optional()
|
||||
})
|
||||
.default({})
|
||||
});
|
||||
};
|
||||
|
||||
@@ -113,15 +115,6 @@ type TCertificateDetails = {
|
||||
|
||||
export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }: Props) => {
|
||||
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 [allowedSanTypes, setAllowedSanTypes] = useState<string[]>(["dns", "ip", "email", "uri"]);
|
||||
const [shouldShowSanSection, setShouldShowSanSection] = useState<boolean>(true);
|
||||
const [shouldShowSubjectSection, setShouldShowSubjectSection] = useState<boolean>(true);
|
||||
const { currentProject } = useProject();
|
||||
|
||||
const inputSerialNumber =
|
||||
@@ -137,44 +130,11 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
||||
|
||||
const { mutateAsync: createCertificate } = useCreateCertificateV3();
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
watch,
|
||||
setValue,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(createSchema(shouldShowSubjectSection)),
|
||||
defaultValues: {
|
||||
profileId: profileId || "",
|
||||
subjectAttributes: shouldShowSubjectSection
|
||||
? [{ type: "common_name", value: "" }]
|
||||
: undefined,
|
||||
subjectAltNames: [],
|
||||
ttl: "30d",
|
||||
signatureAlgorithm: "",
|
||||
keyAlgorithm: "",
|
||||
keyUsages: {},
|
||||
extendedKeyUsages: {}
|
||||
}
|
||||
});
|
||||
const selectedProfileId = useMemo(() => {
|
||||
const form = document.querySelector('select[name="profileId"]') as HTMLSelectElement;
|
||||
return form?.value || profileId || "";
|
||||
}, [profileId]);
|
||||
|
||||
const resetAllState = useCallback(() => {
|
||||
setCertificateDetails(null);
|
||||
setAllowedKeyUsages([]);
|
||||
setAllowedExtendedKeyUsages([]);
|
||||
setRequiredKeyUsages([]);
|
||||
setRequiredExtendedKeyUsages([]);
|
||||
setAllowedSignatureAlgorithms([]);
|
||||
setAllowedKeyAlgorithms([]);
|
||||
setAllowedSanTypes(["dns", "ip", "email", "uri"]);
|
||||
setShouldShowSanSection(true);
|
||||
setShouldShowSubjectSection(true);
|
||||
reset();
|
||||
}, [reset]);
|
||||
|
||||
const selectedProfileId = watch("profileId");
|
||||
const selectedProfile = useMemo(
|
||||
() => profilesData?.certificateProfiles?.find((p) => p.id === selectedProfileId),
|
||||
[profilesData?.certificateProfiles, selectedProfileId]
|
||||
@@ -184,135 +144,54 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
||||
templateId: selectedProfile?.certificateTemplateId || ""
|
||||
});
|
||||
|
||||
const filteredKeyUsages = useMemo(() => {
|
||||
return KEY_USAGES_OPTIONS.filter(({ value }) => allowedKeyUsages.includes(value));
|
||||
}, [allowedKeyUsages]);
|
||||
|
||||
const filteredExtendedKeyUsages = useMemo(() => {
|
||||
return EXTENDED_KEY_USAGES_OPTIONS.filter(({ value }) =>
|
||||
allowedExtendedKeyUsages.includes(value)
|
||||
);
|
||||
}, [allowedExtendedKeyUsages]);
|
||||
|
||||
const mapBackendSanTypeToFrontend = (backendType: string): string => {
|
||||
switch (backendType) {
|
||||
case "dns_name":
|
||||
return "dns";
|
||||
case "ip_address":
|
||||
return "ip";
|
||||
case "email":
|
||||
return "email";
|
||||
case "uri":
|
||||
return "uri";
|
||||
default:
|
||||
return backendType;
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
watch,
|
||||
setValue,
|
||||
formState,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(createSchema((templateData?.subject?.length || 0) > 0)),
|
||||
defaultValues: {
|
||||
profileId: profileId || "",
|
||||
subjectAttributes: [],
|
||||
subjectAltNames: [],
|
||||
ttl: "30d",
|
||||
signatureAlgorithm: "",
|
||||
keyAlgorithm: "",
|
||||
keyUsages: {},
|
||||
extendedKeyUsages: {}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const availableSignatureAlgorithms = useMemo(() => {
|
||||
return allowedSignatureAlgorithms.map((templateAlgorithm) => {
|
||||
const apiAlgorithm = mapTemplateSignatureAlgorithmToApi(templateAlgorithm);
|
||||
return {
|
||||
value: apiAlgorithm,
|
||||
label: apiAlgorithm
|
||||
};
|
||||
});
|
||||
}, [allowedSignatureAlgorithms]);
|
||||
const actualSelectedProfileId = watch("profileId");
|
||||
const actualSelectedProfile = useMemo(
|
||||
() => profilesData?.certificateProfiles?.find((p) => p.id === actualSelectedProfileId),
|
||||
[profilesData?.certificateProfiles, actualSelectedProfileId]
|
||||
);
|
||||
|
||||
const availableKeyAlgorithms = useMemo(() => {
|
||||
return allowedKeyAlgorithms.map((templateAlgorithm) => {
|
||||
const apiAlgorithm = mapTemplateKeyAlgorithmToApi(templateAlgorithm);
|
||||
return {
|
||||
value: apiAlgorithm,
|
||||
label: apiAlgorithm
|
||||
};
|
||||
});
|
||||
}, [allowedKeyAlgorithms]);
|
||||
const {
|
||||
constraints,
|
||||
filteredKeyUsages,
|
||||
filteredExtendedKeyUsages,
|
||||
availableSignatureAlgorithms,
|
||||
availableKeyAlgorithms,
|
||||
resetConstraints
|
||||
} = useCertificateTemplate(
|
||||
templateData,
|
||||
actualSelectedProfile,
|
||||
popUp?.certificateIssuance?.isOpen || false,
|
||||
setValue,
|
||||
watch
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (templateData && selectedProfile && popUp?.certificateIssuance?.isOpen) {
|
||||
if (templateData.algorithms?.signature && templateData.algorithms.signature.length > 0) {
|
||||
setAllowedSignatureAlgorithms(templateData.algorithms.signature);
|
||||
} else {
|
||||
setAllowedSignatureAlgorithms([]);
|
||||
}
|
||||
|
||||
if (
|
||||
templateData.algorithms?.keyAlgorithm &&
|
||||
templateData.algorithms.keyAlgorithm.length > 0
|
||||
) {
|
||||
setAllowedKeyAlgorithms(templateData.algorithms.keyAlgorithm);
|
||||
} else {
|
||||
setAllowedKeyAlgorithms([]);
|
||||
}
|
||||
|
||||
if (templateData.validity?.max) {
|
||||
setValue("ttl", templateData.validity.max);
|
||||
}
|
||||
|
||||
const keyUsages: string[] = [];
|
||||
if (templateData.keyUsages?.required) {
|
||||
keyUsages.push(...templateData.keyUsages.required);
|
||||
}
|
||||
if (templateData.keyUsages?.allowed) {
|
||||
keyUsages.push(...templateData.keyUsages.allowed);
|
||||
}
|
||||
setAllowedKeyUsages(keyUsages);
|
||||
|
||||
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 || []);
|
||||
|
||||
if (templateData.sans && templateData.sans.length > 0) {
|
||||
const sanTypes: string[] = [];
|
||||
templateData.sans.forEach((sanPolicy) => {
|
||||
const frontendType = mapBackendSanTypeToFrontend(sanPolicy.type);
|
||||
if (!sanTypes.includes(frontendType)) {
|
||||
sanTypes.push(frontendType);
|
||||
}
|
||||
});
|
||||
setAllowedSanTypes(sanTypes);
|
||||
setShouldShowSanSection(true);
|
||||
} else {
|
||||
setAllowedSanTypes([]);
|
||||
setShouldShowSanSection(false);
|
||||
setValue("subjectAltNames", []);
|
||||
}
|
||||
|
||||
if (templateData.subject && templateData.subject.length > 0) {
|
||||
setShouldShowSubjectSection(true);
|
||||
const currentSubjectAttrs = watch("subjectAttributes");
|
||||
if (!currentSubjectAttrs || currentSubjectAttrs.length === 0) {
|
||||
setValue("subjectAttributes", [{ type: "common_name", value: "" }]);
|
||||
}
|
||||
} else {
|
||||
setShouldShowSubjectSection(false);
|
||||
setValue("subjectAttributes", undefined);
|
||||
}
|
||||
|
||||
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, watch, popUp?.certificateIssuance?.isOpen]);
|
||||
const resetAllState = useCallback(() => {
|
||||
setCertificateDetails(null);
|
||||
resetConstraints();
|
||||
reset();
|
||||
}, [reset, resetConstraints]);
|
||||
|
||||
useEffect(() => {
|
||||
if (cert) {
|
||||
@@ -327,14 +206,16 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
||||
subjectAltNames: cert.subjectAltNames
|
||||
? cert.subjectAltNames.split(",").map((name) => {
|
||||
const trimmed = name.trim();
|
||||
if (trimmed.includes("@")) return { type: "email" as const, value: trimmed };
|
||||
if (trimmed.includes("@")) return { type: FrontendSanType.EMAIL, value: trimmed };
|
||||
if (trimmed.match(/^\d+\.\d+\.\d+\.\d+$/))
|
||||
return { type: "ip" as const, value: trimmed };
|
||||
if (trimmed.startsWith("http")) return { type: "uri" as const, value: trimmed };
|
||||
return { type: "dns" as const, value: trimmed };
|
||||
return { type: FrontendSanType.IP, value: trimmed };
|
||||
if (trimmed.startsWith("http")) return { type: FrontendSanType.URI, value: trimmed };
|
||||
return { type: FrontendSanType.DNS, value: trimmed };
|
||||
})
|
||||
: [],
|
||||
ttl: "",
|
||||
signatureAlgorithm: "",
|
||||
keyAlgorithm: "",
|
||||
keyUsages: Object.fromEntries((cert.keyUsages || []).map((name) => [name, true])),
|
||||
extendedKeyUsages: Object.fromEntries(
|
||||
(cert.extendedKeyUsages || []).map((name) => [name, true])
|
||||
@@ -349,27 +230,6 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
||||
}
|
||||
}, [popUp?.certificateIssuance?.isOpen, profileId, cert, setValue]);
|
||||
|
||||
const getAttributeValue = useCallback(
|
||||
(subjectAttributes: FormData["subjectAttributes"], type: string) => {
|
||||
const foundAttr = subjectAttributes?.find((attr) => attr.type === type);
|
||||
return foundAttr?.value || "";
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const formatSubjectAltNames = useCallback((subjectAltNames: FormData["subjectAltNames"]) => {
|
||||
return subjectAltNames
|
||||
.filter((san) => san.value.trim())
|
||||
.map((san) => san.value.trim())
|
||||
.join(", ");
|
||||
}, []);
|
||||
|
||||
const filterUsages = useCallback(<T extends Record<string, boolean>>(usages: T) => {
|
||||
return Object.entries(usages)
|
||||
.filter(([, value]) => value)
|
||||
.map(([key]) => key);
|
||||
}, []);
|
||||
|
||||
const onFormSubmit = useCallback(
|
||||
async ({
|
||||
profileId: formProfileId,
|
||||
@@ -399,7 +259,11 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
||||
}
|
||||
|
||||
let commonName = "";
|
||||
if (shouldShowSubjectSection && subjectAttributes && subjectAttributes.length > 0) {
|
||||
if (
|
||||
constraints.shouldShowSubjectSection &&
|
||||
subjectAttributes &&
|
||||
subjectAttributes.length > 0
|
||||
) {
|
||||
commonName = getAttributeValue(subjectAttributes, "common_name");
|
||||
if (!commonName.trim()) {
|
||||
createNotification({
|
||||
@@ -420,13 +284,13 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
||||
extendedKeyUsages: filterUsages(extendedKeyUsages) as CertExtendedKeyUsage[]
|
||||
};
|
||||
|
||||
if (shouldShowSubjectSection && commonName) {
|
||||
if (constraints.shouldShowSubjectSection && commonName) {
|
||||
certificateRequest.commonName = commonName;
|
||||
}
|
||||
if (shouldShowSanSection && subjectAltNames && subjectAltNames.length > 0) {
|
||||
if (constraints.shouldShowSanSection && subjectAltNames && subjectAltNames.length > 0) {
|
||||
const formattedSans = formatSubjectAltNames(subjectAltNames);
|
||||
if (formattedSans) {
|
||||
certificateRequest.subjectAltNames = formattedSans;
|
||||
if (formattedSans && formattedSans.length > 0) {
|
||||
certificateRequest.altNames = formattedSans;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -459,11 +323,8 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
||||
[
|
||||
currentProject?.slug,
|
||||
createCertificate,
|
||||
shouldShowSubjectSection,
|
||||
shouldShowSanSection,
|
||||
getAttributeValue,
|
||||
formatSubjectAltNames,
|
||||
filterUsages
|
||||
constraints.shouldShowSubjectSection,
|
||||
constraints.shouldShowSanSection
|
||||
]
|
||||
);
|
||||
|
||||
@@ -479,21 +340,6 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
||||
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";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp?.certificateIssuance?.isOpen}
|
||||
@@ -576,165 +422,36 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
||||
/>
|
||||
)}
|
||||
|
||||
{(selectedProfile || profileId) && (
|
||||
{(actualSelectedProfile || profileId) && (
|
||||
<>
|
||||
{shouldShowSubjectSection && (
|
||||
{constraints.shouldShowSubjectSection && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="subjectAttributes"
|
||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Subject Attributes"
|
||||
label="Common Name"
|
||||
isRequired
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
{(value || []).map((attr, index) => (
|
||||
// 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) => {
|
||||
const newValue = [...(value || [])];
|
||||
newValue[index] = {
|
||||
...attr,
|
||||
type: newType as typeof attr.type
|
||||
};
|
||||
onChange(newValue);
|
||||
}}
|
||||
className="w-48"
|
||||
>
|
||||
<SelectItem value="common_name">Common Name</SelectItem>
|
||||
</Select>
|
||||
<Input
|
||||
value={attr.value}
|
||||
onChange={(e) => {
|
||||
const newValue = [...(value || [])];
|
||||
newValue[index] = { ...attr, value: e.target.value };
|
||||
onChange(newValue);
|
||||
}}
|
||||
placeholder="example.com"
|
||||
className="flex-1"
|
||||
/>
|
||||
{(value || []).length > 1 && (
|
||||
<IconButton
|
||||
ariaLabel="Remove Subject Attribute"
|
||||
variant="plain"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const newValue = (value || []).filter((_, i) => i !== index);
|
||||
onChange(newValue);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline_bg"
|
||||
size="xs"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => {
|
||||
onChange([...(value || []), { type: "common_name", value: "" }]);
|
||||
}}
|
||||
className="w-full"
|
||||
>
|
||||
Add Subject Attribute
|
||||
</Button>
|
||||
</div>
|
||||
<Input
|
||||
value={value?.[0]?.value || ""}
|
||||
onChange={(e) => {
|
||||
onChange([{ type: "common_name", value: e.target.value }]);
|
||||
}}
|
||||
placeholder="example.com"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{shouldShowSanSection && (
|
||||
<Controller
|
||||
{constraints.shouldShowSanSection && (
|
||||
<SubjectAltNamesField
|
||||
control={control}
|
||||
name="subjectAltNames"
|
||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Subject Alternative Names (SANs)"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
{value.map((san, index) => (
|
||||
<div
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={`subject-alt-name-${index}`}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Select
|
||||
value={san.type}
|
||||
onValueChange={(newType) => {
|
||||
const newValue = [...value];
|
||||
newValue[index] = {
|
||||
...san,
|
||||
type: newType as "dns" | "ip" | "email" | "uri"
|
||||
};
|
||||
onChange(newValue);
|
||||
}}
|
||||
className="w-24"
|
||||
>
|
||||
{allowedSanTypes.includes("dns") && (
|
||||
<SelectItem value="dns">DNS</SelectItem>
|
||||
)}
|
||||
{allowedSanTypes.includes("ip") && (
|
||||
<SelectItem value="ip">IP</SelectItem>
|
||||
)}
|
||||
{allowedSanTypes.includes("email") && (
|
||||
<SelectItem value="email">Email</SelectItem>
|
||||
)}
|
||||
{allowedSanTypes.includes("uri") && (
|
||||
<SelectItem value="uri">URI</SelectItem>
|
||||
)}
|
||||
</Select>
|
||||
<Input
|
||||
value={san.value}
|
||||
onChange={(e) => {
|
||||
const newValue = [...value];
|
||||
newValue[index] = { ...san, value: e.target.value };
|
||||
onChange(newValue);
|
||||
}}
|
||||
placeholder={getSanPlaceholder(san.type)}
|
||||
className="flex-1"
|
||||
/>
|
||||
<IconButton
|
||||
ariaLabel="Remove SAN"
|
||||
variant="plain"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const newValue = value.filter((_, i) => i !== index);
|
||||
onChange(newValue);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline_bg"
|
||||
size="xs"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => {
|
||||
const defaultType =
|
||||
allowedSanTypes.length > 0 ? allowedSanTypes[0] : "dns";
|
||||
onChange([
|
||||
...value,
|
||||
{ type: defaultType as "dns" | "ip" | "email" | "uri", value: "" }
|
||||
]);
|
||||
}}
|
||||
className="w-full"
|
||||
>
|
||||
Add SAN
|
||||
</Button>
|
||||
</div>
|
||||
</FormControl>
|
||||
)}
|
||||
allowedSanTypes={constraints.allowedSanTypes}
|
||||
error={formState.errors.subjectAltNames?.message}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -753,158 +470,31 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="signatureAlgorithm"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Signature Algorithm"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<Select
|
||||
defaultValue=""
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
placeholder={
|
||||
availableSignatureAlgorithms.length > 0
|
||||
? "Select signature algorithm"
|
||||
: "No algorithms available"
|
||||
}
|
||||
position="popper"
|
||||
>
|
||||
{availableSignatureAlgorithms.map((algorithm) => (
|
||||
<SelectItem key={algorithm.value} value={algorithm.value}>
|
||||
{algorithm.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="keyAlgorithm"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Key Algorithm"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<Select
|
||||
defaultValue=""
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
placeholder={
|
||||
availableKeyAlgorithms.length > 0
|
||||
? "Select key algorithm"
|
||||
: "No algorithms available"
|
||||
}
|
||||
position="popper"
|
||||
>
|
||||
{availableKeyAlgorithms.map((algorithm) => (
|
||||
<SelectItem key={algorithm.value} value={algorithm.value}>
|
||||
{algorithm.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<AlgorithmSelectors
|
||||
control={control}
|
||||
availableSignatureAlgorithms={availableSignatureAlgorithms}
|
||||
availableKeyAlgorithms={availableKeyAlgorithms}
|
||||
signatureError={formState.errors.signatureAlgorithm?.message}
|
||||
keyError={formState.errors.keyAlgorithm?.message}
|
||||
/>
|
||||
|
||||
<Accordion type="single" collapsible className="w-full">
|
||||
{filteredKeyUsages.length > 0 && (
|
||||
<AccordionItem value="key-usages">
|
||||
<AccordionTrigger>Key Usages</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<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>
|
||||
)}
|
||||
|
||||
{filteredExtendedKeyUsages.length > 0 && (
|
||||
<AccordionItem value="extended-key-usages">
|
||||
<AccordionTrigger>Extended Key Usages</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<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>
|
||||
)}
|
||||
<KeyUsageSection
|
||||
control={control}
|
||||
title="Key Usages"
|
||||
accordionValue="key-usages"
|
||||
namePrefix="keyUsages"
|
||||
options={filteredKeyUsages}
|
||||
requiredUsages={constraints.requiredKeyUsages}
|
||||
/>
|
||||
<KeyUsageSection
|
||||
control={control}
|
||||
title="Extended Key Usages"
|
||||
accordionValue="extended-key-usages"
|
||||
namePrefix="extendedKeyUsages"
|
||||
options={filteredExtendedKeyUsages}
|
||||
requiredUsages={constraints.requiredExtendedKeyUsages}
|
||||
/>
|
||||
</Accordion>
|
||||
</>
|
||||
)}
|
||||
@@ -915,7 +505,7 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting || (!selectedProfile && !profileId)}
|
||||
isDisabled={isSubmitting || (!actualSelectedProfile && !profileId)}
|
||||
>
|
||||
{cert ? "Update" : "Issue Certificate"}
|
||||
</Button>
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { Control, Controller } from "react-hook-form";
|
||||
|
||||
import {
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
Checkbox,
|
||||
FormLabel
|
||||
} from "@app/components/v2";
|
||||
|
||||
type KeyUsageOption = {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
type KeyUsageSectionProps = {
|
||||
control: Control<any>;
|
||||
title: string;
|
||||
accordionValue: string;
|
||||
namePrefix: "keyUsages" | "extendedKeyUsages";
|
||||
options: KeyUsageOption[];
|
||||
requiredUsages: string[];
|
||||
};
|
||||
|
||||
export const KeyUsageSection = ({
|
||||
control,
|
||||
title,
|
||||
accordionValue,
|
||||
namePrefix,
|
||||
options,
|
||||
requiredUsages
|
||||
}: KeyUsageSectionProps) => {
|
||||
if (options.length === 0) return null;
|
||||
|
||||
return (
|
||||
<AccordionItem value={accordionValue}>
|
||||
<AccordionTrigger>{title}</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="grid grid-cols-2 gap-2 pl-2">
|
||||
{options.map(({ label, value }) => {
|
||||
const isRequired = requiredUsages.includes(value);
|
||||
return (
|
||||
<Controller
|
||||
key={label}
|
||||
control={control}
|
||||
name={`${namePrefix}.${value}` as any}
|
||||
render={({ field }) => (
|
||||
<div className="flex items-center space-x-3">
|
||||
<Checkbox
|
||||
id={`${namePrefix}-${value}`}
|
||||
isChecked={field.value || false}
|
||||
onCheckedChange={(checked) => {
|
||||
if (!isRequired) {
|
||||
field.onChange(checked);
|
||||
}
|
||||
}}
|
||||
isDisabled={isRequired}
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<FormLabel
|
||||
id={`${namePrefix}-${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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,101 @@
|
||||
import { Control, Controller } from "react-hook-form";
|
||||
import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { Button, FormControl, IconButton, Input, Select, SelectItem } from "@app/components/v2";
|
||||
|
||||
import {
|
||||
FrontendSanType,
|
||||
getSanPlaceholder,
|
||||
getSanTypeLabels,
|
||||
SubjectAltName
|
||||
} from "./certificateUtils";
|
||||
|
||||
type SubjectAltNamesFieldProps = {
|
||||
control: Control<any>;
|
||||
allowedSanTypes: FrontendSanType[];
|
||||
error?: string;
|
||||
};
|
||||
|
||||
export const SubjectAltNamesField = ({
|
||||
control,
|
||||
allowedSanTypes,
|
||||
error
|
||||
}: SubjectAltNamesFieldProps) => {
|
||||
const sanTypeLabels = getSanTypeLabels();
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
name="subjectAltNames"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<FormControl
|
||||
label="Subject Alternative Names (SANs)"
|
||||
errorText={error}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
{value.map((san: SubjectAltName, index: number) => (
|
||||
// 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) => {
|
||||
const newValue = [...value];
|
||||
newValue[index] = {
|
||||
...san,
|
||||
type: newType as FrontendSanType
|
||||
};
|
||||
onChange(newValue);
|
||||
}}
|
||||
className="w-24"
|
||||
>
|
||||
{allowedSanTypes.map((sanType) => (
|
||||
<SelectItem key={sanType} value={sanType}>
|
||||
{sanTypeLabels[sanType]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
<Input
|
||||
value={san.value}
|
||||
onChange={(e) => {
|
||||
const newValue = [...value];
|
||||
newValue[index] = { ...san, value: e.target.value };
|
||||
onChange(newValue);
|
||||
}}
|
||||
placeholder={getSanPlaceholder(san.type)}
|
||||
className="flex-1"
|
||||
/>
|
||||
<IconButton
|
||||
ariaLabel="Remove SAN"
|
||||
variant="plain"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const newValue = value.filter((_: any, i: number) => i !== index);
|
||||
onChange(newValue);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline_bg"
|
||||
size="xs"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => {
|
||||
const defaultType =
|
||||
allowedSanTypes.length > 0 ? allowedSanTypes[0] : FrontendSanType.DNS;
|
||||
onChange([...value, { type: defaultType, value: "" }]);
|
||||
}}
|
||||
className="w-full"
|
||||
>
|
||||
Add SAN
|
||||
</Button>
|
||||
</div>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
import { CertSubjectAlternativeNameType } from "@app/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/shared/certificate-constants";
|
||||
|
||||
export enum FrontendSanType {
|
||||
DNS = "dns",
|
||||
IP = "ip",
|
||||
EMAIL = "email",
|
||||
URI = "uri"
|
||||
}
|
||||
|
||||
export const mapBackendSanTypeToFrontend = (backendType: string): FrontendSanType => {
|
||||
switch (backendType) {
|
||||
case CertSubjectAlternativeNameType.DNS_NAME:
|
||||
return FrontendSanType.DNS;
|
||||
case CertSubjectAlternativeNameType.IP_ADDRESS:
|
||||
return FrontendSanType.IP;
|
||||
case CertSubjectAlternativeNameType.EMAIL:
|
||||
return FrontendSanType.EMAIL;
|
||||
case CertSubjectAlternativeNameType.URI:
|
||||
return FrontendSanType.URI;
|
||||
default:
|
||||
return backendType as FrontendSanType;
|
||||
}
|
||||
};
|
||||
|
||||
export const mapFrontendSanTypeToBackend = (frontendType: FrontendSanType): string => {
|
||||
switch (frontendType) {
|
||||
case FrontendSanType.DNS:
|
||||
return CertSubjectAlternativeNameType.DNS_NAME;
|
||||
case FrontendSanType.IP:
|
||||
return CertSubjectAlternativeNameType.IP_ADDRESS;
|
||||
case FrontendSanType.EMAIL:
|
||||
return CertSubjectAlternativeNameType.EMAIL;
|
||||
case FrontendSanType.URI:
|
||||
return CertSubjectAlternativeNameType.URI;
|
||||
default:
|
||||
return frontendType;
|
||||
}
|
||||
};
|
||||
|
||||
export const getSanPlaceholder = (sanType: FrontendSanType): string => {
|
||||
switch (sanType) {
|
||||
case FrontendSanType.DNS:
|
||||
return "example.com or *.example.com";
|
||||
case FrontendSanType.IP:
|
||||
return "192.168.1.1";
|
||||
case FrontendSanType.EMAIL:
|
||||
return "[email protected]";
|
||||
case FrontendSanType.URI:
|
||||
return "https://example.com";
|
||||
default:
|
||||
return "Enter value";
|
||||
}
|
||||
};
|
||||
|
||||
export const getSanTypeLabels = () => ({
|
||||
[FrontendSanType.DNS]: "DNS",
|
||||
[FrontendSanType.IP]: "IP",
|
||||
[FrontendSanType.EMAIL]: "Email",
|
||||
[FrontendSanType.URI]: "URI"
|
||||
});
|
||||
|
||||
export type SubjectAltName = {
|
||||
type: FrontendSanType;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export const formatSubjectAltNames = (subjectAltNames: SubjectAltName[]) => {
|
||||
return subjectAltNames
|
||||
.filter((san) => san.value.trim())
|
||||
.map((san) => ({
|
||||
type: mapFrontendSanTypeToBackend(san.type),
|
||||
value: san.value.trim()
|
||||
}));
|
||||
};
|
||||
|
||||
export const filterUsages = <T extends Record<string, boolean>>(usages: T): string[] => {
|
||||
return Object.entries(usages)
|
||||
.filter(([, value]) => value)
|
||||
.map(([key]) => key);
|
||||
};
|
||||
|
||||
export const getAttributeValue = (
|
||||
subjectAttributes: Array<{ type: string; value: string }> | undefined,
|
||||
type: string
|
||||
): string => {
|
||||
const foundAttr = subjectAttributes?.find((attr) => attr.type === type);
|
||||
return foundAttr?.value || "";
|
||||
};
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { UseFormSetValue, UseFormWatch } from "react-hook-form";
|
||||
|
||||
import {
|
||||
EXTENDED_KEY_USAGES_OPTIONS,
|
||||
KEY_USAGES_OPTIONS
|
||||
} from "@app/hooks/api/certificates/constants";
|
||||
import {
|
||||
mapTemplateKeyAlgorithmToApi,
|
||||
mapTemplateSignatureAlgorithmToApi
|
||||
} from "@app/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/shared/certificate-constants";
|
||||
|
||||
import { FrontendSanType, mapBackendSanTypeToFrontend } from "./certificateUtils";
|
||||
|
||||
export type TemplateConstraints = {
|
||||
allowedKeyUsages: string[];
|
||||
allowedExtendedKeyUsages: string[];
|
||||
requiredKeyUsages: string[];
|
||||
requiredExtendedKeyUsages: string[];
|
||||
allowedSignatureAlgorithms: string[];
|
||||
allowedKeyAlgorithms: string[];
|
||||
allowedSanTypes: FrontendSanType[];
|
||||
shouldShowSanSection: boolean;
|
||||
shouldShowSubjectSection: boolean;
|
||||
};
|
||||
|
||||
export const useCertificateTemplate = (
|
||||
templateData: any,
|
||||
selectedProfile: any,
|
||||
isModalOpen: boolean,
|
||||
setValue: UseFormSetValue<any>,
|
||||
watch: UseFormWatch<any>
|
||||
) => {
|
||||
const [constraints, setConstraints] = useState<TemplateConstraints>({
|
||||
allowedKeyUsages: [],
|
||||
allowedExtendedKeyUsages: [],
|
||||
requiredKeyUsages: [],
|
||||
requiredExtendedKeyUsages: [],
|
||||
allowedSignatureAlgorithms: [],
|
||||
allowedKeyAlgorithms: [],
|
||||
allowedSanTypes: [
|
||||
FrontendSanType.DNS,
|
||||
FrontendSanType.IP,
|
||||
FrontendSanType.EMAIL,
|
||||
FrontendSanType.URI
|
||||
],
|
||||
shouldShowSanSection: true,
|
||||
shouldShowSubjectSection: true
|
||||
});
|
||||
|
||||
const filteredKeyUsages = useMemo(() => {
|
||||
return KEY_USAGES_OPTIONS.filter(({ value }) => constraints.allowedKeyUsages.includes(value));
|
||||
}, [constraints.allowedKeyUsages]);
|
||||
|
||||
const filteredExtendedKeyUsages = useMemo(() => {
|
||||
return EXTENDED_KEY_USAGES_OPTIONS.filter(({ value }) =>
|
||||
constraints.allowedExtendedKeyUsages.includes(value)
|
||||
);
|
||||
}, [constraints.allowedExtendedKeyUsages]);
|
||||
|
||||
const availableSignatureAlgorithms = useMemo(() => {
|
||||
return constraints.allowedSignatureAlgorithms.map((templateAlgorithm) => {
|
||||
const apiAlgorithm = mapTemplateSignatureAlgorithmToApi(templateAlgorithm);
|
||||
return {
|
||||
value: apiAlgorithm,
|
||||
label: apiAlgorithm
|
||||
};
|
||||
});
|
||||
}, [constraints.allowedSignatureAlgorithms]);
|
||||
|
||||
const availableKeyAlgorithms = useMemo(() => {
|
||||
return constraints.allowedKeyAlgorithms.map((templateAlgorithm) => {
|
||||
const apiAlgorithm = mapTemplateKeyAlgorithmToApi(templateAlgorithm);
|
||||
return {
|
||||
value: apiAlgorithm,
|
||||
label: apiAlgorithm
|
||||
};
|
||||
});
|
||||
}, [constraints.allowedKeyAlgorithms]);
|
||||
|
||||
const resetConstraints = () => {
|
||||
setConstraints({
|
||||
allowedKeyUsages: [],
|
||||
allowedExtendedKeyUsages: [],
|
||||
requiredKeyUsages: [],
|
||||
requiredExtendedKeyUsages: [],
|
||||
allowedSignatureAlgorithms: [],
|
||||
allowedKeyAlgorithms: [],
|
||||
allowedSanTypes: [
|
||||
FrontendSanType.DNS,
|
||||
FrontendSanType.IP,
|
||||
FrontendSanType.EMAIL,
|
||||
FrontendSanType.URI
|
||||
],
|
||||
shouldShowSanSection: true,
|
||||
shouldShowSubjectSection: true
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (templateData && selectedProfile && isModalOpen) {
|
||||
const newConstraints: TemplateConstraints = {
|
||||
allowedSignatureAlgorithms: templateData.algorithms?.signature || [],
|
||||
allowedKeyAlgorithms: templateData.algorithms?.keyAlgorithm || [],
|
||||
allowedKeyUsages: [
|
||||
...(templateData.keyUsages?.required || []),
|
||||
...(templateData.keyUsages?.allowed || [])
|
||||
],
|
||||
allowedExtendedKeyUsages: [
|
||||
...(templateData.extendedKeyUsages?.required || []),
|
||||
...(templateData.extendedKeyUsages?.allowed || [])
|
||||
],
|
||||
requiredKeyUsages: templateData.keyUsages?.required || [],
|
||||
requiredExtendedKeyUsages: templateData.extendedKeyUsages?.required || [],
|
||||
allowedSanTypes: [],
|
||||
shouldShowSanSection: true,
|
||||
shouldShowSubjectSection: true
|
||||
};
|
||||
|
||||
// Set TTL if available
|
||||
if (templateData.validity?.max) {
|
||||
setValue("ttl", templateData.validity.max);
|
||||
}
|
||||
|
||||
// Handle SAN types
|
||||
if (templateData.sans && templateData.sans.length > 0) {
|
||||
const sanTypes: FrontendSanType[] = [];
|
||||
templateData.sans.forEach((sanPolicy: any) => {
|
||||
const frontendType = mapBackendSanTypeToFrontend(sanPolicy.type);
|
||||
if (!sanTypes.includes(frontendType)) {
|
||||
sanTypes.push(frontendType);
|
||||
}
|
||||
});
|
||||
newConstraints.allowedSanTypes = sanTypes;
|
||||
newConstraints.shouldShowSanSection = true;
|
||||
} else {
|
||||
newConstraints.allowedSanTypes = [];
|
||||
newConstraints.shouldShowSanSection = false;
|
||||
setValue("subjectAltNames", []);
|
||||
}
|
||||
|
||||
// Handle subject section
|
||||
if (templateData.subject && templateData.subject.length > 0) {
|
||||
newConstraints.shouldShowSubjectSection = true;
|
||||
const currentSubjectAttrs = watch("subjectAttributes");
|
||||
if (!currentSubjectAttrs || currentSubjectAttrs.length === 0) {
|
||||
setValue("subjectAttributes", [{ type: "common_name", value: "" }]);
|
||||
}
|
||||
} else {
|
||||
newConstraints.shouldShowSubjectSection = false;
|
||||
setValue("subjectAttributes", undefined);
|
||||
}
|
||||
|
||||
setConstraints(newConstraints);
|
||||
|
||||
// Set initial required usages
|
||||
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, watch, isModalOpen]);
|
||||
|
||||
return {
|
||||
constraints,
|
||||
filteredKeyUsages,
|
||||
filteredExtendedKeyUsages,
|
||||
availableSignatureAlgorithms,
|
||||
availableKeyAlgorithms,
|
||||
resetConstraints
|
||||
};
|
||||
};
|
||||
+25
-19
@@ -47,7 +47,7 @@ const createSchema = z
|
||||
estConfig: z
|
||||
.object({
|
||||
disableBootstrapCaValidation: z.boolean().optional(),
|
||||
passphrase: z.string().min(1, "EST passphrase is required"),
|
||||
passphraseInput: z.string().min(1, "EST passphrase is required"),
|
||||
caChain: z.string().min(1, "EST CA chain is required").optional()
|
||||
})
|
||||
.refine(
|
||||
@@ -58,7 +58,8 @@ const createSchema = z
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: "EST CA chain is required"
|
||||
message: "EST CA chain is required when bootstrap CA validation is enabled",
|
||||
path: ["caChain"]
|
||||
}
|
||||
)
|
||||
.optional(),
|
||||
@@ -106,7 +107,7 @@ const editSchema = z
|
||||
estConfig: z
|
||||
.object({
|
||||
disableBootstrapCaValidation: z.boolean().optional(),
|
||||
passphrase: z.string().optional(),
|
||||
passphraseInput: z.string().optional(),
|
||||
caChain: z.string().optional()
|
||||
})
|
||||
.optional(),
|
||||
@@ -168,15 +169,22 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
||||
enrollmentType: profile.enrollmentType,
|
||||
certificateAuthorityId: profile.caId,
|
||||
certificateTemplateId: profile.certificateTemplateId,
|
||||
estConfig: {
|
||||
disableBootstrapCaValidation: profile.estConfig?.disableBootstrapCaValidation || false,
|
||||
passphrase: "",
|
||||
caChain: undefined
|
||||
},
|
||||
apiConfig: {
|
||||
autoRenew: profile.apiConfig?.autoRenew || false,
|
||||
autoRenewDays: profile.apiConfig?.autoRenewDays || 30
|
||||
}
|
||||
estConfig:
|
||||
profile.enrollmentType === "est"
|
||||
? {
|
||||
disableBootstrapCaValidation:
|
||||
profile.estConfig?.disableBootstrapCaValidation || false,
|
||||
passphraseInput: "",
|
||||
caChain: profile.estConfig?.encryptedCaChain || ""
|
||||
}
|
||||
: undefined,
|
||||
apiConfig:
|
||||
profile.enrollmentType === "api"
|
||||
? {
|
||||
autoRenew: profile.apiConfig?.autoRenew || false,
|
||||
autoRenewDays: profile.apiConfig?.autoRenewDays || 30
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
: {
|
||||
slug: "",
|
||||
@@ -229,8 +237,8 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
||||
|
||||
if (data.enrollmentType === "est" && data.estConfig) {
|
||||
createData.estConfig = {
|
||||
passphrase: data.estConfig.passphrase,
|
||||
caChain: data.estConfig.caChain || "",
|
||||
passphraseInput: data.estConfig.passphraseInput,
|
||||
caChain: data.estConfig.caChain || undefined,
|
||||
disableBootstrapCaValidation: data.estConfig.disableBootstrapCaValidation
|
||||
};
|
||||
} else if (data.enrollmentType === "api" && data.apiConfig) {
|
||||
@@ -346,8 +354,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
||||
if (watchedEnrollmentType === "est") {
|
||||
setValue("estConfig", {
|
||||
disableBootstrapCaValidation: false,
|
||||
passphrase: "",
|
||||
caChain: ""
|
||||
passphraseInput: ""
|
||||
});
|
||||
setValue("apiConfig", undefined);
|
||||
} else {
|
||||
@@ -391,8 +398,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
||||
setValue("apiConfig", undefined);
|
||||
setValue("estConfig", {
|
||||
disableBootstrapCaValidation: false,
|
||||
passphrase: "",
|
||||
caChain: ""
|
||||
passphraseInput: ""
|
||||
});
|
||||
} else {
|
||||
setValue("estConfig", undefined);
|
||||
@@ -444,7 +450,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="estConfig.passphrase"
|
||||
name="estConfig.passphraseInput"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="EST Passphrase"
|
||||
|
||||
+1
-1
@@ -201,7 +201,7 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
|
||||
Edit Profile
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{canIssueCertificate && (
|
||||
{canIssueCertificate && profile.enrollmentType === "api" && (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
+7
-6
@@ -9,8 +9,8 @@ import {
|
||||
ProjectPermissionPkiTemplateActions,
|
||||
ProjectPermissionSub
|
||||
} from "@app/context/ProjectPermissionContext/types";
|
||||
import { useDeleteCertificateTemplateV2New } from "@app/hooks/api/certificateTemplates/mutations";
|
||||
import { TCertificateTemplateV2New } from "@app/hooks/api/certificateTemplates/types";
|
||||
import { useDeleteCertificateTemplateV2WithPolicies } from "@app/hooks/api/certificateTemplates/mutations";
|
||||
import { TCertificateTemplateV2WithPolicies } from "@app/hooks/api/certificateTemplates/types";
|
||||
|
||||
import { CreateTemplateModal } from "./CreateTemplateModal";
|
||||
import { TemplateList } from "./TemplateList";
|
||||
@@ -21,9 +21,10 @@ export const CertificateTemplatesV2Tab = () => {
|
||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<TCertificateTemplateV2New | null>(null);
|
||||
const [selectedTemplate, setSelectedTemplate] =
|
||||
useState<TCertificateTemplateV2WithPolicies | null>(null);
|
||||
|
||||
const deleteTemplateV2 = useDeleteCertificateTemplateV2New();
|
||||
const deleteTemplateV2 = useDeleteCertificateTemplateV2WithPolicies();
|
||||
|
||||
const canCreateTemplate = permission.can(
|
||||
ProjectPermissionPkiTemplateActions.Create,
|
||||
@@ -34,12 +35,12 @@ export const CertificateTemplatesV2Tab = () => {
|
||||
setIsCreateModalOpen(true);
|
||||
};
|
||||
|
||||
const handleEditTemplate = (template: TCertificateTemplateV2New) => {
|
||||
const handleEditTemplate = (template: TCertificateTemplateV2WithPolicies) => {
|
||||
setSelectedTemplate(template);
|
||||
setIsEditModalOpen(true);
|
||||
};
|
||||
|
||||
const handleDeleteTemplate = (template: TCertificateTemplateV2New) => {
|
||||
const handleDeleteTemplate = (template: TCertificateTemplateV2WithPolicies) => {
|
||||
setSelectedTemplate(template);
|
||||
setIsDeleteModalOpen(true);
|
||||
};
|
||||
|
||||
+59
-22
@@ -24,12 +24,12 @@ import {
|
||||
} from "@app/components/v2";
|
||||
import { useProject } from "@app/context";
|
||||
import {
|
||||
useCreateCertificateTemplateV2New,
|
||||
useUpdateCertificateTemplateV2New
|
||||
useCreateCertificateTemplateV2WithPolicies,
|
||||
useUpdateCertificateTemplateV2WithPolicies
|
||||
} from "@app/hooks/api/certificateTemplates/mutations";
|
||||
import {
|
||||
TCertificateTemplateV2New,
|
||||
TCertificateTemplateV2Policy
|
||||
TCertificateTemplateV2Policy,
|
||||
TCertificateTemplateV2WithPolicies
|
||||
} from "@app/hooks/api/certificateTemplates/types";
|
||||
|
||||
import {
|
||||
@@ -57,7 +57,7 @@ type ValidityTransform = TCertificateTemplateV2Policy["validity"];
|
||||
interface Props {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
template?: TCertificateTemplateV2New;
|
||||
template?: TCertificateTemplateV2WithPolicies;
|
||||
mode?: "create" | "edit";
|
||||
}
|
||||
|
||||
@@ -106,12 +106,12 @@ const KEY_ALGORITHMS = [
|
||||
|
||||
export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create" }: Props) => {
|
||||
const { currentProject } = useProject();
|
||||
const createTemplate = useCreateCertificateTemplateV2New();
|
||||
const updateTemplate = useUpdateCertificateTemplateV2New();
|
||||
const createTemplate = useCreateCertificateTemplateV2WithPolicies();
|
||||
const updateTemplate = useUpdateCertificateTemplateV2WithPolicies();
|
||||
|
||||
const isEdit = mode === "edit" && template;
|
||||
|
||||
const convertApiToUiFormat = (templateData: TCertificateTemplateV2New): FormData => {
|
||||
const convertApiToUiFormat = (templateData: TCertificateTemplateV2WithPolicies): FormData => {
|
||||
const attributes: FormData["attributes"] = [];
|
||||
if (templateData.subject && Array.isArray(templateData.subject)) {
|
||||
templateData.subject.forEach((subj) => {
|
||||
@@ -244,12 +244,10 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
||||
maxDuration: { value: 365, unit: CertDurationUnit.DAYS }
|
||||
},
|
||||
signatureAlgorithm: {
|
||||
allowedAlgorithms: [],
|
||||
defaultAlgorithm: ""
|
||||
allowedAlgorithms: []
|
||||
},
|
||||
keyAlgorithm: {
|
||||
allowedKeyTypes: [],
|
||||
defaultKeyType: ""
|
||||
allowedKeyTypes: []
|
||||
}
|
||||
});
|
||||
|
||||
@@ -275,8 +273,38 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
||||
optionalUsages: []
|
||||
};
|
||||
|
||||
const transformToNewApiFormat = (data: FormData) => {
|
||||
const subject =
|
||||
const consolidateByType = <
|
||||
T extends { type: string; allowed?: string[]; required?: string[]; denied?: string[] }
|
||||
>(
|
||||
items: T[]
|
||||
): T[] => {
|
||||
const consolidated = new Map<string, T>();
|
||||
|
||||
items.forEach((item) => {
|
||||
const existing = consolidated.get(item.type);
|
||||
if (existing) {
|
||||
const mergedItem = {
|
||||
...item,
|
||||
allowed: [...new Set([...(existing.allowed || []), ...(item.allowed || [])])],
|
||||
required: [...new Set([...(existing.required || []), ...(item.required || [])])],
|
||||
denied: [...new Set([...(existing.denied || []), ...(item.denied || [])])]
|
||||
} as T;
|
||||
|
||||
if (mergedItem.allowed?.length === 0) delete mergedItem.allowed;
|
||||
if (mergedItem.required?.length === 0) delete mergedItem.required;
|
||||
if (mergedItem.denied?.length === 0) delete mergedItem.denied;
|
||||
|
||||
consolidated.set(item.type, mergedItem);
|
||||
} else {
|
||||
consolidated.set(item.type, item);
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(consolidated.values());
|
||||
};
|
||||
|
||||
const transformToApiFormat = (data: FormData) => {
|
||||
const subjectRaw =
|
||||
data.attributes?.map((attr) => {
|
||||
const result: AttributeTransform = { type: attr.type };
|
||||
|
||||
@@ -289,7 +317,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
||||
return result;
|
||||
}) || [];
|
||||
|
||||
const sans =
|
||||
const sansRaw =
|
||||
data.subjectAlternativeNames?.map((san) => {
|
||||
const result: SanTransform = { type: san.type };
|
||||
|
||||
@@ -304,7 +332,13 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
||||
return result;
|
||||
}) || [];
|
||||
|
||||
const keyUsages: KeyUsagesTransform = {};
|
||||
const subject = consolidateByType(subjectRaw);
|
||||
const sans = consolidateByType(sansRaw);
|
||||
|
||||
const keyUsages: KeyUsagesTransform = {
|
||||
required: [],
|
||||
allowed: []
|
||||
};
|
||||
if (data.keyUsages?.requiredUsages && data.keyUsages.requiredUsages.length > 0) {
|
||||
keyUsages.required = data.keyUsages.requiredUsages;
|
||||
}
|
||||
@@ -312,7 +346,10 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
||||
keyUsages.allowed = data.keyUsages.optionalUsages;
|
||||
}
|
||||
|
||||
const extendedKeyUsages: ExtendedKeyUsagesTransform = {};
|
||||
const extendedKeyUsages: ExtendedKeyUsagesTransform = {
|
||||
required: [],
|
||||
allowed: []
|
||||
};
|
||||
if (
|
||||
data.extendedKeyUsages?.requiredUsages &&
|
||||
data.extendedKeyUsages.requiredUsages.length > 0
|
||||
@@ -364,10 +401,10 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
||||
description: data.description,
|
||||
subject,
|
||||
sans,
|
||||
keyUsages: Object.keys(keyUsages).length > 0 ? keyUsages : undefined,
|
||||
extendedKeyUsages: Object.keys(extendedKeyUsages).length > 0 ? extendedKeyUsages : undefined,
|
||||
algorithms: Object.keys(algorithms).length > 0 ? algorithms : undefined,
|
||||
validity: Object.keys(validity).length > 0 ? validity : undefined
|
||||
keyUsages,
|
||||
extendedKeyUsages,
|
||||
algorithms,
|
||||
validity
|
||||
};
|
||||
};
|
||||
|
||||
@@ -391,7 +428,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
||||
return;
|
||||
}
|
||||
|
||||
const transformedData = transformToNewApiFormat(data);
|
||||
const transformedData = transformToApiFormat(data);
|
||||
|
||||
if (isEdit) {
|
||||
const updateData = {
|
||||
|
||||
+3
-3
@@ -23,11 +23,11 @@ import {
|
||||
ProjectPermissionSub
|
||||
} from "@app/context/ProjectPermissionContext/types";
|
||||
import { useListCertificateTemplatesV2 } from "@app/hooks/api/certificateTemplates/queries";
|
||||
import { TCertificateTemplateV2New } from "@app/hooks/api/certificateTemplates/types";
|
||||
import { TCertificateTemplateV2WithPolicies } from "@app/hooks/api/certificateTemplates/types";
|
||||
|
||||
interface Props {
|
||||
onEditTemplate: (template: TCertificateTemplateV2New) => void;
|
||||
onDeleteTemplate: (template: TCertificateTemplateV2New) => void;
|
||||
onEditTemplate: (template: TCertificateTemplateV2WithPolicies) => void;
|
||||
onDeleteTemplate: (template: TCertificateTemplateV2WithPolicies) => void;
|
||||
}
|
||||
|
||||
export const TemplateList = ({ onEditTemplate, onDeleteTemplate }: Props) => {
|
||||
|
||||
+8
-6
@@ -40,13 +40,15 @@ export const uiValiditySchema = z.object({
|
||||
});
|
||||
|
||||
export const uiSignatureAlgorithmSchema = z.object({
|
||||
allowedAlgorithms: z.array(z.string()).optional(),
|
||||
defaultAlgorithm: z.string().optional()
|
||||
allowedAlgorithms: z
|
||||
.array(z.string().min(1, "Algorithm cannot be empty"))
|
||||
.min(1, "At least one algorithm must be selected")
|
||||
});
|
||||
|
||||
export const uiKeyAlgorithmSchema = z.object({
|
||||
allowedKeyTypes: z.array(z.string()).optional(),
|
||||
defaultKeyType: z.string().optional()
|
||||
allowedKeyTypes: z
|
||||
.array(z.string().min(1, "Key type cannot be empty"))
|
||||
.min(1, "At least one key type must be selected")
|
||||
});
|
||||
|
||||
export const templateSchema = z.object({
|
||||
@@ -69,8 +71,8 @@ export const templateSchema = z.object({
|
||||
keyUsages: uiKeyUsagesSchema.optional(),
|
||||
extendedKeyUsages: uiExtendedKeyUsagesSchema.optional(),
|
||||
validity: uiValiditySchema.optional(),
|
||||
signatureAlgorithm: uiSignatureAlgorithmSchema.optional(),
|
||||
keyAlgorithm: uiKeyAlgorithmSchema.optional()
|
||||
signatureAlgorithm: uiSignatureAlgorithmSchema,
|
||||
keyAlgorithm: uiKeyAlgorithmSchema
|
||||
});
|
||||
|
||||
export type TemplateFormData = z.infer<typeof templateSchema>;
|
||||
|
||||
Reference in New Issue
Block a user