This is an automated email from the ASF dual-hosted git repository. apratim pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/incubator-resilientdb-resvault.git
The following commit(s) were added to refs/heads/main by this push: new e73bfad Added delete keys functionality, and export all keys e73bfad is described below commit e73bfad69477f22df6a71e58d9fa3d95dbdf4422 Author: Apratim Shukla <apratimshuk...@gmail.com> AuthorDate: Thu Oct 10 00:41:23 2024 -0700 Added delete keys functionality, and export all keys - Specific key pairs can now be deleted for a ResVault account. - All key pairs can now be exported with a button. - UI updates corresponding to these changes. --- src/context/GlobalContext.js | 40 +++++++++++++-- src/css/App.css | 31 +++++++++++- src/pages/Dashboard.jsx | 117 ++++++++++++++++++++++++++++++++----------- 3 files changed, 156 insertions(+), 32 deletions(-) diff --git a/src/context/GlobalContext.js b/src/context/GlobalContext.js index 7a88a7a..5615c56 100644 --- a/src/context/GlobalContext.js +++ b/src/context/GlobalContext.js @@ -104,9 +104,44 @@ export const GlobalProvider = ({ children }) => { }); }; + // Function to delete a key pair + const deleteKeyPair = (index, callback) => { + const password = storedPassword; + if (!password) { + console.error('Password is not available'); + return; + } + + loadKeyPairsFromStorage(password, (existingKeyPairs) => { + if (existingKeyPairs.length <= 1) { + console.error('Cannot delete the last remaining key pair.'); + return; + } + + // Remove the key pair at the specified index + const updatedKeyPairs = [...existingKeyPairs]; + updatedKeyPairs.splice(index, 1); + + // Immediately update the key pairs state + setKeyPairs(updatedKeyPairs); + + // Save the updated keyPairs back to storage + saveKeyPairsToStorage(updatedKeyPairs, password); + + // Reset to the first key pair after deletion + setSelectedKeyPairIndex(0); + setPublicKey(updatedKeyPairs.length > 0 ? updatedKeyPairs[0].publicKey : ''); + setPrivateKey(updatedKeyPairs.length > 0 ? updatedKeyPairs[0].privateKey : ''); + + // Optionally call the callback + if (callback) { + callback(); + } + }); + }; + // Load key pairs from storage when context is initialized useEffect(() => { - // Retrieve password from storage chrome.storage.local.get(['password'], (result) => { const password = result.password; if (password) { @@ -114,7 +149,6 @@ export const GlobalProvider = ({ children }) => { loadKeyPairsFromStorage(password, (loadedKeyPairs) => { if (loadedKeyPairs.length > 0) { setKeyPairs(loadedKeyPairs); - // Load selected key pair index loadSelectedKeyPairIndex((index) => { if (loadedKeyPairs[index]) { setPublicKey(loadedKeyPairs[index].publicKey); @@ -150,7 +184,6 @@ export const GlobalProvider = ({ children }) => { console.error('Password is not available'); return; } - // Encrypt the private key and save in 'store' const encryptedPrivateKey = CryptoJS.AES.encrypt(keyPairs[index].privateKey, password).toString(); const hash = CryptoJS.SHA256(password).toString(CryptoJS.enc.Hex); const store = { @@ -188,6 +221,7 @@ export const GlobalProvider = ({ children }) => { setIsAuthenticated, storedPassword, setStoredPassword, + deleteKeyPair, }} > {children} diff --git a/src/css/App.css b/src/css/App.css index 1ab679e..d95dd29 100644 --- a/src/css/App.css +++ b/src/css/App.css @@ -346,7 +346,7 @@ h2 { position: absolute; left: 0px; bottom: 0; - z-index: 444; } + z-index: 0; } h3 { font-size: 1.6rem; } @@ -3706,4 +3706,33 @@ tr:hover { opacity: 0.7; } +.keypair-actions { + display: flex; + justify-content: space-between; + margin-top: 10px; +} + +.badge-button { + background-color: #3c4e63; /* Matches your theme's primary color */ + color: white; + border: none; + border-radius: 20px; + padding: 10px 20px; + font-size: 14px; + cursor: pointer; + width: 48%; /* Ensures equal width for both buttons */ + text-align: center; + display: flex; + align-items: center; + justify-content: center; +} + +.badge-button:hover { + background-color: #50647e; /* A slightly lighter shade for hover effect */ +} + +.badge-button svg { + margin-left: 8px; /* Adds space between the text and icon */ +} + diff --git a/src/pages/Dashboard.jsx b/src/pages/Dashboard.jsx index 10cafc2..df05436 100644 --- a/src/pages/Dashboard.jsx +++ b/src/pages/Dashboard.jsx @@ -23,6 +23,8 @@ import ExitToAppIcon from '@mui/icons-material/ExitToApp'; import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline'; import ContentCopyIcon from '@mui/icons-material/ContentCopy'; import DownloadIcon from '@mui/icons-material/Download'; +import DeleteIcon from '@mui/icons-material/Delete'; +import SaveAltIcon from '@mui/icons-material/SaveAlt'; import React, { useRef, useState, useEffect, useContext } from 'react'; import Lottie from 'react-lottie'; import versionData from '../data/version.json'; @@ -44,6 +46,7 @@ function Dashboard() { setSelectedKeyPairIndex, setSelectedKeyPair, setIsAuthenticated, + deleteKeyPair, } = useContext(GlobalContext); const [tabId, setTabId] = useState(null); @@ -68,6 +71,7 @@ function Dashboard() { const [transactionError, setTransactionError] = useState(''); const [showSuccessModal, setShowSuccessModal] = useState(false); const [successResponse, setSuccessResponse] = useState(null); + const [showDeleteModal, setShowDeleteModal] = useState(false); // New state for copying transaction ID const [isIdCopied, setIsIdCopied] = useState(false); @@ -352,6 +356,19 @@ function Dashboard() { }); }; + const handleDeleteKeyPair = () => { + if (keyPairs.length > 1) { + deleteKeyPair(selectedKeyPairIndex, () => { + // Reset to the first key pair after deletion + setSelectedKeyPairIndex(0); + setSelectedKeyPair(0); + }); + + // Close the delete confirmation modal + setShowDeleteModal(false); + } + }; + // Function to copy public key const handleCopyPublicKey = () => { try { @@ -385,6 +402,21 @@ function Dashboard() { downloadAnchorNode.remove(); }; + // Function to download all key pairs as JSON + const handleDownloadAllKeyPairs = () => { + const allKeyPairs = keyPairs.map(({ publicKey, privateKey }) => ({ + publicKey, + privateKey, + })); + const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(allKeyPairs)); + const downloadAnchorNode = document.createElement('a'); + downloadAnchorNode.setAttribute("href", dataStr); + downloadAnchorNode.setAttribute("download", "all-keypairs.json"); + document.body.appendChild(downloadAnchorNode); // required for firefox + downloadAnchorNode.click(); + downloadAnchorNode.remove(); + }; + const handleFileUpload = (e) => { const file = e.target.files[0]; if (file && file.type === 'application/json') { @@ -612,12 +644,12 @@ function Dashboard() { </div> </div> <div className="save-container"> - <button onClick={addNet} className="button-save"> - Save - </button> - <button onClick={() => setShowModal(false)} className="button-close"> - Close - </button> + <button onClick={addNet} className="button-save"> + Save + </button> + <button onClick={() => setShowModal(false)} className="button-close"> + Close + </button> </div> {error && <p className="error-message">{error}</p>} </div> @@ -732,35 +764,64 @@ function Dashboard() { <div className="net"> <div className="keypair"> <div className="keypair-header"> - <div className="select-wrapper"> - <select - value={selectedKeyPairIndex} - onChange={(e) => switchKeyPair(e.target.value)} - className="select" - > - {keyPairs.map((keyPair, index) => ( - <option key={index} value={index}> - {`${keyPair.publicKey.slice(0, 4)}...${keyPair.publicKey.slice(-4)}`} - </option> - ))} - </select> - <i className="fas fa-chevron-down"></i> + <div className="select-wrapper"> + <select + value={selectedKeyPairIndex} + onChange={(e) => switchKeyPair(e.target.value)} + className="select" + > + {keyPairs.map((keyPair, index) => ( + <option key={index} value={index}> + {`${keyPair.publicKey.slice(0, 4)}...${keyPair.publicKey.slice(-4)}`} + </option> + ))} + </select> + <i className="fas fa-chevron-down"></i> + </div> + <div className="keypair-icons"> + {keyPairs.length > 1 && ( + <button onClick={() => setShowDeleteModal(true)} className="icon-button"> + <DeleteIcon style={{ color: 'white' }} /> + </button> + )} + <button onClick={handleCopyPublicKey} className="icon-button"> + <ContentCopyIcon style={{ color: isCopied ? 'grey' : 'white' }} /> + </button> + <button onClick={handleDownloadKeyPair} className="icon-button"> + <DownloadIcon style={{ color: 'white' }} /> + </button> + </div> </div> - <div className="keypair-icons"> - <button onClick={handleGenerateKeyPair} className="icon-button"> - <AddCircleOutlineIcon style={{ color: 'white' }} /> - </button> - <button onClick={handleCopyPublicKey} className="icon-button"> - <ContentCopyIcon style={{ color: isCopied ? 'green' : 'white' }} /> + <div className="keypair-actions"> + <button onClick={handleGenerateKeyPair} className="badge-button"> + Generate <AddCircleOutlineIcon style={{ color: 'white' }} /> </button> - <button onClick={handleDownloadKeyPair} className="icon-button"> - <DownloadIcon style={{ color: 'white' }} /> + <button onClick={handleDownloadAllKeyPairs} className="badge-button"> + Export <SaveAltIcon style={{ color: 'white' }} /> </button> </div> - </div> </div> </div> + {showDeleteModal && ( + <div className="modal-overlay"> + <div className="modal"> + <div className="modal-content"> + <h2>Are you sure?</h2> + <p>This action is irreversible and will delete the selected key pair forever.</p> + <div className="save-container"> + <button onClick={handleDeleteKeyPair} className="button-save"> + Delete + </button> + <button onClick={() => setShowDeleteModal(false)} className="button-close"> + Cancel + </button> + </div> + </div> + </div> + </div> + )} + <button className="button button--full button--main open-popup" onClick={handleSubmit}> Submit </button>