Skip to content

Commit dbc259b

Browse files
committed
refactor(ui-react): use KeyFileInput in secure-vault KeyDrawer
Replace the inline file/text input block with the shared KeyFileInput component, removing duplicated file-input logic.
1 parent 6360bd8 commit dbc259b

File tree

1 file changed

+23
-159
lines changed

1 file changed

+23
-159
lines changed

ui-react/apps/admin/src/pages/secure-vault/KeyDrawer.tsx

Lines changed: 23 additions & 159 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
1-
import { useState, useEffect, useRef, useCallback, FormEvent, DragEvent } from "react";
2-
import {
3-
ExclamationCircleIcon,
4-
ArrowUpTrayIcon,
5-
CheckCircleIcon,
6-
} from "@heroicons/react/24/outline";
1+
import { useState, useEffect, FormEvent } from "react";
2+
import { ExclamationCircleIcon } from "@heroicons/react/24/outline";
73
import { useVaultStore, DuplicateKeyError } from "@/stores/vaultStore";
84
import { validatePrivateKey, getFingerprint } from "@/utils/ssh-keys";
95
import Drawer from "@/components/common/Drawer";
10-
import { LABEL, INPUT, INPUT_MONO } from "@/utils/styles";
6+
import KeyFileInput from "@/components/common/KeyFileInput";
7+
import { LABEL, INPUT } from "@/utils/styles";
118
import type { VaultKeyEntry } from "@/types/vault";
129

1310
interface Props {
@@ -29,29 +26,23 @@ export default function KeyDrawer({ open, editKey, onClose }: Props) {
2926
const [passphraseError, setPassphraseError] = useState<string | null>(null);
3027
const [submitting, setSubmitting] = useState(false);
3128
const [error, setError] = useState<string | null>(null);
32-
const [inputMode, setInputMode] = useState<"file" | "text">("file");
33-
const [dragging, setDragging] = useState(false);
34-
const fileInputRef = useRef<HTMLInputElement>(null);
3529

3630
useEffect(() => {
3731
if (!open) return;
3832
if (editKey) {
3933
setName(editKey.name);
4034
setKeyData(editKey.data);
4135
setEncrypted(editKey.hasPassphrase);
42-
setInputMode("text");
4336
} else {
4437
setName("");
4538
setKeyData("");
4639
setEncrypted(false);
47-
setInputMode("file");
4840
}
4941
setNameError(null);
5042
setPassphrase("");
5143
setKeyError(null);
5244
setPassphraseError(null);
5345
setError(null);
54-
setDragging(false);
5546
}, [open, editKey]);
5647

5748
const handleNameChange = (value: string) => {
@@ -81,46 +72,11 @@ export default function KeyDrawer({ open, editKey, onClose }: Props) {
8172
setEncrypted(result.encrypted);
8273
};
8374

84-
const processFile = (file: File) => {
85-
if (file.size > 512 * 1024) return;
86-
const reader = new FileReader();
87-
reader.onload = () => {
88-
const text = reader.result as string;
89-
handleKeyChange(text);
90-
if (!name) {
91-
const base = file.name.replace(/\.[^.]+$/, "");
92-
setName(base);
93-
}
94-
};
95-
reader.readAsText(file);
96-
};
97-
98-
const handleDrop = (e: DragEvent) => {
99-
e.preventDefault();
100-
setDragging(false);
101-
const file = e.dataTransfer?.files?.[0];
102-
if (file) processFile(file);
75+
const handleFileName = (fileName: string) => {
76+
if (!name) setName(fileName);
10377
};
10478

105-
const handleKeyChangeRef = useRef(handleKeyChange);
106-
handleKeyChangeRef.current = handleKeyChange;
107-
108-
const handlePaste = useCallback((e: Event) => {
109-
const ce = e as ClipboardEvent;
110-
const text = ce.clipboardData?.getData("text");
111-
if (!text) return;
112-
const result = validatePrivateKey(text.trim());
113-
if (result.valid) {
114-
e.preventDefault();
115-
handleKeyChangeRef.current(text);
116-
}
117-
}, []);
118-
119-
useEffect(() => {
120-
if (!open || isEdit) return;
121-
document.addEventListener("paste", handlePaste);
122-
return () => document.removeEventListener("paste", handlePaste);
123-
}, [open, isEdit, handlePaste]);
79+
const validateForPaste = (text: string) => validatePrivateKey(text.trim()).valid;
12480

12581
const handlePassphraseChange = (value: string) => {
12682
setPassphrase(value);
@@ -259,114 +215,22 @@ export default function KeyDrawer({ open, editKey, onClose }: Props) {
259215
)}
260216
</div>
261217

262-
{/* Private key data — file or text input */}
263-
<div>
264-
<div className="flex items-center justify-between mb-1.5">
265-
<label htmlFor="key-data" className={LABEL + " !mb-0"}>
266-
Private Key
267-
</label>
268-
{!isEdit && (
269-
<div className="flex gap-1">
270-
<button
271-
type="button"
272-
onClick={() => setInputMode("file")}
273-
className={`px-2 py-0.5 rounded text-2xs font-medium transition-all ${inputMode === "file" ? "bg-primary/10 text-primary" : "text-text-muted hover:text-text-secondary"}`}
274-
>
275-
File
276-
</button>
277-
<button
278-
type="button"
279-
onClick={() => setInputMode("text")}
280-
className={`px-2 py-0.5 rounded text-2xs font-medium transition-all ${inputMode === "text" ? "bg-primary/10 text-primary" : "text-text-muted hover:text-text-secondary"}`}
281-
>
282-
Text
283-
</button>
284-
</div>
285-
)}
286-
</div>
287-
288-
{inputMode === "file" && !isEdit ? (
289-
<div
290-
onDragOver={(e) => {
291-
e.preventDefault();
292-
setDragging(true);
293-
}}
294-
onDragLeave={() => setDragging(false)}
295-
onDrop={handleDrop}
296-
onClick={() => fileInputRef.current?.click()}
297-
className={`flex flex-col items-center justify-center gap-2 px-4 py-6 border-2 border-dashed rounded-lg cursor-pointer transition-all ${
298-
dragging
299-
? "border-primary bg-primary/5"
300-
: keyData
301-
? "border-accent-green/30 bg-accent-green/5"
302-
: `border-border hover:border-primary/30 ${keyError ? "border-accent-red/30" : ""}`
303-
}`}
304-
>
305-
<input
306-
ref={fileInputRef}
307-
type="file"
308-
accept=".pem,.key,.txt"
309-
className="hidden"
310-
onChange={(e) => {
311-
const file = e.target.files?.[0];
312-
if (file) processFile(file);
313-
e.target.value = "";
314-
}}
315-
/>
316-
{keyData ? (
317-
<>
318-
<CheckCircleIcon className="w-5 h-5 text-accent-green" />
319-
<span className="text-xs text-accent-green font-medium">
320-
Key loaded {encrypted && "(encrypted)"}
321-
</span>
322-
<button
323-
type="button"
324-
onClick={(e) => {
325-
e.stopPropagation();
326-
handleKeyChange("");
327-
}}
328-
className="text-2xs text-text-muted hover:text-text-primary transition-colors"
329-
>
330-
Clear
331-
</button>
332-
</>
333-
) : (
334-
<>
335-
<ArrowUpTrayIcon className="w-5 h-5 text-text-muted" />
336-
<span className="text-xs text-text-secondary">
337-
Drop private key file, paste, or browse
338-
</span>
339-
</>
340-
)}
341-
</div>
342-
) : (
343-
<textarea
344-
id="key-data"
345-
value={keyData}
346-
onChange={(e) => handleKeyChange(e.target.value)}
347-
placeholder={"-----BEGIN OPENSSH PRIVATE KEY-----\n..."}
348-
rows={8}
349-
aria-invalid={!!keyError}
350-
aria-describedby={keyError ? "key-data-error" : undefined}
351-
className={`${INPUT_MONO} resize-none`}
352-
/>
353-
)}
354-
355-
{!isEdit && (
356-
<p className="mt-1 text-2xs text-text-muted">
357-
RSA, DSA, ECDSA, ED25519 — PEM and OpenSSH formats.
358-
</p>
359-
)}
360-
{keyError && (
361-
<p
362-
id="key-data-error"
363-
className="text-2xs text-accent-red mt-1.5 flex items-center gap-1"
364-
>
365-
<ExclamationCircleIcon className="w-3.5 h-3.5 shrink-0" />
366-
{keyError}
367-
</p>
368-
)}
369-
</div>
218+
<KeyFileInput
219+
label="Private Key"
220+
id="key-data"
221+
value={keyData}
222+
onChange={handleKeyChange}
223+
validate={validateForPaste}
224+
onFileName={handleFileName}
225+
disabled={isEdit}
226+
error={keyError}
227+
accept=".pem,.key,.txt"
228+
placeholder={"-----BEGIN OPENSSH PRIVATE KEY-----\n..."}
229+
rows={8}
230+
hint="RSA, DSA, ECDSA, ED25519 — PEM and OpenSSH formats."
231+
loadedLabel={`Key loaded${encrypted ? " (encrypted)" : ""}`}
232+
emptyLabel="Drop private key file, paste, or browse"
233+
/>
370234

371235
{encrypted && (
372236
<div>

0 commit comments

Comments
 (0)