diff --git a/src/app.tsx b/src/app.tsx
index a6fb9151..fa8fc849 100644
--- a/src/app.tsx
+++ b/src/app.tsx
@@ -88,6 +88,8 @@ export function App() {
dialog: certificateViewerDialog,
} = useCertificateViewerDialog({
providers,
+ currentProviderId,
+ fortifyClient,
});
const { open: handleProviderInfoDialogOpen, dialog: providerInfoDialog } =
diff --git a/src/components/certificate-viewer-dialog/CertificateViewerDialog.test.tsx b/src/components/certificate-viewer-dialog/CertificateViewerDialog.test.tsx
index 7bae24cc..5e310945 100644
--- a/src/components/certificate-viewer-dialog/CertificateViewerDialog.test.tsx
+++ b/src/components/certificate-viewer-dialog/CertificateViewerDialog.test.tsx
@@ -8,43 +8,76 @@ vi.mock("@peculiar/certificates-viewer-react", () => ({
}));
describe("", () => {
- const certificate = {
- raw: new ArrayBuffer(0),
- subjectName: "Certificate name",
- type: "x509",
- label: "Certificate name",
- subject: {
- commonName: "Certificate name",
+ const certificates = [
+ {
+ raw: new ArrayBuffer(0),
+ subjectName: "Certificate name 1",
+ type: "x509",
+ label: "Certificate name 1",
+ subject: {
+ commonName: "Certificate name 1",
+ },
},
- } as unknown as CertificateProps;
+ {
+ raw: new ArrayBuffer(0),
+ subjectName: "Certificate name 2",
+ type: "x509",
+ label: "Certificate name 2",
+ subject: {
+ commonName: "Certificate name 2",
+ },
+ },
+ ] as unknown as CertificateProps[];
it("Should render as x509 and handle close", async () => {
const onCloseMock = vi.fn();
render(
);
- expect(screen.getByText(`“${certificate.label}” details`));
- expect(screen.getByText(/x509 certificate viewer component/));
+ expect(
+ screen.getByText(`“${certificates[0].label}” details`)
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText(/x509 certificate viewer component/)
+ ).toBeInTheDocument();
- await userEvent.click(screen.getByRole("button", { name: /Cancel/ }));
+ await userEvent.click(screen.getByRole("button", { name: /Close/ }));
expect(onCloseMock).toBeCalledTimes(1);
});
+ it("Should render & switches tabs", async () => {
+ render(
+
+ );
+
+ expect(
+ screen.getByText(`${certificates[0].label}`).closest("button")
+ ).toHaveAttribute("aria-selected", "true");
+
+ await userEvent.click(screen.getByText(`${certificates[1].label}`));
+
+ expect(
+ screen.getByText(`${certificates[1].label}`).closest("button")
+ ).toHaveAttribute("aria-selected", "true");
+ });
+
it("Should render as CSR", () => {
render(
);
- expect(screen.getByText(/CSR certificate viewer component/));
+ expect(
+ screen.getByText(/CSR certificate viewer component/)
+ ).toBeInTheDocument();
});
});
diff --git a/src/components/certificate-viewer-dialog/CertificateViewerDialog.tsx b/src/components/certificate-viewer-dialog/CertificateViewerDialog.tsx
index 2746c0f8..0beed6a0 100644
--- a/src/components/certificate-viewer-dialog/CertificateViewerDialog.tsx
+++ b/src/components/certificate-viewer-dialog/CertificateViewerDialog.tsx
@@ -1,4 +1,4 @@
-import React from "react";
+import React, { useState } from "react";
import { Convert } from "pvtsutils";
import {
Button,
@@ -6,46 +6,86 @@ import {
DialogActions,
DialogContent,
DialogTitle,
+ Tab,
+ Tabs,
} from "@peculiar/react-components";
import {
PeculiarCertificateViewer,
PeculiarCsrViewer,
} from "@peculiar/certificates-viewer-react";
import { useTranslation } from "react-i18next";
+import clsx from "clsx";
import { getCertificateName } from "../../utils/certificate";
-import { CertificateProps, CertificateType } from "../../types";
+import { CertificateProps } from "../../types";
import styles from "./styles/index.module.scss";
interface CertificateViewerDialogProps {
- certificate: CertificateProps;
+ certificates: CertificateProps[];
onClose?: () => void;
}
export const CertificateViewerDialog: React.FunctionComponent<
CertificateViewerDialogProps
> = (props) => {
- const { certificate, onClose } = props;
+ const { certificates, onClose } = props;
const { t } = useTranslation();
+ const [tabIndex, setTabIndex] = useState("0");
- const certBase64 = Convert.ToBase64(certificate?.raw);
+ const renderTabs = () =>
+ certificates.length > 1 ? (
+
+ setTabIndex(value)}
+ >
+ {certificates.map((cert, index) => {
+ const name = getCertificateName(cert);
+ return (
+
+ {name}
+
+ );
+ })}
+
+
+ ) : null;
+
+ const renderContent = () =>
+ certificates[0]?.type === "x509" ? (
+ certificates.map((cert, index) => (
+
+ ))
+ ) : (
+
+ );
return (
diff --git a/src/components/certificate-viewer-dialog/styles/index.module.scss b/src/components/certificate-viewer-dialog/styles/index.module.scss
index 35a6c8e9..5af9d52f 100644
--- a/src/components/certificate-viewer-dialog/styles/index.module.scss
+++ b/src/components/certificate-viewer-dialog/styles/index.module.scss
@@ -4,3 +4,29 @@
padding: 0;
}
}
+
+.dialog_title_text {
+ padding: var(--pv-size-base-3) var(--pv-size-base-4);
+}
+
+.dialog_title_tabs_wraper {
+ border-top: 1px solid var(--pv-color-gray-4);
+ max-width: 870px;
+ overflow: auto;
+ .dialog_title_tabs {
+ padding: 0 var(--pv-size-base-4);
+ }
+}
+
+.dialog_title {
+ padding: 0 !important;
+ display: flex;
+ flex-direction: column;
+}
+
+.dialog_tab_content {
+ display: none;
+}
+.dialog_tab_content_current {
+ display: block;
+}
diff --git a/src/dialogs/certificate-viewer-dialog/useCertificateViewerDialog.test.tsx b/src/dialogs/certificate-viewer-dialog/useCertificateViewerDialog.test.tsx
index 3b7c0a25..570e7cb2 100644
--- a/src/dialogs/certificate-viewer-dialog/useCertificateViewerDialog.test.tsx
+++ b/src/dialogs/certificate-viewer-dialog/useCertificateViewerDialog.test.tsx
@@ -1,8 +1,23 @@
import { renderHook, act } from "@testing";
+import { vi } from "vitest";
import { useCertificateViewerDialog } from "./useCertificateViewerDialog";
import { CertificateProps } from "../../types";
-import type { IProviderInfo } from "@peculiar/fortify-client-core";
+import type { IProviderInfo, FortifyAPI } from "@peculiar/fortify-client-core";
+
+vi.mock("@peculiar/x509", async (importOriginal) => {
+ const originalModule =
+ await importOriginal();
+
+ return {
+ ...originalModule,
+ X509Certificate: vi.fn().mockImplementation(() => ({
+ rawData: new ArrayBuffer(0),
+ subject: "Test Subject",
+ subjectName: "Rest Subject Name",
+ })),
+ };
+});
describe("useCertificateViewerDialog", () => {
const providers = [
@@ -15,8 +30,15 @@ describe("useCertificateViewerDialog", () => {
const certificate = {
id: "1",
label: "Certificate name",
+ type: "x509",
} as CertificateProps;
+ const defaultProps = {
+ providers,
+ fortifyClient: null,
+ currentProviderId: providers[0].id,
+ };
+
const defaultOpenProps = {
certificate,
providerId: providers[0].id,
@@ -24,9 +46,7 @@ describe("useCertificateViewerDialog", () => {
it("Should initialize, open & close dialog", () => {
const { result } = renderHook(() =>
- useCertificateViewerDialog({
- providers,
- })
+ useCertificateViewerDialog(defaultProps)
);
expect(result.current.dialog).toBeInstanceOf(Function);
@@ -39,34 +59,56 @@ describe("useCertificateViewerDialog", () => {
const DialogComponent = result.current.dialog();
expect(DialogComponent).not.toBeNull();
- expect(DialogComponent?.props.certificate).toBe(certificate);
-
- act(() => {
- DialogComponent?.props.onClose();
- });
+ expect(DialogComponent?.props.certificates).toStrictEqual([certificate]);
+ DialogComponent?.props.onClose();
expect(result.current.dialog()).toBeNull();
});
- it("Should close dialog if current provider is not found", async () => {
- const { result, rerender } = renderHook(
- (localProviders) =>
- useCertificateViewerDialog({
- providers: localProviders,
- }),
- { initialProps: providers }
+ it("Should close the dialog when the current provider is not found", () => {
+ const { result } = renderHook(() =>
+ useCertificateViewerDialog(defaultProps)
);
act(() => {
- result.current.open(defaultOpenProps);
+ result.current.open({
+ ...defaultOpenProps,
+ providerId: "2",
+ });
});
- rerender([
- {
- id: "2",
- },
- ] as IProviderInfo[]);
+ const DialogComponent = result.current.dialog();
+ expect(DialogComponent).toBeNull();
+ });
- expect(result.current.dialog()).toBeNull();
+ it("Should make a chain of certificates", async () => {
+ const mockFortifyClient: Partial = {
+ getProviderById: vi.fn().mockResolvedValue({
+ certStorage: {
+ getChain: vi
+ .fn()
+ .mockResolvedValue([
+ { value: new ArrayBuffer(0) },
+ { value: new ArrayBuffer(0) },
+ ]),
+ },
+ }),
+ };
+
+ const { result } = renderHook(() =>
+ useCertificateViewerDialog({
+ ...defaultProps,
+ fortifyClient: mockFortifyClient as FortifyAPI,
+ })
+ );
+
+ await act(async () => {
+ await result.current.open(defaultOpenProps);
+ });
+
+ const DialogComponent = result.current.dialog();
+ expect(mockFortifyClient.getProviderById).toHaveBeenCalled();
+ expect(DialogComponent).not.toBeNull();
+ expect(DialogComponent?.props.certificates.length).toBe(2);
});
});
diff --git a/src/dialogs/certificate-viewer-dialog/useCertificateViewerDialog.tsx b/src/dialogs/certificate-viewer-dialog/useCertificateViewerDialog.tsx
index 71787009..17d65730 100644
--- a/src/dialogs/certificate-viewer-dialog/useCertificateViewerDialog.tsx
+++ b/src/dialogs/certificate-viewer-dialog/useCertificateViewerDialog.tsx
@@ -1,6 +1,9 @@
import React from "react";
-import { IProviderInfo } from "@peculiar/fortify-client-core";
+import { IProviderInfo, FortifyAPI } from "@peculiar/fortify-client-core";
import { useLockBodyScroll } from "react-use";
+import cloneDeep from "lodash/cloneDeep";
+import { X509Certificate } from "@peculiar/x509";
+import { getCertificateSubject } from "../../utils/certificate";
import { CertificateViewerDialog } from "../../components/certificate-viewer-dialog";
import { CertificateProps } from "../../types";
@@ -11,21 +14,65 @@ type UseCertificateViewerDialogOpenParams = {
type UseCertificateViewerInitialParams = {
providers: IProviderInfo[];
+ currentProviderId?: string;
+ fortifyClient: FortifyAPI | null;
};
export function useCertificateViewerDialog(
props: UseCertificateViewerInitialParams
) {
- const { providers } = props;
+ const { providers, currentProviderId, fortifyClient } = props;
const [isOpen, setIsOpen] = React.useState(false);
const openParamsRef = React.useRef();
+ const certificatesRef = React.useRef();
- const handleOpen = (params: UseCertificateViewerDialogOpenParams) => {
+ const handleOpen = async (params: UseCertificateViewerDialogOpenParams) => {
+ let currentCertificates: CertificateProps[] = [params.certificate];
+
+ try {
+ if (
+ fortifyClient &&
+ currentProviderId &&
+ params.certificate.type === "x509"
+ ) {
+ const localProvider =
+ await fortifyClient.getProviderById(currentProviderId);
+
+ const certificateCopy = cloneDeep(params.certificate);
+ certificateCopy.raw = null as unknown as ArrayBuffer;
+
+ const chain = await localProvider.certStorage.getChain(certificateCopy);
+
+ if (chain.length > 1) {
+ currentCertificates = chain.map((chainItem) => {
+ const cert = new X509Certificate(chainItem.value);
+
+ return {
+ raw: cert.rawData,
+ subjectName: cert.subject,
+ subject: getCertificateSubject(cert.subject),
+ type: "x509",
+ index: cert.serialNumber,
+ issuerName: cert.issuer,
+ notBefore: cert.notBefore,
+ notAfter: cert.notAfter,
+ serialNumber: cert.serialNumber,
+ publicKey: cert.publicKey as unknown as CryptoKey,
+ providerID: localProvider.id,
+ };
+ });
+ }
+ }
+ } catch (error) {
+ //
+ }
+ certificatesRef.current = currentCertificates;
openParamsRef.current = params;
setIsOpen(true);
};
const handleClose = () => {
+ certificatesRef.current = undefined;
openParamsRef.current = undefined;
setIsOpen(false);
};
@@ -43,9 +90,9 @@ export function useCertificateViewerDialog(
return {
open: handleOpen,
dialog: () =>
- isOpen && openParamsRef.current ? (
+ isOpen && openParamsRef.current && certificatesRef?.current ? (
) : null,
diff --git a/src/i18n/locales/en/main.json b/src/i18n/locales/en/main.json
index a25d68d9..2917a373 100644
--- a/src/i18n/locales/en/main.json
+++ b/src/i18n/locales/en/main.json
@@ -216,7 +216,8 @@
"cancel": "Cancel",
"clear": "Clear",
"import-certificate": "Import certificate",
- "try-again": "Try again"
+ "try-again": "Try again",
+ "close": "Close"
},
"app-error": {
"message": "Oops! Something went wrong.",
diff --git a/src/utils/certificate.ts b/src/utils/certificate.ts
index b88e0093..40ad5570 100644
--- a/src/utils/certificate.ts
+++ b/src/utils/certificate.ts
@@ -1,4 +1,8 @@
-import { Pkcs10CertificateRequest, X509Certificate } from "@peculiar/x509";
+import {
+ Pkcs10CertificateRequest,
+ X509Certificate,
+ Name,
+} from "@peculiar/x509";
import {
CertificateProps,
@@ -38,6 +42,22 @@ export function certificateSubjectToString(
return parts.join(", ");
}
+export function getCertificateSubject(subjectString: string) {
+ const jsonName = new Name(subjectString).toJSON();
+
+ return jsonName.reduce>((result, currentValue) => {
+ Object.keys(currentValue).forEach((keyName) => {
+ if (!result[keyName]) {
+ result[keyName] = currentValue[keyName];
+ } else {
+ result[keyName].push(...currentValue[keyName]);
+ }
+ });
+
+ return result;
+ }, {});
+}
+
export function getCertificateName(certificate: CertificateProps) {
if (!certificate.subject) {
return certificate.subjectName;