Address greptile comments

This commit is contained in:
Carlos Monastyrski
2025-10-17 02:36:44 -03:00
parent 87a91ce423
commit 534ce7158b
49 changed files with 1844 additions and 617 deletions
+1 -1
View File
@@ -235,7 +235,7 @@ export type TOrderCertificateResponse = {
url: string;
token: string;
validated?: string;
error?: any;
error?: string | Error;
}>;
}>;
certificate?: string;
@@ -30,7 +30,7 @@ export type TCertificateProfileWithDetails = TCertificateProfile & {
id: string;
disableBootstrapCaValidation: boolean;
hashedPassphrase: string;
encryptedCaChain: any;
encryptedCaChain: string;
};
apiConfig?: {
id: string;
@@ -108,10 +108,10 @@ export type TProfileCertificate = {
serialNumber: string;
cn: string;
status: string;
notBefore: Date;
notAfter: Date;
notBefore: string;
notAfter: string;
isRevoked: boolean;
createdAt: Date;
createdAt: string;
};
export type TGetProfileCertificatesDTO = {
@@ -146,8 +146,10 @@ export type TCertificateTemplateV2Policy = {
denied?: string[];
};
algorithms?: {
signature?: string[];
keyAlgorithm?: string[];
signature?: Array<
"SHA256-RSA" | "SHA384-RSA" | "SHA512-RSA" | "SHA256-ECDSA" | "SHA384-ECDSA" | "SHA512-ECDSA"
>;
keyAlgorithm?: Array<"RSA-2048" | "RSA-3072" | "RSA-4096" | "ECDSA-P256" | "ECDSA-P384">;
};
validity?: {
max?: string;
@@ -109,7 +109,7 @@ export const PkiManagerLayout = () => {
<div className="w-6">
<FontAwesomeIcon icon={faStamp} />
</div>
Certificates Authorities
Certificate Authorities
</div>
</MenuItem>
)}
@@ -60,12 +60,7 @@ const schema = z
commonName: z.string(),
notAfter: z.string().trim().refine(isValidDate, { message: "Invalid date format" }),
maxPathLength: z.string(),
keyAlgorithm: z.enum([
CertKeyAlgorithm.RSA_2048,
CertKeyAlgorithm.RSA_4096,
CertKeyAlgorithm.ECDSA_P256,
CertKeyAlgorithm.ECDSA_P384
])
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm)
})
.required()
})
@@ -145,13 +140,11 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
maxPathLength: ca.configuration.maxPathLength
? String(ca.configuration.maxPathLength)
: "",
keyAlgorithm:
ca.configuration.keyAlgorithm === CertKeyAlgorithm.RSA_2048 ||
ca.configuration.keyAlgorithm === CertKeyAlgorithm.RSA_4096 ||
ca.configuration.keyAlgorithm === CertKeyAlgorithm.ECDSA_P256 ||
ca.configuration.keyAlgorithm === CertKeyAlgorithm.ECDSA_P384
? ca.configuration.keyAlgorithm
: CertKeyAlgorithm.RSA_2048
keyAlgorithm: (Object.values(CertKeyAlgorithm) as string[]).includes(
ca.configuration.keyAlgorithm
)
? ca.configuration.keyAlgorithm
: CertKeyAlgorithm.RSA_2048
}
});
} else {
@@ -112,9 +112,11 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
const [allowedKeyAlgorithms, setAllowedKeyAlgorithms] = useState<string[]>([]);
const { currentProject } = useProject();
const { data: cert } = useGetCert(
(popUp?.certificateIssuance?.data as { serialNumber: string })?.serialNumber || ""
);
const inputSerialNumber =
(popUp?.certificateIssuance?.data as { serialNumber: string })?.serialNumber || "";
const sanitizedSerialNumber = inputSerialNumber.replace(/[^a-fA-F0-9:]/g, "");
const { data: cert } = useGetCert(sanitizedSerialNumber);
const { data: profilesData } = useListCertificateProfiles({
projectId: currentProject?.id || "",
@@ -263,15 +265,14 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
useEffect(() => {
if (cert) {
const subjectAttrs: Array<{ type: string; value: string }> = [];
if (cert.commonName) subjectAttrs.push({ type: "common_name", value: cert.commonName });
const subjectAttrs: Array<{ type: "common_name"; value: string }> = [];
if (cert.commonName)
subjectAttrs.push({ type: "common_name" as const, value: cert.commonName });
reset({
profileId: "",
subjectAttributes:
subjectAttrs.length > 0
? (subjectAttrs as any)
: [{ type: "common_name" as const, value: "" }],
subjectAttrs.length > 0 ? subjectAttrs : [{ type: "common_name" as const, value: "" }],
subjectAltNames: cert.subjectAltNames
? cert.subjectAltNames.split(",").map((name) => {
const trimmed = name.trim();
@@ -2,15 +2,16 @@ import { useState } from "react";
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { ProjectPermissionCan } from "@app/components/permissions";
import { ContentLoader, PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { useProject } from "@app/context";
import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context";
import { CertificateProfilesTab } from "./components/CertificateProfilesTab";
import { CertificateTemplatesV2Tab } from "./components/CertificateTemplatesV2Tab";
enum TabSections {
CertificateTemplatesV2 = "templates-v2",
CertificateProfiles = "profiles"
CertificateProfiles = "profiles",
CertificateTemplatesV2 = "templates-v2"
}
export const PoliciesPage = () => {
@@ -23,34 +24,53 @@ export const PoliciesPage = () => {
}
return (
<div className="container mx-auto flex h-full flex-col justify-between bg-bunker-800 text-white">
<Helmet>
<title>{t("common.head-title", { title: "Certificate Policies" })}</title>
</Helmet>
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader
scope="project"
title="Certificate Policies"
description="Manage certificate templates and profiles for unified certificate issuance"
/>
<Tabs value={activeTab} onValueChange={(value) => setActiveTab(value as TabSections)}>
<TabList className="mb-6 w-full">
<div className="flex w-full border-b border-mineshaft-600">
<Tab value={TabSections.CertificateProfiles}>Certificate Profiles</Tab>
<Tab value={TabSections.CertificateTemplatesV2}>Certificate Templates</Tab>
<ProjectPermissionCan
I={ProjectPermissionActions.Read}
a={ProjectPermissionSub.CertificateAuthorities}
>
{(isAllowed) => {
if (!isAllowed) {
return (
<div className="container mx-auto flex h-full flex-col justify-center bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl text-center">
<p>You don&apos;t have permission to access certificate policies.</p>
</div>
</div>
</TabList>
);
}
<TabPanel value={TabSections.CertificateProfiles}>
<CertificateProfilesTab />
</TabPanel>
return (
<div className="container mx-auto flex h-full flex-col justify-between bg-bunker-800 text-white">
<Helmet>
<title>{t("common.head-title", { title: "Certificate Policies" })}</title>
</Helmet>
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader
scope="project"
title="Certificate Policies"
description="Manage certificate templates and profiles for unified certificate issuance"
/>
<TabPanel value={TabSections.CertificateTemplatesV2}>
<CertificateTemplatesV2Tab />
</TabPanel>
</Tabs>
</div>
</div>
<Tabs value={activeTab} onValueChange={(value) => setActiveTab(value as TabSections)}>
<TabList className="mb-6 w-full">
<div className="flex w-full border-b border-mineshaft-600">
<Tab value={TabSections.CertificateProfiles}>Certificate Profiles</Tab>
<Tab value={TabSections.CertificateTemplatesV2}>Certificate Templates</Tab>
</div>
</TabList>
<TabPanel value={TabSections.CertificateProfiles}>
<CertificateProfilesTab />
</TabPanel>
<TabPanel value={TabSections.CertificateTemplatesV2}>
<CertificateTemplatesV2Tab />
</TabPanel>
</Tabs>
</div>
</div>
);
}}
</ProjectPermissionCan>
);
};
@@ -60,7 +60,10 @@ export const CertificateProfilesTab = () => {
type: "success"
});
} catch (error) {
console.error("Failed to delete profile:", error);
console.error(
`Failed to delete profile "${selectedProfile.slug}" (ID: ${selectedProfile.id}):`,
error
);
}
};
@@ -78,7 +81,7 @@ export const CertificateProfilesTab = () => {
{canCreateProfile && (
<Button
colorSchema="primary"
type="submit"
type="button"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={handleCreateProfile}
>
@@ -18,6 +18,8 @@ import { useProject } from "@app/context";
import { useListCasByProjectId } from "@app/hooks/api/ca/queries";
import {
TCertificateProfileWithDetails,
TCreateCertificateProfileDTO,
TUpdateCertificateProfileDTO,
useCreateCertificateProfile,
useUpdateCertificateProfile
} from "@app/hooks/api/certificateProfiles";
@@ -163,7 +165,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
if (!currentProject?.id && !isEdit) return;
if (isEdit) {
const updateData: any = {
const updateData: TUpdateCertificateProfileDTO = {
profileId: profile.id,
slug: data.slug,
description: data.description
@@ -177,8 +179,12 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
await updateProfile.mutateAsync(updateData);
} else {
const createData: any = {
projectId: currentProject!.id,
if (!currentProject?.id) {
throw new Error("Project ID is required for creating a profile");
}
const createData: TCreateCertificateProfileDTO = {
projectId: currentProject.id,
slug: data.slug,
description: data.description,
enrollmentType: data.enrollmentType,
@@ -273,9 +279,11 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
position="popper"
isDisabled={Boolean(isEdit)}
>
{certificateAuthorities.map((ca: any) => (
{certificateAuthorities.map((ca) => (
<SelectItem key={ca.id} value={ca.id}>
{ca.friendlyName || ca.name || ca.commonName}
{ca.type === "internal" && ca.configuration.friendlyName
? ca.configuration.friendlyName
: ca.name}
</SelectItem>
))}
</Select>
@@ -481,7 +489,20 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
max="365"
className="w-full"
isDisabled={!watchedAutoRenew}
onChange={(e) => field.onChange(parseInt(e.target.value, 10) || 30)}
onChange={(e) => {
const { value } = e.target;
if (value === "") {
field.onChange("");
} else {
const parsed = parseInt(value, 10);
if (!Number.isNaN(parsed) && parsed >= 1 && parsed <= 365) {
field.onChange(parsed);
} else {
// Preserve the original field value instead of defaulting to 30
field.onChange(field.value || "");
}
}
}}
/>
</FormControl>
)}
@@ -34,6 +34,32 @@ export const ProfileList = ({ onEditProfile, onDeleteProfile }: Props) => {
const profiles = data?.certificateProfiles || [];
if (!currentProject?.id) {
return (
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Name</Th>
<Th>Enrollment Type</Th>
<Th>Issuing CA</Th>
<Th>Certificate Template</Th>
<Th>Certificates</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
<Tr>
<Td colSpan={6}>
<EmptyState title="No Project Selected" />
</Td>
</Tr>
</TBody>
</Table>
</TableContainer>
);
}
return (
<TableContainer>
<Table>
@@ -62,7 +62,7 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
// eslint-disable-next-line consistent-return
return () => clearTimeout(timer);
}, [isIdCopied]);
}, [isIdCopied, setIsIdCopied]);
const { data: templateData } = useGetCertificateTemplateV2ById({
templateId: profile.certificateTemplateId
@@ -87,9 +87,12 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
const config = {
api: { variant: "success" as const, label: "API" },
est: { variant: "primary" as const, label: "EST" }
};
} as const;
const { variant, label } = config[enrollmentType as keyof typeof config] || config.api;
const configKey = Object.keys(config).includes(enrollmentType)
? (enrollmentType as keyof typeof config)
: "api";
const { variant, label } = config[configKey];
return <Badge variant={variant}>{label}</Badge>;
};
@@ -57,8 +57,12 @@ export const CertificateTemplatesV2Tab = () => {
text: `Certificate template "${selectedTemplate.name}" deleted successfully`,
type: "success"
});
} catch (error: any) {
} catch (error) {
console.error("Failed to delete template:", error);
createNotification({
text: "Failed to delete certificate template",
type: "error"
});
}
};
@@ -76,7 +80,7 @@ export const CertificateTemplatesV2Tab = () => {
{canCreateTemplate && (
<Button
colorSchema="primary"
type="submit"
type="button"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={handleCreateTemplate}
>
@@ -27,12 +27,17 @@ import {
useCreateCertificateTemplateV2New,
useUpdateCertificateTemplateV2New
} from "@app/hooks/api/certificateTemplates/mutations";
import { TCertificateTemplateV2New } from "@app/hooks/api/certificateTemplates/types";
import {
TCertificateTemplateV2New,
TCertificateTemplateV2Policy
} from "@app/hooks/api/certificateTemplates/types";
import {
CertDurationUnit,
CertExtendedKeyUsageType,
CertKeyUsageType,
CertSubjectAlternativeNameType,
CertSubjectAttributeType,
SAN_INCLUDE_OPTIONS,
SAN_TYPE_OPTIONS,
SUBJECT_ATTRIBUTE_INCLUDE_OPTIONS,
@@ -42,6 +47,13 @@ import { KeyUsagesSection, TemplateFormData, templateSchema } from "./shared";
export type FormData = TemplateFormData;
type AttributeTransform = NonNullable<TCertificateTemplateV2Policy["subject"]>[0];
type SanTransform = NonNullable<TCertificateTemplateV2Policy["sans"]>[0];
type KeyUsagesTransform = TCertificateTemplateV2Policy["keyUsages"];
type ExtendedKeyUsagesTransform = TCertificateTemplateV2Policy["extendedKeyUsages"];
type AlgorithmsTransform = TCertificateTemplateV2Policy["algorithms"];
type ValidityTransform = TCertificateTemplateV2Policy["validity"];
interface Props {
isOpen: boolean;
onClose: () => void;
@@ -100,13 +112,13 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
const isEdit = mode === "edit" && template;
const convertApiToUiFormat = (templateData: TCertificateTemplateV2New): FormData => {
const attributes: any[] = [];
const attributes: FormData["attributes"] = [];
if (templateData.subject && Array.isArray(templateData.subject)) {
templateData.subject.forEach((subj) => {
if (subj.allowed && Array.isArray(subj.allowed)) {
subj.allowed.forEach((allowedValue) => {
attributes.push({
type: subj.type,
type: subj.type as CertSubjectAttributeType,
include: "optional",
value: [allowedValue]
});
@@ -115,7 +127,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
if (subj.denied && Array.isArray(subj.denied)) {
subj.denied.forEach((deniedValue) => {
attributes.push({
type: subj.type,
type: subj.type as CertSubjectAttributeType,
include: "prohibit",
value: [deniedValue]
});
@@ -124,13 +136,13 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
});
}
const subjectAlternativeNames: any[] = [];
const subjectAlternativeNames: FormData["subjectAlternativeNames"] = [];
if (templateData.sans && Array.isArray(templateData.sans)) {
templateData.sans.forEach((san) => {
if (san.required && Array.isArray(san.required)) {
san.required.forEach((requiredValue) => {
subjectAlternativeNames.push({
type: san.type,
type: san.type as CertSubjectAlternativeNameType,
include: "mandatory",
value: [requiredValue]
});
@@ -139,7 +151,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
if (san.allowed && Array.isArray(san.allowed)) {
san.allowed.forEach((allowedValue) => {
subjectAlternativeNames.push({
type: san.type,
type: san.type as CertSubjectAlternativeNameType,
include: "optional",
value: [allowedValue]
});
@@ -148,7 +160,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
if (san.denied && Array.isArray(san.denied)) {
san.denied.forEach((deniedValue) => {
subjectAlternativeNames.push({
type: san.type,
type: san.type as CertSubjectAlternativeNameType,
include: "prohibit",
value: [deniedValue]
});
@@ -171,23 +183,30 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
const validity = templateData.validity?.max
? (() => {
const maxValue = templateData.validity.max;
const match = maxValue.match(/^(\d+)([dmy])$/);
if (match) {
const value = parseInt(match[1], 10);
const unitChar = match[2];
let unit: CertDurationUnit = CertDurationUnit.DAYS;
if (unitChar === "d") {
unit = CertDurationUnit.DAYS;
} else if (unitChar === "m") {
unit = CertDurationUnit.MONTHS;
} else {
unit = CertDurationUnit.YEARS;
}
return {
maxDuration: { value, unit }
};
if (maxValue.length < 2) return undefined;
const lastChar = maxValue.slice(-1);
const numberPart = maxValue.slice(0, -1);
const value = parseInt(numberPart, 10);
if (Number.isNaN(value) || value <= 0 || numberPart !== value.toString()) {
return undefined;
}
return { maxDuration: { value: 365, unit: CertDurationUnit.DAYS } };
let unit: CertDurationUnit = CertDurationUnit.DAYS;
if (lastChar === "d") {
unit = CertDurationUnit.DAYS;
} else if (lastChar === "m") {
unit = CertDurationUnit.MONTHS;
} else if (lastChar === "y") {
unit = CertDurationUnit.YEARS;
} else {
return undefined;
}
return {
maxDuration: { value, unit }
};
})()
: { maxDuration: { value: 365, unit: CertDurationUnit.DAYS } };
@@ -202,7 +221,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
};
return {
slug: templateData.name || "",
name: templateData.name || "",
description: templateData.description || "",
attributes,
subjectAlternativeNames,
@@ -215,7 +234,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
};
const getDefaultValues = (): FormData => ({
slug: "",
name: "",
description: "",
attributes: [],
keyUsages: { requiredUsages: [], optionalUsages: [] },
@@ -259,7 +278,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
const transformToNewApiFormat = (data: FormData) => {
const subject =
data.attributes?.map((attr) => {
const result: any = { type: attr.type };
const result: AttributeTransform = { type: attr.type };
if (attr.include === "optional" && attr.value && attr.value.length > 0) {
result.allowed = attr.value;
@@ -272,7 +291,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
const sans =
data.subjectAlternativeNames?.map((san) => {
const result: any = { type: san.type };
const result: SanTransform = { type: san.type };
if (san.include === "mandatory" && san.value && san.value.length > 0) {
result.required = san.value;
@@ -285,7 +304,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
return result;
}) || [];
const keyUsages: any = {};
const keyUsages: KeyUsagesTransform = {};
if (data.keyUsages?.requiredUsages && data.keyUsages.requiredUsages.length > 0) {
keyUsages.required = data.keyUsages.requiredUsages;
}
@@ -293,7 +312,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
keyUsages.allowed = data.keyUsages.optionalUsages;
}
const extendedKeyUsages: any = {};
const extendedKeyUsages: ExtendedKeyUsagesTransform = {};
if (
data.extendedKeyUsages?.requiredUsages &&
data.extendedKeyUsages.requiredUsages.length > 0
@@ -307,18 +326,27 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
extendedKeyUsages.allowed = data.extendedKeyUsages.optionalUsages;
}
const algorithms: any = {};
const algorithms: AlgorithmsTransform = {};
if (
data.signatureAlgorithm?.allowedAlgorithms &&
data.signatureAlgorithm.allowedAlgorithms.length > 0
) {
algorithms.signature = data.signatureAlgorithm.allowedAlgorithms;
algorithms.signature = data.signatureAlgorithm.allowedAlgorithms as Array<
| "SHA256-RSA"
| "SHA384-RSA"
| "SHA512-RSA"
| "SHA256-ECDSA"
| "SHA384-ECDSA"
| "SHA512-ECDSA"
>;
}
if (data.keyAlgorithm?.allowedKeyTypes && data.keyAlgorithm.allowedKeyTypes.length > 0) {
algorithms.keyAlgorithm = data.keyAlgorithm.allowedKeyTypes;
algorithms.keyAlgorithm = data.keyAlgorithm.allowedKeyTypes as Array<
"RSA-2048" | "RSA-3072" | "RSA-4096" | "ECDSA-P256" | "ECDSA-P384"
>;
}
const validity: any = {};
const validity: ValidityTransform = {};
if (data.validity?.maxDuration) {
let unit = "d";
if (data.validity.maxDuration.unit === CertDurationUnit.DAYS) {
@@ -332,7 +360,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
}
return {
name: data.slug,
name: data.name,
description: data.description,
subject: subject.length > 0 ? subject : undefined,
sans: sans.length > 0 ? sans : undefined,
@@ -372,8 +400,12 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
};
await updateTemplate.mutateAsync(updateData);
} else {
if (!currentProject?.id) {
throw new Error("Project ID is required for creating a template");
}
const createData = {
projectId: currentProject!.id,
projectId: currentProject.id,
...transformedData
};
await createTemplate.mutateAsync(createData);
@@ -424,22 +456,22 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
};
const handleKeyUsagesChange = (usages: {
requiredUsages: string[];
optionalUsages: string[];
requiredUsages: CertKeyUsageType[];
optionalUsages: CertKeyUsageType[];
}) => {
setValue("keyUsages", {
requiredUsages: usages.requiredUsages as any,
optionalUsages: usages.optionalUsages as any
requiredUsages: usages.requiredUsages,
optionalUsages: usages.optionalUsages
});
};
const handleExtendedKeyUsagesChange = (usages: {
requiredUsages: string[];
optionalUsages: string[];
requiredUsages: CertExtendedKeyUsageType[];
optionalUsages: CertExtendedKeyUsageType[];
}) => {
setValue("extendedKeyUsages", {
requiredUsages: usages.requiredUsages as any,
optionalUsages: usages.optionalUsages as any
requiredUsages: usages.requiredUsages,
optionalUsages: usages.optionalUsages
});
};
@@ -467,7 +499,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
<div className="space-y-4">
<Controller
control={control}
name="slug"
name="name"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Template Name"
@@ -528,7 +560,10 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
value={attr.type}
onValueChange={(value) => {
const newAttributes = [...watchedAttributes];
newAttributes[index] = { ...attr, type: value as any };
newAttributes[index] = {
...attr,
type: value as CertSubjectAttributeType
};
setValue("attributes", newAttributes);
}}
className="w-48"
@@ -544,7 +579,11 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
value={attr.include}
onValueChange={(value) => {
const newAttributes = [...watchedAttributes];
newAttributes[index] = { ...attr, include: value as any };
newAttributes[index] = {
...attr,
include:
value as (typeof SUBJECT_ATTRIBUTE_INCLUDE_OPTIONS)[number]
};
setValue("attributes", newAttributes);
}}
className="w-32"
@@ -627,7 +666,10 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
value={san.type}
onValueChange={(value) => {
const newSans = [...watchedSans];
newSans[index] = { ...san, type: value as any };
newSans[index] = {
...san,
type: value as CertSubjectAlternativeNameType
};
setValue("subjectAlternativeNames", newSans);
}}
className="w-24"
@@ -643,7 +685,10 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
value={san.include}
onValueChange={(value) => {
const newSans = [...watchedSans];
newSans[index] = { ...san, include: value as any };
newSans[index] = {
...san,
include: value as (typeof SAN_INCLUDE_OPTIONS)[number]
};
setValue("subjectAlternativeNames", newSans);
}}
className="w-32"
@@ -674,14 +719,16 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
required
/>
<IconButton
ariaLabel="Remove SAN"
variant="plain"
size="sm"
onClick={() => removeSan(index)}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
{watchedSans.length > 1 && (
<IconButton
ariaLabel="Remove SAN"
variant="plain"
size="sm"
onClick={() => removeSan(index)}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
)}
</div>
))
)}
@@ -19,7 +19,7 @@ import {
} from "@app/components/v2";
import { useProject, useProjectPermission } from "@app/context";
import {
ProjectPermissionActions,
ProjectPermissionPkiTemplateActions,
ProjectPermissionSub
} from "@app/context/ProjectPermissionContext/types";
import { useListCertificateTemplatesV2 } from "@app/hooks/api/certificateTemplates/queries";
@@ -43,19 +43,21 @@ export const TemplateList = ({ onEditTemplate, onDeleteTemplate }: Props) => {
const templates = data?.certificateTemplates || [];
const canEditTemplate = permission.can(
ProjectPermissionActions.Edit,
ProjectPermissionSub.CertificateAuthorities
ProjectPermissionPkiTemplateActions.Edit,
ProjectPermissionSub.CertificateTemplates
);
const canDeleteTemplate = permission.can(
ProjectPermissionActions.Delete,
ProjectPermissionSub.CertificateAuthorities
ProjectPermissionPkiTemplateActions.Delete,
ProjectPermissionSub.CertificateTemplates
);
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString();
};
const hasTemplates = !isLoading && templates && templates.length > 0;
return (
<TableContainer>
<Table>
@@ -75,9 +77,7 @@ export const TemplateList = ({ onEditTemplate, onDeleteTemplate }: Props) => {
</Td>
</Tr>
)}
{!isLoading &&
templates &&
templates.length > 0 &&
{hasTemplates &&
templates.map((template) => (
<Tr
key={template.id}
@@ -166,42 +166,63 @@ export const USAGE_STATES = {
export type UsageState = (typeof USAGE_STATES)[keyof typeof USAGE_STATES] | undefined;
export const TEMPLATE_SIGNATURE_ALGORITHMS = [
"SHA256-RSA",
"SHA384-RSA",
"SHA512-RSA",
"SHA256-ECDSA",
"SHA384-ECDSA",
"SHA512-ECDSA"
] as const;
export enum CertKeyAlgorithm {
RSA_2048 = "RSA_2048",
RSA_3072 = "RSA_3072",
RSA_4096 = "RSA_4096",
ECDSA_P256 = "EC_prime256v1",
ECDSA_P384 = "EC_secp384r1"
}
export const TEMPLATE_KEY_ALGORITHMS = [
"RSA-2048",
"RSA-3072",
"RSA-4096",
"ECDSA-P256",
"ECDSA-P384"
] as const;
export enum CertSignatureAlgorithm {
RSA_SHA256 = "RSA-SHA256",
RSA_SHA384 = "RSA-SHA384",
RSA_SHA512 = "RSA-SHA512",
ECDSA_SHA256 = "ECDSA-SHA256",
ECDSA_SHA384 = "ECDSA-SHA384",
ECDSA_SHA512 = "ECDSA-SHA512"
}
// API format algorithm constants
export const API_SIGNATURE_ALGORITHMS = [
"RSA-SHA256",
"RSA-SHA384",
"RSA-SHA512",
"ECDSA-SHA256",
"ECDSA-SHA384",
"ECDSA-SHA512"
] as const;
export const SIGNATURE_ALGORITHM_OPTIONS = Object.values(CertSignatureAlgorithm);
export const KEY_ALGORITHM_OPTIONS = Object.values(CertKeyAlgorithm);
export const API_KEY_ALGORITHMS = [
"RSA_2048",
"RSA_3072",
"RSA_4096",
"EC_prime256v1",
"EC_secp384r1"
] as const;
// Display name mappings for UI
export const getSignatureAlgorithmDisplayName = (algorithm: CertSignatureAlgorithm): string => {
switch (algorithm) {
case CertSignatureAlgorithm.RSA_SHA256:
return "RSA with SHA-256";
case CertSignatureAlgorithm.RSA_SHA384:
return "RSA with SHA-384";
case CertSignatureAlgorithm.RSA_SHA512:
return "RSA with SHA-512";
case CertSignatureAlgorithm.ECDSA_SHA256:
return "ECDSA with SHA-256";
case CertSignatureAlgorithm.ECDSA_SHA384:
return "ECDSA with SHA-384";
case CertSignatureAlgorithm.ECDSA_SHA512:
return "ECDSA with SHA-512";
default:
return algorithm;
}
};
export const getKeyAlgorithmDisplayName = (algorithm: CertKeyAlgorithm): string => {
switch (algorithm) {
case CertKeyAlgorithm.RSA_2048:
return "RSA 2048";
case CertKeyAlgorithm.RSA_3072:
return "RSA 3072";
case CertKeyAlgorithm.RSA_4096:
return "RSA 4096";
case CertKeyAlgorithm.ECDSA_P256:
return "ECDSA P-256";
case CertKeyAlgorithm.ECDSA_P384:
return "ECDSA P-384";
default:
return algorithm;
}
};
// Mapping functions between template and API formats
export const mapTemplateSignatureAlgorithmToApi = (templateFormat: string): string => {
const mapping: Record<string, string> = {
"SHA256-RSA": "RSA-SHA256",
@@ -11,7 +11,7 @@ import {
KEY_USAGE_OPTIONS
} from "./certificate-constants";
type KeyUsagePolicy = "allow" | "require" | "deny" | "none";
type KeyUsagePolicy = "allow" | "require" | "deny";
interface KeyUsagesSectionProps {
watchedKeyUsages: {
@@ -85,13 +85,7 @@ export const KeyUsagesSection: React.FC<KeyUsagesSectionProps> = ({
});
};
const keyUsagePolicyOptions = [
{ value: "deny", label: "Deny" },
{ value: "allow", label: "Allow" },
{ value: "require", label: "Require" }
];
const extendedKeyUsagePolicyOptions = [
const policyOptions = [
{ value: "deny", label: "Deny" },
{ value: "allow", label: "Allow" },
{ value: "require", label: "Require" }
@@ -116,7 +110,7 @@ export const KeyUsagesSection: React.FC<KeyUsagesSectionProps> = ({
}
className="w-32"
>
{keyUsagePolicyOptions.map((option) => (
{policyOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
@@ -145,7 +139,7 @@ export const KeyUsagesSection: React.FC<KeyUsagesSectionProps> = ({
}
className="w-32"
>
{extendedKeyUsagePolicyOptions.map((option) => (
{policyOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
@@ -50,7 +50,7 @@ export const uiKeyAlgorithmSchema = z.object({
});
export const templateSchema = z.object({
slug: z.string().trim().min(1, "Template name is required"),
name: z.string().trim().min(1, "Template name is required"),
description: z.string().optional(),
attributes: z.array(uiAttributeSchema).optional(),
subjectAlternativeNames: z.array(uiSanSchema).optional(),
@@ -114,7 +114,17 @@ export const apiTemplateSchema = z.object({
.object({
max: z
.string()
.regex(/^\d+[dhmy]$/, "Must be in format like '365d', '12m', '1y', or '24h'")
.refine((val) => {
if (!val) return true;
if (val.length < 2 || val.length > 10) return false;
const lastChar = val.slice(-1);
if (!["d", "h", "m", "y"].includes(lastChar)) return false;
const numberPart = val.slice(0, -1);
const num = parseInt(numberPart, 10);
return !Number.isNaN(num) && num > 0 && numberPart === num.toString();
}, "Must be in format like '365d', '12m', '1y', or '24h'")
.optional()
})
.optional()
@@ -8,15 +8,11 @@ import {
} from "./certificate-constants";
export const formatUsageName = (usage: string): string => {
try {
if (Object.values(CertKeyUsageType).includes(usage as CertKeyUsageType)) {
return formatKeyUsage(usage as CertKeyUsageType);
}
if (Object.values(CertExtendedKeyUsageType).includes(usage as CertExtendedKeyUsageType)) {
return formatExtendedKeyUsage(usage as CertExtendedKeyUsageType);
}
} catch {
// Handle any errors in type checking
if (Object.values(CertKeyUsageType).includes(usage as CertKeyUsageType)) {
return formatKeyUsage(usage as CertKeyUsageType);
}
if (Object.values(CertExtendedKeyUsageType).includes(usage as CertExtendedKeyUsageType)) {
return formatExtendedKeyUsage(usage as CertExtendedKeyUsageType);
}
return usage.replace(/_/g, " ");
};