import { useMemo, useState } from "react"; import { faBan, faCertificate, faEllipsis, faEye, faFileExport, faRedo, faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { format } from "date-fns"; import { CircleQuestionMarkIcon } from "lucide-react"; import { twMerge } from "tailwind-merge"; import { createNotification } from "@app/components/notifications"; import { ProjectPermissionCan } from "@app/components/permissions"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, EmptyState, Pagination, Table, TableContainer, TableSkeleton, TBody, Td, Th, THead, Tooltip, Tr } from "@app/components/v2"; import { Badge } from "@app/components/v3"; import { ProjectPermissionCertificateActions, ProjectPermissionSub, useProject, useSubscription } from "@app/context"; import { useListWorkspaceCertificates, useUpdateRenewalConfig } from "@app/hooks/api"; import { caSupportsCapability } from "@app/hooks/api/ca/constants"; import { CaCapability, CaType } from "@app/hooks/api/ca/enums"; import { useListCasByProjectId } from "@app/hooks/api/ca/queries"; import { CertStatus } from "@app/hooks/api/certificates/enums"; import { TCertificate } from "@app/hooks/api/certificates/types"; import { UsePopUpState } from "@app/hooks/usePopUp"; import { getCertValidUntilBadgeDetails } from "./CertificatesTable.utils"; const isExpiringWithinOneDay = (notAfter: string): boolean => { const expiryDate = new Date(notAfter); const now = new Date(); const oneDayFromNow = new Date(now.getTime() + 24 * 60 * 60 * 1000); return expiryDate <= oneDayFromNow; }; const getAutoRenewalInfo = (certificate: TCertificate) => { if (certificate.renewedByCertificateId) { return { text: "Renewed", variant: "neutral" as const }; } const isRevoked = certificate.status === CertStatus.REVOKED; const isExpired = new Date(certificate.notAfter) < new Date(); const hasNoProfile = !certificate.profileId; const isExpiringWithinDay = isExpiringWithinOneDay(certificate.notAfter); if (isRevoked) { return { text: "Not Available", variant: "neutral" as const, tooltip: "Renewal is not available for revoked certificates" }; } if (isExpired) { return { text: "Not Available", variant: "neutral" as const, tooltip: "Renewal is not available for expired certificates" }; } if (hasNoProfile) { return { text: "Not Available", variant: "neutral" as const, tooltip: "Renewal requires a certificate profile" }; } if (certificate.hasPrivateKey === false) { return { text: "Not Available", variant: "neutral" as const, tooltip: "Renewal is not available for certificates with externally generated private keys" }; } if (isExpiringWithinDay) { return { text: "Not Available", variant: "neutral" as const, tooltip: "Auto-renewal is not available for certificates expiring within 24 hours" }; } if (certificate.renewalError) { return { text: "Failed", variant: "danger" as const, tooltip: certificate.renewalError }; } if (!certificate.renewBeforeDays) { return { text: "Auto-Renewal Disabled", variant: "warning" as const }; } const notAfterDate = new Date(certificate.notAfter); const renewalDate = new Date( notAfterDate.getTime() - certificate.renewBeforeDays * 24 * 60 * 60 * 1000 ); const now = new Date(); if (renewalDate <= now) { return { text: "Due Now", variant: "danger" as const }; } const daysUntilRenewal = Math.floor( (renewalDate.getTime() - now.getTime()) / (24 * 60 * 60 * 1000) ); if (daysUntilRenewal === 0) { return { text: "Renews today", variant: "warning" as const }; } if (daysUntilRenewal <= 7) { return { text: `Renews in ${daysUntilRenewal}d`, variant: "warning" as const }; } return { text: `Renews in ${daysUntilRenewal}d`, variant: "success" as const }; }; type Props = { handlePopUpOpen: ( popUpName: keyof UsePopUpState< [ "certificate", "deleteCertificate", "revokeCertificate", "certificateCert", "manageRenewal", "renewCertificate" ] >, data?: { serialNumber?: string; commonName?: string; certificateId?: string; profileId?: string; renewBeforeDays?: number; ttlDays?: number; notAfter?: string; renewalError?: string; renewedFromCertificateId?: string; renewedByCertificateId?: string; } ) => void; }; const PER_PAGE_INIT = 25; export const CertificatesTable = ({ handlePopUpOpen }: Props) => { const [page, setPage] = useState(1); const [perPage, setPerPage] = useState(PER_PAGE_INIT); const { subscription } = useSubscription(); const { currentProject } = useProject(); const { data, isPending } = useListWorkspaceCertificates({ projectId: currentProject?.id ?? "", offset: (page - 1) * perPage, limit: perPage }); const { mutateAsync: updateRenewalConfig } = useUpdateRenewalConfig(); const isLegacyTemplatesEnabled = subscription.pkiLegacyTemplates; const { data: caData } = useListCasByProjectId(currentProject?.id ?? ""); const caCapabilityMap = useMemo(() => { if (!caData) return {}; const map: Record = {}; caData.forEach((ca) => { map[ca.id] = ca.type; }); return map; }, [caData]); const handleDisableAutoRenewal = async (certificateId: string, commonName: string) => { if (!currentProject?.slug) { createNotification({ text: "Unable to disable auto-renewal: Project not found. Please refresh the page and try again.", type: "error" }); return; } await updateRenewalConfig({ certificateId, projectSlug: currentProject.slug, enableAutoRenewal: false }); createNotification({ text: `Auto-renewal disabled for ${commonName}`, type: "success" }); }; return ( {isPending && } {!isPending && data?.certificates.map((certificate) => { const { variant, label } = getCertValidUntilBadgeDetails(certificate.notAfter); const autoRenewalInfo = getAutoRenewalInfo(certificate); const isRevoked = certificate.status === CertStatus.REVOKED; const isExpired = new Date(certificate.notAfter) < new Date(); const isExpiringWithinDay = isExpiringWithinOneDay(certificate.notAfter); const hasFailed = Boolean(certificate.renewalError); const isAutoRenewalEnabled = Boolean( certificate.renewBeforeDays && certificate.renewBeforeDays > 0 ); return ( ); })}
Common Name Status Not Before Not After Renewal Status
{certificate.commonName} {certificate.status === CertStatus.REVOKED ? ( Revoked ) : ( {label} )} {certificate.notBefore ? format(new Date(certificate.notBefore), "yyyy-MM-dd") : "-"} {certificate.notAfter ? format(new Date(certificate.notAfter), "yyyy-MM-dd") : "-"} {autoRenewalInfo && (autoRenewalInfo.tooltip ? (
{autoRenewalInfo.text}
) : ( {autoRenewalInfo.text} ))}
{(isAllowed) => ( handlePopUpOpen("certificateCert", { serialNumber: certificate.serialNumber }) } disabled={!isAllowed} icon={} > Export Certificate )} {isLegacyTemplatesEnabled && ( {(isAllowed) => ( handlePopUpOpen("certificate", { serialNumber: certificate.serialNumber }) } disabled={!isAllowed} icon={} > View Details )} )} {/* Manage auto renewal option - not shown for failed renewals */} {(() => { const canManageRenewal = certificate.profileId && certificate.hasPrivateKey !== false && !certificate.renewedByCertificateId && !isRevoked && !isExpired && !hasFailed && !isExpiringWithinDay; if (!canManageRenewal) return null; return ( {(isAllowed) => { return ( { const notAfterDate = new Date(certificate.notAfter); const notBeforeDate = certificate.notBefore ? new Date(certificate.notBefore) : new Date( notAfterDate.getTime() - 365 * 24 * 60 * 60 * 1000 ); const ttlDays = Math.max( 1, Math.ceil( (notAfterDate.getTime() - notBeforeDate.getTime()) / (24 * 60 * 60 * 1000) ) ); handlePopUpOpen("manageRenewal", { certificateId: certificate.id, commonName: certificate.commonName, profileId: certificate.profileId, renewBeforeDays: certificate.renewBeforeDays, ttlDays, notAfter: certificate.notAfter, renewalError: certificate.renewalError, renewedFromCertificateId: certificate.renewedFromCertificateId, renewedByCertificateId: certificate.renewedByCertificateId }); }} disabled={!isAllowed} icon={} > {isAutoRenewalEnabled ? "Manage auto renewal" : "Enable auto renewal"} ); }} ); })()} {/* Disable auto renewal option - only shown when auto renewal is active */} {(() => { const canDisableRenewal = certificate.profileId && certificate.hasPrivateKey !== false && !certificate.renewedByCertificateId && !isRevoked && !isExpired && !isExpiringWithinDay && isAutoRenewalEnabled; if (!canDisableRenewal) return null; return ( {(isAllowed) => ( { await handleDisableAutoRenewal( certificate.id, certificate.commonName ); }} disabled={!isAllowed} icon={} > Disable auto renewal )} ); })()} {/* Manual renewal action for profile-issued certificates that are not revoked/expired (including failed ones) */} {(() => { const canRenew = certificate.profileId && certificate.hasPrivateKey !== false && !certificate.renewedByCertificateId && !isRevoked && !isExpired; if (!canRenew) return null; return ( {(isAllowed) => ( { handlePopUpOpen("renewCertificate", { certificateId: certificate.id, commonName: certificate.commonName }); }} disabled={!isAllowed} icon={} > Renew Now )} ); })()} {/* Only show revoke button if CA supports revocation */} {(() => { const caType = caCapabilityMap[certificate.caId]; const supportsRevocation = !caType || caSupportsCapability(caType, CaCapability.REVOKE_CERTIFICATES); if (!supportsRevocation) { return null; } return ( {(isAllowed) => ( handlePopUpOpen("revokeCertificate", { serialNumber: certificate.serialNumber }) } disabled={!isAllowed} icon={} > Revoke Certificate )} ); })()} {(isAllowed) => ( handlePopUpOpen("deleteCertificate", { serialNumber: certificate.serialNumber, commonName: certificate.commonName }) } disabled={!isAllowed} icon={} > Delete Certificate )}
{!isPending && data?.totalCount !== undefined && data.totalCount >= PER_PAGE_INIT && ( setPage(newPage)} onChangePerPage={(newPerPage) => setPerPage(newPerPage)} /> )} {!isPending && !data?.certificates?.length && ( )}
); };