From b33edc56252ac6ac8d7cbf802fa68ec441407cdb Mon Sep 17 00:00:00 2001 From: Dhaval Dodiya Date: Fri, 8 Nov 2024 19:14:13 +0530 Subject: [PATCH 01/16] feat: smime option added in composer --- src/store/zustand/editor/hooks/editor.ts | 26 +++++++++++++++++++ src/store/zustand/editor/store.ts | 9 +++++++ src/types/editor/index.d.ts | 2 ++ src/types/state/index.d.ts | 1 + .../edit/parts/options-dropdown.tsx | 16 +++++++++++- .../detail-panel/edit/parts/subject-row.tsx | 9 ++++++- 6 files changed, 61 insertions(+), 2 deletions(-) diff --git a/src/store/zustand/editor/hooks/editor.ts b/src/store/zustand/editor/hooks/editor.ts index 5ffbfbb23..97593c7fd 100644 --- a/src/store/zustand/editor/hooks/editor.ts +++ b/src/store/zustand/editor/hooks/editor.ts @@ -376,3 +376,29 @@ export const useEditorSignatureId = ( [editorId, debouncedSaveDraft, setter, value] ); }; + +/** + * Returns reactive reference to the isUrgent value and to its setter + * @param id + */ +export const useEditorIsSmimeSign = ( + id: MailsEditorV2['id'] +): { + isSmimeSign: MailsEditorV2['isSmimeSign']; + setIsSmimeSign: (isSmimeSign: MailsEditorV2['isSmimeSign']) => void; +} => { + const { debouncedSaveDraft } = useSaveDraftFromEditor(); + const value = useEditorsStore((state) => state.editors[id].isSmimeSign); + const setter = useEditorsStore((state) => state.setIsSmimeSign); + + return useMemo( + () => ({ + isSmimeSign: value, + setIsSmimeSign: (val: MailsEditorV2['isSmimeSign']): void => { + setter(id, val); + debouncedSaveDraft(id); + } + }), + [id, debouncedSaveDraft, setter, value] + ); +}; diff --git a/src/store/zustand/editor/store.ts b/src/store/zustand/editor/store.ts index 98aa9f499..1097dec01 100644 --- a/src/store/zustand/editor/store.ts +++ b/src/store/zustand/editor/store.ts @@ -371,5 +371,14 @@ export const useEditorsStore = create()((set) => ({ } }) ); + }, + setIsSmimeSign: (id: MailsEditorV2['id'], value: MailsEditorV2['isSmimeSign']): void => { + set( + produce((state: EditorsStateTypeV2) => { + if (state?.editors?.[id]) { + state.editors[id].isSmimeSign = value; + } + }) + ); } })); diff --git a/src/types/editor/index.d.ts b/src/types/editor/index.d.ts index bbf26a9e3..4ff8a99c5 100644 --- a/src/types/editor/index.d.ts +++ b/src/types/editor/index.d.ts @@ -177,6 +177,8 @@ export type MailsEditorV2 = { size: number; // the sum of the size of the attachments requiring smart link conversion totalSmartLinksSize: number; + // flag for the S/MIME request + isSmimeSign: boolean; }; type IdentityType = { diff --git a/src/types/state/index.d.ts b/src/types/state/index.d.ts index da111aada..fb852bed3 100644 --- a/src/types/state/index.d.ts +++ b/src/types/state/index.d.ts @@ -81,6 +81,7 @@ export type EditorsStateTypeV2 = { setMessagesStoreDispatch: (id: MailsEditorV2['id'], dispatch: AppDispatch) => void; toggleSmartLink: (id: MailsEditorV2['id'], partName: string) => void; setSignatureId: (id: MailsEditorV2['id'], signId: MailsEditorV2['signatureId']) => void; + setIsSmimeSign: (id: MailsEditorV2['id'], isSmimeSign: MailsEditorV2['isSmimeSign']) => void; }; export type MsgStateType = { diff --git a/src/views/app/detail-panel/edit/parts/options-dropdown.tsx b/src/views/app/detail-panel/edit/parts/options-dropdown.tsx index decfe39b9..a7aeece3b 100644 --- a/src/views/app/detail-panel/edit/parts/options-dropdown.tsx +++ b/src/views/app/detail-panel/edit/parts/options-dropdown.tsx @@ -11,6 +11,7 @@ import { noop } from 'lodash'; import { useEditorIsRichText, + useEditorIsSmimeSign, useEditorIsUrgent, useEditorRequestReadReceipt } from '../../../../../store/zustand/editor'; @@ -24,7 +25,7 @@ export const OptionsDropdown: FC = ({ editorId }) => { const { isRichText, setIsRichText } = useEditorIsRichText(editorId); const { isUrgent, setIsUrgent } = useEditorIsUrgent(editorId); const { requestReadReceipt, setRequestReadReceipt } = useEditorRequestReadReceipt(editorId); - + const { isSmimeSign, setIsSmimeSign } = useEditorIsSmimeSign(editorId); const toggleRichTextEditor = useCallback(() => { setIsRichText(!isRichText); }, [isRichText, setIsRichText]); @@ -37,6 +38,10 @@ export const OptionsDropdown: FC = ({ editorId }) => { setRequestReadReceipt(!requestReadReceipt); }, [requestReadReceipt, setRequestReadReceipt]); + const toggleUseSmimeCertificateRequest = useCallback(() => { + setIsSmimeSign(!isSmimeSign); + }, [isSmimeSign, setIsSmimeSign]); + const options = useMemo( () => [ { @@ -53,6 +58,13 @@ export const OptionsDropdown: FC = ({ editorId }) => { : t('label.mark_as_important', 'Mark as important'), onClick: toggleImportant }, + { + id: 'is_smimesign', + label: isSmimeSign + ? t('label.remove_use_certificate_to_sign', 'Remove certificate to sign (S/MIME)') + : t('label.use_certificate_to_sign', 'Use certificate to sign (S/MIME)'), + onClick: toggleUseSmimeCertificateRequest + }, { id: 'read_receipt', label: requestReadReceipt @@ -66,6 +78,8 @@ export const OptionsDropdown: FC = ({ editorId }) => { toggleRichTextEditor, isUrgent, toggleImportant, + isSmimeSign, + toggleUseSmimeCertificateRequest, requestReadReceipt, toggleReceiptRequest ] diff --git a/src/views/app/detail-panel/edit/parts/subject-row.tsx b/src/views/app/detail-panel/edit/parts/subject-row.tsx index 0849b9832..81c229d45 100644 --- a/src/views/app/detail-panel/edit/parts/subject-row.tsx +++ b/src/views/app/detail-panel/edit/parts/subject-row.tsx @@ -9,6 +9,7 @@ import { Container, Icon, Input, Padding, Tooltip } from '@zextras/carbonio-desi import { t } from '@zextras/carbonio-shell-ui'; import { + useEditorIsSmimeSign, useEditorIsUrgent, useEditorRequestReadReceipt, useEditorSubject @@ -23,6 +24,7 @@ export const SubjectRow: FC = ({ editorId }: SubjectRowProps) = const { subject, setSubject } = useEditorSubject(editorId); const { isUrgent } = useEditorIsUrgent(editorId); const { requestReadReceipt } = useEditorRequestReadReceipt(editorId); + const { isSmimeSign } = useEditorIsSmimeSign(editorId); const onSubjectChange = useCallback( (event: ChangeEvent): void => { @@ -46,7 +48,7 @@ export const SubjectRow: FC = ({ editorId }: SubjectRowProps) = onChange={onSubjectChange} /> - {(requestReadReceipt || isUrgent) && ( + {(requestReadReceipt || isUrgent || isSmimeSign) && ( = ({ editorId }: SubjectRowProps) = )} + {isSmimeSign && ( + + + + )} )} From 08d8aaae6ec1c0e3b58c1efbfe57e02808827eb7 Mon Sep 17 00:00:00 2001 From: Dhaval Dodiya Date: Mon, 11 Nov 2024 16:06:38 +0530 Subject: [PATCH 02/16] chore: managed the sendmsgrequest for smime --- src/store/actions/send-msg.ts | 4 ++-- src/store/zustand/editor/editor-transformations.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/store/actions/send-msg.ts b/src/store/actions/send-msg.ts index 4678fcef2..1ce2d526f 100644 --- a/src/store/actions/send-msg.ts +++ b/src/store/actions/send-msg.ts @@ -69,11 +69,11 @@ export const sendMsgFromEditor = createAsyncThunk( - 'SendMsg', + request, { _jsns: 'urn:zimbraMail', m: msg diff --git a/src/store/zustand/editor/editor-transformations.ts b/src/store/zustand/editor/editor-transformations.ts index 3a39b1076..57aed7e6c 100644 --- a/src/store/zustand/editor/editor-transformations.ts +++ b/src/store/zustand/editor/editor-transformations.ts @@ -369,6 +369,7 @@ const createSoapMessageRequestFromEditor = ( autoSendTime: editor.autoSendTime, ...(command === 'savedraft' ? { id: editor.did } : {}), ...(command === 'sendmsg' ? { did: editor.did } : {}), + ...(editor.isSmimeSign ? { sign: true } : {}), su: { _content: editor.subject ?? '' }, rt: editor.replyType, origid: editor.originalId, From 58b215d41eeb74e4e3fbed0437cc59e07c891bae Mon Sep 17 00:00:00 2001 From: Dhaval Dodiya Date: Mon, 11 Nov 2024 18:57:08 +0530 Subject: [PATCH 03/16] chore: update sendsecuremsg request --- src/store/actions/send-msg.ts | 3 ++- src/store/zustand/editor/editor-transformations.ts | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/store/actions/send-msg.ts b/src/store/actions/send-msg.ts index 1ce2d526f..7e307013c 100644 --- a/src/store/actions/send-msg.ts +++ b/src/store/actions/send-msg.ts @@ -76,7 +76,8 @@ export const sendMsgFromEditor = createAsyncThunk Date: Tue, 12 Nov 2024 12:54:59 +0530 Subject: [PATCH 04/16] chore: removed the sendsecuremsg request --- src/store/actions/send-msg.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/store/actions/send-msg.ts b/src/store/actions/send-msg.ts index 7e307013c..81799a3d2 100644 --- a/src/store/actions/send-msg.ts +++ b/src/store/actions/send-msg.ts @@ -70,10 +70,9 @@ export const sendMsgFromEditor = createAsyncThunk( - request, + 'SendMsg', { _jsns: 'urn:zimbraMail', m: msg, From ced720175c933decc54e71eacd3a56ce9122a5b5 Mon Sep 17 00:00:00 2001 From: Dhaval Dodiya Date: Thu, 14 Nov 2024 13:53:44 +0530 Subject: [PATCH 05/16] chore: added store for certificate --- package-lock.json | 73 +++++++++++- package.json | 5 +- src/store/actions/send-msg.ts | 12 +- src/store/zustand/certificates/store.ts | 35 ++++++ src/views/app/detail-panel/edit/edit-view.tsx | 75 ++++++++++++- .../edit/parts/certificate-utils.ts | 105 ++++++++++++++++++ .../edit/parts/options-dropdown.tsx | 8 +- 7 files changed, 301 insertions(+), 12 deletions(-) create mode 100644 src/store/zustand/certificates/store.ts create mode 100644 src/views/app/detail-panel/edit/parts/certificate-utils.ts diff --git a/package-lock.json b/package-lock.json index cd5fdf699..03eafb7f0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,8 +17,9 @@ "@types/webpack": "^5.28.2", "@types/webpack-env": "^1.18.1", "@zextras/carbonio-design-system": "^8.0.0", - "@zextras/carbonio-shell-ui": "8.0.3-devel.1729235939785", + "@zextras/carbonio-shell-ui": "devel", "@zextras/carbonio-ui-preview": "^3.0.0", + "asn1js": "^3.0.5", "axios": "^1.6.7", "babel-jest": "^29.4.3", "core-js": "^3.36.0", @@ -28,6 +29,8 @@ "jest-junit": "^15.0.0", "lodash": "^4.17.21", "moment": "^2.29.4", + "node-forge": "^1.3.1", + "pkijs": "^3.2.4", "prop-types": "^15.8.1", "react": "^18.3.1", "react-colorful": "^5.6.1", @@ -3725,6 +3728,17 @@ } } }, + "node_modules/@noble/hashes": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.5.0.tgz", + "integrity": "sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -6245,6 +6259,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/asn1js": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.5.tgz", + "integrity": "sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ==", + "dependencies": { + "pvtsutils": "^1.3.2", + "pvutils": "^1.1.3", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/ast-types-flow": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", @@ -6761,6 +6788,14 @@ "node": ">= 0.8" } }, + "node_modules/bytestreamjs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/bytestreamjs/-/bytestreamjs-2.0.1.tgz", + "integrity": "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", @@ -14586,7 +14621,6 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", - "dev": true, "engines": { "node": ">= 6.13.0" } @@ -18328,6 +18362,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pkijs": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.2.4.tgz", + "integrity": "sha512-Et9V5QpvBilPFgagJcaKBqXjKrrgF5JL2mSDELk1vvbOTt4fuBhSSsGn9Tcz0TQTfS5GCpXQ31Whrpqeqp0VRg==", + "dependencies": { + "@noble/hashes": "^1.4.0", + "asn1js": "^3.0.5", + "bytestreamjs": "^2.0.0", + "pvtsutils": "^1.3.2", + "pvutils": "^1.1.3", + "tslib": "^2.6.3" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/polished": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/polished/-/polished-4.3.1.tgz", @@ -18919,6 +18969,22 @@ } ] }, + "node_modules/pvtsutils": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.5.tgz", + "integrity": "sha512-ARvb14YB9Nm2Xi6nBq1ZX6dAM0FsJnuk+31aUp4TrcZEdKUlSqOqsxJHUPJDNE3qiIp+iUPEIeR6Je/tgV7zsA==", + "dependencies": { + "tslib": "^2.6.1" + } + }, + "node_modules/pvutils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.3.tgz", + "integrity": "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/qs": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", @@ -20917,8 +20983,7 @@ "node_modules/tslib": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", - "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", - "dev": true + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" }, "node_modules/tsutils": { "version": "3.21.0", diff --git a/package.json b/package.json index 4ed63a938..fa2de4225 100644 --- a/package.json +++ b/package.json @@ -46,10 +46,10 @@ "@commitlint/cli": "^19.1.0", "@commitlint/config-conventional": "^19.1.0", "@faker-js/faker": "^8.4.1", + "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.4.5", "@testing-library/react": "^16.0.1", "@testing-library/user-event": "^14.5.2", - "@testing-library/dom": "^10.4.0", "@types/jest": "^29.5.12", "@types/lodash": "^4.17.4", "@types/react": "^18.3.2", @@ -85,6 +85,7 @@ "@zextras/carbonio-design-system": "^8.0.0", "@zextras/carbonio-shell-ui": "devel", "@zextras/carbonio-ui-preview": "^3.0.0", + "asn1js": "^3.0.5", "axios": "^1.6.7", "babel-jest": "^29.4.3", "core-js": "^3.36.0", @@ -94,6 +95,8 @@ "jest-junit": "^15.0.0", "lodash": "^4.17.21", "moment": "^2.29.4", + "node-forge": "^1.3.1", + "pkijs": "^3.2.4", "prop-types": "^15.8.1", "react": "^18.3.1", "react-colorful": "^5.6.1", diff --git a/src/store/actions/send-msg.ts b/src/store/actions/send-msg.ts index 81799a3d2..020e98339 100644 --- a/src/store/actions/send-msg.ts +++ b/src/store/actions/send-msg.ts @@ -14,6 +14,7 @@ import { getParticipantsFromMessage } from '../../helpers/messages'; import { MailMessage, SendMsgResult, SendMsgWithSmartLinksResponse } from '../../types'; import type { SaveDraftRequest, SaveDraftResponse, SendMsgParameters } from '../../types'; import { generateMailRequest } from '../editor-slice-utils'; +import { useCertificatesStore } from '../zustand/certificates/store'; import { createSoapSendMsgRequestFromEditor } from '../zustand/editor/editor-transformations'; export const sendMsg = createAsyncThunk( @@ -69,6 +70,15 @@ export const sendMsgFromEditor = createAsyncThunk state.getCertificate); + + const accountId = 'account123'; + const certificate = getCertificate(accountId); + if (certificate) { + console.log('Certificate found:', certificate); + } + let resp: SendMsgWithSmartLinksResponse; try { resp = await soapFetch( @@ -76,7 +86,7 @@ export const sendMsgFromEditor = createAsyncThunk + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +import { create } from 'zustand'; + +type Certificate = { + privateKey: string; + endEntityCert: string; + caCertificate: string; +}; + +type CertificatesState = { + certificates: Record; + addCertificate: (accountId: string, certificate: Certificate) => void; + removeCertificate: (accountId: string) => void; + getCertificate: (accountId: string) => Certificate | undefined; +}; +export const useCertificatesStore = create((set, get) => ({ + certificates: {}, + addCertificate: (accountId: string, certificate: Certificate): void => + set((state) => ({ + certificates: { + ...state.certificates, + [accountId]: certificate + } + })), + removeCertificate: (accountId: string): void => + set((state) => { + const { [accountId]: _, ...rest } = state.certificates; + return { certificates: rest }; + }), + getCertificate: (accountId: string): Certificate | undefined => get().certificates[accountId] +})); diff --git a/src/views/app/detail-panel/edit/edit-view.tsx b/src/views/app/detail-panel/edit/edit-view.tsx index b0bb41a11..7fa4ad126 100644 --- a/src/views/app/detail-panel/edit/edit-view.tsx +++ b/src/views/app/detail-panel/edit/edit-view.tsx @@ -4,7 +4,15 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import React, { memo, useCallback, useEffect, useImperativeHandle, useMemo, useState } from 'react'; +import React, { + memo, + useCallback, + useEffect, + useImperativeHandle, + useMemo, + useRef, + useState +} from 'react'; import { Button, @@ -23,11 +31,13 @@ import DropZoneAttachment from './dropzone-attachment'; import { EditAttachmentsBlock } from './edit-attachments-block'; import { createEditBoard } from './edit-view-board'; import { AddAttachmentsDropdown } from './parts/add-attachments-dropdown'; +import { handleCertificateFileUpload } from './parts/certificate-utils'; import { ChangeSignaturesDropdown } from './parts/change-signatures-dropdown'; import { useKeepOrDiscardDraft } from './parts/delete-draft'; import { EditViewDraftSaveInfo } from './parts/edit-view-draft-save-info'; import { EditViewIdentitySelector } from './parts/edit-view-identity-selector'; import { EditViewSendButtons } from './parts/edit-view-send-buttons'; +import * as StyledComp from './parts/edit-view-styled-components'; import { OptionsDropdown } from './parts/options-dropdown'; import { RecipientsRows } from './parts/recipients-rows'; import { SizeExceededWarningBanner } from './parts/size-exceeded-waring-banner'; @@ -38,6 +48,7 @@ import { GapContainer, GapRow } from '../../../../commons/gap-container'; import { EDIT_VIEW_CLOSING_REASONS, EditViewActions, TIMEOUTS } from '../../../../constants'; import { buildArrayFromFileList } from '../../../../helpers/files'; import { getAvailableAddresses, getIdentitiesDescriptors } from '../../../../helpers/identities'; +import { useCertificatesStore } from '../../../../store/zustand/certificates/store'; import { useEditorAutoSendTime, useEditorDraftSave, @@ -46,7 +57,8 @@ import { useEditorAttachments, deleteEditor, useEditorDid, - useEditorsStore + useEditorsStore, + useEditorIsSmimeSign } from '../../../../store/zustand/editor'; import { EditViewClosingReasons } from '../../../../types'; import { updateEditorWithSmartLinks } from '../../../../ui-actions/utils'; @@ -119,6 +131,8 @@ export const EditView = React.forwardRef(function const [isMailSizeWarning, setIsMailSizeWarning] = useState(false); const { status: saveDraftAllowedStatus, saveDraft } = useEditorDraftSave(editorId); const { did: draftId } = useEditorDid(editorId); + const { isSmimeSign, setIsSmimeSign } = useEditorIsSmimeSign(editorId); + const inputRef = useRef(null); useEffect(() => { if (!draftId) saveDraft(); @@ -329,6 +343,48 @@ export const EditView = React.forwardRef(function createSmartLinksAction ]); + const addCertificate = useCertificatesStore((state) => state.addCertificate); + + const getCertificate = useCertificatesStore((state) => state.getCertificate); + + const accountId = 'account123'; + const certificate = getCertificate(accountId); + if (certificate) { + console.log('Certificate found:', certificate); + } + + const onCertificateFileClick = useCallback(() => { + if (inputRef.current) { + inputRef.current.value = ''; + inputRef.current.click(); + } + }, []); + + const onCertificateFileSelect = useCallback( + async (fileList: FileList) => { + const password = prompt('Enter the password for the P12 file:'); + const result = await handleCertificateFileUpload(fileList, password ?? ''); + const accountId = 'account123'; + const newCertificate = { + privateKey: result.privateKey, + endEntityCert: result.endEntityCert, + caCertificate: result.caCertificate + }; + addCertificate(accountId, newCertificate); + setIsSmimeSign(true); + }, + [addCertificate, setIsSmimeSign] + ); + + const onSmimeOptionChange = useCallback( + (isSmimeSet: boolean): void => { + if (isSmimeSet) { + onCertificateFileClick(); + } + }, + [onCertificateFileClick] + ); + const onSendLaterClick = useCallback( (scheduledTime: number): void => { const onConfirmCallback = async (): Promise => { @@ -395,7 +451,10 @@ export const EditView = React.forwardRef(function - + (function + { + onCertificateFileSelect && + inputRef?.current?.files && + onCertificateFileSelect(inputRef.current.files); + }} + /> ); }); diff --git a/src/views/app/detail-panel/edit/parts/certificate-utils.ts b/src/views/app/detail-panel/edit/parts/certificate-utils.ts new file mode 100644 index 000000000..147a081e0 --- /dev/null +++ b/src/views/app/detail-panel/edit/parts/certificate-utils.ts @@ -0,0 +1,105 @@ +/* + * SPDX-FileCopyrightText: 2024 Zextras + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +/* + * SPDX-License-IdentifierText: 2024 Zextras + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import * as asn1js from 'asn1js'; +import forge from 'node-forge'; +import * as pkijs from 'pkijs'; + +interface CertificateFileUploadResult { + privateKey: string; + endEntityCert: string; + caCertificate: string; + emailAddress: string; +} + +const getCertificate = async (certArg: string): Promise => { + try { + const sanitizedCert = certArg + .replace(/-----BEGIN CERTIFICATE-----/, '') + .replace(/-----END CERTIFICATE-----/, '') + .replace(/\s+/g, ''); + + const binaryDer = Uint8Array.from(atob(sanitizedCert), (char) => char.charCodeAt(0)); + const asn1 = asn1js.fromBER(binaryDer.buffer); + + if (asn1.offset === -1) throw new Error('Failed to parse certificate'); + + return new pkijs.Certificate({ schema: asn1.result }); + } catch { + throw new Error('Failed to parse certificate'); + } +}; + +export const handleCertificateFileUpload = ( + files: FileList, + password: string +): Promise => + new Promise((resolve, reject) => { + if (files.length === 0) { + reject(new Error('No file provided')); + } + + const reader = new FileReader(); + + reader.onload = async (e: ProgressEvent): Promise => { + try { + const arrayBuffer = e.target?.result; + if (!arrayBuffer) { + return reject(new Error('Failed to read file')); + } + + const p12Der = forge.util.createBuffer(arrayBuffer as ArrayBuffer); + const p12Asn1 = forge.asn1.fromDer(p12Der); + const p12 = forge.pkcs12.pkcs12FromAsn1(p12Asn1, false, password); + + const keyBags = p12.getBags({ bagType: forge.pki.oids.pkcs8ShroudedKeyBag }); + const privateKeyObj = keyBags[forge.pki.oids.pkcs8ShroudedKeyBag]?.[0]?.key; + + const certBags = p12.getBags({ bagType: forge.pki.oids.certBag }); + const certificates = certBags[forge.pki.oids.certBag]; + + if (!privateKeyObj || !certificates || certificates.length === 0) { + return reject(new Error('Failed to extract private key or certificate')); + } + + const endEntityCertFile = certificates[0].cert; + const caCerts = certificates.slice(1); + + const privateKey = privateKeyObj ? forge.pki.privateKeyToPem(privateKeyObj) : ''; + if (!endEntityCertFile) { + return reject(new Error('Failed to extract end-entity certificate')); + } + + const endEntityCert = forge.pki.certificateToPem(endEntityCertFile); + const caCertificate = caCerts + .map((cert) => (cert?.cert ? forge.pki.certificateToPem(cert.cert) : '')) + .join('\n'); + + const certificate = await getCertificate(endEntityCert); + const emailAddress = certificate.subject.typesAndValues + .map((typeAndValue) => typeAndValue.value.valueBlock.value) + .join(', '); + + // Resolve the promise with the result object + return resolve({ + privateKey, + endEntityCert, + caCertificate, + emailAddress + }); + } catch (err) { + // Reject the promise with an error message + return reject(new Error(`Certificate processing error: ${(err as Error).message}`)); + } + }; + + reader.readAsArrayBuffer(files[0]); + }); diff --git a/src/views/app/detail-panel/edit/parts/options-dropdown.tsx b/src/views/app/detail-panel/edit/parts/options-dropdown.tsx index a7aeece3b..9a880d3ff 100644 --- a/src/views/app/detail-panel/edit/parts/options-dropdown.tsx +++ b/src/views/app/detail-panel/edit/parts/options-dropdown.tsx @@ -19,9 +19,10 @@ import { MailsEditorV2 } from '../../../../../types'; export type OptionsDropdownProps = { editorId: MailsEditorV2['id']; + onSmimeOptionChange: (isSmimeSet: boolean) => void; }; -export const OptionsDropdown: FC = ({ editorId }) => { +export const OptionsDropdown: FC = ({ editorId, onSmimeOptionChange }) => { const { isRichText, setIsRichText } = useEditorIsRichText(editorId); const { isUrgent, setIsUrgent } = useEditorIsUrgent(editorId); const { requestReadReceipt, setRequestReadReceipt } = useEditorRequestReadReceipt(editorId); @@ -39,8 +40,9 @@ export const OptionsDropdown: FC = ({ editorId }) => { }, [requestReadReceipt, setRequestReadReceipt]); const toggleUseSmimeCertificateRequest = useCallback(() => { - setIsSmimeSign(!isSmimeSign); - }, [isSmimeSign, setIsSmimeSign]); + onSmimeOptionChange(!isSmimeSign); + // setIsSmimeSign(!isSmimeSign); + }, [isSmimeSign, onSmimeOptionChange]); const options = useMemo( () => [ From 38f4a975336529f0159b0291cccd5d4016da5351 Mon Sep 17 00:00:00 2001 From: Dhaval Dodiya Date: Thu, 14 Nov 2024 14:58:47 +0530 Subject: [PATCH 06/16] chore: send the certificate details in sendmsgrequest --- src/store/actions/send-msg.ts | 8 ++++---- src/store/zustand/certificates/certificate.ts | 9 +++++++++ src/store/zustand/certificates/store.ts | 2 +- 3 files changed, 14 insertions(+), 5 deletions(-) create mode 100644 src/store/zustand/certificates/certificate.ts diff --git a/src/store/actions/send-msg.ts b/src/store/actions/send-msg.ts index 020e98339..97a09717b 100644 --- a/src/store/actions/send-msg.ts +++ b/src/store/actions/send-msg.ts @@ -14,7 +14,7 @@ import { getParticipantsFromMessage } from '../../helpers/messages'; import { MailMessage, SendMsgResult, SendMsgWithSmartLinksResponse } from '../../types'; import type { SaveDraftRequest, SaveDraftResponse, SendMsgParameters } from '../../types'; import { generateMailRequest } from '../editor-slice-utils'; -import { useCertificatesStore } from '../zustand/certificates/store'; +import { getCertificate } from '../zustand/certificates/certificate'; import { createSoapSendMsgRequestFromEditor } from '../zustand/editor/editor-transformations'; export const sendMsg = createAsyncThunk( @@ -71,12 +71,12 @@ export const sendMsgFromEditor = createAsyncThunk state.getCertificate); + // const getCertificate = useCertificatesStore((state) => state.getCertificate); const accountId = 'account123'; - const certificate = getCertificate(accountId); + const certificate = getCertificate({ accountId }); if (certificate) { - console.log('Certificate found:', certificate); + console.log('====== in Certificate found:', certificate); } let resp: SendMsgWithSmartLinksResponse; diff --git a/src/store/zustand/certificates/certificate.ts b/src/store/zustand/certificates/certificate.ts new file mode 100644 index 000000000..9a7e9c1a8 --- /dev/null +++ b/src/store/zustand/certificates/certificate.ts @@ -0,0 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2023 Zextras + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +import { Certificate, useCertificatesStore } from './store'; + +export const getCertificate = ({ accountId }: { accountId: string }): Certificate | null => + useCertificatesStore.getState()?.certificates?.[accountId] ?? null; diff --git a/src/store/zustand/certificates/store.ts b/src/store/zustand/certificates/store.ts index 72c898d9f..9ee0806e3 100644 --- a/src/store/zustand/certificates/store.ts +++ b/src/store/zustand/certificates/store.ts @@ -5,7 +5,7 @@ */ import { create } from 'zustand'; -type Certificate = { +export type Certificate = { privateKey: string; endEntityCert: string; caCertificate: string; From 65cd691231e3946828939c1c272501e9295a1786 Mon Sep 17 00:00:00 2001 From: Manan Patel Date: Thu, 14 Nov 2024 16:03:45 +0530 Subject: [PATCH 07/16] chore: certificate error fixed --- src/store/actions/send-msg.ts | 3 --- src/store/zustand/certificates/store.ts | 2 +- src/views/app/detail-panel/edit/edit-view.tsx | 2 +- .../detail-panel/edit/parts/certificate-utils.ts | 13 ++++++++----- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/store/actions/send-msg.ts b/src/store/actions/send-msg.ts index 97a09717b..49b2e9696 100644 --- a/src/store/actions/send-msg.ts +++ b/src/store/actions/send-msg.ts @@ -75,9 +75,6 @@ export const sendMsgFromEditor = createAsyncThunk(function const accountId = 'account123'; const newCertificate = { privateKey: result.privateKey, - endEntityCert: result.endEntityCert, + certificate: result.certificate, caCertificate: result.caCertificate }; addCertificate(accountId, newCertificate); diff --git a/src/views/app/detail-panel/edit/parts/certificate-utils.ts b/src/views/app/detail-panel/edit/parts/certificate-utils.ts index 147a081e0..9e0700b4f 100644 --- a/src/views/app/detail-panel/edit/parts/certificate-utils.ts +++ b/src/views/app/detail-panel/edit/parts/certificate-utils.ts @@ -15,7 +15,7 @@ import * as pkijs from 'pkijs'; interface CertificateFileUploadResult { privateKey: string; - endEntityCert: string; + certificate: string; caCertificate: string; emailAddress: string; } @@ -73,7 +73,10 @@ export const handleCertificateFileUpload = ( const endEntityCertFile = certificates[0].cert; const caCerts = certificates.slice(1); - const privateKey = privateKeyObj ? forge.pki.privateKeyToPem(privateKeyObj) : ''; + const pkcs8PrivateKey = forge.pki.privateKeyToAsn1(privateKeyObj); + + const wrapPrivateKey = forge.pki.wrapRsaPrivateKey(pkcs8PrivateKey); + const privateKey = forge.pki.privateKeyInfoToPem(wrapPrivateKey); if (!endEntityCertFile) { return reject(new Error('Failed to extract end-entity certificate')); } @@ -90,9 +93,9 @@ export const handleCertificateFileUpload = ( // Resolve the promise with the result object return resolve({ - privateKey, - endEntityCert, - caCertificate, + privateKey: privateKey.replace(/\r\n/g, '\n'), + certificate: endEntityCert.replace(/\r\n/g, '\n'), + caCertificate: caCertificate.replace(/\r\n/g, '\n'), emailAddress }); } catch (err) { From cbdc4a997558514eaf72e2f9ba81cd4d64b4e1c6 Mon Sep 17 00:00:00 2001 From: Manan Patel Date: Thu, 14 Nov 2024 17:38:41 +0530 Subject: [PATCH 08/16] chore: certificate error message added --- .../edit/parts/certificate-utils.ts | 29 +++++++------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/src/views/app/detail-panel/edit/parts/certificate-utils.ts b/src/views/app/detail-panel/edit/parts/certificate-utils.ts index 9e0700b4f..520a2577d 100644 --- a/src/views/app/detail-panel/edit/parts/certificate-utils.ts +++ b/src/views/app/detail-panel/edit/parts/certificate-utils.ts @@ -3,12 +3,7 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -/* - * SPDX-License-IdentifierText: 2024 Zextras - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - +import { t } from '@zextras/carbonio-shell-ui'; import * as asn1js from 'asn1js'; import forge from 'node-forge'; import * as pkijs from 'pkijs'; @@ -20,6 +15,10 @@ interface CertificateFileUploadResult { emailAddress: string; } +const ERROR_MESSAGE = t( + 'messages.snackbar.fail_to_parse_certificate', + 'Failed to parse certificate' +); const getCertificate = async (certArg: string): Promise => { try { const sanitizedCert = certArg @@ -30,11 +29,11 @@ const getCertificate = async (certArg: string): Promise => { const binaryDer = Uint8Array.from(atob(sanitizedCert), (char) => char.charCodeAt(0)); const asn1 = asn1js.fromBER(binaryDer.buffer); - if (asn1.offset === -1) throw new Error('Failed to parse certificate'); + if (asn1.offset === -1) throw new Error(ERROR_MESSAGE); return new pkijs.Certificate({ schema: asn1.result }); } catch { - throw new Error('Failed to parse certificate'); + throw new Error(ERROR_MESSAGE); } }; @@ -43,17 +42,13 @@ export const handleCertificateFileUpload = ( password: string ): Promise => new Promise((resolve, reject) => { - if (files.length === 0) { - reject(new Error('No file provided')); - } - const reader = new FileReader(); reader.onload = async (e: ProgressEvent): Promise => { try { const arrayBuffer = e.target?.result; if (!arrayBuffer) { - return reject(new Error('Failed to read file')); + return reject(new Error(ERROR_MESSAGE)); } const p12Der = forge.util.createBuffer(arrayBuffer as ArrayBuffer); @@ -67,7 +62,7 @@ export const handleCertificateFileUpload = ( const certificates = certBags[forge.pki.oids.certBag]; if (!privateKeyObj || !certificates || certificates.length === 0) { - return reject(new Error('Failed to extract private key or certificate')); + return reject(new Error(ERROR_MESSAGE)); } const endEntityCertFile = certificates[0].cert; @@ -78,7 +73,7 @@ export const handleCertificateFileUpload = ( const wrapPrivateKey = forge.pki.wrapRsaPrivateKey(pkcs8PrivateKey); const privateKey = forge.pki.privateKeyInfoToPem(wrapPrivateKey); if (!endEntityCertFile) { - return reject(new Error('Failed to extract end-entity certificate')); + return reject(new Error(ERROR_MESSAGE)); } const endEntityCert = forge.pki.certificateToPem(endEntityCertFile); @@ -91,7 +86,6 @@ export const handleCertificateFileUpload = ( .map((typeAndValue) => typeAndValue.value.valueBlock.value) .join(', '); - // Resolve the promise with the result object return resolve({ privateKey: privateKey.replace(/\r\n/g, '\n'), certificate: endEntityCert.replace(/\r\n/g, '\n'), @@ -99,8 +93,7 @@ export const handleCertificateFileUpload = ( emailAddress }); } catch (err) { - // Reject the promise with an error message - return reject(new Error(`Certificate processing error: ${(err as Error).message}`)); + return reject(new Error(ERROR_MESSAGE)); } }; From de31c98640129a27262d5a24d6804a18bd99aa92 Mon Sep 17 00:00:00 2001 From: Dhaval Dodiya Date: Thu, 14 Nov 2024 20:25:25 +0530 Subject: [PATCH 09/16] chore: managed the different cases for smime --- src/store/actions/send-msg.ts | 6 +- src/types/editor/index.d.ts | 2 +- src/views/app/detail-panel/edit/edit-view.tsx | 121 ++++++++-------- .../edit/parts/certificate-upload-modal.tsx | 137 ++++++++++++++++++ .../edit/parts/certificate-utils.ts | 4 +- .../edit/parts/options-dropdown.tsx | 3 +- 6 files changed, 206 insertions(+), 67 deletions(-) create mode 100644 src/views/app/detail-panel/edit/parts/certificate-upload-modal.tsx diff --git a/src/store/actions/send-msg.ts b/src/store/actions/send-msg.ts index 49b2e9696..35c68925b 100644 --- a/src/store/actions/send-msg.ts +++ b/src/store/actions/send-msg.ts @@ -70,11 +70,7 @@ export const sendMsgFromEditor = createAsyncThunk state.getCertificate); - - const accountId = 'account123'; - const certificate = getCertificate({ accountId }); + const certificate = getCertificate({ accountId: identity?.fromAddress ?? '' }); let resp: SendMsgWithSmartLinksResponse; try { diff --git a/src/types/editor/index.d.ts b/src/types/editor/index.d.ts index 4ff8a99c5..97f5bcf99 100644 --- a/src/types/editor/index.d.ts +++ b/src/types/editor/index.d.ts @@ -178,7 +178,7 @@ export type MailsEditorV2 = { // the sum of the size of the attachments requiring smart link conversion totalSmartLinksSize: number; // flag for the S/MIME request - isSmimeSign: boolean; + isSmimeSign?: boolean; }; type IdentityType = { diff --git a/src/views/app/detail-panel/edit/edit-view.tsx b/src/views/app/detail-panel/edit/edit-view.tsx index 5527cd44c..8d3ffc79a 100644 --- a/src/views/app/detail-panel/edit/edit-view.tsx +++ b/src/views/app/detail-panel/edit/edit-view.tsx @@ -4,15 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import React, { - memo, - useCallback, - useEffect, - useImperativeHandle, - useMemo, - useRef, - useState -} from 'react'; +import React, { memo, useCallback, useEffect, useImperativeHandle, useMemo, useState } from 'react'; import { Button, @@ -31,13 +23,12 @@ import DropZoneAttachment from './dropzone-attachment'; import { EditAttachmentsBlock } from './edit-attachments-block'; import { createEditBoard } from './edit-view-board'; import { AddAttachmentsDropdown } from './parts/add-attachments-dropdown'; -import { handleCertificateFileUpload } from './parts/certificate-utils'; +import { CertificateUploadModal } from './parts/certificate-upload-modal'; import { ChangeSignaturesDropdown } from './parts/change-signatures-dropdown'; import { useKeepOrDiscardDraft } from './parts/delete-draft'; import { EditViewDraftSaveInfo } from './parts/edit-view-draft-save-info'; import { EditViewIdentitySelector } from './parts/edit-view-identity-selector'; import { EditViewSendButtons } from './parts/edit-view-send-buttons'; -import * as StyledComp from './parts/edit-view-styled-components'; import { OptionsDropdown } from './parts/options-dropdown'; import { RecipientsRows } from './parts/recipients-rows'; import { SizeExceededWarningBanner } from './parts/size-exceeded-waring-banner'; @@ -47,8 +38,12 @@ import { WarningBanner } from './parts/warning-banner'; import { GapContainer, GapRow } from '../../../../commons/gap-container'; import { EDIT_VIEW_CLOSING_REASONS, EditViewActions, TIMEOUTS } from '../../../../constants'; import { buildArrayFromFileList } from '../../../../helpers/files'; -import { getAvailableAddresses, getIdentitiesDescriptors } from '../../../../helpers/identities'; -import { useCertificatesStore } from '../../../../store/zustand/certificates/store'; +import { + getAvailableAddresses, + getIdentitiesDescriptors, + getIdentityDescriptor +} from '../../../../helpers/identities'; +import { Certificate, useCertificatesStore } from '../../../../store/zustand/certificates/store'; import { useEditorAutoSendTime, useEditorDraftSave, @@ -58,7 +53,8 @@ import { deleteEditor, useEditorDid, useEditorsStore, - useEditorIsSmimeSign + useEditorIsSmimeSign, + useEditorIdentityId } from '../../../../store/zustand/editor'; import { EditViewClosingReasons } from '../../../../types'; import { updateEditorWithSmartLinks } from '../../../../ui-actions/utils'; @@ -131,13 +127,26 @@ export const EditView = React.forwardRef(function const [isMailSizeWarning, setIsMailSizeWarning] = useState(false); const { status: saveDraftAllowedStatus, saveDraft } = useEditorDraftSave(editorId); const { did: draftId } = useEditorDid(editorId); + const { identityId } = useEditorIdentityId(editorId); + const identityEmailAddress = getIdentityDescriptor(identityId)?.fromAddress; const { isSmimeSign, setIsSmimeSign } = useEditorIsSmimeSign(editorId); - const inputRef = useRef(null); + const getCertificate = useCertificatesStore((state) => state.getCertificate); useEffect(() => { if (!draftId) saveDraft(); }, [draftId, saveDraft]); + useEffect(() => { + if (identityEmailAddress && isSmimeSign) { + const certificate = getCertificate(identityEmailAddress); + if (certificate) { + setIsSmimeSign(true); + } else { + setIsSmimeSign(false); + } + } + }, [identityEmailAddress, getCertificate, setIsSmimeSign, isSmimeSign]); + const { status: sendAllowedStatus, send: sendMessage } = useEditorSend(editorId); const draftSaveProcessStatus = useEditorDraftSaveProcessStatus(editorId); const createSnackbar = useSnackbar(); @@ -344,45 +353,53 @@ export const EditView = React.forwardRef(function ]); const addCertificate = useCertificatesStore((state) => state.addCertificate); - - const getCertificate = useCertificatesStore((state) => state.getCertificate); - - const accountId = 'account123'; - const certificate = getCertificate(accountId); - if (certificate) { - console.log('Certificate found:', certificate); - } - - const onCertificateFileClick = useCallback(() => { - if (inputRef.current) { - inputRef.current.value = ''; - inputRef.current.click(); - } - }, []); - - const onCertificateFileSelect = useCallback( - async (fileList: FileList) => { - const password = prompt('Enter the password for the P12 file:'); - const result = await handleCertificateFileUpload(fileList, password ?? ''); - const accountId = 'account123'; - const newCertificate = { - privateKey: result.privateKey, - certificate: result.certificate, - caCertificate: result.caCertificate - }; - addCertificate(accountId, newCertificate); - setIsSmimeSign(true); + const onCertificateUploadConfirm = useCallback( + (certificate: Certificate) => { + if (identityEmailAddress) { + addCertificate(identityEmailAddress, certificate); + setIsSmimeSign(true); + } }, - [addCertificate, setIsSmimeSign] + [addCertificate, identityEmailAddress, setIsSmimeSign] ); const onSmimeOptionChange = useCallback( (isSmimeSet: boolean): void => { - if (isSmimeSet) { - onCertificateFileClick(); + if (isSmimeSet && identityEmailAddress) { + const certificate = getCertificate(identityEmailAddress); + if (certificate) { + setIsSmimeSign(true); + } else { + const id = Date.now().toString(); + createModal( + { + id, + size: 'large', + children: ( + + closeModal?.(id)} + /> + + ) + }, + true + ); + } + } else { + setIsSmimeSign(false); } }, - [onCertificateFileClick] + [ + closeModal, + createModal, + getCertificate, + identityEmailAddress, + onCertificateUploadConfirm, + setIsSmimeSign + ] ); const onSendLaterClick = useCallback( @@ -516,16 +533,6 @@ export const EditView = React.forwardRef(function - { - onCertificateFileSelect && - inputRef?.current?.files && - onCertificateFileSelect(inputRef.current.files); - }} - /> ); }); diff --git a/src/views/app/detail-panel/edit/parts/certificate-upload-modal.tsx b/src/views/app/detail-panel/edit/parts/certificate-upload-modal.tsx new file mode 100644 index 000000000..adb6dad14 --- /dev/null +++ b/src/views/app/detail-panel/edit/parts/certificate-upload-modal.tsx @@ -0,0 +1,137 @@ +/* + * SPDX-FileCopyrightText: 2024 Zextras + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +import React, { useCallback, useRef, useState } from 'react'; + +import { + Button, + Container, + Input, + Padding, + PasswordInput, + Row, + Tooltip, + useSnackbar +} from '@zextras/carbonio-design-system'; +import { t } from '@zextras/carbonio-shell-ui'; +import styled from 'styled-components'; + +import { handleCertificateFileUpload } from './certificate-utils'; +import ModalFooter from '../../../../../carbonio-ui-commons/components/modals/modal-footer'; +import ModalHeader from '../../../../../carbonio-ui-commons/components/modals/modal-header'; +import { Certificate } from '../../../../../store/zustand/certificates/store'; + +const FileInput = styled.input` + display: none; +`; + +type CertificateUploadModalPropType = { + emailAddress?: string; + onConfirm: (certificate: Certificate) => void; + onClose: () => void; +}; +export const CertificateUploadModal = ({ + emailAddress, + onConfirm, + onClose +}: CertificateUploadModalPropType): React.JSX.Element => { + const [selectedFile, setSelectedFile] = useState(); + const [password, setPassword] = useState(''); + const inputRef = useRef(null); + const createSnackbar = useSnackbar(); + + const modalHeaderTitle = t('label.upload certificate', 'Upload Certificate'); + const onCertificateFileBrowse = useCallback(() => { + if (inputRef.current) { + inputRef.current.value = ''; + inputRef.current.click(); + } + }, []); + + const onChange = useCallback((): void => { + if (inputRef?.current?.files) { + const file = inputRef?.current?.files[0]; + setSelectedFile(file); + } + }, []); + + const onCertificateFileUpload = useCallback(async (): Promise => { + if (selectedFile) { + try { + const result = await handleCertificateFileUpload(selectedFile, password ?? ''); + if (result.emailAddress === emailAddress) { + const certificate = { + privateKey: result.privateKey, + certificate: result.certificate, + caCertificate: result.caCertificate + }; + onConfirm(certificate); + onClose(); + } else { + throw new Error( + t('composer.uploadCertificate.email_not_match', 'Email address does not match') + ); + } + } catch (error) { + createSnackbar({ + key: `error-on-certificate-upload`, + replace: true, + severity: 'error', + label: t('composer.uploadCertificate.failed', 'Failed to upload certificate'), + autoHideTimeout: 3000, + hideButton: true + }); + } + } + }, [createSnackbar, emailAddress, onClose, onConfirm, password, selectedFile]); + + return ( + + + + + + + + + +