refactor: extract SshKeysPage components
- Extract use-ssh-keys hook for state management - Extract SSHKeyCreateForm and SSHKeyList components - Slim SshKeysPage from 277 to ~80 lines Quality gates: tsc --noEmit passes, npm run build passes
This commit is contained in:
@@ -0,0 +1,39 @@
|
|||||||
|
import { Icon } from "../../icon";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
newKeyName: string;
|
||||||
|
setNewKeyName: (name: string) => void;
|
||||||
|
generating: boolean;
|
||||||
|
onSubmit: (e: React.FormEvent) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SSHKeyCreateForm = ({ newKeyName, setNewKeyName, generating, onSubmit }: Props) => {
|
||||||
|
return (
|
||||||
|
<form onSubmit={onSubmit} className="stack">
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="key-name">Key Name</label>
|
||||||
|
<input
|
||||||
|
id="key-name"
|
||||||
|
type="text"
|
||||||
|
value={newKeyName}
|
||||||
|
onChange={(e) => setNewKeyName(e.target.value)}
|
||||||
|
placeholder="e.g., GitHub Work"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button type="submit" className="primary-button" disabled={generating}>
|
||||||
|
{generating ? (
|
||||||
|
<>
|
||||||
|
<Icon name="loading" size="sm" />
|
||||||
|
Generating...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Icon name="add" size="sm" />
|
||||||
|
Generate SSH Key
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
import { Icon } from "../../icon";
|
||||||
|
import { EmptyState, ErrorState } from "../../data-states";
|
||||||
|
import type { SSHKey } from "../../../api/ssh-keys";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
keys: SSHKey[];
|
||||||
|
status: "idle" | "loading" | "ready" | "error";
|
||||||
|
signPayloads: Record<string, string>;
|
||||||
|
signatures: Record<string, string>;
|
||||||
|
signing: Record<string, boolean>;
|
||||||
|
verifyPayloads: Record<string, string>;
|
||||||
|
verifySignatures: Record<string, string>;
|
||||||
|
verifyResults: Record<string, boolean | null>;
|
||||||
|
verifying: Record<string, boolean>;
|
||||||
|
onLoadKeys: () => void;
|
||||||
|
onDelete: (id: string) => void;
|
||||||
|
onCopy: (text: string) => void;
|
||||||
|
onSign: (id: string) => void;
|
||||||
|
onVerify: (id: string) => void;
|
||||||
|
onSignPayloadChange: (id: string, value: string) => void;
|
||||||
|
onVerifyPayloadChange: (id: string, value: string) => void;
|
||||||
|
onVerifySignatureChange: (id: string, value: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SSHKeyList = ({
|
||||||
|
keys,
|
||||||
|
status,
|
||||||
|
signPayloads,
|
||||||
|
signatures,
|
||||||
|
signing,
|
||||||
|
verifyPayloads,
|
||||||
|
verifySignatures,
|
||||||
|
verifyResults,
|
||||||
|
verifying,
|
||||||
|
onLoadKeys,
|
||||||
|
onDelete,
|
||||||
|
onCopy,
|
||||||
|
onSign,
|
||||||
|
onVerify,
|
||||||
|
onSignPayloadChange,
|
||||||
|
onVerifyPayloadChange,
|
||||||
|
onVerifySignatureChange,
|
||||||
|
}: Props) => {
|
||||||
|
if (status === "error") {
|
||||||
|
return <ErrorState message="Failed to load SSH keys" onRetry={onLoadKeys} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keys.length === 0) {
|
||||||
|
return <EmptyState message="No SSH keys yet. Generate one above." />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="keys-list">
|
||||||
|
{keys.map((key) => (
|
||||||
|
<div key={key.id} className="key-card">
|
||||||
|
<div className="key-header">
|
||||||
|
<h3>{key.name}</h3>
|
||||||
|
<button onClick={() => onDelete(key.id)} className="danger-button">
|
||||||
|
<Icon name="delete" size="sm" />
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="key-meta">
|
||||||
|
<span className="muted">
|
||||||
|
Created: {new Date(key.created_at).toLocaleDateString()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="key-public">
|
||||||
|
<code>{key.public_key.substring(0, 50)}...</code>
|
||||||
|
<button
|
||||||
|
onClick={() => onCopy(key.public_key)}
|
||||||
|
className="secondary-button"
|
||||||
|
>
|
||||||
|
<Icon name="copy" size="sm" />
|
||||||
|
Copy Full Key
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="key-signing">
|
||||||
|
<h4>Sign Payload</h4>
|
||||||
|
<div className="form-group">
|
||||||
|
<textarea
|
||||||
|
value={signPayloads[key.id] || ""}
|
||||||
|
onChange={(e) => onSignPayloadChange(key.id, e.target.value)}
|
||||||
|
placeholder="Enter payload to sign..."
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => onSign(key.id)}
|
||||||
|
disabled={signing[key.id] || !signPayloads[key.id]?.trim()}
|
||||||
|
className="primary-button"
|
||||||
|
>
|
||||||
|
{signing[key.id] ? (
|
||||||
|
<>
|
||||||
|
<Icon name="loading" size="sm" />
|
||||||
|
Signing...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Icon name="edit" size="sm" />
|
||||||
|
Sign
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
{signatures[key.id] && (
|
||||||
|
<div className="signature-result">
|
||||||
|
<label>Signature (base64):</label>
|
||||||
|
<code>{signatures[key.id]}</code>
|
||||||
|
<button
|
||||||
|
onClick={() => onCopy(signatures[key.id])}
|
||||||
|
className="secondary-button"
|
||||||
|
>
|
||||||
|
<Icon name="copy" size="sm" />
|
||||||
|
Copy Signature
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="key-verification">
|
||||||
|
<h4>Verify Signature</h4>
|
||||||
|
<div className="form-group">
|
||||||
|
<textarea
|
||||||
|
value={verifyPayloads[key.id] || ""}
|
||||||
|
onChange={(e) => onVerifyPayloadChange(key.id, e.target.value)}
|
||||||
|
placeholder="Enter payload..."
|
||||||
|
rows={2}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<textarea
|
||||||
|
value={verifySignatures[key.id] || ""}
|
||||||
|
onChange={(e) => onVerifySignatureChange(key.id, e.target.value)}
|
||||||
|
placeholder="Enter base64 signature..."
|
||||||
|
rows={2}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => onVerify(key.id)}
|
||||||
|
disabled={
|
||||||
|
verifying[key.id] ||
|
||||||
|
!verifyPayloads[key.id]?.trim() ||
|
||||||
|
!verifySignatures[key.id]?.trim()
|
||||||
|
}
|
||||||
|
className="primary-button"
|
||||||
|
>
|
||||||
|
{verifying[key.id] ? (
|
||||||
|
<>
|
||||||
|
<Icon name="loading" size="sm" />
|
||||||
|
Verifying...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Icon name="success" size="sm" />
|
||||||
|
Verify
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
{verifyResults[key.id] !== undefined && verifyResults[key.id] !== null && (
|
||||||
|
<div className={`verify-result ${verifyResults[key.id] ? "valid" : "invalid"}`}>
|
||||||
|
{verifyResults[key.id] ? (
|
||||||
|
<>
|
||||||
|
<Icon name="success" size="sm" />
|
||||||
|
Signature is valid
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Icon name="error" size="sm" />
|
||||||
|
Signature is invalid
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { createSSHKey, deleteSSHKey, listSSHKeys, signPayload, verifySignature, type SSHKey } from "../api/ssh-keys";
|
||||||
|
import { useAsyncData } from "./use-async-data";
|
||||||
|
|
||||||
|
export const useSSHKeys = () => {
|
||||||
|
const { data: keys, status, reload: loadKeys } = useAsyncData<SSHKey[]>(listSSHKeys, []);
|
||||||
|
const [newKeyName, setNewKeyName] = useState("");
|
||||||
|
const [generating, setGenerating] = useState(false);
|
||||||
|
const [signPayloads, setSignPayloads] = useState<Record<string, string>>({});
|
||||||
|
const [signatures, setSignatures] = useState<Record<string, string>>({});
|
||||||
|
const [signing, setSigning] = useState<Record<string, boolean>>({});
|
||||||
|
const [verifyPayloads, setVerifyPayloads] = useState<Record<string, string>>({});
|
||||||
|
const [verifySignatures, setVerifySignatures] = useState<Record<string, string>>({});
|
||||||
|
const [verifyResults, setVerifyResults] = useState<Record<string, boolean | null>>({});
|
||||||
|
const [verifying, setVerifying] = useState<Record<string, boolean>>({});
|
||||||
|
const [mutationError, setMutationError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const safeKeys = keys ?? [];
|
||||||
|
|
||||||
|
async function handleGenerate(e: React.FormEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!newKeyName.trim()) return;
|
||||||
|
try {
|
||||||
|
setGenerating(true);
|
||||||
|
await createSSHKey({ name: newKeyName.trim() });
|
||||||
|
setNewKeyName("");
|
||||||
|
await loadKeys();
|
||||||
|
} catch {
|
||||||
|
setMutationError("Failed to generate SSH key");
|
||||||
|
} finally {
|
||||||
|
setGenerating(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDelete(keyId: string) {
|
||||||
|
if (!confirm("Are you sure you want to delete this SSH key?")) return;
|
||||||
|
try {
|
||||||
|
await deleteSSHKey(keyId);
|
||||||
|
await loadKeys();
|
||||||
|
} catch {
|
||||||
|
setMutationError("Failed to delete SSH key");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyToClipboard(text: string) {
|
||||||
|
navigator.clipboard.writeText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSign(keyId: string) {
|
||||||
|
const payload = signPayloads[keyId];
|
||||||
|
if (!payload?.trim()) return;
|
||||||
|
try {
|
||||||
|
setSigning((prev) => ({ ...prev, [keyId]: true }));
|
||||||
|
const result = await signPayload(keyId, { payload: payload.trim() });
|
||||||
|
setSignatures((prev) => ({ ...prev, [keyId]: result.signature }));
|
||||||
|
setMutationError(null);
|
||||||
|
} catch {
|
||||||
|
setMutationError("Failed to sign payload");
|
||||||
|
} finally {
|
||||||
|
setSigning((prev) => ({ ...prev, [keyId]: false }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleVerify(keyId: string) {
|
||||||
|
const payload = verifyPayloads[keyId];
|
||||||
|
const signature = verifySignatures[keyId];
|
||||||
|
if (!payload?.trim() || !signature?.trim()) return;
|
||||||
|
try {
|
||||||
|
setVerifying((prev) => ({ ...prev, [keyId]: true }));
|
||||||
|
const result = await verifySignature(keyId, {
|
||||||
|
payload: payload.trim(),
|
||||||
|
signature: signature.trim(),
|
||||||
|
});
|
||||||
|
setVerifyResults((prev) => ({ ...prev, [keyId]: result.valid }));
|
||||||
|
setMutationError(null);
|
||||||
|
} catch {
|
||||||
|
setMutationError("Failed to verify signature");
|
||||||
|
} finally {
|
||||||
|
setVerifying((prev) => ({ ...prev, [keyId]: false }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
keys: safeKeys,
|
||||||
|
status,
|
||||||
|
loadKeys,
|
||||||
|
newKeyName,
|
||||||
|
setNewKeyName,
|
||||||
|
generating,
|
||||||
|
mutationError,
|
||||||
|
setMutationError,
|
||||||
|
signPayloads,
|
||||||
|
setSignPayloads,
|
||||||
|
signatures,
|
||||||
|
signing,
|
||||||
|
verifyPayloads,
|
||||||
|
setVerifyPayloads,
|
||||||
|
verifySignatures,
|
||||||
|
setVerifySignatures,
|
||||||
|
verifyResults,
|
||||||
|
verifying,
|
||||||
|
handleGenerate,
|
||||||
|
handleDelete,
|
||||||
|
copyToClipboard,
|
||||||
|
handleSign,
|
||||||
|
handleVerify,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,92 +1,35 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { createSSHKey, deleteSSHKey, listSSHKeys, signPayload, verifySignature, type SSHKey } from "../api/ssh-keys";
|
import { LoadingState } from "../components/data-states";
|
||||||
import { EmptyState, ErrorState, LoadingState } from "../components/data-states";
|
import { useSSHKeys } from "../hooks/use-ssh-keys";
|
||||||
import { Icon } from "../components/icon";
|
import { SSHKeyCreateForm } from "../components/features/ssh-keys/SSHKeyCreateForm";
|
||||||
import { useAsyncData } from "../hooks/use-async-data";
|
import { SSHKeyList } from "../components/features/ssh-keys/SSHKeyList";
|
||||||
|
|
||||||
export const SSHKeysPage = () => {
|
export const SSHKeysPage = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { data: keys, status, reload: loadKeys } = useAsyncData<SSHKey[]>(listSSHKeys, []);
|
const {
|
||||||
const [newKeyName, setNewKeyName] = useState("");
|
keys,
|
||||||
const [generating, setGenerating] = useState(false);
|
status,
|
||||||
const [signPayloads, setSignPayloads] = useState<Record<string, string>>({});
|
loadKeys,
|
||||||
const [signatures, setSignatures] = useState<Record<string, string>>({});
|
newKeyName,
|
||||||
const [signing, setSigning] = useState<Record<string, boolean>>({});
|
setNewKeyName,
|
||||||
const [verifyPayloads, setVerifyPayloads] = useState<Record<string, string>>({});
|
generating,
|
||||||
const [verifySignatures, setVerifySignatures] = useState<Record<string, string>>({});
|
mutationError,
|
||||||
const [verifyResults, setVerifyResults] = useState<Record<string, boolean | null>>({});
|
signPayloads,
|
||||||
const [verifying, setVerifying] = useState<Record<string, boolean>>({});
|
signatures,
|
||||||
const [mutationError, setMutationError] = useState<string | null>(null);
|
signing,
|
||||||
|
verifyPayloads,
|
||||||
const safeKeys = keys ?? [];
|
verifySignatures,
|
||||||
|
verifyResults,
|
||||||
async function handleGenerate(e: React.FormEvent) {
|
verifying,
|
||||||
e.preventDefault();
|
handleGenerate,
|
||||||
if (!newKeyName.trim()) return;
|
handleDelete,
|
||||||
|
copyToClipboard,
|
||||||
try {
|
handleSign,
|
||||||
setGenerating(true);
|
handleVerify,
|
||||||
await createSSHKey({ name: newKeyName.trim() });
|
setSignPayloads,
|
||||||
setNewKeyName("");
|
setVerifyPayloads,
|
||||||
await loadKeys();
|
setVerifySignatures,
|
||||||
} catch {
|
} = useSSHKeys();
|
||||||
setMutationError("Failed to generate SSH key");
|
|
||||||
} finally {
|
|
||||||
setGenerating(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleDelete(keyId: string) {
|
|
||||||
if (!confirm("Are you sure you want to delete this SSH key?")) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await deleteSSHKey(keyId);
|
|
||||||
await loadKeys();
|
|
||||||
} catch {
|
|
||||||
setMutationError("Failed to delete SSH key");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function copyToClipboard(text: string) {
|
|
||||||
navigator.clipboard.writeText(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSign(keyId: string) {
|
|
||||||
const payload = signPayloads[keyId];
|
|
||||||
if (!payload?.trim()) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
setSigning((prev) => ({ ...prev, [keyId]: true }));
|
|
||||||
const result = await signPayload(keyId, { payload: payload.trim() });
|
|
||||||
setSignatures((prev) => ({ ...prev, [keyId]: result.signature }));
|
|
||||||
setMutationError(null);
|
|
||||||
} catch {
|
|
||||||
setMutationError("Failed to sign payload");
|
|
||||||
} finally {
|
|
||||||
setSigning((prev) => ({ ...prev, [keyId]: false }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleVerify(keyId: string) {
|
|
||||||
const payload = verifyPayloads[keyId];
|
|
||||||
const signature = verifySignatures[keyId];
|
|
||||||
if (!payload?.trim() || !signature?.trim()) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
setVerifying((prev) => ({ ...prev, [keyId]: true }));
|
|
||||||
const result = await verifySignature(keyId, {
|
|
||||||
payload: payload.trim(),
|
|
||||||
signature: signature.trim(),
|
|
||||||
});
|
|
||||||
setVerifyResults((prev) => ({ ...prev, [keyId]: result.valid }));
|
|
||||||
setMutationError(null);
|
|
||||||
} catch {
|
|
||||||
setMutationError("Failed to verify signature");
|
|
||||||
} finally {
|
|
||||||
setVerifying((prev) => ({ ...prev, [keyId]: false }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status === "loading") return <LoadingState message="Loading SSH keys..." />;
|
if (status === "loading") return <LoadingState message="Loading SSH keys..." />;
|
||||||
|
|
||||||
@@ -104,174 +47,38 @@ export const SSHKeysPage = () => {
|
|||||||
|
|
||||||
{mutationError && <div className="error">{mutationError}</div>}
|
{mutationError && <div className="error">{mutationError}</div>}
|
||||||
|
|
||||||
<form onSubmit={handleGenerate} className="stack">
|
<SSHKeyCreateForm
|
||||||
<div className="form-group">
|
newKeyName={newKeyName}
|
||||||
<label htmlFor="key-name">Key Name</label>
|
setNewKeyName={setNewKeyName}
|
||||||
<input
|
generating={generating}
|
||||||
id="key-name"
|
onSubmit={handleGenerate}
|
||||||
type="text"
|
/>
|
||||||
value={newKeyName}
|
|
||||||
onChange={(e) => setNewKeyName(e.target.value)}
|
|
||||||
placeholder="e.g., GitHub Work"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<button type="submit" className="primary-button" disabled={generating}>
|
|
||||||
{generating ? (
|
|
||||||
<>
|
|
||||||
<Icon name="loading" size="sm" />
|
|
||||||
Generating...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Icon name="add" size="sm" />
|
|
||||||
Generate SSH Key
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{status === "error" && <ErrorState message="Failed to load SSH keys" onRetry={loadKeys} />}
|
<SSHKeyList
|
||||||
|
keys={keys}
|
||||||
<div className="keys-list">
|
status={status}
|
||||||
{safeKeys.length === 0 ? (
|
signPayloads={signPayloads}
|
||||||
<EmptyState message="No SSH keys yet. Generate one above." />
|
signatures={signatures}
|
||||||
) : (
|
signing={signing}
|
||||||
safeKeys.map((key) => (
|
verifyPayloads={verifyPayloads}
|
||||||
<div key={key.id} className="key-card">
|
verifySignatures={verifySignatures}
|
||||||
<div className="key-header">
|
verifyResults={verifyResults}
|
||||||
<h3>{key.name}</h3>
|
verifying={verifying}
|
||||||
<button
|
onLoadKeys={loadKeys}
|
||||||
onClick={() => handleDelete(key.id)}
|
onDelete={handleDelete}
|
||||||
className="danger-button"
|
onCopy={copyToClipboard}
|
||||||
>
|
onSign={handleSign}
|
||||||
<Icon name="delete" size="sm" />
|
onVerify={handleVerify}
|
||||||
Delete
|
onSignPayloadChange={(id, value) =>
|
||||||
</button>
|
setSignPayloads((prev) => ({ ...prev, [id]: value }))
|
||||||
</div>
|
}
|
||||||
<div className="key-meta">
|
onVerifyPayloadChange={(id, value) =>
|
||||||
<span className="muted">
|
setVerifyPayloads((prev) => ({ ...prev, [id]: value }))
|
||||||
Created: {new Date(key.created_at).toLocaleDateString()}
|
}
|
||||||
</span>
|
onVerifySignatureChange={(id, value) =>
|
||||||
</div>
|
setVerifySignatures((prev) => ({ ...prev, [id]: value }))
|
||||||
<div className="key-public">
|
}
|
||||||
<code>{key.public_key.substring(0, 50)}...</code>
|
/>
|
||||||
<button
|
|
||||||
onClick={() => copyToClipboard(key.public_key)}
|
|
||||||
className="secondary-button"
|
|
||||||
>
|
|
||||||
<Icon name="copy" size="sm" />
|
|
||||||
Copy Full Key
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="key-signing">
|
|
||||||
<h4>Sign Payload</h4>
|
|
||||||
<div className="form-group">
|
|
||||||
<textarea
|
|
||||||
value={signPayloads[key.id] || ""}
|
|
||||||
onChange={(e) =>
|
|
||||||
setSignPayloads((prev) => ({ ...prev, [key.id]: e.target.value }))
|
|
||||||
}
|
|
||||||
placeholder="Enter payload to sign..."
|
|
||||||
rows={3}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={() => handleSign(key.id)}
|
|
||||||
disabled={signing[key.id] || !signPayloads[key.id]?.trim()}
|
|
||||||
className="primary-button"
|
|
||||||
>
|
|
||||||
{signing[key.id] ? (
|
|
||||||
<>
|
|
||||||
<Icon name="loading" size="sm" />
|
|
||||||
Signing...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Icon name="edit" size="sm" />
|
|
||||||
Sign
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
{signatures[key.id] && (
|
|
||||||
<div className="signature-result">
|
|
||||||
<label>Signature (base64):</label>
|
|
||||||
<code>{signatures[key.id]}</code>
|
|
||||||
<button
|
|
||||||
onClick={() => copyToClipboard(signatures[key.id])}
|
|
||||||
className="secondary-button"
|
|
||||||
>
|
|
||||||
<Icon name="copy" size="sm" />
|
|
||||||
Copy Signature
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="key-verification">
|
|
||||||
<h4>Verify Signature</h4>
|
|
||||||
<div className="form-group">
|
|
||||||
<textarea
|
|
||||||
value={verifyPayloads[key.id] || ""}
|
|
||||||
onChange={(e) =>
|
|
||||||
setVerifyPayloads((prev) => ({ ...prev, [key.id]: e.target.value }))
|
|
||||||
}
|
|
||||||
placeholder="Enter payload..."
|
|
||||||
rows={2}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="form-group">
|
|
||||||
<textarea
|
|
||||||
value={verifySignatures[key.id] || ""}
|
|
||||||
onChange={(e) =>
|
|
||||||
setVerifySignatures((prev) => ({ ...prev, [key.id]: e.target.value }))
|
|
||||||
}
|
|
||||||
placeholder="Enter base64 signature..."
|
|
||||||
rows={2}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={() => handleVerify(key.id)}
|
|
||||||
disabled={
|
|
||||||
verifying[key.id] ||
|
|
||||||
!verifyPayloads[key.id]?.trim() ||
|
|
||||||
!verifySignatures[key.id]?.trim()
|
|
||||||
}
|
|
||||||
className="primary-button"
|
|
||||||
>
|
|
||||||
{verifying[key.id] ? (
|
|
||||||
<>
|
|
||||||
<Icon name="loading" size="sm" />
|
|
||||||
Verifying...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Icon name="success" size="sm" />
|
|
||||||
Verify
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
{verifyResults[key.id] !== undefined && verifyResults[key.id] !== null && (
|
|
||||||
<div className={`verify-result ${verifyResults[key.id] ? "valid" : "invalid"}`}>
|
|
||||||
{verifyResults[key.id] ? (
|
|
||||||
<>
|
|
||||||
<Icon name="success" size="sm" />
|
|
||||||
Signature is valid
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Icon name="error" size="sm" />
|
|
||||||
Signature is invalid
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user