diff --git a/package.json b/package.json
index 46d4d21f..b5acc191 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "@peculiar/fortify-tools",
"homepage": "https://tools.fortifyapp.com",
- "version": "2.0.3",
+ "version": "2.0.5",
"author": "PeculiarVentures Team",
"license": "MIT",
"private": true,
diff --git a/src/app.tsx b/src/app.tsx
index 3e25151a..a6fb9151 100644
--- a/src/app.tsx
+++ b/src/app.tsx
@@ -12,6 +12,7 @@ import { useSortList } from "./hooks/sort-list";
import { useSearchList } from "./hooks/search-list";
import { useCertificateImportDialog } from "./dialogs/certificate-import-dialog";
import { useCertificateCreateDialog } from "./dialogs/certificate-create-dialog";
+import { useProviderInfoDialog } from "./dialogs/provider-info-dialog";
import styles from "./app.module.scss";
@@ -21,13 +22,19 @@ export function App() {
fetching,
challenge,
providers,
- currentProviderId,
+ currentProvider,
certificates,
+ isCurrentProviderLogedin,
handleCertificatesDataReload,
handleProviderChange,
handleRetryConection,
+ handleProviderLoginLogout,
+ handleProviderResetAndRefreshList,
} = useApp();
+ const currentProviderId = currentProvider?.id;
+ const isCurrentProviderReadOnly = Boolean(currentProvider?.readOnly);
+
const {
searchedText,
list: searchedCertificate,
@@ -38,6 +45,7 @@ export function App() {
open: handleCertificateDeleteDialogOpen,
dialog: certificateDeleteDialog,
} = useCertificateDeleteDialog({
+ providers,
fortifyClient,
onSuccess: (providerId) => {
handleCertificatesDataReload(providerId);
@@ -78,7 +86,12 @@ export function App() {
const {
open: handleCertificateViewerDialogOpen,
dialog: certificateViewerDialog,
- } = useCertificateViewerDialog();
+ } = useCertificateViewerDialog({
+ providers,
+ });
+
+ const { open: handleProviderInfoDialogOpen, dialog: providerInfoDialog } =
+ useProviderInfoDialog({ providers });
return (
<>
@@ -92,13 +105,20 @@ export function App() {
- currentProviderId && handleCertificatesDataReload(currentProviderId)
+ onReload={handleProviderResetAndRefreshList}
+ onInfo={() =>
+ currentProvider && handleProviderInfoDialogOpen(currentProvider)
}
+ isLoggedIn={isCurrentProviderLogedin}
+ onLoginLogout={handleProviderLoginLogout}
>
- {certificateViewerDialog()}
- {certificateDeleteDialog()}
- {certificateImportDialog()}
- {certificateCreateDialog()}
+ {providers.length ? (
+ <>
+ {certificateViewerDialog()}
+ {certificateDeleteDialog()}
+ {certificateImportDialog()}
+ {certificateCreateDialog()}
+ {providerInfoDialog()}
+ >
+ ) : null}
+
>
);
diff --git a/src/components/certificate-delete-dialog/CertificateDeleteDialog.stories.tsx b/src/components/certificate-delete-dialog/CertificateDeleteDialog.stories.tsx
index 5e449b6f..f56c2653 100644
--- a/src/components/certificate-delete-dialog/CertificateDeleteDialog.stories.tsx
+++ b/src/components/certificate-delete-dialog/CertificateDeleteDialog.stories.tsx
@@ -4,6 +4,7 @@ import { CertificateDeleteDialog } from "./CertificateDeleteDialog";
const meta: Meta = {
title: "Components/CertificateDeleteDialog",
component: CertificateDeleteDialog,
+ tags: ["!autodocs"],
};
export default meta;
@@ -15,11 +16,3 @@ export const Default: Story = {
certificateId: "12345",
},
};
-
-export const Loading: Story = {
- args: {
- certificateName: "Certificate Name",
- certificateId: "12345",
- loading: true,
- },
-};
diff --git a/src/components/certificate-delete-dialog/CertificateDeleteDialog.test.tsx b/src/components/certificate-delete-dialog/CertificateDeleteDialog.test.tsx
new file mode 100644
index 00000000..477fe00c
--- /dev/null
+++ b/src/components/certificate-delete-dialog/CertificateDeleteDialog.test.tsx
@@ -0,0 +1,39 @@
+import { ComponentProps } from "react";
+import { render, userEvent, screen } from "@testing";
+import { CertificateDeleteDialog } from "./CertificateDeleteDialog";
+
+describe("", () => {
+ const defaultProps: ComponentProps = {
+ certificateName: "Certificate Name",
+ certificateId: "1",
+ onDialogClose: vi.fn(),
+ onDeleteClick: vi.fn((data) => data),
+ };
+
+ it("Should render and handle buttons click", async () => {
+ render();
+
+ expect(
+ screen.getByText(
+ `Are you sure you want to delete “${defaultProps.certificateName}”?`
+ )
+ ).toBeInTheDocument();
+
+ await userEvent.click(screen.getByRole("button", { name: /Cancel/ }));
+
+ expect(defaultProps.onDialogClose).toBeCalledTimes(1);
+
+ await userEvent.click(screen.getByRole("button", { name: /Delete/ }));
+
+ expect(defaultProps.onDeleteClick).toBeCalledTimes(1);
+ expect(defaultProps.onDeleteClick).toBeCalledWith(
+ defaultProps.certificateId
+ );
+ });
+
+ it("Should render loading", () => {
+ render();
+
+ expect(screen.getByText(/Deleting certificate/)).toBeInTheDocument();
+ });
+});
diff --git a/src/components/certificate-type-label/CertificateTypeLabel.tsx b/src/components/certificate-type-label/CertificateTypeLabel.tsx
index e14cce81..3c906f3b 100644
--- a/src/components/certificate-type-label/CertificateTypeLabel.tsx
+++ b/src/components/certificate-type-label/CertificateTypeLabel.tsx
@@ -3,18 +3,20 @@ import { ICertificate } from "@peculiar/fortify-client-core";
import { useTranslation } from "react-i18next";
import clsx from "clsx";
import { Typography } from "@peculiar/react-components";
-import CertificateIcon from "../../icons/certificate.svg?react";
+import CertificateIcon from "../../icons/certificate-30.svg?react";
+import CertificateWithKeyIcon from "../../icons/certificate-with-key-30.svg?react";
import styles from "./styles/index.module.scss";
interface CertificateTypeLabelProps {
type: ICertificate["type"];
+ withPrivatKey: boolean;
className?: ComponentProps<"div">["className"];
}
export const CertificateTypeLabel: React.FunctionComponent<
CertificateTypeLabelProps
> = (props) => {
- const { type, className } = props;
+ const { type, className, withPrivatKey } = props;
const { t } = useTranslation();
return (
@@ -22,11 +24,27 @@ export const CertificateTypeLabel: React.FunctionComponent<
{type === "x509" ? (
<>
-
+ {withPrivatKey ? : }
+
+
+
+ {t("certificates.list.cell.certificate")}
+
+ {withPrivatKey ? (
+
+ {" "}
+ {t("certificates.list.cell.with-privat-key")}
+
+ ) : undefined}
-
- {t("certificates.list.cell.certificate")}
-
>
) : (
diff --git a/src/components/certificate-type-label/styles/index.module.scss b/src/components/certificate-type-label/styles/index.module.scss
index ec02e929..fa04f6ae 100644
--- a/src/components/certificate-type-label/styles/index.module.scss
+++ b/src/components/certificate-type-label/styles/index.module.scss
@@ -2,15 +2,19 @@
display: flex;
gap: var(--pv-size-base-2);
align-items: center;
- .icon_wrapper {
- display: flex;
- flex-direction: column;
- justify-content: center;
+}
+.icon_wrapper {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ color: var(--pv-color-gray-9);
+ width: var(--pv-size-base-6);
+ svg {
width: var(--pv-size-base-6);
- svg {
- width: var(--pv-size-base-6);
- height: var(--pv-size-base-6);
- }
+ height: var(--pv-size-base-6);
}
}
+.label_part {
+ display: inline;
+}
diff --git a/src/components/certificate-viewer-dialog/CertificateViewerDialog.test.tsx b/src/components/certificate-viewer-dialog/CertificateViewerDialog.test.tsx
new file mode 100644
index 00000000..865526ac
--- /dev/null
+++ b/src/components/certificate-viewer-dialog/CertificateViewerDialog.test.tsx
@@ -0,0 +1,50 @@
+import { render, userEvent, screen } from "@testing";
+import { CertificateViewerDialog } from "./CertificateViewerDialog";
+import { CertificateProps } from "../../types";
+
+vi.mock("@peculiar/certificates-viewer-react", () => ({
+ PeculiarCertificateViewer: () => "x509 certificate viewer component",
+ PeculiarCsrViewer: () => "CSR certificate viewer component",
+}));
+
+describe("", () => {
+ const certificate = {
+ raw: new ArrayBuffer(0),
+ subjectName: "Certificate name",
+ type: "x509",
+ label: "Certificate name",
+ subject: {
+ commonName: "Certificate name",
+ },
+ } 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/));
+
+ await userEvent.click(screen.getByRole("button", { name: /Cancel/ }));
+
+ expect(onCloseMock).toBeCalledTimes(1);
+ });
+
+ it("Should render as CSR", async () => {
+ render(
+
+ );
+ expect(screen.getByText(/CSR certificate viewer component/));
+ });
+});
diff --git a/src/components/certificates-list/CertificatesList.tsx b/src/components/certificates-list/CertificatesList.tsx
index c4c30853..a77a85d3 100644
--- a/src/components/certificates-list/CertificatesList.tsx
+++ b/src/components/certificates-list/CertificatesList.tsx
@@ -53,10 +53,15 @@ interface CertificatesListProps {
providerId: string;
label: string;
}) => void;
- onViewDetails: (certificate: CertificateProps) => void;
+ onViewDetails: (params: {
+ certificate: CertificateProps;
+ providerId: string;
+ }) => void;
className?: ComponentProps<"table">["className"];
highlightedText?: string;
loading?: boolean;
+ isLoggedIn: boolean;
+ isReadOnly: boolean;
}
export const CertificatesList: React.FunctionComponent<
@@ -69,6 +74,8 @@ export const CertificatesList: React.FunctionComponent<
currentSortDir,
highlightedText,
loading,
+ isLoggedIn,
+ isReadOnly = false,
onSort,
onViewDetails,
onDelete,
@@ -134,7 +141,11 @@ export const CertificatesList: React.FunctionComponent<
);
return (
-
+
@@ -168,6 +179,7 @@ export const CertificatesList: React.FunctionComponent<
notAfter,
raw,
index,
+ privateKeyId,
} = certificate;
const certificateName = getCertificateName(certificate);
@@ -176,12 +188,14 @@ export const CertificatesList: React.FunctionComponent<
onViewDetails(certificate)}
+ onClick={() =>
+ onViewDetails({ certificate, providerId: providerID })
+ }
onFocus={() => setCurrentRow(id)}
onBlur={() => setCurrentRow(undefined)}
onKeyDown={(event) =>
["Space", "Enter"].includes(event.code) &&
- onViewDetails(certificate)
+ onViewDetails({ certificate, providerId: providerID })
}
onMouseOver={() => currentRow && setCurrentRow(undefined)}
className={clsx({
@@ -189,7 +203,10 @@ export const CertificatesList: React.FunctionComponent<
})}
>
-
+
onViewDetails(certificate)}
+ onClick={() =>
+ onViewDetails({ certificate, providerId: providerID })
+ }
>
{t("certificates.list.action.view-details")}
certificateRawToPem(raw, type)
@@ -236,21 +256,24 @@ export const CertificatesList: React.FunctionComponent<
>
-
- onDelete({
- certificateIndex: index,
- providerId: providerID,
- label: certificateName,
- })
- }
- size="small"
- className={styles.action_icon_button}
- >
-
-
+ {!isReadOnly ? (
+
+ onDelete({
+ certificateIndex: index,
+ providerId: providerID,
+ label: certificateName,
+ })
+ }
+ size="small"
+ className={styles.action_icon_button}
+ disabled={!isLoggedIn}
+ >
+
+
+ ) : null}
@@ -267,12 +290,8 @@ function CertificatesListLoading() {
return [...Array(12).keys()].map((index) => (
{[...Array(4).keys()].map((index) => (
-
-
+
+
))}
diff --git a/src/components/certificates-list/styles/index.module.scss b/src/components/certificates-list/styles/index.module.scss
index 04e90a67..fe4e16b8 100644
--- a/src/components/certificates-list/styles/index.module.scss
+++ b/src/components/certificates-list/styles/index.module.scss
@@ -37,7 +37,7 @@
margin-right: var(--pv-size-base-2);
}
- .action_icon_button {
+ .action_icon_button:not(:disabled) {
color: var(--pv-color-gray-10);
}
}
@@ -46,6 +46,20 @@
width: fit-content;
overflow: visible;
padding-right: var(--pv-size-base-5);
+ &:before {
+ content: "";
+ width: 50px;
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: -50px;
+ background: linear-gradient(
+ 90deg,
+ rgba(255, 255, 255, 0) 0%,
+ rgba(255, 255, 255, 1) 80%,
+ rgba(255, 255, 255, 1) 100%
+ );
+ }
}
}
}
@@ -55,6 +69,13 @@
min-height: calc(100vh - var(--pv-top-header-height));
background-color: var(--pv-color-white);
}
+.table_wrapper_loading {
+ height: calc(100vh - var(--pv-top-header-height));
+ overflow: hidden;
+ thead th {
+ top: 0 !important;
+ }
+}
.table_wrapper {
position: relative;
diff --git a/src/components/certificates-topbar/CertificatesTopbar.test.tsx b/src/components/certificates-topbar/CertificatesTopbar.test.tsx
new file mode 100644
index 00000000..eb3f163d
--- /dev/null
+++ b/src/components/certificates-topbar/CertificatesTopbar.test.tsx
@@ -0,0 +1,191 @@
+import { render, userEvent, vi, screen } from "@testing";
+import { CertificatesTopbar } from "./CertificatesTopbar";
+
+describe("", () => {
+ const defaultProps = {
+ isLoggedIn: true,
+ isDisabled: false,
+ isReadOnly: false,
+ onReload: vi.fn(),
+ onLoginLogout: vi.fn((data) => data),
+ onCreate: vi.fn(),
+ onImport: vi.fn(),
+ onSearch: vi.fn(),
+ onInfo: vi.fn(),
+ };
+
+ it("Should render as disabled", async () => {
+ render(
+
+ );
+
+ expect(screen.getByRole("searchbox")).toBeDisabled();
+ expect(
+ screen.getByRole("button", {
+ name: /Reset session and refresh certificate list/,
+ })
+ ).toBeDisabled();
+ expect(
+ screen.getByRole("button", { name: /Provider information/ })
+ ).toBeDisabled();
+ expect(screen.getByRole("button", { name: /Sign in/ })).toBeDisabled();
+ expect(screen.getByRole("button", { name: /New/ })).toBeDisabled();
+ });
+
+ it("Should render as not logged in", async () => {
+ render();
+
+ expect(screen.getByRole("button", { name: /Sign in/ })).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: /New/ })).toBeDisabled();
+ });
+
+ it("Should render as logged in", async () => {
+ render();
+
+ expect(
+ screen.getByRole("button", { name: /Sign out/ })
+ ).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: /New/ })).toBeEnabled();
+ });
+
+ it("Should render as read only", async () => {
+ render();
+
+ expect(
+ screen.queryByRole("button", { name: /New/ })
+ ).not.toBeInTheDocument();
+ });
+
+ it("Should handle onReload", async () => {
+ render();
+
+ await userEvent.click(
+ screen.getByRole("button", {
+ name: /Reset session and refresh certificate list/,
+ })
+ );
+
+ expect(defaultProps.onReload).toBeCalledTimes(1);
+ });
+
+ it("Should handle onInfo", async () => {
+ render();
+
+ await userEvent.click(
+ screen.getByRole("button", { name: /Provider information/ })
+ );
+
+ expect(defaultProps.onInfo).toBeCalledTimes(1);
+ });
+
+ it("Should handle onLoginLogout", async () => {
+ render();
+
+ await userEvent.click(screen.getByRole("button", { name: /Sign out/ }));
+
+ expect(defaultProps.onLoginLogout).toBeCalledTimes(1);
+ expect(defaultProps.onLoginLogout).toHaveReturnedWith(true);
+ });
+
+ it("Should handle create CSR", async () => {
+ const onCreateMock = vi.fn((data) => data);
+ render();
+
+ const newButton = screen.getByRole("button", { name: "New" });
+
+ await userEvent.click(newButton);
+
+ expect(screen.getByRole("presentation")).toBeInTheDocument();
+
+ await userEvent.click(
+ screen.getByRole("menuitem", {
+ name: /Create certificate signing request \(CSR\)/,
+ })
+ );
+
+ expect(screen.queryByRole("presentation")).not.toBeInTheDocument();
+
+ expect(onCreateMock).toBeCalledTimes(1);
+ expect(onCreateMock).toHaveReturnedWith("csr");
+ });
+
+ it("Should handle create x509", async () => {
+ const onCreateMock = vi.fn((data) => data);
+ render();
+
+ const newButton = screen.getByRole("button", { name: "New" });
+
+ await userEvent.click(newButton);
+
+ expect(screen.getByRole("presentation")).toBeInTheDocument();
+
+ await userEvent.click(
+ screen.getByRole("menuitem", {
+ name: /Create self-signed certificate/,
+ })
+ );
+
+ expect(screen.queryByRole("presentation")).not.toBeInTheDocument();
+
+ expect(onCreateMock).toBeCalledTimes(1);
+ expect(onCreateMock).toHaveReturnedWith("x509");
+ });
+
+ it("Should handle import", async () => {
+ render();
+
+ const newButton = screen.getByRole("button", { name: "New" });
+
+ await userEvent.click(newButton);
+
+ expect(screen.getByRole("presentation")).toBeInTheDocument();
+
+ await userEvent.click(
+ screen.getByRole("menuitem", {
+ name: /Import certificate/,
+ })
+ );
+
+ expect(screen.queryByRole("presentation")).not.toBeInTheDocument();
+
+ expect(defaultProps.onImport).toBeCalledTimes(1);
+ });
+
+ it("Should handle search", async () => {
+ const onSearchMock = vi.fn((data) => data);
+
+ render(
+
+ );
+
+ await userEvent.type(screen.getByRole("searchbox"), "a");
+
+ expect(onSearchMock).toBeCalledTimes(1);
+ expect(onSearchMock).toHaveReturnedWith("testa");
+ });
+
+ it("Should handle search clear", async () => {
+ const onSearchMock = vi.fn((data) => data);
+
+ render(
+
+ );
+
+ await userEvent.click(screen.getByTestId("clear-search-button"));
+
+ expect(onSearchMock).toBeCalledTimes(1);
+ expect(onSearchMock).toHaveReturnedWith("");
+ });
+});
diff --git a/src/components/certificates-topbar/CertificatesTopbar.tsx b/src/components/certificates-topbar/CertificatesTopbar.tsx
index f73b076a..24e26c0e 100644
--- a/src/components/certificates-topbar/CertificatesTopbar.tsx
+++ b/src/components/certificates-topbar/CertificatesTopbar.tsx
@@ -6,7 +6,11 @@ import {
IconButton,
Menu,
TextField,
+ Tooltip,
} from "@peculiar/react-components";
+import InfoIcon from "../../icons/info-20.svg?react";
+import LoginIcon from "../../icons/login-20.svg?react";
+import LogoutIcon from "../../icons/logout-20.svg?react";
import ReloadIcon from "../../icons/reload-20.svg?react";
import ImportIcon from "../../icons/import-30.svg?react";
import SearchIcon from "../../icons/search.svg?react";
@@ -20,10 +24,15 @@ import styles from "./styles/index.module.scss";
interface CertificatesTopbarProps {
className?: ComponentProps<"div">["className"];
searchValue?: string;
+ isDisabled: boolean;
+ isLoggedIn: boolean;
+ isReadOnly: boolean;
onSearch: (value: string) => void;
onImport: () => void;
onCreate: (type: "csr" | "x509") => void;
onReload: () => void;
+ onInfo: () => void;
+ onLoginLogout: (isLoggedin: boolean) => void;
}
export const CertificatesTopbar: React.FunctionComponent<
CertificatesTopbarProps
@@ -31,14 +40,90 @@ export const CertificatesTopbar: React.FunctionComponent<
const {
className,
searchValue = "",
+ isDisabled = true,
+ isLoggedIn,
+ isReadOnly = false,
onSearch,
onImport,
onCreate,
onReload,
+ onInfo,
+ onLoginLogout,
} = props;
const { t } = useTranslation();
+ const renderCreateButton = () => {
+ if (isReadOnly) {
+ return null;
+ }
+
+ if (isDisabled || !isLoggedIn) {
+ return (
+
+ }
+ disabled={true}
+ >
+ {t("topbar.create-certificate")}
+
+
+ );
+ }
+
+ return (
+
+ ),
+ onClick: () => onCreate("csr"),
+ },
+ {
+ label: t("topbar.create-certificate-ssc"),
+ startIcon: (
+
+ ),
+ onClick: () => onCreate("x509"),
+ },
+ {
+ label: t("topbar.import-certificate"),
+ startIcon: ,
+ onClick: onImport,
+ },
+ ]}
+ >
+ }
+ >
+ {t("topbar.create-certificate")}
+
+
+ );
+ };
+
return (
@@ -50,6 +135,7 @@ export const CertificatesTopbar: React.FunctionComponent<
onChange={(event) => {
onSearch(event.target.value);
}}
+ disabled={isDisabled}
/>
{
onSearch("");
}}
+ data-testid="clear-search-button"
>
@@ -77,47 +164,45 @@ export const CertificatesTopbar: React.FunctionComponent<
arrow: true,
size: "large",
}}
+ className={styles.icon_button}
+ disabled={isDisabled}
>
-
+
-
-
-
- ),
- onClick: () => onCreate("csr"),
- },
- {
- label: t("topbar.create-certificate-ssc"),
- startIcon: (
-
- ),
- onClick: () => onCreate("x509"),
- },
- {
- label: t("topbar.import-certificate"),
- startIcon: ,
- onClick: onImport,
- },
- ]}
+ className={styles.icon_button}
+ disabled={isDisabled}
>
- }
- >
- {t("topbar.create-certificate")}
-
-
+
+
+ onLoginLogout(isLoggedIn)}
+ title={t(`topbar.provider-${isLoggedIn ? "logout" : "login"}`)}
+ tooltipProps={{
+ color: "white",
+ offset: 2,
+ placement: "bottom-end",
+ arrow: true,
+ size: "large",
+ }}
+ className={isLoggedIn ? styles.icon_button_wrong : styles.icon_button}
+ disabled={isDisabled}
+ >
+ {isLoggedIn ? : }
+
+ {renderCreateButton()}
);
};
diff --git a/src/components/certificates-topbar/styles/index.module.scss b/src/components/certificates-topbar/styles/index.module.scss
index 26fa34cb..0f953345 100644
--- a/src/components/certificates-topbar/styles/index.module.scss
+++ b/src/components/certificates-topbar/styles/index.module.scss
@@ -55,4 +55,7 @@
.icon_button {
color: var(--pv-color-gray-10);
}
+ .icon_button_wrong {
+ color: var(--pv-color-wrong);
+ }
}
diff --git a/src/components/provider-info-dialog/ProviderInfoDialog.stories.tsx b/src/components/provider-info-dialog/ProviderInfoDialog.stories.tsx
new file mode 100644
index 00000000..0a0c4764
--- /dev/null
+++ b/src/components/provider-info-dialog/ProviderInfoDialog.stories.tsx
@@ -0,0 +1,39 @@
+import type { Meta, StoryObj } from "@storybook/react";
+import { ProviderInfoDialog } from "./ProviderInfoDialog";
+import type { IProviderInfo } from "@peculiar/fortify-client-core";
+
+const meta: Meta = {
+ title: "Components/ProviderInfoDialog",
+ component: ProviderInfoDialog,
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {
+ args: {
+ data: {
+ name: "MacOS Crypto",
+ isHardware: true,
+ isRemovable: true,
+ token: {
+ label: "MacOS Crypto",
+ serialNumber: "1",
+ freePrivateMemory: 18446,
+ // freePrivateMemory: 18446744073709552000,
+ hardwareVersion: {
+ major: 0,
+ minor: 1,
+ version: 0,
+ },
+ firmwareVersion: {
+ major: 0,
+ minor: 1,
+ version: 0,
+ },
+ model: "MacOS Crypto",
+ },
+ algorithms: ["SHA-1", "SHA-256", "SHA-384"],
+ } as IProviderInfo,
+ },
+};
diff --git a/src/components/provider-info-dialog/ProviderInfoDialog.test.tsx b/src/components/provider-info-dialog/ProviderInfoDialog.test.tsx
new file mode 100644
index 00000000..cbb99d5b
--- /dev/null
+++ b/src/components/provider-info-dialog/ProviderInfoDialog.test.tsx
@@ -0,0 +1,122 @@
+import { ComponentProps } from "react";
+import { render, userEvent, vi, screen } from "@testing";
+import type { IProviderInfo } from "@peculiar/fortify-client-core";
+import { ProviderInfoDialog } from "./ProviderInfoDialog";
+
+describe("", () => {
+ const defaultProps: ComponentProps = {
+ data: {
+ name: "name-text",
+ isHardware: false,
+ isRemovable: false,
+ token: {
+ label: "label-text",
+ serialNumber: "1",
+ freePrivateMemory: 18446,
+ hardwareVersion: {
+ major: 1,
+ minor: 1,
+ version: 0,
+ },
+ firmwareVersion: {
+ major: 1,
+ minor: 2,
+ version: 0,
+ },
+ model: "model-text",
+ },
+ algorithms: ["SHA-1", "SHA-256"],
+ } as IProviderInfo,
+ onDialogClose: vi.fn(),
+ };
+
+ it("Should render and handle click close", async () => {
+ render();
+
+ expect(
+ screen.getByText(`${defaultProps.data.name} information`)
+ ).toBeInTheDocument();
+
+ expect(screen.getByText(/Token name/).nextElementSibling).toHaveTextContent(
+ defaultProps.data.token!.label
+ );
+
+ expect(
+ screen.getByText(/Token category/).nextElementSibling
+ ).toHaveTextContent(/Software/);
+
+ expect(
+ screen.getByText(/Extractable/).nextElementSibling
+ ).toHaveTextContent(/No/);
+
+ expect(
+ screen.getByText(/Serial number/).nextElementSibling
+ ).toHaveTextContent(defaultProps.data.token!.serialNumber);
+
+ expect(screen.getByText(/Free space/).nextElementSibling).toHaveTextContent(
+ defaultProps.data.token!.freePrivateMemory.toString()
+ );
+
+ expect(
+ screen.getByText(/Hardware version/).nextElementSibling
+ ).toHaveTextContent("1.1");
+
+ expect(
+ screen.getByText(/Firmware version/).nextElementSibling
+ ).toHaveTextContent("1.2");
+
+ expect(screen.getByText(/Model/).nextElementSibling).toHaveTextContent(
+ defaultProps.data.token!.model
+ );
+
+ expect(screen.getByText(/Algorithms/).nextElementSibling).toHaveTextContent(
+ "SHA-1, SHA-256"
+ );
+
+ await userEvent.click(screen.getByRole("button", { name: /Cancel/ }));
+
+ expect(defaultProps.onDialogClose).toBeCalledTimes(1);
+ });
+
+ it("Should render as hardware", () => {
+ const props: ComponentProps = {
+ ...defaultProps,
+ data: { ...defaultProps.data, isHardware: true } as IProviderInfo,
+ };
+ render();
+
+ expect(
+ screen.getByText(/Token category/).nextElementSibling
+ ).toHaveTextContent(/Hardware/);
+ });
+
+ it("Should render as extractable", () => {
+ const props: ComponentProps = {
+ ...defaultProps,
+ data: { ...defaultProps.data, isRemovable: true } as IProviderInfo,
+ };
+ render();
+
+ expect(
+ screen.getByText(/Extractable/).nextElementSibling
+ ).toHaveTextContent(/Yes/);
+ });
+
+ it("Should render as free space is unavailable", () => {
+ const props: ComponentProps = {
+ ...defaultProps,
+ data: {
+ ...defaultProps.data,
+ token: {
+ ...defaultProps.data.token,
+ freePrivateMemory: 18446744073709552000,
+ },
+ } as IProviderInfo,
+ };
+ render();
+
+ expect(screen.getByText(/Free space/).nextElementSibling).toHaveTextContent(
+ /Information is unavailable/
+ );
+ });
+});
diff --git a/src/components/provider-info-dialog/ProviderInfoDialog.tsx b/src/components/provider-info-dialog/ProviderInfoDialog.tsx
new file mode 100644
index 00000000..3c2ab879
--- /dev/null
+++ b/src/components/provider-info-dialog/ProviderInfoDialog.tsx
@@ -0,0 +1,107 @@
+import React from "react";
+import {
+ Button,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogTitle,
+ Typography,
+} from "@peculiar/react-components";
+import { useTranslation } from "react-i18next";
+import type { IProviderInfo } from "@peculiar/fortify-client-core";
+
+import styles from "./styles/index.module.scss";
+
+interface ProviderInfoDialogProps {
+ data: IProviderInfo;
+ onDialogClose: () => void;
+}
+
+export const ProviderInfoDialog: React.FunctionComponent<
+ ProviderInfoDialogProps
+> = (props) => {
+ const { onDialogClose, data } = props;
+ const { t } = useTranslation();
+
+ const items = [
+ {
+ label: t("providers.dialog.info.list.token-name"),
+ value: data.token?.label,
+ },
+ {
+ label: t("providers.dialog.info.list.token-category.label"),
+ value: t(
+ `providers.dialog.info.list.token-category.value.${data.isHardware ? "hardware" : "software"}`
+ ),
+ },
+ {
+ label: t("providers.dialog.info.list.extractable.label"),
+ value: t(
+ `providers.dialog.info.list.extractable.value.${data.isRemovable ? "yes" : "no"}`
+ ),
+ },
+ {
+ label: t("providers.dialog.info.list.serial-number"),
+ value: data.token?.serialNumber,
+ },
+ {
+ label: t("providers.dialog.info.list.free-space"),
+ value:
+ // Some providers return this value as "unknown number"
+ // We don't want to show the value in this case (requested by @microshine)
+ data.token?.freePrivateMemory == 18446744073709552000
+ ? undefined
+ : data.token?.freePrivateMemory,
+ },
+ {
+ label: t("providers.dialog.info.list.hardware-version"),
+ value: data.token?.hardwareVersion
+ ? `${data.token?.hardwareVersion?.major}.${data.token?.hardwareVersion?.minor}`
+ : undefined,
+ },
+ {
+ label: t("providers.dialog.info.list.firmware-version"),
+ value: data.token?.firmwareVersion
+ ? `${data.token?.firmwareVersion?.major}.${data.token?.firmwareVersion?.minor}`
+ : undefined,
+ },
+ {
+ label: t("providers.dialog.info.list.model"),
+ value: data.token?.model,
+ },
+ {
+ label: t("providers.dialog.info.list.algorithms"),
+ value: data.algorithms?.join(", "),
+ },
+ ];
+
+ const renderInfoItems = () =>
+ items.map(({ label, value }) => (
+
+
+ {label}:
+
+
+ {value || t("providers.dialog.info.empty-value")}
+
+
+ ));
+
+ return (
+
+ );
+};
diff --git a/src/components/provider-info-dialog/index.ts b/src/components/provider-info-dialog/index.ts
new file mode 100644
index 00000000..019f22d6
--- /dev/null
+++ b/src/components/provider-info-dialog/index.ts
@@ -0,0 +1 @@
+export * from "./ProviderInfoDialog";
diff --git a/src/components/provider-info-dialog/styles/index.module.scss b/src/components/provider-info-dialog/styles/index.module.scss
new file mode 100644
index 00000000..b0a402c4
--- /dev/null
+++ b/src/components/provider-info-dialog/styles/index.module.scss
@@ -0,0 +1,22 @@
+.dialog {
+ max-width: 870px !important;
+
+ .dialog_title {
+ padding-left: var(--pv-size-base-6);
+ padding-right: var(--pv-size-base-6);
+ }
+
+ .dialog_content {
+ max-height: 525px;
+ .dialog_content_list {
+ padding: var(--pv-size-base) var(--pv-size-base-2) 0 var(--pv-size-base-2);
+ display: grid;
+ grid-template-columns: 35% auto;
+ gap: var(--pv-size-base-4) var(--pv-size-base-8);
+ }
+ }
+
+ .dialog_footer {
+ padding: var(--pv-size-base-6);
+ }
+}
diff --git a/src/dialogs/certificate-delete-dialog/useCertificateDeleteDialog.test.tsx b/src/dialogs/certificate-delete-dialog/useCertificateDeleteDialog.test.tsx
new file mode 100644
index 00000000..9d7c9b28
--- /dev/null
+++ b/src/dialogs/certificate-delete-dialog/useCertificateDeleteDialog.test.tsx
@@ -0,0 +1,67 @@
+import { renderHook, act } from "@testing";
+import { vi } from "vitest";
+import { useCertificateDeleteDialog } from "./useCertificateDeleteDialog";
+
+import type { IProviderInfo } from "@peculiar/fortify-client-core";
+import type { FortifyAPI } from "@peculiar/fortify-client-core";
+
+vi.mock("@peculiar/react-components", async () => {
+ const actual = await vi.importActual("@peculiar/react-components");
+ return {
+ ...actual,
+ useToast: () => ({
+ addToast: vi.fn(),
+ }),
+ };
+});
+
+describe("useCertificateDeleteDialog", () => {
+ const providers = [
+ {
+ id: "1",
+ name: "Provider 1",
+ },
+ ] as IProviderInfo[];
+
+ it("Should initialize and call onSuccess", async () => {
+ const certificateIndex = "1";
+ const providerId = providers[0].id;
+
+ const mockFortifyClient: Partial = {
+ removeCertificateById: vi.fn().mockResolvedValue({}),
+ };
+ const onSuccessMock = vi.fn();
+
+ const { result } = renderHook(() =>
+ useCertificateDeleteDialog({
+ providers: providers,
+ onSuccess: onSuccessMock,
+ fortifyClient: mockFortifyClient as FortifyAPI,
+ })
+ );
+
+ expect(result.current.dialog).toBeInstanceOf(Function);
+ expect(result.current.open).toBeInstanceOf(Function);
+
+ act(() => {
+ result.current.open({
+ certificateIndex,
+ providerId,
+ label: "Message",
+ });
+ });
+
+ await act(async () => {
+ const DialogComponent = result.current.dialog();
+
+ DialogComponent &&
+ (await DialogComponent.props.onDeleteClick(certificateIndex));
+ });
+
+ expect(onSuccessMock).toHaveBeenCalledWith(providerId);
+ expect(mockFortifyClient.removeCertificateById).toHaveBeenCalledWith(
+ providerId,
+ certificateIndex
+ );
+ });
+});
diff --git a/src/dialogs/certificate-delete-dialog/useCertificateDeleteDialog.tsx b/src/dialogs/certificate-delete-dialog/useCertificateDeleteDialog.tsx
index dbda982e..36f32fe9 100644
--- a/src/dialogs/certificate-delete-dialog/useCertificateDeleteDialog.tsx
+++ b/src/dialogs/certificate-delete-dialog/useCertificateDeleteDialog.tsx
@@ -1,4 +1,5 @@
import React from "react";
+import { IProviderInfo } from "@peculiar/fortify-client-core";
import { useToast } from "@peculiar/react-components";
import { useTranslation } from "react-i18next";
import { useLockBodyScroll } from "react-use";
@@ -12,6 +13,7 @@ type UseCertificateDeleteDialogOpenParams = {
};
type UseCertificateDeleteDialogInitialParams = {
+ providers: IProviderInfo[];
fortifyClient: FortifyAPI | null;
onSuccess: (providerId: string) => void;
};
@@ -19,7 +21,7 @@ type UseCertificateDeleteDialogInitialParams = {
export function useCertificateDeleteDialog(
props: UseCertificateDeleteDialogInitialParams
) {
- const { fortifyClient, onSuccess } = props;
+ const { providers, fortifyClient, onSuccess } = props;
const { addToast } = useToast();
const { t } = useTranslation();
@@ -42,11 +44,13 @@ export function useCertificateDeleteDialog(
return;
}
setIsLoading(true);
+
try {
await fortifyClient.removeCertificateById(
openParamsRef.current.providerId,
openParamsRef.current.certificateIndex
);
+
onSuccess(openParamsRef.current.providerId);
addToast({
message: t("certificates.dialog.delete.success-message"),
@@ -69,6 +73,14 @@ export function useCertificateDeleteDialog(
useLockBodyScroll(isOpen);
+ const currentProvider = providers.find(
+ ({ id }) => openParamsRef.current?.providerId === id
+ );
+
+ if (isOpen && !currentProvider) {
+ handleClose();
+ }
+
return {
open: handleOpen,
dialog: () =>
diff --git a/src/dialogs/certificate-viewer-dialog/useCertificateViewerDialog.test.tsx b/src/dialogs/certificate-viewer-dialog/useCertificateViewerDialog.test.tsx
new file mode 100644
index 00000000..1f6e0cfb
--- /dev/null
+++ b/src/dialogs/certificate-viewer-dialog/useCertificateViewerDialog.test.tsx
@@ -0,0 +1,42 @@
+import { renderHook, act } from "@testing";
+import { useCertificateViewerDialog } from "./useCertificateViewerDialog";
+import { CertificateProps } from "../../types";
+
+import type { IProviderInfo } from "@peculiar/fortify-client-core";
+
+describe("useCertificateViewerDialog", () => {
+ const providers = [
+ {
+ id: "1",
+ name: "Provider 1",
+ },
+ ] as IProviderInfo[];
+
+ it("Should initialize", () => {
+ const { result } = renderHook(() =>
+ useCertificateViewerDialog({
+ providers,
+ })
+ );
+
+ expect(result.current.dialog).toBeInstanceOf(Function);
+ expect(result.current.open).toBeInstanceOf(Function);
+
+ const certificate = {
+ id: "1",
+ label: "Certificate name",
+ } as CertificateProps;
+
+ act(() => {
+ result.current.open({
+ certificate,
+ providerId: providers[0].id,
+ });
+ });
+
+ const DialogComponent = result.current.dialog();
+
+ expect(DialogComponent).not.toBeNull();
+ expect(DialogComponent?.props.certificate).toBe(certificate);
+ });
+});
diff --git a/src/dialogs/certificate-viewer-dialog/useCertificateViewerDialog.tsx b/src/dialogs/certificate-viewer-dialog/useCertificateViewerDialog.tsx
index 98cfc50b..71787009 100644
--- a/src/dialogs/certificate-viewer-dialog/useCertificateViewerDialog.tsx
+++ b/src/dialogs/certificate-viewer-dialog/useCertificateViewerDialog.tsx
@@ -1,30 +1,51 @@
import React from "react";
+import { IProviderInfo } from "@peculiar/fortify-client-core";
import { useLockBodyScroll } from "react-use";
import { CertificateViewerDialog } from "../../components/certificate-viewer-dialog";
import { CertificateProps } from "../../types";
-export function useCertificateViewerDialog() {
+type UseCertificateViewerDialogOpenParams = {
+ providerId: string;
+ certificate: CertificateProps;
+};
+
+type UseCertificateViewerInitialParams = {
+ providers: IProviderInfo[];
+};
+
+export function useCertificateViewerDialog(
+ props: UseCertificateViewerInitialParams
+) {
+ const { providers } = props;
const [isOpen, setIsOpen] = React.useState(false);
- const certificateRef = React.useRef();
+ const openParamsRef = React.useRef();
- const handleOpen = (certificaate: CertificateProps) => {
- certificateRef.current = certificaate;
+ const handleOpen = (params: UseCertificateViewerDialogOpenParams) => {
+ openParamsRef.current = params;
setIsOpen(true);
};
const handleClose = () => {
- certificateRef.current = undefined;
+ openParamsRef.current = undefined;
setIsOpen(false);
};
useLockBodyScroll(isOpen);
+ const currentProvider = providers.find(
+ ({ id }) => openParamsRef.current?.providerId === id
+ );
+
+ if (isOpen && !currentProvider) {
+ handleClose();
+ }
+
return {
open: handleOpen,
dialog: () =>
- isOpen && certificateRef.current ? (
+ isOpen && openParamsRef.current ? (
) : null,
diff --git a/src/dialogs/provider-info-dialog/index.ts b/src/dialogs/provider-info-dialog/index.ts
new file mode 100644
index 00000000..f695a3d8
--- /dev/null
+++ b/src/dialogs/provider-info-dialog/index.ts
@@ -0,0 +1 @@
+export * from "./useProviderInfoDialog";
diff --git a/src/dialogs/provider-info-dialog/useProviderInfoDialog.test.tsx b/src/dialogs/provider-info-dialog/useProviderInfoDialog.test.tsx
new file mode 100644
index 00000000..91c39d33
--- /dev/null
+++ b/src/dialogs/provider-info-dialog/useProviderInfoDialog.test.tsx
@@ -0,0 +1,33 @@
+import { renderHook, act } from "@testing";
+import { useProviderInfoDialog } from "./useProviderInfoDialog";
+
+import type { IProviderInfo } from "@peculiar/fortify-client-core";
+
+describe("useProviderInfoDialog", () => {
+ const providers = [
+ {
+ id: "1",
+ name: "Provider 1",
+ },
+ ] as IProviderInfo[];
+
+ it("Should initialize", async () => {
+ const { result } = renderHook(() =>
+ useProviderInfoDialog({
+ providers,
+ })
+ );
+
+ expect(result.current.dialog).toBeInstanceOf(Function);
+ expect(result.current.open).toBeInstanceOf(Function);
+
+ act(() => {
+ result.current.open(providers[0]);
+ });
+
+ const DialogComponent = result.current.dialog();
+
+ expect(DialogComponent).not.toBeNull();
+ expect(DialogComponent?.props.data).toBe(providers[0]);
+ });
+});
diff --git a/src/dialogs/provider-info-dialog/useProviderInfoDialog.tsx b/src/dialogs/provider-info-dialog/useProviderInfoDialog.tsx
new file mode 100644
index 00000000..c4a58f6f
--- /dev/null
+++ b/src/dialogs/provider-info-dialog/useProviderInfoDialog.tsx
@@ -0,0 +1,47 @@
+import React from "react";
+import { useLockBodyScroll } from "react-use";
+import type { IProviderInfo } from "@peculiar/fortify-client-core";
+import { ProviderInfoDialog } from "../../components/provider-info-dialog";
+
+type UseCertificateViewerInitialParams = {
+ providers: IProviderInfo[];
+};
+
+export function useProviderInfoDialog(
+ props: UseCertificateViewerInitialParams
+) {
+ const { providers } = props;
+ const [isOpen, setIsOpen] = React.useState(false);
+ const providerRef = React.useRef();
+
+ const handleOpen = (provider: IProviderInfo) => {
+ providerRef.current = provider;
+ setIsOpen(true);
+ };
+
+ const handleClose = () => {
+ providerRef.current = undefined;
+ setIsOpen(false);
+ };
+
+ useLockBodyScroll(isOpen);
+
+ const currentProvider = providers.find(
+ ({ id }) => providerRef.current?.id === id
+ );
+
+ if (isOpen && !currentProvider) {
+ handleClose();
+ }
+
+ return {
+ open: handleOpen,
+ dialog: () =>
+ isOpen && providerRef.current ? (
+
+ ) : null,
+ };
+}
diff --git a/src/hooks/app/useApp.tsx b/src/hooks/app/useApp.tsx
index 4abbdad8..f9337600 100644
--- a/src/hooks/app/useApp.tsx
+++ b/src/hooks/app/useApp.tsx
@@ -4,6 +4,8 @@ import {
IProviderInfo,
ICertificate,
} from "@peculiar/fortify-client-core";
+import { useTranslation } from "react-i18next";
+import { useToast } from "@peculiar/react-components";
import { AppFetchingStatus, AppFetchingType } from "./types";
@@ -14,9 +16,11 @@ export function useApp() {
const fortifyClient = React.useRef(null);
const [providers, setProviders] = React.useState([]);
- const [currentProviderId, setCurrentProviderId] = React.useState<
- string | undefined
+ const [currentProvider, setCurrentProvider] = React.useState<
+ IProviderInfo | undefined
>(undefined);
+ const [isCurrentProviderLogedin, setIsCurrentProviderLogedin] =
+ React.useState(false);
const [certificates, setCertificates] = React.useState([]);
const [challenge, setChallenge] = React.useState(null);
const [fetching, setFetching] = React.useState({
@@ -27,6 +31,9 @@ export function useApp() {
*
*/
+ const { addToast } = useToast();
+ const { t } = useTranslation();
+
const setFetchingValue = (
name: keyof AppFetchingType,
value: AppFetchingStatus
@@ -75,35 +82,41 @@ export function useApp() {
return;
}
- let providersLocal: IProviderInfo[] = [];
-
setFetchingValue("providers", "pending");
- setProviders([]);
- setCertificates([]);
try {
- providersLocal = await fortifyClient.current.getProviders();
-
+ const providersLocal = await fortifyClient.current.getProviders();
setProviders(providersLocal);
setFetchingValue("providers", "resolved");
- } catch (error) {
- setFetchingValue("providers", "rejected");
- return;
- }
+ if (!providersLocal.length) {
+ setCurrentProvider(undefined);
+ setIsCurrentProviderLogedin(false);
+ setCertificates([]);
+ return;
+ }
- setFetchingValue("certificates", "pending");
+ const defaultProvider = providersLocal[0];
- try {
- setCertificates(
- await fortifyClient.current.getCertificatesByProviderId(
- providersLocal[0].id
- )
+ if (!currentProvider) {
+ setCurrentProvider(defaultProvider);
+ handleProviderChange(defaultProvider.id);
+ return;
+ }
+ const curProvider = providersLocal.find(
+ ({ id }) => currentProvider.id === id
);
- setCurrentProviderId(providersLocal[0].id);
- setFetchingValue("certificates", "resolved");
+ if (!curProvider) {
+ setCurrentProvider(defaultProvider);
+ handleProviderChange(defaultProvider.id);
+ }
} catch (error) {
- setFetchingValue("certificates", "rejected");
+ setProviders([]);
+ setFetchingValue("providers", "rejected");
+
+ setCurrentProvider(undefined);
+ setIsCurrentProviderLogedin(false);
+ setCertificates([]);
}
};
@@ -153,20 +166,34 @@ export function useApp() {
};
const handleProviderChange = async (id: string) => {
- if (currentProviderId === id || fetching.certificates === "pending") {
+ if (!fortifyClient.current) {
+ return;
+ }
+ if (currentProvider?.id === id || fetching.certificates === "pending") {
return;
}
setFetchingValue("certificates", "pending");
+ try {
+ const localProvider = await fortifyClient.current.getProviderById(id);
+ const isLoggedIn = await localProvider.isLoggedIn();
+ setIsCurrentProviderLogedin(isLoggedIn);
+ } catch (error) {
+ setIsCurrentProviderLogedin(false);
+ }
+
try {
setCertificates(
- await fortifyClient.current!.getCertificatesByProviderId(id)
+ await fortifyClient.current.getCertificatesByProviderId(id)
);
- setCurrentProviderId(id);
+ if (providers?.length) {
+ setCurrentProvider(providers.find((provider) => provider.id === id));
+ }
setFetchingValue("certificates", "resolved");
} catch (error) {
setFetchingValue("certificates", "rejected");
+ setCertificates([]);
}
};
@@ -195,10 +222,10 @@ export function useApp() {
setCertificates(
await fortifyClient.current.getCertificatesByProviderId(providerId)
);
- setCurrentProviderId(providerId);
setFetchingValue("certificates", "resolved");
} catch (error) {
setFetchingValue("certificates", "rejected");
+ setCertificates([]);
}
};
@@ -206,15 +233,71 @@ export function useApp() {
window.location.reload();
};
+ const handleProviderResetAndRefreshList = async () => {
+ if (!fortifyClient.current || !currentProvider) {
+ return;
+ }
+
+ try {
+ const localProvider = await fortifyClient.current.getProviderById(
+ currentProvider.id
+ );
+ await localProvider.reset();
+ } catch (error) {
+ //
+ }
+ handleCertificatesDataReload(currentProvider.id);
+ };
+
+ const handleProviderLoginLogout = async (isLogedin: boolean) => {
+ if (!fortifyClient.current || !currentProvider) {
+ return;
+ }
+
+ try {
+ const localProvider = await fortifyClient.current.getProviderById(
+ currentProvider.id
+ );
+ if (isLogedin) {
+ await localProvider.logout();
+ const isLoggedIn = await localProvider.isLoggedIn();
+ if (!isLoggedIn) {
+ setIsCurrentProviderLogedin(false);
+ handleCertificatesDataReload(currentProvider.id);
+ } else {
+ addToast({
+ message: t("topbar.provider-doesnt-support-signing-in"),
+ variant: "attention",
+ disableIcon: true,
+ isClosable: true,
+ id: "provider-doesnt-support-signing-in",
+ });
+ }
+ } else {
+ await localProvider.login();
+ const isLoggedIn = await localProvider.isLoggedIn();
+ if (isLoggedIn) {
+ setIsCurrentProviderLogedin(true);
+ handleCertificatesDataReload(currentProvider.id);
+ }
+ }
+ } catch (error) {
+ setIsCurrentProviderLogedin(false);
+ }
+ };
+
return {
fortifyClient: fortifyClient.current,
fetching,
challenge,
providers,
- currentProviderId,
+ currentProvider,
certificates,
+ isCurrentProviderLogedin,
handleCertificatesDataReload,
handleProviderChange,
handleRetryConection,
+ handleProviderLoginLogout,
+ handleProviderResetAndRefreshList,
};
}
diff --git a/src/i18n/locales/en/main.json b/src/i18n/locales/en/main.json
index 6461674c..d54c9ed8 100644
--- a/src/i18n/locales/en/main.json
+++ b/src/i18n/locales/en/main.json
@@ -6,6 +6,35 @@
"read-only": "Read only"
},
"empty-text": "No providers connected"
+ },
+ "dialog": {
+ "info": {
+ "title": "{{name}} information",
+ "list": {
+ "token-name": "Token name:",
+ "token-category": {
+ "label": "Token category",
+ "value": {
+ "hardware": "Hardware",
+ "software": "Software"
+ }
+ },
+ "serial-number": "Serial number",
+ "free-space": "Free space (minimum estimated)",
+ "hardware-version": "Hardware version",
+ "firmware-version": "Firmware version",
+ "model": "Model",
+ "algorithms": "Algorithms",
+ "extractable": {
+ "label": "Extractable",
+ "value": {
+ "yes": "Yes",
+ "no": "No"
+ }
+ }
+ },
+ "empty-value": "Information is unavailable"
+ }
}
},
"certificates": {
@@ -17,12 +46,14 @@
"expires": "Expires"
},
"cell": {
- "certificate": "Certificate"
+ "certificate": "Certificate",
+ "with-privat-key": "(with private key)"
},
"action": {
"view-details": "View details",
"delete": "Delete certificate",
- "download": "Download certificate"
+ "download": "Download certificate",
+ "copy": "Copy certificate"
},
"empty-text": "There are no certificates yet.",
"empty-search-text": "There are no results for <0>{{text}}0>"
@@ -169,9 +200,14 @@
"search-placeholder": "Search",
"import-certificate": "Import certificate",
"create-certificate": "New",
+ "create-certificate-disabled-tooltip": "Please sign in to the provider to add new certificates.",
"create-certificate-scr": "Create certificate signing request (CSR)",
"create-certificate-ssc": "Create self-signed certificate",
- "reload-certificates": "Refresh list"
+ "reload-certificates": "Reset session and refresh certificate list",
+ "provider-information": "Provider information",
+ "provider-login": "Sign in",
+ "provider-logout": "Sign out",
+ "provider-doesnt-support-signing-in": "This provider doesn’t support signing in."
},
"certificate-viewer-dialog": {
"title": "“{{name}}” details"
diff --git a/src/icons/certificate-30.svg b/src/icons/certificate-30.svg
index a7255d79..4358a03c 100644
--- a/src/icons/certificate-30.svg
+++ b/src/icons/certificate-30.svg
@@ -1,6 +1,6 @@
-