diff --git a/src/backend/database/routes/credentials.ts b/src/backend/database/routes/credentials.ts index c8f3518d..91a2549f 100644 --- a/src/backend/database/routes/credentials.ts +++ b/src/backend/database/routes/credentials.ts @@ -828,4 +828,56 @@ router.post("/validate-key-pair", authenticateJWT, async (req: Request, res: Res } }); +// Generate public key from private key endpoint +// POST /credentials/generate-public-key +router.post("/generate-public-key", authenticateJWT, async (req: Request, res: Response) => { + const { privateKey, keyPassword } = req.body; + + console.log("=== Generate Public Key API Called ==="); + console.log("Request body keys:", Object.keys(req.body)); + console.log("Private key provided:", !!privateKey); + console.log("Private key type:", typeof privateKey); + + if (!privateKey || typeof privateKey !== "string") { + console.log("Invalid private key provided"); + return res.status(400).json({ error: "Private key is required" }); + } + + try { + console.log("Calling parseSSHKey to generate public key..."); + const keyInfo = parseSSHKey(privateKey, keyPassword); + console.log("parseSSHKey result:", keyInfo); + + if (!keyInfo.success) { + return res.status(400).json({ + success: false, + error: keyInfo.error || "Failed to parse private key" + }); + } + + if (!keyInfo.publicKey || !keyInfo.publicKey.trim()) { + return res.status(400).json({ + success: false, + error: "Unable to generate public key from the provided private key" + }); + } + + const response = { + success: true, + publicKey: keyInfo.publicKey, + keyType: keyInfo.keyType + }; + + console.log("Sending response:", response); + res.json(response); + } catch (error) { + console.error("Exception in generate-public-key endpoint:", error); + authLogger.error("Failed to generate public key", error); + res.status(500).json({ + success: false, + error: error instanceof Error ? error.message : "Failed to generate public key" + }); + } +}); + export default router; diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index f40340c4..04876431 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -130,7 +130,20 @@ "folderRenamed": "Folder \"{{oldName}}\" renamed to \"{{newName}}\" successfully", "failedToRenameFolder": "Failed to rename folder", "movedToFolder": "Credential \"{{name}}\" moved to \"{{folder}}\" successfully", - "failedToMoveToFolder": "Failed to move credential to folder" + "failedToMoveToFolder": "Failed to move credential to folder", + "sshPublicKey": "SSH Public Key", + "publicKeyNote": "Public key is optional but recommended for key validation", + "publicKeyUploaded": "Public Key Uploaded", + "uploadPublicKey": "Upload Public Key", + "uploadPrivateKeyFile": "Upload Private Key File", + "uploadPublicKeyFile": "Upload Public Key File", + "privateKeyRequiredForGeneration": "Private key is required to generate public key", + "failedToGeneratePublicKey": "Failed to generate public key", + "generatePublicKey": "Generate from Private Key", + "publicKeyGeneratedSuccessfully": "Public key generated successfully", + "detectedKeyType": "Detected key type", + "detectingKeyType": "detecting...", + "optional": "Optional" }, "sshTools": { "title": "SSH Tools", @@ -878,6 +891,7 @@ "password": "password", "keyPassword": "key password", "pastePrivateKey": "Paste your private key here...", + "pastePublicKey": "Paste your public key here...", "credentialName": "My SSH Server", "description": "SSH credential description", "searchCredentials": "Search credentials by name, username, or tags...", diff --git a/src/locales/zh/translation.json b/src/locales/zh/translation.json index 370d906f..0d0fa27a 100644 --- a/src/locales/zh/translation.json +++ b/src/locales/zh/translation.json @@ -129,7 +129,20 @@ "folderRenamed": "文件夹\"{{oldName}}\"已成功重命名为\"{{newName}}\"", "failedToRenameFolder": "重命名文件夹失败", "movedToFolder": "凭据\"{{name}}\"已成功移动到\"{{folder}}\"", - "failedToMoveToFolder": "移动凭据到文件夹失败" + "failedToMoveToFolder": "移动凭据到文件夹失败", + "sshPublicKey": "SSH公钥", + "publicKeyNote": "公钥是可选的,但建议提供以验证密钥对", + "publicKeyUploaded": "公钥已上传", + "uploadPublicKey": "上传公钥", + "uploadPrivateKeyFile": "上传私钥文件", + "uploadPublicKeyFile": "上传公钥文件", + "privateKeyRequiredForGeneration": "生成公钥需要先输入私钥", + "failedToGeneratePublicKey": "生成公钥失败", + "generatePublicKey": "从私钥生成", + "publicKeyGeneratedSuccessfully": "公钥生成成功", + "detectedKeyType": "检测到的密钥类型", + "detectingKeyType": "检测中...", + "optional": "可选" }, "sshTools": { "title": "SSH 工具", @@ -874,6 +887,7 @@ "searchCredentials": "按名称、用户名或标签搜索凭据...", "keyPassword": "密钥密码", "pastePrivateKey": "在此粘贴您的私钥...", + "pastePublicKey": "在此粘贴您的公钥...", "sshConfig": "端点 SSH 配置", "homePath": "/home", "clientId": "您的客户端 ID", diff --git a/src/ui/Desktop/Apps/Credentials/CredentialEditor.tsx b/src/ui/Desktop/Apps/Credentials/CredentialEditor.tsx index 30e45de1..fefa6ec7 100644 --- a/src/ui/Desktop/Apps/Credentials/CredentialEditor.tsx +++ b/src/ui/Desktop/Apps/Credentials/CredentialEditor.tsx @@ -24,7 +24,7 @@ import { getCredentialDetails, detectKeyType, detectPublicKeyType, - validateKeyPair, + generatePublicKeyFromPrivate, } from "@/ui/main-axios"; import { useTranslation } from "react-i18next"; import type { @@ -45,9 +45,6 @@ export function CredentialEditor({ useState(null); const [authTab, setAuthTab] = useState<"password" | "key">("password"); - const [keyInputMethod, setKeyInputMethod] = useState<"upload" | "paste">( - "upload", - ); const [detectedKeyType, setDetectedKeyType] = useState(null); const [keyDetectionLoading, setKeyDetectionLoading] = useState(false); const keyDetectionTimeoutRef = useRef(null); @@ -56,12 +53,6 @@ export function CredentialEditor({ const [publicKeyDetectionLoading, setPublicKeyDetectionLoading] = useState(false); const publicKeyDetectionTimeoutRef = useRef(null); - const [keyPairValidation, setKeyPairValidation] = useState<{ - isValid: boolean | null; - loading: boolean; - error?: string; - }>({ isValid: null, loading: false }); - const keyPairValidationTimeoutRef = useRef(null); useEffect(() => { const fetchData = async () => { @@ -234,9 +225,6 @@ export function CredentialEditor({ if (publicKeyDetectionTimeoutRef.current) { clearTimeout(publicKeyDetectionTimeoutRef.current); } - if (keyPairValidationTimeoutRef.current) { - clearTimeout(keyPairValidationTimeoutRef.current); - } }; }, []); @@ -308,39 +296,6 @@ export function CredentialEditor({ }, 1000); }; - // Validate key pair function - const handleKeyPairValidation = async (privateKeyValue: string, publicKeyValue: string, keyPassword?: string) => { - if (!privateKeyValue || privateKeyValue.trim() === '' || !publicKeyValue || publicKeyValue.trim() === '') { - setKeyPairValidation({ isValid: null, loading: false }); - return; - } - - setKeyPairValidation({ isValid: null, loading: true }); - try { - const result = await validateKeyPair(privateKeyValue, publicKeyValue, keyPassword); - setKeyPairValidation({ - isValid: result.isValid, - loading: false, - error: result.error - }); - } catch (error) { - setKeyPairValidation({ - isValid: false, - loading: false, - error: error instanceof Error ? error.message : 'Unknown error during validation' - }); - } - }; - - // Debounced key pair validation - const debouncedKeyPairValidation = (privateKeyValue: string, publicKeyValue: string, keyPassword?: string) => { - if (keyPairValidationTimeoutRef.current) { - clearTimeout(keyPairValidationTimeoutRef.current); - } - keyPairValidationTimeoutRef.current = setTimeout(() => { - handleKeyPairValidation(privateKeyValue, publicKeyValue, keyPassword); - }, 1500); // Slightly longer delay since this is more expensive - }; const getFriendlyKeyTypeName = (keyType: string): string => { const keyTypeMap: Record = { @@ -384,12 +339,7 @@ export function CredentialEditor({ if (data.authType === "password") { submitData.password = data.password; } else if (data.authType === "key") { - if (data.key instanceof File) { - const keyContent = await data.key.text(); - submitData.key = keyContent; - } else { - submitData.key = data.key; - } + submitData.key = data.key; submitData.publicKey = data.publicKey; submitData.keyPassword = data.keyPassword; submitData.keyType = data.keyType; @@ -466,38 +416,6 @@ export function CredentialEditor({ }; }, [folderDropdownOpen]); - const keyTypeOptions = [ - { value: "auto", label: t("hosts.autoDetect") }, - { value: "ssh-rsa", label: t("hosts.rsa") }, - { value: "ssh-ed25519", label: t("hosts.ed25519") }, - { value: "ecdsa-sha2-nistp256", label: t("hosts.ecdsaNistP256") }, - { value: "ecdsa-sha2-nistp384", label: t("hosts.ecdsaNistP384") }, - { value: "ecdsa-sha2-nistp521", label: t("hosts.ecdsaNistP521") }, - { value: "ssh-dss", label: t("hosts.dsa") }, - { value: "ssh-rsa-sha2-256", label: t("hosts.rsaSha2256") }, - { value: "ssh-rsa-sha2-512", label: t("hosts.rsaSha2512") }, - ]; - - const [keyTypeDropdownOpen, setKeyTypeDropdownOpen] = useState(false); - const keyTypeButtonRef = useRef(null); - const keyTypeDropdownRef = useRef(null); - - useEffect(() => { - function onClickOutside(event: MouseEvent) { - if ( - keyTypeDropdownOpen && - keyTypeDropdownRef.current && - !keyTypeDropdownRef.current.contains(event.target as Node) && - keyTypeButtonRef.current && - !keyTypeButtonRef.current.contains(event.target as Node) - ) { - setKeyTypeDropdownOpen(false); - } - } - - document.addEventListener("mousedown", onClickOutside); - return () => document.removeEventListener("mousedown", onClickOutside); - }, [keyTypeDropdownOpen]); return (
- { - setKeyInputMethod(value as "upload" | "paste"); - // Only reset key value if we're not editing an existing credential - if (!editingCredential) { - if (value === "upload") { - form.setValue("key", null); - form.setValue("publicKey", ""); - } else if (value === "paste") { - form.setValue("key", ""); - form.setValue("publicKey", ""); - } - } else { - // For existing credentials, preserve the key data when switching methods - const currentKey = fullCredentialDetails?.key || ""; - const currentPublicKey = fullCredentialDetails?.publicKey || ""; - if (value === "paste") { - form.setValue("key", currentKey); - form.setValue("publicKey", currentPublicKey); - } else { - // For upload mode, keep the current string value to show "existing key" status - form.setValue("key", currentKey); - form.setValue("publicKey", currentPublicKey); - } - } - }} - className="w-full" - > - - - {t("hosts.uploadFile")} - - - {t("hosts.pasteKey")} - - - +
{t("credentials.sshPrivateKey")} - +
{ - const file = e.target.files?.[0]; - field.onChange(file || null); - - // Detect key type from uploaded file - if (file) { - try { - const fileContent = await file.text(); - debouncedKeyDetection(fileContent, form.watch("keyPassword")); - // Trigger key pair validation if public key is available - const publicKeyValue = form.watch("publicKey"); - if (publicKeyValue && publicKeyValue.trim()) { - debouncedKeyPairValidation(fileContent, publicKeyValue, form.watch("keyPassword")); - } - } catch (error) { - console.error('Failed to read uploaded file:', error); - } - } else { - setDetectedKeyType(null); - } - }} - className="absolute inset-0 w-full h-full opacity-0 cursor-pointer" - /> - -
- - {/* Key type detection display for uploaded files */} - {detectedKeyType && field.value instanceof File && ( -
- Detected key type: - - {getFriendlyKeyTypeName(detectedKeyType)} - - {keyDetectionLoading && ( - (detecting...) - )} -
- )} - - )} - /> - ( - - - {t("credentials.sshPublicKey")} ({t("credentials.optional")}) - - -
- { const file = e.target.files?.[0]; if (file) { try { const fileContent = await file.text(); field.onChange(fileContent); - // Detect public key type from uploaded file - debouncedPublicKeyDetection(fileContent); - // Trigger key pair validation if private key is available - const privateKeyValue = form.watch("key"); - if (privateKeyValue && typeof privateKeyValue === "string" && privateKeyValue.trim()) { - debouncedKeyPairValidation(privateKeyValue, fileContent, form.watch("keyPassword")); - } + debouncedKeyDetection(fileContent, form.watch("keyPassword")); } catch (error) { - console.error('Failed to read uploaded public key file:', error); + console.error('Failed to read uploaded file:', error); } - } else { - field.onChange(""); - setDetectedPublicKeyType(null); } }} className="absolute inset-0 w-full h-full opacity-0 cursor-pointer" @@ -911,143 +704,11 @@ export function CredentialEditor({ className="w-full justify-start text-left" > - {field.value - ? t("credentials.publicKeyUploaded") - : t("credentials.uploadPublicKey")} + {t("credentials.uploadPrivateKeyFile")}
-
-
- {t("credentials.publicKeyNote")}
- {/* Public key type detection display for upload mode */} - {detectedPublicKeyType && field.value && ( -
- Detected key type: - - {getFriendlyKeyTypeName(detectedPublicKeyType)} - - {publicKeyDetectionLoading && ( - (detecting...) - )} -
- )} -
- )} - /> -
- {/* Show existing key content preview for upload mode */} - {editingCredential && fullCredentialDetails?.key && typeof form.watch("key") === "string" && ( - - {t("credentials.sshPrivateKey")} ({t("hosts.existingKey")}) - -