import { useRef, useState } from "react"; import { SecretReferenceHint, SecretSourceManager, } from "./SecretSourceManager"; import { useTranslation } from "react-i18next"; import { copyToClipboard } from "@/lib/clipboard"; import { Copy, Info, Lock, Upload, X } from "lucide-react"; import { toast } from "sonner"; import { Button } from "@/components/button"; import { Input } from "@/components/input"; import { PasswordInput } from "@/components/password-input"; import { SectionCard } from "@/components/section-card"; import { createCredential, generateKeyPair, generatePublicKeyFromPrivate, updateCredential, duplicateCredential, adminCreateUserCredential, adminUpdateUserCredential, } from "@/main-axios"; import type { Credential, Host } from "@/types/ui-types"; import { FolderPathPicker } from "./FolderPathPicker"; export function CredentialEditorView({ credential, activeTab, onBack, onSave, adminTargetUserId, existingFolders = [], saveAsNewHost, }: { credential: Credential | null; activeTab: string; onBack: () => void; onSave: ( saved: Record, options?: { assignToHost?: boolean }, ) => void; adminTargetUserId?: string; existingFolders?: string[]; // When set, this credential is being edited from within a host's editor; // shows a "Save as New" action that clones it and reassigns the host. saveAsNewHost?: Host | "new"; }) { const [showSecretSources, setShowSecretSources] = useState(false); const [credForm, setCredForm] = useState(() => ({ name: credential?.name ?? "", username: credential?.username ?? "", folder: credential?.folder ?? "", description: credential?.description ?? "", tags: credential?.tags ?? ([] as string[]), tagInput: "", type: credential?.type ?? "password", value: credential?.type === "key" ? (credential?.value ?? "") : "", password: credential?.type === "password" ? (credential?.value ?? "") : (credential?.password ?? ""), publicKey: credential?.publicKey ?? "", passphrase: credential?.passphrase ?? "", certPublicKey: credential?.certPublicKey ?? "", })); const { t } = useTranslation(); const [generatingKey, setGeneratingKey] = useState(false); const [generatingPublicKey, setGeneratingPublicKey] = useState(false); const credFileInputRef = useRef(null); const certFileInputRef = useRef(null); const setCredField = ( k: K, v: (typeof credForm)[K], ) => setCredForm((p) => ({ ...p, [k]: v })); const [saving, setSaving] = useState(false); const [savingAsNew, setSavingAsNew] = useState(false); const buildCredentialData = () => { const hasKey = credForm.value === "existing_key" || credForm.value.trim() !== ""; return { hasKey, data: { name: credForm.name, username: credForm.username, folder: credForm.folder || null, description: credForm.description || null, tags: credForm.tags, authType: hasKey ? "key" : "password", password: credForm.password || null, key: hasKey ? credForm.value === "existing_key" ? undefined : credForm.value || null : null, publicKey: hasKey ? credForm.publicKey : null, certPublicKey: hasKey ? credForm.certPublicKey || null : null, keyPassword: hasKey ? credForm.passphrase === "existing_key_password" ? undefined : credForm.passphrase || null : null, }, }; }; const validateForm = () => { if (!credForm.name.trim()) { toast.error(t("hosts.credentialNameRequired")); return false; } const hasKey = credForm.value === "existing_key" || credForm.value.trim() !== ""; if (!hasKey && !credForm.password) { toast.error(t("hosts.credentialAuthRequired")); return false; } return true; }; const handleSave = async () => { if (!validateForm()) return; setSaving(true); try { const { data } = buildCredentialData(); let saved: Record; if (adminTargetUserId) { saved = credential ? await adminUpdateUserCredential( adminTargetUserId, Number(credential.id), data, ) : await adminCreateUserCredential(adminTargetUserId, data); } else { saved = credential ? await updateCredential(Number(credential.id), data) : await createCredential(data); } toast.success( credential ? t("hosts.credentialUpdated") : t("hosts.credentialCreated"), ); if (!adminTargetUserId) { window.dispatchEvent(new CustomEvent("termix:credentials-changed")); } onSave(saved); } catch (err) { const msg = err instanceof Error ? err.message : null; toast.error(msg || t("hosts.failedToSaveCredential")); } finally { setSaving(false); } }; const handleSaveAsNew = async () => { if (!credential || adminTargetUserId) return; if (!validateForm()) return; setSavingAsNew(true); try { const { data } = buildCredentialData(); const saved = await duplicateCredential(Number(credential.id), data); toast.success(t("hosts.credentialSavedAsNew")); window.dispatchEvent(new CustomEvent("termix:credentials-changed")); onSave(saved, { assignToHost: true }); } catch (err) { const msg = err instanceof Error ? err.message : null; toast.error(msg || t("hosts.failedToSaveCredentialAsNew")); } finally { setSavingAsNew(false); } }; return (
{activeTab === "general" && ( } >
setCredField("name", e.target.value)} />
setCredField("folder", path)} folderPaths={existingFolders} />
setCredField("description", e.target.value)} />
{credForm.tags.map((tag) => ( {tag} ))} setCredField("tagInput", e.target.value)} onKeyDown={(e) => { if ( (e.key === " " || e.key === "Enter") && credForm.tagInput.trim() ) { e.preventDefault(); const tag = credForm.tagInput.trim(); if (!credForm.tags.includes(tag)) setCredField("tags", [...credForm.tags, tag]); setCredField("tagInput", ""); } else if ( e.key === "Backspace" && !credForm.tagInput && credForm.tags.length > 0 ) { setCredField("tags", credForm.tags.slice(0, -1)); } }} />
)} {activeTab === "auth" && ( } >
setCredField("username", e.target.value)} />
setCredField("password", e.target.value)} /> setShowSecretSources((v) => !v)} />
{showSecretSources && ( setShowSecretSources(false)} /> )}

{t("hosts.generateKeyPairTitle")}

{t("hosts.generateKeyPairDescription")}

{[ { label: "Ed25519", type: "ssh-ed25519" }, { label: "ECDSA (nistp256)", type: "ecdsa-sha2-nistp256", }, { label: "RSA (2048)", type: "ssh-rsa", bits: 2048 }, ].map(({ label, type: keyType, bits }) => ( ))}
{ const file = e.target.files?.[0]; if (!file) return; const text = await file.text(); setCredField("value", text.trim()); e.target.value = ""; }} /> {credForm.value === "existing_key" && (
{t("hosts.keySaved")} — {t("hosts.keyReplaceNotice")}
)}