UNPKG

leumas-private-shared

Version:

Private React JSX Package For Leumas Shared Components, Headers, Footers, Asides, Login Pages, API Key Manager and much more. Styles and everything reusable to avoid DRY code across all of our subdomains

230 lines (179 loc) 8.44 kB
import { useState } from 'react'; import useAuthUser from 'react-auth-kit/hooks/useAuthUser'; import React from 'react'; import { encryptData, decryptData, saveToIndexedDB, getFromIndexedDB, generateEncryptionKey , deleteApiKeyFromDB , deleteAllApiKeysFromDB } from './cryptoUtils'; import { AtomSpinner } from 'react-epic-spinners'; import { FaTrash } from 'react-icons/fa'; const ApiKeyManager = () => { const [apiKey, setApiKey] = useState(''); const [apiKeyType, setApiKeyType] = useState(''); // State to store the API key type const authUser = useAuthUser(); // Use the hook to get the authenticated user const userId = authUser?.id; // Assuming the user object has an id property const [apiKeys, setApiKeys] = useState([]); // Function to delete an API key const deleteApiKey = async (indexToDelete) => { const currentApiKeys = await getFromIndexedDB('apiKeys', userId); if (!Array.isArray(currentApiKeys) || indexToDelete < 0 || indexToDelete >= currentApiKeys.length) { console.error('API keys data structure error or invalid index:', currentApiKeys, indexToDelete); alert('Error deleting API key. Please try again.'); return; } await deleteApiKeyFromDB('apiKeys', userId, indexToDelete); // Update local state if you're maintaining one const updatedApiKeys = await getFromIndexedDB('apiKeys', userId); setApiKeys(updatedApiKeys); console.log('API key deleted successfully.'); }; const deleteAll = async () => { // First confirmation if (!window.confirm("Are you sure you want to delete all API Keys?")) { return; } // Second confirmation if (!window.confirm("Are you positive?")) { return; } // Proceed with deletion if both confirmations are true await deleteAllApiKeysFromDB('apiKeys', userId); // Clear the local state as well setApiKeys([]); alert('All API keys have been deleted.'); }; const handleSaveKey = async () => { if (!userId) { alert('User is not authenticated.'); return; } try { const { key: encryptionKey, salt } = await generateEncryptionKey(userId, true); // True indicates generation with a new salt const { encryptedData, iv } = await encryptData(JSON.stringify({ apiKey, apiKeyType }), encryptionKey); const dataToStore = { encryptedData, iv, salt }; // Include salt await saveToIndexedDB('apiKeys', userId, dataToStore); setApiKey(''); setApiKeyType(''); alert('API Key encrypted and stored securely.'); } catch (error) { console.error('Error saving the API key:', error); alert('Failed to save API Key.'); } }; const handleRetrieveKey = async () => { if (!userId) { alert('User is not authenticated.'); return; } try { const dataFromDB = await getFromIndexedDB('apiKeys', userId); if (!dataFromDB || dataFromDB.length === 0) { alert('No API keys found for this user.'); return; } // Ensure dataFromDB is an array of objects, not nested arrays const apiKeysArray = Array.isArray(dataFromDB) ? dataFromDB : [dataFromDB]; const filteredApiKeysArray = apiKeysArray.filter(item => !Array.isArray(item)); const keys = await Promise.all(filteredApiKeysArray.map(async (keyObj) => { const { encryptedData, iv, salt } = keyObj; const { key: encryptionKey } = await generateEncryptionKey(userId, false, salt); const decryptedData = await decryptData({ encryptedData, iv }, encryptionKey); return JSON.parse(decryptedData); })); setApiKeys(keys); } catch (error) { console.error('Error retrieving the API keys:', error); alert(`Failed to retrieve API Keys: ${error instanceof Error ? error : 'Unknown error'}`); } }; // Styles const buttonStyle = `border border-blue-400 p-2 rounded-lg bg-blue-400 hover:bg-black text-white text-xs min-w-24 max-w-24` return ( <div className="container mx-auto p-4 border rounded-lg max-w-screen"> <div className="mb-4 w-full flex gap-2 py-4 px-2"> <input type="text" value={apiKey} onChange={(e) => setApiKey(e.target.value)} placeholder="Enter API Key" className="bg-transparent w-full border border-blue-400 rounded-lg p-2" /> <select value={apiKeyType} onChange={(e) => setApiKeyType(e.target.value)} className="w-full bg-transparent shadow-3xl border border-blue-400 rounded-lg px-2" > <option className='bg-blue-400 text-white' value="">Select Key Type</option> <option className='bg-blue-400 text-white' value="OpenAI">OpenAI</option> <option className='bg-blue-400 text-white' value="Printify">Printify</option> {/* Add more API key types here as needed */} </select> </div> {/* Responsive adjustment for stacking on mobile/tablet and side-by-side on larger screens */} <div className='flex flex-col md:flex-row w-full h-full'> {/* Buttons container, will stack above on mobile/tablet */} <div className='flex md:flex-col gap-2 md:w-1/3 items-center justify-between p-4 border rounded-lg'> <button onClick={deleteAll} className={`${buttonStyle}`}>Delete All</button> <button onClick={handleSaveKey} className={`${buttonStyle}`}>Save Key</button> <button onClick={handleRetrieveKey} className={`${buttonStyle}`}>View Keys</button> </div> {/* API keys container, will follow the buttons on mobile/tablet */} <div className="flex-grow w-full flex flex-col items-center gap-2 p-2 border h-full min-h-64 max-h-64 overflow-y-scroll rounded-lg"> {apiKeys.length === 0 ? ( <div className="flex flex-col items-center justify-center border rounded-lg w-full h-full"> <AtomSpinner size={60} color="#4299E1" /> <button onClick={handleRetrieveKey} className="mt-4 btn bg-blue-500 hover:bg-blue-700 text-white rounded-lg shadow px-4 py-2" > Fetch API Keys </button> </div> ) : ( apiKeys.map((keyData, index) => ( <div key={index} className="flex items-center justify-between border border-blue-400 shadow-md rounded-lg mb-2 p-4 w-full max-w-[300px] mx-auto lg:max-w-[700px] lg:w-full"> <div className="overflow-hidden"> <p className="truncate">Type: <span className="font-semibold">{keyData.apiKeyType}</span></p> <p className="truncate">Key: <span className="font-semibold">{keyData.apiKey}</span></p> </div> <button className={`${buttonStyle} flex items-center justify-center`} onClick={() => deleteApiKey(index)} title="Delete API Key" > <FaTrash /> </button> </div> )) )} </div> </div> </div> ); }; export default ApiKeyManager; // // Alternative Base64 to ArrayBuffer conversion that bypasses atob() // function base64ToUint8Array(base64) { // const padding = '='.repeat((4 - (base64.length % 4)) % 4); // const base64Safe = (base64 + padding) // .replace(/\-/g, '+') // .replace(/_/g, '/'); // const rawData = window.atob(base64Safe); // const outputArray = new Uint8Array(rawData.length); // for (let i = 0; i < rawData.length; ++i) { // outputArray[i] = rawData.charCodeAt(i); // } // return outputArray; // } // function byteArrayStringToUint8Array(byteArray) { // // Check if the input is already a Uint8Array // if (byteArray instanceof Uint8Array) { // console.log("Input is already a Uint8Array", byteArray); // return byteArray; // } else if (typeof byteArray === 'string') { // // If the input is a string, then split and convert // console.log("Converting string to Uint8Array", byteArray); // const byteValues = byteArray.split(',').map(Number); // return new Uint8Array(byteValues); // } else { // throw new Error('Input format not recognized'); // } // }