96ce3f4c53
- 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
181 lines
5.5 KiB
TypeScript
181 lines
5.5 KiB
TypeScript
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>
|
|
);
|
|
};
|