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
JSX
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');
// }
// }