mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-20 14:42:51 +00:00
Merge pull request #4777 from Infisical/feat/pki-sync-v2
PKI Syncs: Certificate Syncs and AWS ARN option
This commit is contained in:
+1
-1
@@ -124,7 +124,7 @@ type Props = {
|
||||
|
||||
const caTypes = [
|
||||
{ label: "ACME", value: CaType.ACME },
|
||||
{ label: "Azure AD Certificate Service", value: CaType.AZURE_AD_CS }
|
||||
{ label: "Active Directory Certificate Services (AD CS)", value: CaType.AZURE_AD_CS }
|
||||
];
|
||||
|
||||
export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { PageHeader } from "@app/components/v2";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionCertificateActions,
|
||||
ProjectPermissionSub,
|
||||
useProjectPermission
|
||||
} from "@app/context";
|
||||
import { ProjectType } from "@app/hooks/api/projects/types";
|
||||
|
||||
import { PkiCollectionSection } from "../AlertingPage/components";
|
||||
import { CertificatesSection } from "./components";
|
||||
|
||||
export const CertificatesPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const { permission } = useProjectPermission();
|
||||
|
||||
const canAccessPkiColl = permission.can(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.PkiCollections
|
||||
);
|
||||
const canAccessCerts = permission.can(
|
||||
ProjectPermissionCertificateActions.Read,
|
||||
ProjectPermissionSub.Certificates
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex h-full flex-col justify-between bg-bunker-800 text-white">
|
||||
<Helmet>
|
||||
<title>{t("common.head-title", { title: "Certificates" })}</title>
|
||||
</Helmet>
|
||||
<div className="mx-auto mb-6 w-full max-w-8xl">
|
||||
<PageHeader
|
||||
scope={ProjectType.CertificateManager}
|
||||
title="Certificates"
|
||||
description="View and track issued certificates, monitor expiration dates, and manage certificate lifecycles."
|
||||
/>
|
||||
{/* If both are false, the section does not render. This is to prevent duplicate banners. */}
|
||||
{(canAccessCerts || canAccessPkiColl) && (
|
||||
<ProjectPermissionCan
|
||||
renderGuardBanner
|
||||
I={ProjectPermissionActions.Read}
|
||||
a={ProjectPermissionSub.PkiCollections}
|
||||
>
|
||||
<PkiCollectionSection />
|
||||
</ProjectPermissionCan>
|
||||
)}
|
||||
<ProjectPermissionCan
|
||||
renderGuardBanner
|
||||
I={ProjectPermissionCertificateActions.Read}
|
||||
a={ProjectPermissionSub.Certificates}
|
||||
>
|
||||
<CertificatesSection />
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+3
-2
@@ -122,11 +122,12 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
||||
|
||||
const { data: profilesData } = useListCertificateProfiles({
|
||||
projectId: currentProject?.id || "",
|
||||
includeMetrics: false,
|
||||
enrollmentType: "api"
|
||||
});
|
||||
|
||||
const { mutateAsync: createCertificate } = useCreateCertificateV3();
|
||||
const { mutateAsync: createCertificate } = useCreateCertificateV3({
|
||||
projectId: currentProject?.id
|
||||
});
|
||||
|
||||
const formResolver = useMemo(() => {
|
||||
return zodResolver(createSchema(shouldShowSubjectSection));
|
||||
|
||||
+296
@@ -0,0 +1,296 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { faPlus, faSearch } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
EmptyState,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Pagination,
|
||||
Table,
|
||||
TableContainer,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import { useProject } from "@app/context";
|
||||
import {
|
||||
PkiSync,
|
||||
useAddCertificatesToPkiSync,
|
||||
useListPkiSyncsWithCertificate,
|
||||
useRemoveCertificatesFromPkiSync
|
||||
} from "@app/hooks/api/pkiSyncs";
|
||||
import { IntegrationsListPageTabs } from "@app/types/integrations";
|
||||
|
||||
type Props = {
|
||||
popUp: {
|
||||
isOpen: boolean;
|
||||
data?: {
|
||||
certificateId?: string;
|
||||
commonName?: string;
|
||||
};
|
||||
};
|
||||
handlePopUpToggle: (popUpName: "managePkiSyncs", state?: boolean) => void;
|
||||
};
|
||||
|
||||
const PER_PAGE = 10;
|
||||
|
||||
export const CertificateManagePkiSyncsModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const [selectedSyncIds, setSelectedSyncIds] = useState<Set<string>>(new Set());
|
||||
const [initialSyncIds, setInitialSyncIds] = useState<Set<string>>(new Set());
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
|
||||
const { currentProject } = useProject();
|
||||
const navigate = useNavigate();
|
||||
const { certificateId, commonName } = popUp.data || {};
|
||||
|
||||
const { data: pkiSyncs = [], isPending } = useListPkiSyncsWithCertificate(
|
||||
currentProject?.id || "",
|
||||
certificateId || "",
|
||||
{
|
||||
enabled: !!currentProject?.id && !!certificateId
|
||||
}
|
||||
);
|
||||
const addCertificatesToSync = useAddCertificatesToPkiSync();
|
||||
const removeCertificatesFromSync = useRemoveCertificatesFromPkiSync();
|
||||
|
||||
const filteredSyncs = useMemo(() => {
|
||||
if (!searchTerm.trim()) return pkiSyncs;
|
||||
|
||||
const searchLower = searchTerm.toLowerCase();
|
||||
return pkiSyncs.filter((sync) => sync.name.toLowerCase().includes(searchLower));
|
||||
}, [pkiSyncs, searchTerm]);
|
||||
|
||||
const startIndex = (currentPage - 1) * PER_PAGE;
|
||||
const endIndex = startIndex + PER_PAGE;
|
||||
const paginatedSyncs = filteredSyncs.slice(startIndex, endIndex);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentPage(1);
|
||||
}, [searchTerm]);
|
||||
|
||||
const handleClose = () => {
|
||||
handlePopUpToggle("managePkiSyncs", false);
|
||||
setSelectedSyncIds(new Set());
|
||||
setInitialSyncIds(new Set());
|
||||
setSearchTerm("");
|
||||
setCurrentPage(1);
|
||||
};
|
||||
|
||||
const handleNavigateToPkiSyncs = () => {
|
||||
if (!currentProject?.id) return;
|
||||
|
||||
navigate({
|
||||
to: ROUTE_PATHS.CertManager.IntegrationsListPage.path,
|
||||
params: {
|
||||
projectId: currentProject.id
|
||||
},
|
||||
search: {
|
||||
selectedTab: IntegrationsListPageTabs.PkiSyncs
|
||||
}
|
||||
});
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const getDestinationDisplayName = (destination: string) => {
|
||||
switch (destination) {
|
||||
case PkiSync.AzureKeyVault:
|
||||
return "Azure Key Vault";
|
||||
case PkiSync.AwsCertificateManager:
|
||||
return "AWS Certificate Manager";
|
||||
default:
|
||||
return destination;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!certificateId || !pkiSyncs || pkiSyncs.length === 0) return;
|
||||
|
||||
const currentSyncIds = new Set(
|
||||
pkiSyncs.filter((sync) => sync.hasCertificate).map((sync) => sync.id)
|
||||
);
|
||||
setSelectedSyncIds(currentSyncIds);
|
||||
setInitialSyncIds(new Set(currentSyncIds));
|
||||
}, [certificateId, pkiSyncs]);
|
||||
|
||||
const handleSyncToggle = (syncId: string) => {
|
||||
setSelectedSyncIds((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(syncId)) {
|
||||
newSet.delete(syncId);
|
||||
} else {
|
||||
newSet.add(syncId);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
};
|
||||
|
||||
const handleSaveChanges = async () => {
|
||||
if (!certificateId) return;
|
||||
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
|
||||
const syncsToAdd = Array.from(selectedSyncIds).filter((id) => !initialSyncIds.has(id));
|
||||
const syncsToRemove = Array.from(initialSyncIds).filter((id) => !selectedSyncIds.has(id));
|
||||
|
||||
await Promise.all(
|
||||
syncsToAdd.map((syncId) =>
|
||||
addCertificatesToSync.mutateAsync({
|
||||
pkiSyncId: syncId,
|
||||
certificateIds: [certificateId]
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
syncsToRemove.map((syncId) =>
|
||||
removeCertificatesFromSync.mutateAsync({
|
||||
pkiSyncId: syncId,
|
||||
certificateIds: [certificateId]
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
createNotification({
|
||||
text: `PKI sync settings updated for certificate "${commonName}"`,
|
||||
type: "success"
|
||||
});
|
||||
|
||||
handleClose();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
createNotification({
|
||||
text: "Failed to update PKI sync settings",
|
||||
type: "error"
|
||||
});
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={popUp.isOpen} onOpenChange={handleClose}>
|
||||
<ModalContent
|
||||
title="Manage PKI Syncs"
|
||||
subTitle={`Select which PKI syncs "${commonName}" should be part of`}
|
||||
className="max-w-3xl"
|
||||
>
|
||||
<div className="mb-4">
|
||||
<Input
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
placeholder="Search PKI syncs by name..."
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4 max-h-96 overflow-y-auto">
|
||||
{isPending && (
|
||||
<div className="flex h-32 items-center justify-center">
|
||||
<div className="text-bunker-300">Loading PKI syncs...</div>
|
||||
</div>
|
||||
)}
|
||||
{!isPending && pkiSyncs.length === 0 && (
|
||||
<EmptyState title="No PKI syncs available" icon={faPlus}>
|
||||
<div className="mt-1">
|
||||
Create a{" "}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleNavigateToPkiSyncs}
|
||||
className="cursor-pointer underline hover:text-mineshaft-300"
|
||||
>
|
||||
PKI sync
|
||||
</button>{" "}
|
||||
first to manage certificate syncing.
|
||||
</div>
|
||||
</EmptyState>
|
||||
)}
|
||||
{!isPending && pkiSyncs.length > 0 && filteredSyncs.length === 0 && searchTerm && (
|
||||
<EmptyState title="No PKI syncs found" icon={faSearch}>
|
||||
<div className="mt-1">
|
||||
No PKI syncs match your search criteria. Try a different search term.
|
||||
</div>
|
||||
</EmptyState>
|
||||
)}
|
||||
{!isPending && filteredSyncs.length > 0 && (
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="w-12" />
|
||||
<Th className="w-1/2">Name</Th>
|
||||
<Th className="w-1/2">Destination</Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{paginatedSyncs.map((sync) => (
|
||||
<Tr
|
||||
key={sync.id}
|
||||
className="cursor-pointer hover:bg-mineshaft-700"
|
||||
onClick={() => handleSyncToggle(sync.id)}
|
||||
>
|
||||
<Td>
|
||||
<Checkbox
|
||||
isChecked={selectedSyncIds.has(sync.id)}
|
||||
onCheckedChange={() => handleSyncToggle(sync.id)}
|
||||
id={`sync-${sync.id}`}
|
||||
/>
|
||||
</Td>
|
||||
<Td className="w-1/2 max-w-0">
|
||||
<div className="truncate" title={sync.name}>
|
||||
{sync.name}
|
||||
</div>
|
||||
</Td>
|
||||
<Td className="w-1/2 max-w-0">
|
||||
<div
|
||||
className="truncate capitalize"
|
||||
title={getDestinationDisplayName(sync.destination)}
|
||||
>
|
||||
{getDestinationDisplayName(sync.destination)}
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)}
|
||||
{!isPending && filteredSyncs.length > PER_PAGE && (
|
||||
<div className="mt-4">
|
||||
<Pagination
|
||||
count={filteredSyncs.length}
|
||||
page={currentPage}
|
||||
perPage={PER_PAGE}
|
||||
onChangePage={setCurrentPage}
|
||||
onChangePerPage={() => {}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex justify-end gap-3">
|
||||
<Button variant="outline_bg" onClick={handleClose} isDisabled={isSubmitting}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
colorSchema="primary"
|
||||
onClick={handleSaveChanges}
|
||||
isDisabled={isSubmitting}
|
||||
isLoading={isSubmitting}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -15,6 +15,7 @@ import { usePopUp } from "@app/hooks/usePopUp";
|
||||
import { CertificateCertModal } from "./CertificateCertModal";
|
||||
import { CertificateImportModal } from "./CertificateImportModal";
|
||||
import { CertificateIssuanceModal } from "./CertificateIssuanceModal";
|
||||
import { CertificateManagePkiSyncsModal } from "./CertificateManagePkiSyncsModal";
|
||||
import { CertificateManageRenewalModal } from "./CertificateManageRenewalModal";
|
||||
import { CertificateModal } from "./CertificateModal";
|
||||
import { CertificateRenewalModal } from "./CertificateRenewalModal";
|
||||
@@ -36,7 +37,8 @@ export const CertificatesSection = () => {
|
||||
"deleteCertificate",
|
||||
"revokeCertificate",
|
||||
"manageRenewal",
|
||||
"renewCertificate"
|
||||
"renewCertificate",
|
||||
"managePkiSyncs"
|
||||
] as const);
|
||||
|
||||
const onRemoveCertificateSubmit = async (serialNumber: string) => {
|
||||
@@ -104,6 +106,10 @@ export const CertificatesSection = () => {
|
||||
<CertificateManageRenewalModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<CertificateRenewalModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<CertificateRevocationModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<CertificateManagePkiSyncsModal
|
||||
popUp={popUp.managePkiSyncs}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteCertificate.isOpen}
|
||||
title={`Are you sure you want to remove the certificate ${
|
||||
|
||||
+118
-117
@@ -2,19 +2,24 @@ import { useMemo, useState } from "react";
|
||||
import {
|
||||
faBan,
|
||||
faCertificate,
|
||||
faClockRotateLeft,
|
||||
faEllipsis,
|
||||
faEye,
|
||||
faFileExport,
|
||||
faLink,
|
||||
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 {
|
||||
CertificateDisplayName,
|
||||
getCertificateDisplayName
|
||||
} from "@app/components/utilities/certificateDisplayUtils";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -38,12 +43,12 @@ import {
|
||||
ProjectPermissionSub,
|
||||
useProject
|
||||
} from "@app/context";
|
||||
import { useListWorkspaceCertificates, useUpdateRenewalConfig } from "@app/hooks/api";
|
||||
import { 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 { useListWorkspaceCertificates } from "@app/hooks/api/projects";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
import { getCertValidUntilBadgeDetails } from "./CertificatesTable.utils";
|
||||
@@ -55,93 +60,6 @@ const isExpiringWithinOneDay = (notAfter: string): boolean => {
|
||||
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<
|
||||
@@ -151,7 +69,8 @@ type Props = {
|
||||
"revokeCertificate",
|
||||
"certificateCert",
|
||||
"manageRenewal",
|
||||
"renewCertificate"
|
||||
"renewCertificate",
|
||||
"managePkiSyncs"
|
||||
]
|
||||
>,
|
||||
data?: {
|
||||
@@ -232,20 +151,18 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Common Name</Th>
|
||||
<Th>Status</Th>
|
||||
<Th>Not Before</Th>
|
||||
<Th>Not After</Th>
|
||||
<Th>Renewal Status</Th>
|
||||
<Th />
|
||||
<Th className="w-1/2">SAN / CN</Th>
|
||||
<Th className="w-1/6">Status</Th>
|
||||
<Th className="w-1/6">Not Before</Th>
|
||||
<Th className="w-1/6">Not After</Th>
|
||||
<Th className="w-12" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isPending && <TableSkeleton columns={5} innerKey="project-cas" />}
|
||||
{isPending && <TableSkeleton columns={4} innerKey="project-cas" />}
|
||||
{!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();
|
||||
@@ -254,9 +171,24 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
||||
const isAutoRenewalEnabled = Boolean(
|
||||
certificate.renewBeforeDays && certificate.renewBeforeDays > 0
|
||||
);
|
||||
|
||||
const canShowAutoRenewalIcon = Boolean(
|
||||
certificate.profileId &&
|
||||
certificate.hasPrivateKey !== false &&
|
||||
!certificate.renewedByCertificateId &&
|
||||
!isRevoked &&
|
||||
!isExpired &&
|
||||
!isExpiringWithinDay
|
||||
);
|
||||
|
||||
// Still need originalDisplayName for other uses in the component
|
||||
const { originalDisplayName } = getCertificateDisplayName(certificate, 64, "—");
|
||||
|
||||
return (
|
||||
<Tr className="h-10" key={`certificate-${certificate.id}`}>
|
||||
<Td>{certificate.commonName}</Td>
|
||||
<Tr className="group h-10" key={`certificate-${certificate.id}`}>
|
||||
<Td className="max-w-0">
|
||||
<CertificateDisplayName cert={certificate} maxLength={64} fallback="—" />
|
||||
</Td>
|
||||
<Td>
|
||||
{certificate.status === CertStatus.REVOKED ? (
|
||||
<Badge variant="danger">Revoked</Badge>
|
||||
@@ -274,22 +206,64 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
||||
? format(new Date(certificate.notAfter), "yyyy-MM-dd")
|
||||
: "-"}
|
||||
</Td>
|
||||
<Td>
|
||||
{autoRenewalInfo &&
|
||||
(autoRenewalInfo.tooltip ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Tooltip content={autoRenewalInfo.tooltip}>
|
||||
<Badge variant={autoRenewalInfo.variant}>
|
||||
{autoRenewalInfo.text}
|
||||
<CircleQuestionMarkIcon />
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
</div>
|
||||
) : (
|
||||
<Badge variant={autoRenewalInfo.variant}>{autoRenewalInfo.text}</Badge>
|
||||
))}
|
||||
</Td>
|
||||
<Td className="flex justify-end">
|
||||
<Td className="flex items-center justify-end gap-2">
|
||||
<div
|
||||
className={`transition-opacity ${(() => {
|
||||
if (!canShowAutoRenewalIcon) return "";
|
||||
if (isAutoRenewalEnabled) return "opacity-100";
|
||||
return "opacity-0 group-hover:opacity-100";
|
||||
})()}`}
|
||||
>
|
||||
{canShowAutoRenewalIcon && (
|
||||
<Tooltip
|
||||
content={(() => {
|
||||
if (hasFailed && certificate.renewalError) {
|
||||
return `Auto-renewal failed: ${certificate.renewalError}`;
|
||||
}
|
||||
if (isAutoRenewalEnabled) {
|
||||
const expiryDate = new Date(certificate.notAfter);
|
||||
const now = new Date();
|
||||
const daysUntilExpiry = Math.ceil(
|
||||
(expiryDate.getTime() - now.getTime()) / (24 * 60 * 60 * 1000)
|
||||
);
|
||||
const daysUntilRenewal = Math.max(
|
||||
0,
|
||||
daysUntilExpiry - (certificate.renewBeforeDays || 0)
|
||||
);
|
||||
return `Auto-renews in ${daysUntilRenewal}d`;
|
||||
}
|
||||
return "Set auto renewal";
|
||||
})()}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={(() => {
|
||||
if (hasFailed) return "pr-1 text-red-500 hover:text-red-400";
|
||||
return "pr-1 text-primary-500 hover:text-primary-400";
|
||||
})()}
|
||||
aria-label="Certificate auto-renewal"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (hasFailed) return;
|
||||
|
||||
handlePopUpOpen("manageRenewal", {
|
||||
certificateId: certificate.id,
|
||||
commonName: originalDisplayName,
|
||||
profileId: certificate.profileId || "",
|
||||
renewBeforeDays: certificate.renewBeforeDays || 7,
|
||||
ttlDays: Math.ceil(
|
||||
(new Date(certificate.notAfter).getTime() -
|
||||
new Date(certificate.notBefore).getTime()) /
|
||||
(24 * 60 * 60 * 1000)
|
||||
)
|
||||
});
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faClockRotateLeft} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||
@@ -483,6 +457,33 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
||||
</ProjectPermissionCan>
|
||||
);
|
||||
})()}
|
||||
{/* PKI Sync management - only for active certificates that are not renewed */}
|
||||
{certificate.status === CertStatus.ACTIVE &&
|
||||
!certificate.renewedByCertificateId && (
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionCertificateActions.Edit}
|
||||
a={ProjectPermissionSub.Certificates}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed &&
|
||||
"pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={async () =>
|
||||
handlePopUpOpen("managePkiSyncs", {
|
||||
certificateId: certificate.id,
|
||||
commonName: certificate.commonName
|
||||
})
|
||||
}
|
||||
disabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faLink} />}
|
||||
>
|
||||
Manage PKI Syncs
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
)}
|
||||
{/* Only show revoke button if CA supports revocation */}
|
||||
{(() => {
|
||||
const caType = caCapabilityMap[certificate.caId];
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
import { CertificatesPage } from "./CertificatesPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificates"
|
||||
)({
|
||||
component: CertificatesPage,
|
||||
beforeLoad: ({ context }) => {
|
||||
return {
|
||||
breadcrumbs: [
|
||||
...context.breadcrumbs,
|
||||
{
|
||||
label: "Certificates"
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
-19
@@ -12,7 +12,6 @@ import {
|
||||
faToggleOff,
|
||||
faToggleOn,
|
||||
faTrash,
|
||||
faTriangleExclamation,
|
||||
faXmark
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
@@ -47,7 +46,6 @@ import { useToggle } from "@app/hooks";
|
||||
import { PkiSyncStatus, TPkiSync, usePkiSyncOption } from "@app/hooks/api/pkiSyncs";
|
||||
|
||||
import { PkiSyncDestinationCol } from "./PkiSyncDestinationCol";
|
||||
import { PkiSyncTableCell } from "./PkiSyncTableCell";
|
||||
|
||||
type Props = {
|
||||
pkiSync: TPkiSync;
|
||||
@@ -163,23 +161,6 @@ export const PkiSyncRow = ({
|
||||
<p className="truncate text-xs leading-4 text-bunker-300">{destinationDetails.name}</p>
|
||||
</div>
|
||||
</Td>
|
||||
{subscriberId ? (
|
||||
<PkiSyncTableCell
|
||||
primaryText={pkiSync.subscriber?.name || subscriberId}
|
||||
secondaryText="PKI Subscriber"
|
||||
/>
|
||||
) : (
|
||||
<Td>
|
||||
<Tooltip content="The PKI subscriber for this sync has been deleted. Configure a new source or remove this sync.">
|
||||
<div className="w-min">
|
||||
<Badge variant="warning">
|
||||
<FontAwesomeIcon icon={faTriangleExclamation} />
|
||||
<span>Source Deleted</span>
|
||||
</Badge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Td>
|
||||
)}
|
||||
<PkiSyncDestinationCol pkiSync={pkiSync} />
|
||||
<Td>
|
||||
<div className="flex items-center gap-1">
|
||||
|
||||
+3
-25
@@ -57,7 +57,6 @@ import { PkiSyncRow } from "./PkiSyncRow";
|
||||
|
||||
enum PkiSyncsOrderBy {
|
||||
Destination = "destination",
|
||||
Source = "source",
|
||||
Name = "name",
|
||||
Status = "status"
|
||||
}
|
||||
@@ -160,14 +159,6 @@ export const PkiSyncsTable = ({ pkiSyncs }: Props) => {
|
||||
const [syncOne, syncTwo] = orderDirection === OrderByDirection.ASC ? [a, b] : [b, a];
|
||||
|
||||
switch (orderBy) {
|
||||
case PkiSyncsOrderBy.Source:
|
||||
return (syncOne.subscriber?.name ?? syncOne.subscriberId ?? "")
|
||||
.toLowerCase()
|
||||
.localeCompare(
|
||||
syncTwo.subscriber?.name?.toLowerCase() ??
|
||||
syncTwo.subscriberId?.toLowerCase() ??
|
||||
""
|
||||
);
|
||||
case PkiSyncsOrderBy.Destination:
|
||||
return getPkiSyncDestinationColValues(syncOne)
|
||||
.primaryText.toLowerCase()
|
||||
@@ -370,7 +361,7 @@ export const PkiSyncsTable = ({ pkiSyncs }: Props) => {
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="w-2" />
|
||||
<Th className="w-1/4">
|
||||
<Th className="w-1/2">
|
||||
<div className="flex items-center">
|
||||
Name
|
||||
<IconButton
|
||||
@@ -383,20 +374,7 @@ export const PkiSyncsTable = ({ pkiSyncs }: Props) => {
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th className="w-1/3">
|
||||
<div className="flex items-center">
|
||||
Source
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={getClassName(PkiSyncsOrderBy.Source)}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(PkiSyncsOrderBy.Source)}
|
||||
>
|
||||
<FontAwesomeIcon icon={getColSortIcon(PkiSyncsOrderBy.Source)} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th className="w-1/3">
|
||||
<Th className="w-1/4">
|
||||
<div className="flex items-center">
|
||||
Destination
|
||||
<IconButton
|
||||
@@ -409,7 +387,7 @@ export const PkiSyncsTable = ({ pkiSyncs }: Props) => {
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th className="min-w-42">
|
||||
<Th className="w-1/4 min-w-42">
|
||||
<div className="flex items-center">
|
||||
Status
|
||||
<IconButton
|
||||
|
||||
+40
-2
@@ -12,13 +12,14 @@ import { ProjectPermissionSub, useProject } from "@app/context";
|
||||
import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useListPkiSyncs } from "@app/hooks/api/pkiSyncs";
|
||||
import { IntegrationsListPageTabs } from "@app/types/integrations";
|
||||
|
||||
import { PkiSyncsTable } from "./PkiSyncTable";
|
||||
|
||||
export const PkiSyncsTab = () => {
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["addSync"] as const);
|
||||
|
||||
const { addSync, ...search } = useSearch({
|
||||
const { addSync, connectionId, connectionName, ...search } = useSearch({
|
||||
from: ROUTE_PATHS.CertManager.IntegrationsListPage.id
|
||||
});
|
||||
|
||||
@@ -45,6 +46,42 @@ export const PkiSyncsTab = () => {
|
||||
navigateToBase();
|
||||
}, [addSync, handlePopUpOpen, navigateToBase]);
|
||||
|
||||
useEffect(() => {
|
||||
const storedFormData = localStorage.getItem("pkiSyncFormData");
|
||||
if (storedFormData && !popUp.addSync.isOpen) {
|
||||
try {
|
||||
const parsedData = JSON.parse(storedFormData);
|
||||
if (connectionId && connectionName) {
|
||||
const initialData = {
|
||||
...parsedData,
|
||||
connection: { id: connectionId, name: connectionName }
|
||||
};
|
||||
handlePopUpOpen("addSync", { destination: parsedData.destination, initialData });
|
||||
navigate({
|
||||
to: ROUTE_PATHS.CertManager.IntegrationsListPage.path,
|
||||
params: { projectId: currentProject?.id },
|
||||
search: { selectedTab: IntegrationsListPageTabs.PkiSyncs },
|
||||
replace: true
|
||||
});
|
||||
} else {
|
||||
handlePopUpOpen("addSync", { destination: parsedData.destination });
|
||||
}
|
||||
localStorage.removeItem("pkiSyncFormData");
|
||||
} catch (error) {
|
||||
console.error("Failed to parse stored PKI sync form data:", error);
|
||||
localStorage.removeItem("pkiSyncFormData");
|
||||
handlePopUpOpen("addSync");
|
||||
}
|
||||
}
|
||||
}, [
|
||||
handlePopUpOpen,
|
||||
popUp.addSync.isOpen,
|
||||
connectionId,
|
||||
connectionName,
|
||||
navigate,
|
||||
currentProject?.id
|
||||
]);
|
||||
|
||||
const { data: pkiSyncs = [], isPending: isPkiSyncsPending } = useListPkiSyncs(
|
||||
currentProject?.id || "",
|
||||
{
|
||||
@@ -94,7 +131,8 @@ export const PkiSyncsTab = () => {
|
||||
<PkiSyncsTable pkiSyncs={pkiSyncs} />
|
||||
</div>
|
||||
<CreatePkiSyncModal
|
||||
selectSync={popUp.addSync.data}
|
||||
selectSync={popUp.addSync.data?.destination || popUp.addSync.data}
|
||||
initialData={popUp.addSync.data?.initialData}
|
||||
isOpen={popUp.addSync.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("addSync", isOpen)}
|
||||
/>
|
||||
|
||||
@@ -9,7 +9,9 @@ import { IntegrationsListPage } from "./IntegrationsListPage";
|
||||
|
||||
const IntegrationsListPageQuerySchema = z.object({
|
||||
selectedTab: z.nativeEnum(IntegrationsListPageTabs).optional(),
|
||||
addSync: z.nativeEnum(PkiSync).optional()
|
||||
addSync: z.nativeEnum(PkiSync).optional(),
|
||||
connectionId: z.string().optional(),
|
||||
connectionName: z.string().optional()
|
||||
});
|
||||
|
||||
export const Route = createFileRoute(
|
||||
|
||||
+4
-4
@@ -59,9 +59,9 @@ export const PkiCollectionPage = () => {
|
||||
});
|
||||
handlePopUpClose("deletePkiCollection");
|
||||
navigate({
|
||||
to: "/projects/cert-management/$projectId/certificates",
|
||||
to: "/projects/cert-management/$projectId/policies",
|
||||
params: {
|
||||
projectId
|
||||
projectId: params.projectId
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
@@ -77,9 +77,9 @@ export const PkiCollectionPage = () => {
|
||||
{data && (
|
||||
<div className="mx-auto mb-6 w-full max-w-8xl">
|
||||
<Link
|
||||
to="/projects/cert-management/$projectId/certificates"
|
||||
to="/projects/cert-management/$projectId/policies"
|
||||
params={{
|
||||
projectId
|
||||
projectId: params.projectId
|
||||
}}
|
||||
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
|
||||
>
|
||||
|
||||
+2
-6
@@ -5,13 +5,9 @@ import { z } from "zod";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Modal, ModalContent, Select, SelectItem } from "@app/components/v2";
|
||||
import { useProject } from "@app/context";
|
||||
import {
|
||||
CaStatus,
|
||||
useAddItemToPkiCollection,
|
||||
useListWorkspaceCas,
|
||||
useListWorkspaceCertificates
|
||||
} from "@app/hooks/api";
|
||||
import { CaStatus, useAddItemToPkiCollection, useListWorkspaceCas } from "@app/hooks/api";
|
||||
import { PkiItemType, pkiItemTypeToNameMap } from "@app/hooks/api/pkiCollections/constants";
|
||||
import { useListWorkspaceCertificates } from "@app/hooks/api/projects";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const schema = z
|
||||
|
||||
@@ -13,7 +13,7 @@ export const Route = createFileRoute(
|
||||
{
|
||||
label: "Certificate Collections",
|
||||
link: linkOptions({
|
||||
to: "/projects/cert-management/$projectId/certificates",
|
||||
to: "/projects/cert-management/$projectId/policies",
|
||||
params: {
|
||||
projectId: params.projectId
|
||||
}
|
||||
|
||||
@@ -18,10 +18,10 @@ import { IntegrationsListPageTabs } from "@app/types/integrations";
|
||||
import {
|
||||
PkiSyncActionTriggers,
|
||||
PkiSyncAuditLogsSection,
|
||||
PkiSyncCertificatesSection,
|
||||
PkiSyncDestinationSection,
|
||||
PkiSyncDetailsSection,
|
||||
PkiSyncOptionsSection,
|
||||
PkiSyncSourceSection
|
||||
PkiSyncOptionsSection
|
||||
} from "./components";
|
||||
|
||||
const PageContent = () => {
|
||||
@@ -62,7 +62,6 @@ const PageContent = () => {
|
||||
const destinationDetails = PKI_SYNC_MAP[pkiSync.destination];
|
||||
|
||||
const handleEditDetails = () => handlePopUpOpen("editSync", PkiSyncEditFields.Details);
|
||||
const handleEditSource = () => handlePopUpOpen("editSync", PkiSyncEditFields.Source);
|
||||
const handleEditOptions = () => handlePopUpOpen("editSync", PkiSyncEditFields.Options);
|
||||
const handleEditDestination = () => handlePopUpOpen("editSync", PkiSyncEditFields.Destination);
|
||||
|
||||
@@ -103,7 +102,6 @@ const PageContent = () => {
|
||||
<div className="flex justify-center">
|
||||
<div className="mr-4 flex w-72 flex-col gap-4">
|
||||
<PkiSyncDetailsSection pkiSync={pkiSync} onEditDetails={handleEditDetails} />
|
||||
<PkiSyncSourceSection pkiSync={pkiSync} onEditSource={handleEditSource} />
|
||||
<PkiSyncOptionsSection pkiSync={pkiSync} onEditOptions={handleEditOptions} />
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col gap-4">
|
||||
@@ -111,6 +109,7 @@ const PageContent = () => {
|
||||
pkiSync={pkiSync}
|
||||
onEditDestination={handleEditDestination}
|
||||
/>
|
||||
<PkiSyncCertificatesSection pkiSync={pkiSync} />
|
||||
<PkiSyncAuditLogsSection pkiSync={pkiSync} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@ export const PkiSyncAuditLogsSection = ({ pkiSync }: Props) => {
|
||||
return (
|
||||
<div className="flex max-h-full w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
|
||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
|
||||
<h3 className="font-medium text-mineshaft-100">Sync Logs</h3>
|
||||
<h3 className="text-lg font-medium text-mineshaft-100">Sync Logs</h3>
|
||||
{subscription.auditLogs && (
|
||||
<p className="text-xs text-bunker-300">
|
||||
Displaying audit logs from the last {Math.min(auditLogsRetentionDays, 60)} days
|
||||
|
||||
+328
@@ -0,0 +1,328 @@
|
||||
import { useState } from "react";
|
||||
import { subject } from "@casl/ability";
|
||||
import {
|
||||
faCertificate,
|
||||
faClockRotateLeft,
|
||||
faEdit,
|
||||
faTrash
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { CertificateManagementModal } from "@app/components/pki-syncs/CertificateManagementModal";
|
||||
import {
|
||||
CertificateDisplayName,
|
||||
getCertificateDisplayName
|
||||
} from "@app/components/utilities/certificateDisplayUtils";
|
||||
import {
|
||||
DeleteActionModal,
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Pagination,
|
||||
Table,
|
||||
TableContainer,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v3";
|
||||
import { ProjectPermissionSub } from "@app/context";
|
||||
import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { useListPkiSyncCertificates, useRemoveCertificatesFromPkiSync } from "@app/hooks/api";
|
||||
import { CertificateSyncStatus, TPkiSync } from "@app/hooks/api/pkiSyncs";
|
||||
|
||||
type Props = {
|
||||
pkiSync: TPkiSync;
|
||||
};
|
||||
|
||||
const getSyncStatusVariant = (status?: CertificateSyncStatus | null) => {
|
||||
if (status === CertificateSyncStatus.Succeeded) return "success";
|
||||
if (status === CertificateSyncStatus.Failed) return "danger";
|
||||
if (status === CertificateSyncStatus.Syncing) return "neutral";
|
||||
return "project";
|
||||
};
|
||||
|
||||
const getSyncStatusText = (status?: CertificateSyncStatus | null) => {
|
||||
if (status === CertificateSyncStatus.Succeeded) return "Synced";
|
||||
if (status === CertificateSyncStatus.Failed) return "Failed";
|
||||
if (status === CertificateSyncStatus.Syncing) return "Syncing";
|
||||
if (status === CertificateSyncStatus.Pending) return "Pending";
|
||||
return "Unknown";
|
||||
};
|
||||
|
||||
const getCertificateStatusVariant = (isExpired: boolean, isRevoked: boolean) => {
|
||||
if (isRevoked) return "danger";
|
||||
if (isExpired) return "danger";
|
||||
return "success";
|
||||
};
|
||||
|
||||
const getCertificateStatusText = (isExpired: boolean, isRevoked: boolean) => {
|
||||
if (isRevoked) return "Revoked";
|
||||
if (isExpired) return "Expired";
|
||||
return "Active";
|
||||
};
|
||||
|
||||
export const PkiSyncCertificatesSection = ({ pkiSync }: Props) => {
|
||||
const [isManageModalOpen, setIsManageModalOpen] = useState(false);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [certificateToDelete, setCertificateToDelete] = useState<{
|
||||
id: string;
|
||||
displayName: string;
|
||||
} | null>(null);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const pageSize = 10;
|
||||
|
||||
const { data, refetch: refetchSyncCertificates } = useListPkiSyncCertificates(pkiSync.id, {
|
||||
offset: (currentPage - 1) * pageSize,
|
||||
limit: pageSize
|
||||
});
|
||||
const syncCertificates = data?.certificates || [];
|
||||
const totalCount = data?.totalCount || 0;
|
||||
const removeCertificatesFromSync = useRemoveCertificatesFromPkiSync();
|
||||
|
||||
const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, {
|
||||
subscriberId: pkiSync.subscriberId || ""
|
||||
});
|
||||
|
||||
const handleRemoveCertificate = async (certificateId: string) => {
|
||||
try {
|
||||
await removeCertificatesFromSync.mutateAsync({
|
||||
pkiSyncId: pkiSync.id,
|
||||
certificateIds: [certificateId]
|
||||
});
|
||||
|
||||
await refetchSyncCertificates();
|
||||
|
||||
createNotification({
|
||||
text: "Certificate removed from sync",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
setIsDeleteModalOpen(false);
|
||||
setCertificateToDelete(null);
|
||||
} catch {
|
||||
createNotification({
|
||||
text: "Failed to remove certificate from sync",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteClick = (certificateId: string, displayName: string) => {
|
||||
setCertificateToDelete({ id: certificateId, displayName });
|
||||
setIsDeleteModalOpen(true);
|
||||
};
|
||||
|
||||
const totalPages = Math.ceil(totalCount / pageSize);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
|
||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
|
||||
<h3 className="text-lg font-medium text-mineshaft-100">Certificates</h3>
|
||||
<ProjectPermissionCan I={ProjectPermissionPkiSyncActions.Edit} a={permissionSubject}>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
variant="plain"
|
||||
colorSchema="secondary"
|
||||
isDisabled={!isAllowed}
|
||||
ariaLabel="Edit certificates"
|
||||
onClick={() => setIsManageModalOpen(true)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faEdit} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="space-y-4">
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="w-2/8">SAN / CN</Th>
|
||||
<Th className="w-3/16">Certificate Status</Th>
|
||||
<Th className="w-3/16">Serial Number</Th>
|
||||
<Th className="w-1/8">Sync Status</Th>
|
||||
<Th className="w-1/8">Expires At</Th>
|
||||
<Th className="w-1/8" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{syncCertificates.map((syncCert) => {
|
||||
const isExpired = syncCert.certificateNotAfter
|
||||
? new Date(syncCert.certificateNotAfter) < new Date()
|
||||
: false;
|
||||
const isRevoked = syncCert.certificateStatus === "revoked";
|
||||
|
||||
// Calculate auto-renewal timeline
|
||||
const hasAutoRenewal = Boolean(
|
||||
syncCert.certificateRenewBeforeDays &&
|
||||
syncCert.certificateRenewBeforeDays > 0 &&
|
||||
!syncCert.certificateRenewalError &&
|
||||
syncCert.certificateNotAfter
|
||||
);
|
||||
|
||||
const daysUntilRenewal =
|
||||
hasAutoRenewal && syncCert.certificateNotAfter
|
||||
? (() => {
|
||||
const expiryDate = new Date(syncCert.certificateNotAfter);
|
||||
const renewalDate = new Date(
|
||||
expiryDate.getTime() -
|
||||
syncCert.certificateRenewBeforeDays! * 24 * 60 * 60 * 1000
|
||||
);
|
||||
const now = new Date();
|
||||
const diffInMs = renewalDate.getTime() - now.getTime();
|
||||
return Math.max(0, Math.ceil(diffInMs / (24 * 60 * 60 * 1000)));
|
||||
})()
|
||||
: null;
|
||||
|
||||
const { originalDisplayName } = getCertificateDisplayName(
|
||||
{
|
||||
altNames: syncCert.certificateAltNames,
|
||||
commonName: syncCert.certificateCommonName
|
||||
},
|
||||
34,
|
||||
"Unknown"
|
||||
);
|
||||
|
||||
return (
|
||||
<Tr key={syncCert.id}>
|
||||
<Td className="max-w-0">
|
||||
<CertificateDisplayName
|
||||
cert={{
|
||||
altNames: syncCert.certificateAltNames,
|
||||
commonName: syncCert.certificateCommonName
|
||||
}}
|
||||
maxLength={34}
|
||||
fallback="Unknown"
|
||||
/>
|
||||
</Td>
|
||||
<Td>
|
||||
<Badge variant={getCertificateStatusVariant(isExpired, isRevoked)}>
|
||||
{getCertificateStatusText(isExpired, isRevoked)}
|
||||
</Badge>
|
||||
</Td>
|
||||
<Td className="max-w-0">
|
||||
<div
|
||||
className="truncate text-xs"
|
||||
title={syncCert.certificateSerialNumber || "Unknown"}
|
||||
>
|
||||
{(() => {
|
||||
const serial = syncCert.certificateSerialNumber;
|
||||
if (!serial || serial === "Unknown") return "Unknown";
|
||||
if (serial.length <= 8) return serial;
|
||||
return `${serial.substring(0, 4)}...${serial.substring(serial.length - 4)}`;
|
||||
})()}
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
{syncCert.lastSyncMessage &&
|
||||
syncCert.syncStatus === CertificateSyncStatus.Failed ? (
|
||||
<Tooltip content={syncCert.lastSyncMessage}>
|
||||
<Badge variant="danger">Failed</Badge>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Badge variant={getSyncStatusVariant(syncCert.syncStatus)}>
|
||||
{getSyncStatusText(syncCert.syncStatus)}
|
||||
</Badge>
|
||||
)}
|
||||
</Td>
|
||||
<Td>
|
||||
<span
|
||||
className={`text-sm ${isExpired ? "text-red-400" : "text-bunker-300"}`}
|
||||
>
|
||||
{syncCert.certificateNotAfter
|
||||
? new Date(syncCert.certificateNotAfter).toLocaleDateString()
|
||||
: "Unknown"}
|
||||
</span>
|
||||
</Td>
|
||||
<Td className="flex items-center justify-end gap-2 pr-4">
|
||||
{hasAutoRenewal && daysUntilRenewal !== null && (
|
||||
<Tooltip content={`Auto-renews in ${daysUntilRenewal}d`}>
|
||||
<div className="text-primary-500">
|
||||
<FontAwesomeIcon icon={faClockRotateLeft} size="sm" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionPkiSyncActions.Edit}
|
||||
a={permissionSubject}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
size="xs"
|
||||
variant="plain"
|
||||
colorSchema="danger"
|
||||
ariaLabel="Remove certificate"
|
||||
isDisabled={!isAllowed}
|
||||
onClick={() =>
|
||||
handleDeleteClick(syncCert.certificateId, originalDisplayName)
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</TBody>
|
||||
</Table>
|
||||
{syncCertificates.length === 0 && (
|
||||
<EmptyState
|
||||
title="No certificates are part of this certificate sync"
|
||||
icon={faCertificate}
|
||||
/>
|
||||
)}
|
||||
</TableContainer>
|
||||
{/* Pagination */}
|
||||
{totalPages > 1 && (
|
||||
<div className="flex justify-center">
|
||||
<Pagination
|
||||
count={totalCount}
|
||||
page={currentPage}
|
||||
perPage={pageSize}
|
||||
onChangePage={(page: number) => setCurrentPage(page)}
|
||||
onChangePerPage={() => {}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CertificateManagementModal
|
||||
pkiSync={pkiSync}
|
||||
isOpen={isManageModalOpen}
|
||||
onClose={() => setIsManageModalOpen(false)}
|
||||
onCertificatesUpdated={() => {
|
||||
refetchSyncCertificates();
|
||||
}}
|
||||
/>
|
||||
|
||||
<DeleteActionModal
|
||||
isOpen={isDeleteModalOpen}
|
||||
onClose={() => {
|
||||
setIsDeleteModalOpen(false);
|
||||
setCertificateToDelete(null);
|
||||
}}
|
||||
title="Remove Certificate from Sync"
|
||||
subTitle={`Are you sure you want to remove "${certificateToDelete?.displayName}" from this PKI sync?`}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={async () => {
|
||||
if (certificateToDelete) {
|
||||
await handleRemoveCertificate(certificateToDelete.id);
|
||||
}
|
||||
}}
|
||||
buttonText="Remove Certificate"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+12
-6
@@ -11,12 +11,15 @@ import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionC
|
||||
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
|
||||
import { PkiSync, TPkiSync } from "@app/hooks/api/pkiSyncs";
|
||||
|
||||
import { AzureKeyVaultPkiSyncDestinationSection } from "./PkiSyncDestinationSection/index";
|
||||
import {
|
||||
AwsCertificateManagerPkiSyncDestinationSection,
|
||||
AzureKeyVaultPkiSyncDestinationSection
|
||||
} from "./PkiSyncDestinationSection/index";
|
||||
|
||||
const GenericFieldLabel = ({ label, children }: { label: string; children: React.ReactNode }) => (
|
||||
<div>
|
||||
<label className="text-sm text-bunker-300">{label}</label>
|
||||
<div className="mt-1">{children}</div>
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-medium text-mineshaft-300">{label}</p>
|
||||
<div className="text-sm text-mineshaft-300">{children}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -32,6 +35,9 @@ export const PkiSyncDestinationSection = ({ pkiSync, onEditDestination }: Props)
|
||||
|
||||
let DestinationComponents: ReactNode;
|
||||
switch (destination) {
|
||||
case PkiSync.AwsCertificateManager:
|
||||
DestinationComponents = <AwsCertificateManagerPkiSyncDestinationSection pkiSync={pkiSync} />;
|
||||
break;
|
||||
case PkiSync.AzureKeyVault:
|
||||
DestinationComponents = <AzureKeyVaultPkiSyncDestinationSection pkiSync={pkiSync} />;
|
||||
break;
|
||||
@@ -47,7 +53,7 @@ export const PkiSyncDestinationSection = ({ pkiSync, onEditDestination }: Props)
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
|
||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
|
||||
<h3 className="font-medium text-mineshaft-100">Destination Configuration</h3>
|
||||
<h3 className="text-lg font-medium text-mineshaft-100">Destination Configuration</h3>
|
||||
<ProjectPermissionCan I={ProjectPermissionPkiSyncActions.Edit} a={permissionSubject}>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
@@ -62,7 +68,7 @@ export const PkiSyncDestinationSection = ({ pkiSync, onEditDestination }: Props)
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<div className="flex w-full flex-wrap gap-8">
|
||||
<div className="flex w-full flex-wrap gap-8 pt-2">
|
||||
<GenericFieldLabel label={`${destinationDetails.name} Connection`}>
|
||||
{pkiSync.appConnectionName || "Default Connection"}
|
||||
</GenericFieldLabel>
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { TPkiSync } from "@app/hooks/api/pkiSyncs";
|
||||
|
||||
const GenericFieldLabel = ({ label, children }: { label: string; children: React.ReactNode }) => (
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-medium text-mineshaft-300">{label}</p>
|
||||
<div className="text-sm text-mineshaft-300">{children}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
type Props = {
|
||||
pkiSync: TPkiSync;
|
||||
};
|
||||
|
||||
export const AwsCertificateManagerPkiSyncDestinationSection = ({ pkiSync }: Props) => {
|
||||
const region =
|
||||
pkiSync.destinationConfig && "region" in pkiSync.destinationConfig
|
||||
? pkiSync.destinationConfig.region
|
||||
: undefined;
|
||||
|
||||
return <GenericFieldLabel label="AWS Region">{region || "Not specified"}</GenericFieldLabel>;
|
||||
};
|
||||
+3
-3
@@ -2,9 +2,9 @@
|
||||
import { TAzureKeyVaultPkiSync } from "@app/hooks/api/pkiSyncs/types/azure-key-vault-sync";
|
||||
|
||||
const GenericFieldLabel = ({ label, children }: { label: string; children: React.ReactNode }) => (
|
||||
<div>
|
||||
<label className="text-sm text-bunker-300">{label}</label>
|
||||
<div className="mt-1">{children}</div>
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-medium text-mineshaft-300">{label}</p>
|
||||
<div className="text-sm text-mineshaft-300">{children}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
+1
@@ -1 +1,2 @@
|
||||
export { AwsCertificateManagerPkiSyncDestinationSection } from "./AwsCertificateManagerPkiSyncDestinationSection";
|
||||
export { AzureKeyVaultPkiSyncDestinationSection } from "./AzureKeyVaultPkiSyncDestinationSection";
|
||||
|
||||
+24
-28
@@ -21,9 +21,9 @@ const GenericFieldLabel = ({
|
||||
children: React.ReactNode;
|
||||
labelClassName?: string;
|
||||
}) => (
|
||||
<div>
|
||||
<label className={`text-sm text-bunker-300 ${labelClassName || ""}`}>{label}</label>
|
||||
<div className="mt-1">{children}</div>
|
||||
<div className="mb-4">
|
||||
<p className={`text-sm font-medium text-mineshaft-300 ${labelClassName || ""}`}>{label}</p>
|
||||
<div className="text-sm text-mineshaft-300">{children}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -57,7 +57,7 @@ export const PkiSyncDetailsSection = ({ pkiSync, onEditDetails }: Props) => {
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
|
||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
|
||||
<h3 className="font-medium text-mineshaft-100">Details</h3>
|
||||
<h3 className="text-lg font-medium text-mineshaft-100">Details</h3>
|
||||
<ProjectPermissionCan I={ProjectPermissionPkiSyncActions.Edit} a={permissionSubject}>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
@@ -72,31 +72,27 @@ export const PkiSyncDetailsSection = ({ pkiSync, onEditDetails }: Props) => {
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<div>
|
||||
<div className="space-y-3">
|
||||
<GenericFieldLabel label="Name">{name}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Description">{description || "None"}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Source Subscriber">
|
||||
{subscriber ? subscriber.name : "Subscriber deleted"}
|
||||
<div className="pt-2">
|
||||
<GenericFieldLabel label="Name">{name}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Description">{description || "None"}</GenericFieldLabel>
|
||||
{subscriber && (
|
||||
<GenericFieldLabel label="Source Subscriber">{subscriber.name}</GenericFieldLabel>
|
||||
)}
|
||||
{syncStatus && (
|
||||
<GenericFieldLabel label="Status">
|
||||
<PkiSyncStatusBadge status={syncStatus} />
|
||||
</GenericFieldLabel>
|
||||
{syncStatus && (
|
||||
<GenericFieldLabel label="Status">
|
||||
<PkiSyncStatusBadge status={syncStatus} />
|
||||
</GenericFieldLabel>
|
||||
)}
|
||||
{lastSyncedAt && (
|
||||
<GenericFieldLabel label="Last Synced">
|
||||
{format(new Date(lastSyncedAt), "yyyy-MM-dd, h:mm aaa")}
|
||||
</GenericFieldLabel>
|
||||
)}
|
||||
{syncStatus === PkiSyncStatus.Failed && failureMessage && (
|
||||
<GenericFieldLabel labelClassName="text-red" label="Last Sync Error">
|
||||
<p className="rounded-sm bg-mineshaft-600 p-2 text-xs break-words">
|
||||
{failureMessage}
|
||||
</p>
|
||||
</GenericFieldLabel>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{lastSyncedAt && (
|
||||
<GenericFieldLabel label="Last Synced">
|
||||
{format(new Date(lastSyncedAt), "yyyy-MM-dd, h:mm aaa")}
|
||||
</GenericFieldLabel>
|
||||
)}
|
||||
{syncStatus === PkiSyncStatus.Failed && failureMessage && (
|
||||
<GenericFieldLabel labelClassName="text-red" label="Last Sync Error">
|
||||
<p className="rounded-sm bg-mineshaft-600 p-2 text-xs break-words">{failureMessage}</p>
|
||||
</GenericFieldLabel>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
+23
-11
@@ -3,13 +3,27 @@ import { faEdit } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { GenericFieldLabel } from "@app/components/secret-syncs";
|
||||
import { IconButton } from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v3";
|
||||
import { ProjectPermissionSub } from "@app/context";
|
||||
import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { TPkiSync } from "@app/hooks/api/pkiSyncs";
|
||||
|
||||
const GenericFieldLabel = ({
|
||||
label,
|
||||
children,
|
||||
labelClassName
|
||||
}: {
|
||||
label: string;
|
||||
children: React.ReactNode;
|
||||
labelClassName?: string;
|
||||
}) => (
|
||||
<div className="mb-4">
|
||||
<p className={`text-sm font-medium text-mineshaft-300 ${labelClassName || ""}`}>{label}</p>
|
||||
<div className="text-sm text-mineshaft-300">{children}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
type Props = {
|
||||
pkiSync: TPkiSync;
|
||||
onEditOptions: VoidFunction;
|
||||
@@ -28,7 +42,7 @@ export const PkiSyncOptionsSection = ({ pkiSync, onEditOptions }: Props) => {
|
||||
<div>
|
||||
<div className="flex w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
|
||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
|
||||
<h3 className="font-medium text-mineshaft-100">Sync Options</h3>
|
||||
<h3 className="text-lg font-medium text-mineshaft-100">Sync Options</h3>
|
||||
<ProjectPermissionCan I={ProjectPermissionPkiSyncActions.Edit} a={permissionSubject}>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
@@ -43,21 +57,19 @@ export const PkiSyncOptionsSection = ({ pkiSync, onEditOptions }: Props) => {
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<div>
|
||||
<div className="space-y-3">
|
||||
{/* Hidden for now - Import certificates functionality disabled
|
||||
<div className="pt-1">
|
||||
{/* Hidden for now - Import certificates functionality disabled
|
||||
<GenericFieldLabel label="Certificate Import">
|
||||
<Badge variant={canImportCertificates ? "success" : "danger"}>
|
||||
{canImportCertificates ? "Enabled" : "Disabled"}
|
||||
</Badge>
|
||||
</GenericFieldLabel>
|
||||
*/}
|
||||
<GenericFieldLabel label="Certificate Removal">
|
||||
<Badge variant={canRemoveCertificates ? "success" : "danger"}>
|
||||
{canRemoveCertificates ? "Enabled" : "Disabled"}
|
||||
</Badge>
|
||||
</GenericFieldLabel>
|
||||
</div>
|
||||
<GenericFieldLabel label="Inactive Certificate Removal" labelClassName="mb-1">
|
||||
<Badge variant={canRemoveCertificates ? "success" : "danger"}>
|
||||
{canRemoveCertificates ? "Enabled" : "Disabled"}
|
||||
</Badge>
|
||||
</GenericFieldLabel>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export { PkiSyncActionTriggers } from "./PkiSyncActionTriggers";
|
||||
export { PkiSyncAuditLogsSection } from "./PkiSyncAuditLogsSection";
|
||||
export { PkiSyncCertificatesSection } from "./PkiSyncCertificatesSection";
|
||||
export { PkiSyncDestinationSection } from "./PkiSyncDestinationSection";
|
||||
export { PkiSyncDetailsSection } from "./PkiSyncDetailsSection";
|
||||
export { PkiSyncOptionsSection } from "./PkiSyncOptionsSection";
|
||||
|
||||
@@ -2,17 +2,20 @@ 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 { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context";
|
||||
import { useProject } from "@app/context";
|
||||
import { ProjectType } from "@app/hooks/api/projects/types";
|
||||
|
||||
import { CertificateProfilesTab } from "./components/CertificateProfilesTab";
|
||||
import { CertificatesTab } from "./components/CertificatesTab";
|
||||
import { CertificateTemplatesV2Tab } from "./components/CertificateTemplatesV2Tab";
|
||||
import { PkiCollectionsTab } from "./components/PkiCollectionsTab";
|
||||
|
||||
enum TabSections {
|
||||
CertificateProfiles = "profiles",
|
||||
CertificateTemplatesV2 = "templates-v2"
|
||||
CertificateTemplatesV2 = "templates-v2",
|
||||
Certificates = "certificates",
|
||||
PkiCollections = "pki-collections"
|
||||
}
|
||||
|
||||
export const PoliciesPage = () => {
|
||||
@@ -25,59 +28,54 @@ export const PoliciesPage = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Read}
|
||||
a={ProjectPermissionSub.CertificateAuthorities}
|
||||
>
|
||||
{(isAllowed) => {
|
||||
if (!isAllowed) {
|
||||
return (
|
||||
<div className="mx-auto flex h-full flex-col justify-center bg-bunker-800 text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-8xl text-center">
|
||||
<p>You don't have permission to access certificate policies.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
<div className="mx-auto flex h-full flex-col justify-between bg-bunker-800 text-white">
|
||||
<Helmet>
|
||||
<title>{t("common.head-title", { title: "Certificate Management" })}</title>
|
||||
</Helmet>
|
||||
<div className="mx-auto mb-6 w-full max-w-8xl">
|
||||
<PageHeader
|
||||
scope={ProjectType.CertificateManager}
|
||||
title="Certificate Management"
|
||||
description="Streamline certificate management by creating and maintaining templates, profiles, and certificates in one place"
|
||||
/>
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex 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-8xl">
|
||||
<PageHeader
|
||||
scope={ProjectType.CertificateManager}
|
||||
title="Certificate Policies"
|
||||
description="Manage certificate templates and profiles for unified certificate issuance"
|
||||
/>
|
||||
<Tabs
|
||||
orientation="vertical"
|
||||
value={activeTab}
|
||||
onValueChange={(value) => setActiveTab(value as TabSections)}
|
||||
>
|
||||
<TabList>
|
||||
<Tab variant="project" value={TabSections.CertificateProfiles}>
|
||||
Certificate Profiles
|
||||
</Tab>
|
||||
<Tab variant="project" value={TabSections.CertificateTemplatesV2}>
|
||||
Certificate Templates
|
||||
</Tab>
|
||||
<Tab variant="project" value={TabSections.Certificates}>
|
||||
Certificates
|
||||
</Tab>
|
||||
<Tab variant="project" value={TabSections.PkiCollections}>
|
||||
Certificate Collections
|
||||
</Tab>
|
||||
</TabList>
|
||||
|
||||
<Tabs
|
||||
orientation="vertical"
|
||||
value={activeTab}
|
||||
onValueChange={(value) => setActiveTab(value as TabSections)}
|
||||
>
|
||||
<TabList>
|
||||
<Tab variant="project" value={TabSections.CertificateProfiles}>
|
||||
Certificate Profiles
|
||||
</Tab>
|
||||
<Tab variant="project" value={TabSections.CertificateTemplatesV2}>
|
||||
Certificate Templates
|
||||
</Tab>
|
||||
</TabList>
|
||||
<TabPanel value={TabSections.CertificateProfiles}>
|
||||
<CertificateProfilesTab />
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={TabSections.CertificateProfiles}>
|
||||
<CertificateProfilesTab />
|
||||
</TabPanel>
|
||||
<TabPanel value={TabSections.CertificateTemplatesV2}>
|
||||
<CertificateTemplatesV2Tab />
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={TabSections.CertificateTemplatesV2}>
|
||||
<CertificateTemplatesV2Tab />
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</ProjectPermissionCan>
|
||||
<TabPanel value={TabSections.Certificates}>
|
||||
<CertificatesTab />
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={TabSections.PkiCollections}>
|
||||
<PkiCollectionsTab />
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+1
-1
@@ -418,7 +418,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
||||
name="enrollmentType"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Enrollment Type"
|
||||
label="Enrollment Method"
|
||||
isRequired
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
|
||||
+3
-6
@@ -29,8 +29,7 @@ export const ProfileList = ({ onEditProfile, onDeleteProfile }: Props) => {
|
||||
projectId: currentProject?.id || "",
|
||||
limit: 100,
|
||||
offset: 0,
|
||||
includeConfigs: true,
|
||||
includeMetrics: true
|
||||
includeConfigs: true
|
||||
});
|
||||
|
||||
const profiles = data?.certificateProfiles || [];
|
||||
@@ -42,10 +41,9 @@ export const ProfileList = ({ onEditProfile, onDeleteProfile }: Props) => {
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Name</Th>
|
||||
<Th>Enrollment Type</Th>
|
||||
<Th>Enrollment Method</Th>
|
||||
<Th>Issuing CA</Th>
|
||||
<Th>Certificate Template</Th>
|
||||
<Th>Certificates</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
@@ -67,10 +65,9 @@ export const ProfileList = ({ onEditProfile, onDeleteProfile }: Props) => {
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Name</Th>
|
||||
<Th>Enrollment Type</Th>
|
||||
<Th>Enrollment Method</Th>
|
||||
<Th>Issuing CA</Th>
|
||||
<Th>Certificate Template</Th>
|
||||
<Th>Certificates</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
|
||||
+2
-44
@@ -33,43 +33,6 @@ import { TCertificateProfile } from "@app/hooks/api/certificateProfiles";
|
||||
import { useGetCertificateTemplateV2ById } from "@app/hooks/api/certificateTemplates/queries";
|
||||
import { CertificateIssuanceModal } from "@app/pages/cert-manager/CertificatesPage/components/CertificateIssuanceModal";
|
||||
|
||||
const MetricsBadges = ({
|
||||
metrics
|
||||
}: {
|
||||
metrics?: {
|
||||
totalCertificates: number;
|
||||
activeCertificates: number;
|
||||
expiringCertificates: number;
|
||||
expiredCertificates: number;
|
||||
revokedCertificates: number;
|
||||
};
|
||||
}) => {
|
||||
if (!metrics) {
|
||||
return <Badge variant="warning">No metrics</Badge>;
|
||||
}
|
||||
|
||||
if (metrics.totalCertificates === 0) {
|
||||
return <Badge variant="warning">No certificates</Badge>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{metrics.activeCertificates > 0 && (
|
||||
<Badge variant="success">{metrics.activeCertificates} active</Badge>
|
||||
)}
|
||||
{metrics.expiringCertificates > 0 && (
|
||||
<Badge variant="warning">{metrics.expiringCertificates} expiring</Badge>
|
||||
)}
|
||||
{metrics.expiredCertificates > 0 && (
|
||||
<Badge variant="danger">{metrics.expiredCertificates} expired</Badge>
|
||||
)}
|
||||
{metrics.revokedCertificates > 0 && (
|
||||
<Badge variant="danger">{metrics.revokedCertificates} revoked</Badge>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface Props {
|
||||
profile: TCertificateProfile;
|
||||
onEditProfile: (profile: TCertificateProfile) => void;
|
||||
@@ -118,8 +81,8 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
|
||||
|
||||
const getEnrollmentTypeBadge = (enrollmentType: string) => {
|
||||
const config = {
|
||||
api: { variant: "success" as const, label: "API" },
|
||||
est: { variant: "warning" as const, label: "EST" }
|
||||
api: { variant: "ghost" as const, label: "API" },
|
||||
est: { variant: "ghost" as const, label: "EST" }
|
||||
} as const;
|
||||
|
||||
const configKey = Object.keys(config).includes(enrollmentType)
|
||||
@@ -153,11 +116,6 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
|
||||
{templateData?.name || profile.certificateTemplateId}
|
||||
</span>
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
<MetricsBadges metrics={profile.metrics} />
|
||||
</div>
|
||||
</Td>
|
||||
<Td className="text-right">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import { CertificatesSection } from "../../../CertificatesPage/components/CertificatesSection";
|
||||
|
||||
export const CertificatesTab = () => {
|
||||
return <CertificatesSection />;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { CertificatesTab } from "./CertificatesTab";
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import { PkiCollectionSection } from "../../../AlertingPage/components/PkiCollectionSection";
|
||||
|
||||
export const PkiCollectionsTab = () => {
|
||||
return <PkiCollectionSection />;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { PkiCollectionsTab } from "./PkiCollectionsTab";
|
||||
@@ -1,2 +1,4 @@
|
||||
export { CertificateProfilesTab } from "./CertificateProfilesTab";
|
||||
export { CertificatesTab } from "./CertificatesTab";
|
||||
export { CertificateTemplatesV2Tab } from "./CertificateTemplatesV2Tab";
|
||||
export { PkiCollectionsTab } from "./PkiCollectionsTab";
|
||||
|
||||
+3
-1
@@ -633,7 +633,9 @@ export const OAuthCallbackPage = () => {
|
||||
connectionName: data.connection.name,
|
||||
...(data.returnUrl.includes("integrations")
|
||||
? {
|
||||
selectedTab: IntegrationsListPageTabs.SecretSyncs
|
||||
selectedTab: localStorage.getItem("pkiSyncFormData")
|
||||
? IntegrationsListPageTabs.PkiSyncs
|
||||
: IntegrationsListPageTabs.SecretSyncs
|
||||
}
|
||||
: {})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user