Last active
November 25, 2024 22:25
-
-
Save sscarduzio/df955bde29364a6670c926b293437e58 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useState, useEffect } from 'react'; | |
import { Card, CardContent } from '../ui/card'; | |
import { Textarea } from '../ui/textarea'; | |
import Editor from "@monaco-editor/react"; | |
import { Button } from '../ui/button'; | |
import { X, Check, Copy } from 'lucide-react'; | |
import { Algorithm, SUPPORTED_ALGORITHMS, KeyPair } from '../../types/activationKey'; | |
import { decodeJWT, getJwtMetadata, signJWT, validateJWTSignature, ValidationResult } from '../../utils/activationKey'; | |
import { ActivationKeyMetadataDisplay } from '../activationkey/ActivationKeyMetadata'; | |
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'; | |
import { PageHeader } from '../ui/page-header'; | |
import { ValidationStatus } from '../activationkey/validation-status'; | |
import './ActivationkeyEditor.css'; | |
import { useTheme } from '../../hooks/use-theme'; | |
const ActivationKeyEditor = () => { | |
const { theme } = useTheme(); | |
const [inputValue, setInputValue] = useState(''); | |
const [jwt, setJwt] = useState(''); | |
const [keyPairs, setKeyPairs] = useState<KeyPair[]>([]); | |
const [selectedKeyId, setSelectedKeyId] = useState<string>(''); | |
const [expiryDate, setExpiryDate] = useState<Date | undefined>(undefined); | |
const [showCopied, setShowCopied] = useState(false); | |
const [algorithm, setAlgorithm] = useState<Algorithm>('ES512'); | |
const [editorValue, setEditorValue] = useState('{}'); | |
const [signatureValidation, setSignatureValidation] = useState<ValidationResult | null>(null); | |
const exampleJwt = "eyJhbGciOiJFUzUxMiJ9.eyJleHAiOjE3NDMyMDI4MDAsImlzcyI6Imh0dHBzOi8vYW5hcGhvcmEtd2Vic2l0ZS5wYWdlcy5kZXYvIiwiaWF0IjoxNzMyNTQyNzU4LCJqdGkiOiJhbmFwaG9yYV9lbnRlcnByaXNlXzE3MjY5OTM1NDcuODQwMzUzX2pvaG4uZG9lQGFjbWUuY29tIiwiYXVkIjoiYW5hcGhvcmEuZW50ZXJwcmlzZV9saWNlbnNlIiwic3ViIjozMCwibGljZW5zb3IiOnsibmFtZSI6IkJlc2h1IExpbWl0ZWQgdC9hIGFzZCBTZWN1cml0eSIsImNvbnRhY3QiOlsic3VwcG9ydEBhY21lLmNvbSIsImZpbmFuY2VAYWNtZS5jb20iXSwiaXNzdWVyIjoic3VwcG9ydEByZWFkb25seXJlc3QuY29tIn0sImxpY2Vuc2VlIjp7Im5hbWUiOiJKb2huIERvZSIsImJ1eWluZ19mb3IiOm51bGwsImJpbGxpbmdfZW1haWwiOiJqb2huLmRvZUBhY21lLmNvbSIsImFsdF9lbWFpbHMiOlsiamFuZS5kb2VAYWNtZS5jb20iXSwiYWRkcmVzcyI6WyJSdWUgNTYsIFBhcmlzIiwiRnJhbmNlIl19LCJsaWNlbnNlIjp7ImVkaXRpb24iOiJFTlRFUlBSSVNFIiwiZWRpdGlvbl9uYW1lIjoiRU5URVJQUklTRSBFZGl0aW9uIiwiaXNUcmlhbCI6dHJ1ZX19.AUjAqQnxs9tBEgupxO2fYIxLfZthD00cGYOIzsJ7ZgbnDku0sNU_BR5P9u64s-lSv9cvM1pHKVmIXmCgsCbIjMQzACyveVTP4iJXKBM7FSf1nC1TKPIrm3Oq6uuQa1qrcWcR4tfMp4QXUGn396B2hxuMKtS9Q_Tj-cQ-LkL9kk6q4oMw"; | |
useEffect(() => { | |
const savedKeys = localStorage.getItem('keyPairs'); | |
if (savedKeys) { | |
const keys = JSON.parse(savedKeys); | |
setKeyPairs(keys); | |
if (keys.length > 0 && !selectedKeyId) { | |
setSelectedKeyId(keys[0].id); | |
} | |
} | |
}, [selectedKeyId]); | |
const handleInputChange = async (value: string) => { | |
setInputValue(value); | |
if (!value) { | |
clearJwt(); | |
return; | |
} | |
if (!selectedKeyId && keyPairs.length > 0) { | |
setSelectedKeyId(keyPairs[0].id); | |
} | |
// Try to parse as JWT | |
const metadata = getJwtMetadata(value); | |
if (metadata) { | |
if (metadata.algorithm && SUPPORTED_ALGORITHMS.includes(metadata.algorithm as Algorithm)) { | |
setAlgorithm(metadata.algorithm as Algorithm); | |
} | |
if (metadata.expiresAt) { | |
setExpiryDate(new Date(metadata.expiresAt)); | |
} | |
} | |
const decoded = await decodeJWT(value); | |
try { | |
const parsedDecoded = JSON.parse(decoded); | |
const payloadOnly = JSON.stringify(parsedDecoded.payload || {}, null, 2); | |
setEditorValue(payloadOnly); | |
} catch (e) { | |
setEditorValue('{}'); | |
} | |
setJwt(value); | |
try { | |
// Only validate against the selected key pair | |
const selectedKey = keyPairs.find(k => k.id === selectedKeyId); | |
const validation = await validateJWTSignature(value, selectedKey); | |
setSignatureValidation(validation); | |
} catch (error) { | |
setSignatureValidation({ | |
isValid: false, | |
errors: { | |
signature: true, | |
message: 'Invalid JWT format' | |
} | |
}); | |
} | |
}; | |
// Also update validation when the selected key changes | |
useEffect(() => { | |
if (jwt && selectedKeyId) { | |
const selectedKey = keyPairs.find(k => k.id === selectedKeyId); | |
validateJWTSignature(jwt, selectedKey).then(setSignatureValidation); | |
} | |
}, [selectedKeyId, jwt]); | |
const clearJwt = () => { | |
setInputValue(''); | |
setJwt(''); | |
setSignatureValidation(null); | |
if (keyPairs.length > 0) { | |
setSelectedKeyId(keyPairs[0].id); | |
} else { | |
setSelectedKeyId(''); | |
} | |
}; | |
const handleSign = async () => { | |
try { | |
const selectedKey = keyPairs.find(k => k.id === selectedKeyId); | |
if (!selectedKey) return; | |
let payload; | |
try { | |
// Parse the editor value directly as the payload | |
payload = JSON.parse(editorValue); | |
} catch (e) { | |
alert('Invalid JSON payload'); | |
return; | |
} | |
// Only override exp if a new expiry date is selected | |
if (expiryDate) { | |
payload.exp = Math.floor(expiryDate.getTime() / 1000); | |
} | |
const newToken = await signJWT( | |
payload, | |
algorithm, | |
selectedKey, | |
// Use the expiry date from the payload if no new date is selected | |
expiryDate || (payload.exp ? new Date(payload.exp * 1000) : new Date()) | |
); | |
setJwt(newToken); | |
const newDecoded = await decodeJWT(newToken); | |
// Extract just the payload object for the editor | |
try { | |
const parsedDecoded = JSON.parse(newDecoded); | |
const payloadOnly = JSON.stringify(parsedDecoded.payload || {}, null, 2); | |
setEditorValue(payloadOnly); | |
} catch (e) { | |
setEditorValue('{}'); | |
} | |
} catch (error) { | |
console.error('Signing error:', error); | |
alert('Failed to sign JWT'); | |
} | |
}; | |
const copyToClipboard = async (text: string) => { | |
await navigator.clipboard.writeText(text); | |
setShowCopied(true); | |
setTimeout(() => setShowCopied(false), 2000); | |
}; | |
return ( | |
<div className="ak-editor-container"> | |
<PageHeader | |
title="Activation Key Editor" | |
description="Create, decode, and validate Activation Keys" | |
/> | |
{!jwt ? ( | |
<div className="space-y-2"> | |
<Textarea | |
placeholder="Paste your Activation Key here" | |
value={inputValue} | |
onChange={(e) => handleInputChange(e.target.value)} | |
className="ak-input" | |
/> | |
<button | |
onClick={() => handleInputChange(exampleJwt)} | |
className="ak-example-link" | |
> | |
Use example | |
</button> | |
</div> | |
) : ( | |
<Card className="relative"> | |
<Button | |
variant="ghost" | |
size="icon" | |
className="ak-clear-button" | |
onClick={clearJwt} | |
> | |
<X className="h-4 w-4" /> | |
</Button> | |
<CardContent className="space-y-6 pt-6"> | |
<div className="ak-content-wrapper"> | |
<div className="ak-editor-monaco"> | |
<Editor | |
key={`monaco-${jwt}`} | |
defaultLanguage="json" | |
value={editorValue} | |
onChange={(value) => setEditorValue(value || '{}')} | |
options={{ | |
minimap: { enabled: false }, | |
formatOnPaste: true, | |
formatOnType: true, | |
automaticLayout: true, | |
scrollBeyondLastLine: false, | |
tabSize: 2, | |
fontSize: 12, | |
lineNumbers: 'off', | |
folding: false, | |
glyphMargin: false, | |
lineDecorationsWidth: 0, | |
lineNumbersMinChars: 0, | |
overviewRulerBorder: false, | |
overviewRulerLanes: 0, | |
hideCursorInOverviewRuler: true, | |
scrollbar: { | |
vertical: 'auto', | |
horizontal: 'auto', | |
verticalScrollbarSize: 10, | |
horizontalScrollbarSize: 10, | |
alwaysConsumeMouseWheel: false | |
} | |
}} | |
theme={theme === 'dark' ? 'vs-dark' : 'light'} | |
/> | |
</div> | |
<div className="ak-right-column"> | |
<div className="ak-header-row"> | |
<ValidationStatus validation={signatureValidation} /> | |
<Select value={selectedKeyId} onValueChange={setSelectedKeyId}> | |
<SelectTrigger className="ak-key-select"> | |
<SelectValue placeholder="Select a key for signing" /> | |
</SelectTrigger> | |
<SelectContent> | |
{keyPairs.map((key) => ( | |
<SelectItem key={key.id} value={key.id}> | |
{key.name} ({algorithm.startsWith('HS') ? 'Symmetric' : 'Asymmetric'}) | |
</SelectItem> | |
))} | |
</SelectContent> | |
</Select> | |
</div> | |
{getJwtMetadata(jwt) && ( | |
<ActivationKeyMetadataDisplay | |
metadata={getJwtMetadata(jwt)!} | |
expiryDate={expiryDate} | |
onExpiryChange={setExpiryDate} | |
/> | |
)} | |
<div className="ak-controls"> | |
<Button | |
onClick={handleSign} | |
disabled={!selectedKeyId || !editorValue} | |
className="sign-button" | |
> | |
Generate Activation Key | |
</Button> | |
</div> | |
{jwt && ( | |
<div className="ak-output"> | |
<div className="ak-output-text"> | |
{jwt} | |
</div> | |
<Button | |
variant="ghost" | |
size="icon" | |
className="ak-copy-button" | |
onClick={() => copyToClipboard(jwt)} | |
> | |
{showCopied ? ( | |
<Check className="h-4 w-4 text-green-500" /> | |
) : ( | |
<Copy className="h-4 w-4" /> | |
)} | |
</Button> | |
</div> | |
)} | |
</div> | |
</div> | |
</CardContent> | |
</Card> | |
)} | |
</div> | |
); | |
}; | |
export default ActivationKeyEditor; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as jose from 'jose'; | |
import { formatDistanceToNow, formatDistanceToNowStrict } from 'date-fns'; | |
import { ActivationKeyMetadata, KeyPair, Algorithm } from '../types/activationKey'; | |
import { SignJWT } from 'jose'; | |
import { generateKeyPair } from 'jose'; | |
// Polyfill Buffer for browser environments | |
import { Buffer } from 'buffer'; | |
const globalAny = typeof window !== 'undefined' ? window : global; | |
if (!globalAny.Buffer) { | |
globalAny.Buffer = Buffer; | |
} | |
export const getJwtMetadata = (token: string): ActivationKeyMetadata | null => { | |
try { | |
const decoded = jose.decodeJwt(token); | |
return { | |
algorithm: jose.decodeProtectedHeader(token).alg ?? null, | |
issuedAt: decoded.iat ? new Date(decoded.iat * 1000) : null, | |
expiresAt: decoded.exp ? new Date(decoded.exp * 1000) : null, | |
}; | |
} catch (e) { | |
return null; | |
} | |
}; | |
export const formatRelativeTime = (date: Date | null, isExpiry = false): string => { | |
if (!date) return 'Not specified'; | |
const now = new Date(); | |
const isPast = date < now; | |
if (isExpiry) { | |
return isPast | |
? `Expired ${formatDistanceToNow(date, { addSuffix: true })}` | |
: `${formatDistanceToNowStrict(date, { addSuffix: true })}`; | |
} | |
return `${formatDistanceToNow(date, { addSuffix: true })}`; | |
}; | |
export const formatJwtDisplay = (token: string): string => { | |
if (token.length <= 20) return token; | |
return `${token.slice(0, 10)}...${token.slice(-10)}`; | |
}; | |
export const decodeJWT = async (token: string ): Promise<string> => { | |
try { | |
const decoded = jose.decodeJwt(token); | |
const header = jose.decodeProtectedHeader(token); | |
return JSON.stringify( | |
{ | |
header, | |
payload: decoded, | |
}, | |
null, | |
2 | |
); | |
} catch (e) { | |
return '{}'; | |
} | |
}; | |
const checkPrivateKey = async (key: string): Promise<string> => { | |
// Check if key is in SEC1 format | |
if (key.includes('-----BEGIN EC PRIVATE KEY-----')) { | |
throw new Error('Only PKCS8 format private keys are supported. Please convert your SEC1 key to PKCS8 format.'); | |
} | |
return key; | |
}; | |
export const signJWT = async ( | |
payload: any, | |
algorithm: Algorithm, | |
keyPair: KeyPair, | |
expiryDate: Date | |
): Promise<string> => { | |
try { | |
if (!keyPair?.privateKey) { | |
throw new Error('Private key is required'); | |
} | |
// Normalize the private key format if needed | |
const normalizedKey = await checkPrivateKey(keyPair.privateKey); | |
// Get the correct curve for the algorithm | |
const privateKey = await jose.importPKCS8(normalizedKey, 'ES512'); | |
const jwt = await new SignJWT(payload) | |
.setProtectedHeader({ alg: algorithm }) | |
.setIssuedAt() | |
.setExpirationTime(expiryDate) | |
.sign(privateKey); | |
return jwt; | |
} catch (error) { | |
console.error('Signing error:', error); | |
throw error; | |
} | |
}; | |
export interface ValidationResult { | |
isValid: boolean; | |
keyName?: string; | |
errors?: { | |
expired?: boolean; | |
signature?: boolean; | |
message?: string; | |
}; | |
} | |
export const validateJWTSignature = async ( | |
jwt: string, | |
keyPair: KeyPair | undefined | |
): Promise<ValidationResult> => { | |
if (!keyPair) { | |
return { | |
isValid: false, | |
errors: { | |
signature: true, | |
message: 'No matching key found in your list of keys' | |
} | |
}; | |
} | |
try { | |
const [header] = jwt.split('.'); | |
const decodedHeader = JSON.parse(atob(header)); | |
const algorithm = decodedHeader.alg; | |
const normalizedKey = await checkPrivateKey(keyPair.publicKey); | |
const publicKey = await jose.importSPKI(normalizedKey, algorithm); | |
await jose.jwtVerify(jwt, publicKey); | |
return { | |
isValid: true, | |
errors: undefined | |
}; | |
} catch (error) { | |
return { | |
isValid: false, | |
errors: { | |
signature: true, | |
message: error instanceof Error ? error.message : 'Unknown error' | |
} | |
}; | |
} | |
}; | |
export const generateEC512KeyPair = async (): Promise<KeyPair> => { | |
try { | |
const { privateKey, publicKey } = await generateKeyPair('ES512', { | |
extractable: true // Make sure keys are extractable | |
}); | |
// Export keys in PKCS8 (private) and SPKI (public) formats | |
const exportedPrivateKey = await jose.exportPKCS8(privateKey); | |
const exportedPublicKey = await jose.exportSPKI(publicKey); | |
return { | |
id: crypto.randomUUID(), | |
name: 'Generated ES512 Key Pair', | |
privateKey: exportedPrivateKey, | |
publicKey: exportedPublicKey, | |
createdAt: new Date().toISOString() | |
}; | |
} catch (error) { | |
console.error('Key pair generation error:', error); | |
throw error; | |
} | |
}; | |
// Optional helper to extract public key from private key | |
export const extractPublicKeyFromPrivate = async (privateKeyString: string): Promise<string> => { | |
try { | |
const privateKey = await jose.importPKCS8(privateKeyString, 'ES512'); | |
const publicKey = await jose.exportSPKI(privateKey); | |
return publicKey; | |
} catch (error) { | |
console.error('Public key extraction error:', error); | |
throw error; | |
} | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useState, useEffect } from 'react'; | |
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card'; | |
import { Button } from '../ui/button'; | |
import { Plus, ChevronDown, ChevronUp, Pencil, Trash2, Download, Upload } from 'lucide-react'; | |
import { | |
Tooltip, | |
TooltipContent, | |
TooltipProvider, | |
TooltipTrigger, | |
} from "../ui/tooltip"; | |
import { Label } from '../ui/label'; | |
import { PageHeader } from '../ui/page-header'; | |
import { KeyPair } from '../../types/activationKey'; | |
import { KeyPairForm } from '../keys/key-pair-form'; | |
import { validatePublicKey } from '../../utils/key-validation'; | |
import { validatePrivateKey } from '../../utils/key-validation'; | |
import { generateEC512KeyPair } from '../../utils/activationKey'; | |
import { exportKeyPairToZip, importKeyPairFromZip } from '../../utils/file-utils'; | |
import './Keys.css'; | |
const Keys = () => { | |
const [keyPairs, setKeyPairs] = useState<KeyPair[]>([]); | |
const [isAddingNew, setIsAddingNew] = useState(false); | |
const [newKeyPair, setNewKeyPair] = useState<Omit<KeyPair, 'id'>>({ | |
name: '', | |
publicKey: '', | |
privateKey: '', | |
createdAt: '', | |
}); | |
const [expandedId, setExpandedId] = useState<string | null>(null); | |
const [editingId, setEditingId] = useState<string | null>(null); | |
const [editingKeyPair, setEditingKeyPair] = useState<KeyPair | null>(null); | |
useEffect(() => { | |
const savedKeys = localStorage.getItem('keyPairs'); | |
if (savedKeys) { | |
setKeyPairs(JSON.parse(savedKeys)); | |
} | |
}, []); | |
const handleSave = async () => { | |
try { | |
const trimmedKeyPair = { | |
name: newKeyPair.name.trim(), | |
publicKey: newKeyPair.publicKey.trim(), | |
privateKey: newKeyPair.privateKey.trim(), | |
}; | |
if (!trimmedKeyPair.name || !trimmedKeyPair.publicKey || !trimmedKeyPair.privateKey) { | |
alert('Please fill in all fields'); | |
return; | |
} | |
const privateKeyError = validatePrivateKey(trimmedKeyPair.privateKey); | |
if (privateKeyError) { | |
alert(privateKeyError); | |
return; | |
} | |
const publicKeyError = validatePublicKey(trimmedKeyPair.publicKey); | |
if (publicKeyError) { | |
alert(publicKeyError); | |
return; | |
} | |
const newPair: KeyPair = { | |
...trimmedKeyPair, | |
id: crypto.randomUUID(), | |
createdAt: new Date().toISOString(), | |
}; | |
const updatedPairs = [...keyPairs, newPair]; | |
setKeyPairs(updatedPairs); | |
localStorage.setItem('keyPairs', JSON.stringify(updatedPairs)); | |
setIsAddingNew(false); | |
setNewKeyPair({ name: '', publicKey: '', privateKey: '', createdAt: '' }); | |
} catch (error) { | |
alert(error instanceof Error ? error.message : 'Error saving key pair'); | |
} | |
}; | |
const handleDelete = (id: string) => { | |
if (window.confirm('Are you sure you want to delete this key pair?')) { | |
const updatedPairs = keyPairs.filter(pair => pair.id !== id); | |
setKeyPairs(updatedPairs); | |
localStorage.setItem('keyPairs', JSON.stringify(updatedPairs)); | |
} | |
}; | |
const toggleExpand = (id: string) => { | |
setExpandedId(expandedId === id ? null : id); | |
}; | |
const handleEdit = (pair: KeyPair) => { | |
if (editingId === pair.id) { | |
setEditingId(null); | |
setEditingKeyPair(null); | |
} else { | |
setEditingId(pair.id); | |
setEditingKeyPair(pair); | |
} | |
}; | |
const handleUpdateKey = () => { | |
if (!editingKeyPair) return; | |
const trimmedKeyPair = { | |
...editingKeyPair, | |
name: editingKeyPair.name.trim(), | |
publicKey: editingKeyPair.publicKey.trim(), | |
privateKey: editingKeyPair.privateKey.trim(), | |
}; | |
const privateKeyError = validatePrivateKey(trimmedKeyPair.privateKey); | |
if (privateKeyError) { | |
alert(privateKeyError); | |
return; | |
} | |
const publicKeyError = validatePublicKey(trimmedKeyPair.publicKey); | |
if (publicKeyError) { | |
alert(publicKeyError); | |
return; | |
} | |
const updatedPairs = keyPairs.map(pair => | |
pair.id === trimmedKeyPair.id ? trimmedKeyPair : pair | |
); | |
setKeyPairs(updatedPairs); | |
localStorage.setItem('keyPairs', JSON.stringify(updatedPairs)); | |
setEditingId(null); | |
setEditingKeyPair(null); | |
}; | |
const handleGenerateKeys = async () => { | |
try { | |
const generatedPair = await generateEC512KeyPair(); | |
setNewKeyPair({ | |
name: generatedPair.name, | |
publicKey: generatedPair.publicKey, | |
privateKey: generatedPair.privateKey, | |
createdAt: generatedPair.createdAt | |
}); | |
} catch (error) { | |
alert(error instanceof Error ? error.message : 'Error generating key pair'); | |
} | |
}; | |
const handleExport = async (pair: KeyPair) => { | |
try { | |
await exportKeyPairToZip(pair); | |
} catch (error) { | |
alert(error instanceof Error ? error.message : 'Error exporting key pair'); | |
} | |
}; | |
const handleImport = async (event: React.ChangeEvent<HTMLInputElement>) => { | |
const file = event.target.files?.[0]; | |
if (!file) return; | |
try { | |
const importedPair = await importKeyPairFromZip(file); | |
setNewKeyPair({ | |
...importedPair, | |
createdAt: new Date().toISOString(), | |
}); | |
setIsAddingNew(true); | |
} catch (error) { | |
alert(error instanceof Error ? error.message : 'Error importing key pair'); | |
} | |
// Reset the input | |
event.target.value = ''; | |
}; | |
return ( | |
<div className="p-6 max-w-4xl mx-auto"> | |
<PageHeader | |
title="Keys Management" | |
description="Manage your key pairs. All keys should be in PKCS#8 PEM format." | |
/> | |
<div className="space-y-4"> | |
<div className="flex justify-end gap-2"> | |
<input | |
type="file" | |
accept=".zip" | |
onChange={handleImport} | |
className="hidden" | |
id="import-key" | |
/> | |
<TooltipProvider> | |
<Tooltip delayDuration={0}> | |
<TooltipTrigger asChild> | |
<label htmlFor="import-key"> | |
<Button variant="outline" size="icon" asChild> | |
<span> | |
<Upload className="h-4 w-4" /> | |
</span> | |
</Button> | |
</label> | |
</TooltipTrigger> | |
<TooltipContent className="w-80"> | |
<div className="space-y-2"> | |
<h4 className="font-medium">Import Key Pair</h4> | |
<p className="text-sm"> | |
Upload a ZIP file containing: | |
<ul className="list-disc list-inside mt-1"> | |
<li>public_key.pem</li> | |
<li>private_key.pem</li> | |
</ul> | |
The key pair name will be taken from the ZIP filename. | |
</p> | |
</div> | |
</TooltipContent> | |
</Tooltip> | |
</TooltipProvider> | |
<Button | |
onClick={() => setIsAddingNew(true)} | |
disabled={isAddingNew} | |
size="icon" | |
> | |
<Plus className="h-4 w-4" /> | |
</Button> | |
</div> | |
{isAddingNew && ( | |
<Card className="mb-6"> | |
<CardContent className="pt-6 space-y-6"> | |
<KeyPairForm | |
mode="create" | |
keyPair={newKeyPair} | |
onChange={(e) => setNewKeyPair(e as KeyPair)} | |
onSave={handleSave} | |
onCancel={() => { | |
setIsAddingNew(false); | |
setNewKeyPair({ name: '', publicKey: '', privateKey: '', createdAt: '' }); | |
}} | |
onGenerateKeys={handleGenerateKeys} | |
/> | |
</CardContent> | |
</Card> | |
)} | |
<div className="space-y-4"> | |
{keyPairs.map((pair) => ( | |
<Card key={pair.id}> | |
<CardHeader | |
className="relative cursor-pointer hover:bg-secondary/10 transition-colors px-6 py-4" | |
onClick={() => toggleExpand(pair.id)} | |
> | |
<div className="flex items-center justify-between"> | |
<div className="flex items-center gap-3"> | |
<Button | |
variant="ghost" | |
size="sm" | |
className="pointer-events-none p-0 hover:bg-transparent" | |
> | |
{expandedId === pair.id ? ( | |
<ChevronUp className="h-5 w-5" /> | |
) : ( | |
<ChevronDown className="h-5 w-5" /> | |
)} | |
</Button> | |
<CardTitle className="text-xl font-semibold">{pair.name}</CardTitle> | |
</div> | |
<div className="flex gap-2"> | |
<TooltipProvider> | |
<Tooltip delayDuration={0}> | |
<TooltipTrigger asChild> | |
<Button | |
variant="ghost" | |
size="sm" | |
onClick={(e) => { | |
e.stopPropagation(); | |
handleExport(pair); | |
}} | |
> | |
<Download className="h-5 w-5" /> | |
</Button> | |
</TooltipTrigger> | |
<TooltipContent className="w-80" onClick={(e) => e.stopPropagation()}> | |
<div className="space-y-2"> | |
<h4 className="font-medium">Export Key Pair</h4> | |
<p className="text-sm"> | |
Downloads '{pair.name}.zip' containing: | |
<ul className="list-disc list-inside mt-1"> | |
<li>public_key.pem</li> | |
<li>private_key.pem</li> | |
</ul> | |
</p> | |
</div> | |
</TooltipContent> | |
</Tooltip> | |
</TooltipProvider> | |
{editingId === pair.id && ( | |
<Button | |
variant="ghost" | |
size="sm" | |
onClick={(e) => { | |
e.stopPropagation(); | |
handleDelete(pair.id); | |
}} | |
> | |
<Trash2 className="h-5 w-5" /> | |
</Button> | |
)} | |
<Button | |
variant="ghost" | |
size="sm" | |
onClick={(e) => { | |
e.stopPropagation(); | |
handleEdit(pair); | |
}} | |
> | |
<Pencil className={`h-5 w-5 ${editingId === pair.id ? "text-primary" : ""}`} /> | |
</Button> | |
</div> | |
</div> | |
</CardHeader> | |
{(expandedId === pair.id || editingId === pair.id) && ( | |
<CardContent className="space-y-4 px-6 py-4"> | |
{editingId === pair.id ? ( | |
<> | |
<KeyPairForm | |
mode="edit" | |
keyPair={editingKeyPair || {}} | |
onChange={(e) => setEditingKeyPair((prev) => prev ? { ...prev, ...e } : null)} | |
onSave={handleUpdateKey} | |
onCancel={() => { | |
setEditingId(null); | |
setEditingKeyPair(null); | |
}} | |
/> | |
</> | |
) : ( | |
<> | |
<div className="space-y-2"> | |
<Label className="key-label">Public Key (PEM)</Label> | |
<div className="key-display blurred"> | |
{pair.publicKey} | |
</div> | |
</div> | |
<div className="space-y-2"> | |
<Label className="key-label">Private Key (PEM)</Label> | |
<div className="key-display blurred"> | |
{pair.privateKey} | |
</div> | |
</div> | |
</> | |
)} | |
</CardContent> | |
)} | |
</Card> | |
))} | |
</div> | |
</div> | |
</div> | |
); | |
}; | |
export default Keys; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment