diff --git a/backend/src/server/routes/v1/certificate-profiles-router.ts b/backend/src/server/routes/v1/certificate-profiles-router.ts index 6d9bdcc66..cc39b5d48 100644 --- a/backend/src/server/routes/v1/certificate-profiles-router.ts +++ b/backend/src/server/routes/v1/certificate-profiles-router.ts @@ -34,7 +34,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid estConfig: z .object({ disableBootstrapCaValidation: z.boolean().default(false), - passphrase: z.string().min(1), + passphraseInput: z.string().min(1), encryptedCaChain: z.string() }) .optional(), @@ -332,7 +332,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid estConfig: z .object({ disableBootstrapCaValidation: z.boolean().default(false), - passphrase: z.string().min(1), + passphraseInput: z.string().min(1), encryptedCaChain: z.string() }) .optional(), diff --git a/backend/src/server/routes/v3/certificates-router.ts b/backend/src/server/routes/v3/certificates-router.ts index 7e50eeee5..1e5b093a5 100644 --- a/backend/src/server/routes/v3/certificates-router.ts +++ b/backend/src/server/routes/v3/certificates-router.ts @@ -26,6 +26,21 @@ import { import { mapEnumsForValidation } from "@app/services/certificate-common/certificate-utils"; import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators"; +const validateTtlAndDateFields = (data: { notBefore?: string; notAfter?: string; ttl?: string }) => { + const hasDateFields = data.notBefore || data.notAfter; + const hasTtl = data.ttl; + return !(hasDateFields && hasTtl); +}; + +const validateDateOrder = (data: { notBefore?: string; notAfter?: string }) => { + if (data.notBefore && data.notAfter) { + const notBefore = new Date(data.notBefore); + const notAfter = new Date(data.notAfter); + return notBefore < notAfter; + } + return true; +}; + export const registerCertificatesRouter = async (server: FastifyZodProvider) => { server.route({ method: "POST", @@ -49,30 +64,13 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) => signatureAlgorithm: z.nativeEnum(CertSignatureAlgorithm).optional(), keyAlgorithm: z.nativeEnum(CertKeyAlgorithm).optional() }) - .refine( - (data) => { - const hasDateFields = data.notBefore || data.notAfter; - const hasTtl = data.ttl; - return !(hasDateFields && hasTtl); - }, - { - message: - "Cannot specify both TTL and notBefore/notAfter. Use either TTL for duration-based validity or notBefore/notAfter for explicit date range." - } - ) - .refine( - (data) => { - if (data.notBefore && data.notAfter) { - const notBefore = new Date(data.notBefore); - const notAfter = new Date(data.notAfter); - return notBefore < notAfter; - } - return true; - }, - { - message: "notBefore must be earlier than notAfter" - } - ), + .refine(validateTtlAndDateFields, { + message: + "Cannot specify both TTL and notBefore/notAfter. Use either TTL for duration-based validity or notBefore/notAfter for explicit date range." + }) + .refine(validateDateOrder, { + message: "notBefore must be earlier than notAfter" + }), response: { 200: z.object({ certificate: z.string().trim(), @@ -169,30 +167,13 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) => notBefore: validateCaDateField.optional(), notAfter: validateCaDateField.optional() }) - .refine( - (data) => { - const hasDateFields = data.notBefore || data.notAfter; - const hasTtl = data.ttl; - return !(hasDateFields && hasTtl); - }, - { - message: - "Cannot specify both TTL and notBefore/notAfter. Use either TTL for duration-based validity or notBefore/notAfter for explicit date range." - } - ) - .refine( - (data) => { - if (data.notBefore && data.notAfter) { - const notBefore = new Date(data.notBefore); - const notAfter = new Date(data.notAfter); - return notBefore < notAfter; - } - return true; - }, - { - message: "notBefore must be earlier than notAfter" - } - ), + .refine(validateTtlAndDateFields, { + message: + "Cannot specify both TTL and notBefore/notAfter. Use either TTL for duration-based validity or notBefore/notAfter for explicit date range." + }) + .refine(validateDateOrder, { + message: "notBefore must be earlier than notAfter" + }), response: { 200: z.object({ certificate: z.string().trim(), @@ -266,30 +247,13 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) => signatureAlgorithm: z.nativeEnum(CertSignatureAlgorithm).optional(), keyAlgorithm: z.nativeEnum(CertKeyAlgorithm).optional() }) - .refine( - (data) => { - const hasDateFields = data.notBefore || data.notAfter; - const hasTtl = data.ttl; - return !(hasDateFields && hasTtl); - }, - { - message: - "Cannot specify both TTL and notBefore/notAfter. Use either TTL for duration-based validity or notBefore/notAfter for explicit date range." - } - ) - .refine( - (data) => { - if (data.notBefore && data.notAfter) { - const notBefore = new Date(data.notBefore); - const notAfter = new Date(data.notAfter); - return notBefore < notAfter; - } - return true; - }, - { - message: "notBefore must be earlier than notAfter" - } - ), + .refine(validateTtlAndDateFields, { + message: + "Cannot specify both TTL and notBefore/notAfter. Use either TTL for duration-based validity or notBefore/notAfter for explicit date range." + }) + .refine(validateDateOrder, { + message: "notBefore must be earlier than notAfter" + }), response: { 200: z.object({ orderId: z.string(), diff --git a/backend/src/services/certificate-authority/internal/internal-certificate-authority-types.ts b/backend/src/services/certificate-authority/internal/internal-certificate-authority-types.ts index d05596c56..d402b20c1 100644 --- a/backend/src/services/certificate-authority/internal/internal-certificate-authority-types.ts +++ b/backend/src/services/certificate-authority/internal/internal-certificate-authority-types.ts @@ -3,7 +3,12 @@ import { z } from "zod"; import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal"; import { TProjectPermission } from "@app/lib/types"; import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; -import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types"; +import { + CertExtendedKeyUsage, + CertKeyAlgorithm, + CertKeyUsage, + CertSignatureAlgorithm +} from "@app/services/certificate/certificate-types"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TProjectDALFactory } from "@app/services/project/project-dal"; @@ -131,8 +136,8 @@ export type TIssueCertFromCaDTO = { notAfter?: string; keyUsages?: CertKeyUsage[]; extendedKeyUsages?: CertExtendedKeyUsage[]; - signatureAlgorithm?: string; - keyAlgorithm?: string; + signatureAlgorithm?: CertSignatureAlgorithm; + keyAlgorithm?: CertKeyAlgorithm; } & Omit; export type TSignCertFromCaDTO = diff --git a/backend/src/services/certificate-est-v3/certificate-est-v3-service.ts b/backend/src/services/certificate-est-v3/certificate-est-v3-service.ts index a22cef37b..f287614a0 100644 --- a/backend/src/services/certificate-est-v3/certificate-est-v3-service.ts +++ b/backend/src/services/certificate-est-v3/certificate-est-v3-service.ts @@ -94,14 +94,13 @@ export const certificateEstV3ServiceFactory = ({ kmsId: certificateManagerKmsId }); - let decryptedCaChain = ""; - if (estConfig.encryptedCaChain) { - decryptedCaChain = ( - await kmsDecryptor({ - cipherTextBlob: estConfig.encryptedCaChain - }) - ).toString(); - } + const decryptedCaChain = estConfig.encryptedCaChain + ? ( + await kmsDecryptor({ + cipherTextBlob: estConfig.encryptedCaChain + }) + ).toString() + : ""; const caCerts = extractX509CertFromChain(decryptedCaChain)?.map((cert) => { return new x509.X509Certificate(cert); @@ -114,7 +113,7 @@ export const certificateEstV3ServiceFactory = ({ const leafCertificate = extractX509CertFromChain(decodeURIComponent(sslClientCert))?.[0]; if (!leafCertificate) { - throw new BadRequestError({ message: "Missing client certificate" }); + throw new UnauthorizedError({ message: "Missing client certificate" }); } const certObj = new x509.X509Certificate(leafCertificate); diff --git a/backend/src/services/certificate-profile/certificate-profile-service.test.ts b/backend/src/services/certificate-profile/certificate-profile-service.test.ts index ff5e918a2..7f390e580 100644 --- a/backend/src/services/certificate-profile/certificate-profile-service.test.ts +++ b/backend/src/services/certificate-profile/certificate-profile-service.test.ts @@ -711,7 +711,7 @@ describe("CertificateProfileService", () => { certificateTemplateId: "template-123", estConfig: { disableBootstrapCaValidation: false, - passphrase: "secret-passphrase", + passphraseInput: "secret-passphrase", encryptedCaChain: Buffer.from("test-ca-chain-data").toString("base64") } }; diff --git a/backend/src/services/certificate-profile/certificate-profile-service.ts b/backend/src/services/certificate-profile/certificate-profile-service.ts index 037775916..35c390af6 100644 --- a/backend/src/services/certificate-profile/certificate-profile-service.ts +++ b/backend/src/services/certificate-profile/certificate-profile-service.ts @@ -27,6 +27,21 @@ import { TCertificateProfileWithRawMetrics } from "./certificate-profile-types"; +const validateAndEncodeBase64CaChain = (caChain: unknown) => { + try { + if (typeof caChain !== "string") { + throw new BadRequestError({ message: "CA chain must be a string" }); + } + const buffer = Buffer.from(caChain, "base64"); + if (buffer.toString("base64") !== caChain) { + throw new BadRequestError({ message: "Invalid Base64 encoding in CA chain data" }); + } + return { encryptedCaChain: buffer }; + } catch (error) { + throw new BadRequestError({ message: "Failed to decode CA chain data: Invalid Base64 format" }); + } +}; + export type TCertificateProfileCreateData = Omit & { estConfig?: TEstConfigData; apiConfig?: TApiConfigData; @@ -125,7 +140,7 @@ export const certificateProfileServiceFactory = ({ if (data.enrollmentType === EnrollmentType.EST && data.estConfig) { const appCfg = getConfig(); // Hash the passphrase - const hashedPassphrase = await crypto.hashing().createHash(data.estConfig.passphrase, appCfg.SALT_ROUNDS); + const hashedPassphrase = await crypto.hashing().createHash(data.estConfig.passphraseInput, appCfg.SALT_ROUNDS); let encryptedCaChainBuffer: Buffer; try { @@ -243,24 +258,10 @@ export const certificateProfileServiceFactory = ({ existingProfile.estConfigId, { disableBootstrapCaValidation: estConfig.disableBootstrapCaValidation, - ...(estConfig.passphrase && { - hashedPassphrase: await crypto.hashing().createHash(estConfig.passphrase, getConfig().SALT_ROUNDS) + ...(estConfig.passphraseInput && { + hashedPassphrase: await crypto.hashing().createHash(estConfig.passphraseInput, getConfig().SALT_ROUNDS) }), - ...(estConfig.caChain && - (() => { - try { - if (typeof estConfig.caChain !== "string") { - throw new BadRequestError({ message: "CA chain must be a string" }); - } - const buffer = Buffer.from(estConfig.caChain, "base64"); - if (buffer.toString("base64") !== estConfig.caChain) { - throw new BadRequestError({ message: "Invalid Base64 encoding in CA chain data" }); - } - return { encryptedCaChain: buffer }; - } catch (error) { - throw new BadRequestError({ message: "Failed to decode CA chain data: Invalid Base64 format" }); - } - })()) + ...(estConfig.caChain && validateAndEncodeBase64CaChain(estConfig.caChain)) }, tx ); diff --git a/backend/src/services/certificate-profile/certificate-profile-types.ts b/backend/src/services/certificate-profile/certificate-profile-types.ts index e40a54810..76bfd004a 100644 --- a/backend/src/services/certificate-profile/certificate-profile-types.ts +++ b/backend/src/services/certificate-profile/certificate-profile-types.ts @@ -21,7 +21,7 @@ export type TCertificateProfileUpdate = Omit { const parts = sigAlg.split("-"); + if (parts.length === 0) { + return false; + } const keyType = parts[parts.length - 1]; if (caKeyAlgorithm.startsWith("RSA")) { @@ -221,8 +228,8 @@ export const certificateV3ServiceFactory = ({ validateAlgorithmCompatibility(ca, template); - const effectiveSignatureAlgorithm = certificateRequest.signatureAlgorithm; - const effectiveKeyAlgorithm = certificateRequest.keyAlgorithm; + const effectiveSignatureAlgorithm = certificateRequest.signatureAlgorithm as CertSignatureAlgorithm | undefined; + const effectiveKeyAlgorithm = certificateRequest.keyAlgorithm as CertKeyAlgorithm | undefined; if (template.algorithms?.keyAlgorithm && !effectiveKeyAlgorithm) { throw new BadRequestError({ @@ -350,7 +357,7 @@ export const certificateV3ServiceFactory = ({ caId: ca.id, csr, ttl: validity.ttl, - altNames: "", + altNames: undefined, notBefore: normalizeDateForApi(notBefore), notAfter: normalizeDateForApi(notAfter), signatureAlgorithm: effectiveSignatureAlgorithm, diff --git a/backend/src/services/certificate/certificate-types.ts b/backend/src/services/certificate/certificate-types.ts index ff231e88d..fac7913a7 100644 --- a/backend/src/services/certificate/certificate-types.ts +++ b/backend/src/services/certificate/certificate-types.ts @@ -114,6 +114,13 @@ export type TGetCertificateCredentialsDTO = { kmsService: Pick; }; +export enum CertSubjectAlternativeNameType { + DNS_NAME = "dns_name", + IP_ADDRESS = "ip_address", + EMAIL = "email", + URI = "uri" +} + export enum TAltNameType { EMAIL = "email", DNS = "dns", @@ -121,12 +128,21 @@ export enum TAltNameType { URL = "url" } -export enum CertSubjectAlternativeNameType { - DNS_NAME = "dns_name", - IP_ADDRESS = "ip_address", - EMAIL = "email", - URI = "uri" -} +export const mapLegacyAltNameType = (legacyType: TAltNameType): CertSubjectAlternativeNameType => { + switch (legacyType) { + case TAltNameType.EMAIL: + return CertSubjectAlternativeNameType.EMAIL; + case TAltNameType.DNS: + return CertSubjectAlternativeNameType.DNS_NAME; + case TAltNameType.IP: + return CertSubjectAlternativeNameType.IP_ADDRESS; + case TAltNameType.URL: + return CertSubjectAlternativeNameType.URI; + default: + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + throw new Error(`Unknown legacy alt name type: ${legacyType}`); + } +}; export type TAltNameMapping = { type: TAltNameType; value: string; diff --git a/backend/src/services/enrollment-config/api-enrollment-config-dal.ts b/backend/src/services/enrollment-config/api-enrollment-config-dal.ts index 1768c76e9..1edfdae6c 100644 --- a/backend/src/services/enrollment-config/api-enrollment-config-dal.ts +++ b/backend/src/services/enrollment-config/api-enrollment-config-dal.ts @@ -52,21 +52,25 @@ export const apiEnrollmentConfigDALFactory = (db: TDbClient) => { } }; - const findProfilesForAutoRenewal = async (renewalThresholdDays: number = 30, tx?: Knex) => { + const findProfilesForAutoRenewal = async (renewalThresholdDays: number = 30, projectId?: string, tx?: Knex) => { try { - const profiles = await (tx || db)(TableName.PkiCertificateProfile) + let query = (tx || db)(TableName.PkiCertificateProfile) .join( TableName.PkiApiEnrollmentConfig, `${TableName.PkiCertificateProfile}.apiConfigId`, `${TableName.PkiApiEnrollmentConfig}.id` ) - .where(`${TableName.PkiApiEnrollmentConfig}.autoRenew`, true) - .where((query) => { - void query.where((qb) => { - void qb - .whereNull(`${TableName.PkiApiEnrollmentConfig}.autoRenewDays`) - .orWhere(`${TableName.PkiApiEnrollmentConfig}.autoRenewDays`, "<=", renewalThresholdDays); - }); + .where(`${TableName.PkiApiEnrollmentConfig}.autoRenew`, true); + + if (projectId) { + query = query.where(`${TableName.PkiCertificateProfile}.projectId`, projectId); + } + + const profiles = await query + .where((qb) => { + void qb + .whereNull(`${TableName.PkiApiEnrollmentConfig}.autoRenewDays`) + .orWhere(`${TableName.PkiApiEnrollmentConfig}.autoRenewDays`, "<=", renewalThresholdDays); }) .select((tx || db).ref("id").withSchema(TableName.PkiCertificateProfile)) .select((tx || db).ref("name").withSchema(TableName.PkiCertificateProfile)) @@ -83,7 +87,20 @@ export const apiEnrollmentConfigDALFactory = (db: TDbClient) => { try { const doc = await (tx || db)(TableName.PkiCertificateProfile).where({ apiConfigId: configId }).count("*").first(); - return parseInt((doc as { count?: string })?.count || "0", 10); + if (!doc || typeof doc !== "object") { + return 0; + } + + const countValue = (doc as Record).count; + if (typeof countValue === "number") { + return countValue; + } + if (typeof countValue === "string") { + const parsed = parseInt(countValue, 10); + return Number.isNaN(parsed) ? 0 : parsed; + } + + return 0; } catch (error) { throw new DatabaseError({ error, name: "Check if API enrollment config is in use" }); } diff --git a/backend/src/services/enrollment-config/enrollment-config-types.ts b/backend/src/services/enrollment-config/enrollment-config-types.ts index 329d98278..8ab55a7d4 100644 --- a/backend/src/services/enrollment-config/enrollment-config-types.ts +++ b/backend/src/services/enrollment-config/enrollment-config-types.ts @@ -19,7 +19,7 @@ export type TApiEnrollmentConfigUpdate = TPkiApiEnrollmentConfigsUpdate; export interface TEstConfigData { disableBootstrapCaValidation: boolean; - passphrase: string; + passphraseInput: string; encryptedCaChain: string; } diff --git a/frontend/src/context/ProjectPermissionContext/types.ts b/frontend/src/context/ProjectPermissionContext/types.ts index 0899d6b2a..36685d542 100644 --- a/frontend/src/context/ProjectPermissionContext/types.ts +++ b/frontend/src/context/ProjectPermissionContext/types.ts @@ -129,7 +129,8 @@ export enum ProjectPermissionCertificateProfileActions { Create = "create", Edit = "edit", Delete = "delete", - IssueCert = "issue-cert" + IssueCert = "issue-cert", + ListCerts = "list-certs" } export enum ProjectPermissionSecretRotationActions { diff --git a/frontend/src/layouts/PkiManagerLayout/PkiManagerLayout.tsx b/frontend/src/layouts/PkiManagerLayout/PkiManagerLayout.tsx index 66c73abcb..2deefb599 100644 --- a/frontend/src/layouts/PkiManagerLayout/PkiManagerLayout.tsx +++ b/frontend/src/layouts/PkiManagerLayout/PkiManagerLayout.tsx @@ -176,7 +176,7 @@ export const PkiManagerLayout = () => { }} > {({ isActive }) => ( - +
@@ -195,7 +195,7 @@ export const PkiManagerLayout = () => { }} > {({ isActive }) => ( - +
diff --git a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaModal.tsx b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaModal.tsx index e726f2fce..5bc606c45 100644 --- a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaModal.tsx +++ b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaModal.tsx @@ -140,8 +140,8 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => { maxPathLength: ca.configuration.maxPathLength ? String(ca.configuration.maxPathLength) : "", - keyAlgorithm: (Object.values(CertKeyAlgorithm) as string[]).includes( - ca.configuration.keyAlgorithm + keyAlgorithm: Object.values(CertKeyAlgorithm).includes( + ca.configuration.keyAlgorithm as CertKeyAlgorithm ) ? ca.configuration.keyAlgorithm : CertKeyAlgorithm.RSA_2048 diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesSection.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesSection.tsx index cfeec2ac2..bce21123b 100644 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesSection.tsx +++ b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesSection.tsx @@ -25,7 +25,7 @@ export const CertificatesSection = () => { const { subscription } = useSubscription(); const { mutateAsync: deleteCert } = useDeleteCert(); - const useOldCertificateFlow = subscription.pkiLegacyTemplates; + const isLegacyTemplatesEnabled = subscription.pkiLegacyTemplates; const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ "certificateIssuance", @@ -80,7 +80,7 @@ export const CertificatesSection = () => { type="submit" leftIcon={} onClick={() => - handlePopUpOpen(useOldCertificateFlow ? "certificate" : "certificateIssuance") + handlePopUpOpen(isLegacyTemplatesEnabled ? "certificate" : "certificateIssuance") } isDisabled={!isAllowed} > @@ -91,7 +91,7 @@ export const CertificatesSection = () => {
- {useOldCertificateFlow ? ( + {isLegacyTemplatesEnabled ? ( ) : ( diff --git a/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscriberSection.tsx b/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscriberSection.tsx index 9f6e6c60d..cb4b9ef39 100644 --- a/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscriberSection.tsx +++ b/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscriberSection.tsx @@ -22,7 +22,7 @@ export const PkiSubscriberSection = () => { const { subscription } = useSubscription(); const projectId = currentProject.id; - const allowNewSubscriberCreation = subscription.pkiLegacyTemplates; + const canCreateLegacySubscribers = subscription.pkiLegacyTemplates; const { mutateAsync: deletePkiSubscriber } = useDeletePkiSubscriber(); const { mutateAsync: updatePkiSubscriber } = useUpdatePkiSubscriber(); @@ -104,7 +104,7 @@ export const PkiSubscriberSection = () => { /> - {allowNewSubscriberCreation && ( + {canCreateLegacySubscribers && ( { + if (!metrics) { + return ( + + No metrics + + ); + } + + if (metrics.totalCertificates === 0) { + return ( + + No certificates + + ); + } + + return ( + <> + {metrics.activeCertificates > 0 && ( + + {metrics.activeCertificates} active + + )} + {metrics.expiringCertificates > 0 && ( + + {metrics.expiringCertificates} expiring + + )} + {metrics.expiredCertificates > 0 && ( + + {metrics.expiredCertificates} expired + + )} + {metrics.revokedCertificates > 0 && ( + + {metrics.revokedCertificates} revoked + + )} + + ); +}; + interface Props { profile: TCertificateProfile; onEditProfile: (profile: TCertificateProfile) => void; @@ -58,11 +110,8 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) = type: "info" }); - const timer = setTimeout(() => setIsIdCopied.off(), 2000); - - // eslint-disable-next-line consistent-return - return () => clearTimeout(timer); - }, [isIdCopied, setIsIdCopied]); + setTimeout(() => setIsIdCopied.off(), 2000); + }, [setIsIdCopied]); const { data: templateData } = useGetCertificateTemplateV2ById({ templateId: profile.certificateTemplateId @@ -122,40 +171,7 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
- {profile.metrics ? ( - profile.metrics.totalCertificates === 0 ? ( - - No certificates - - ) : ( - <> - {profile.metrics.activeCertificates > 0 && ( - - {profile.metrics.activeCertificates} active - - )} - {profile.metrics.expiringCertificates > 0 && ( - - {profile.metrics.expiringCertificates} expiring - - )} - {profile.metrics.expiredCertificates > 0 && ( - - {profile.metrics.expiredCertificates} expired - - )} - {profile.metrics.revokedCertificates > 0 && ( - - {profile.metrics.revokedCertificates} revoked - - )} - - ) - ) : ( - - No metrics - - )} +
diff --git a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/CertificateTemplatesV2Tab.tsx b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/CertificateTemplatesV2Tab.tsx index 234a2850e..b4806a503 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/CertificateTemplatesV2Tab.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/CertificateTemplatesV2Tab.tsx @@ -6,7 +6,7 @@ import { createNotification } from "@app/components/notifications"; import { Button, DeleteActionModal } from "@app/components/v2"; import { useProjectPermission } from "@app/context"; import { - ProjectPermissionActions, + ProjectPermissionPkiTemplateActions, ProjectPermissionSub } from "@app/context/ProjectPermissionContext/types"; import { useDeleteCertificateTemplateV2New } from "@app/hooks/api/certificateTemplates/mutations"; @@ -26,8 +26,8 @@ export const CertificateTemplatesV2Tab = () => { const deleteTemplateV2 = useDeleteCertificateTemplateV2New(); const canCreateTemplate = permission.can( - ProjectPermissionActions.Create, - ProjectPermissionSub.CertificateAuthorities + ProjectPermissionPkiTemplateActions.Create, + ProjectPermissionSub.CertificateTemplates ); const handleCreateTemplate = () => { diff --git a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/TemplateList.tsx b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/TemplateList.tsx index d877e2924..178fe7d17 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/TemplateList.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/TemplateList.tsx @@ -42,6 +42,10 @@ export const TemplateList = ({ onEditTemplate, onDeleteTemplate }: Props) => { const templates = data?.certificateTemplates || []; + if (!currentProject?.id) { + return null; + } + const canEditTemplate = permission.can( ProjectPermissionPkiTemplateActions.Edit, ProjectPermissionSub.CertificateTemplates diff --git a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/shared/schemas.ts b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/shared/schemas.ts index f96bb1b2e..b4f6fc9eb 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/shared/schemas.ts +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/shared/schemas.ts @@ -114,17 +114,7 @@ export const apiTemplateSchema = z.object({ .object({ max: z .string() - .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'") + .regex(/^[1-9]\d*[dhmy]$/, "Must be in format like '365d', '12m', '1y', or '24h'") .optional() }) .optional()