From 522fe3e571b5a818d016119cb8ba0aa7beea30a0 Mon Sep 17 00:00:00 2001 From: ZacharyZcR Date: Sat, 4 Oct 2025 04:17:07 +0800 Subject: [PATCH] Add support for passwordless host authentication Allow adding SSH hosts without authentication credentials by introducing a new "none" auth type. This enables users to configure hosts for later use with SSH agent or manual credential addition. Changes: - Add authType "none" to type definitions - Update frontend form to support "None" authentication option - Skip credential validation for "none" auth type - Update backend to accept hosts without credentials - Add i18n support for English and Chinese Fixes #278 --- src/backend/database/routes/ssh.ts | 22 ++++++++++- src/locales/en/translation.json | 2 + src/locales/zh/translation.json | 2 + src/types/index.ts | 4 +- .../Apps/Host Manager/HostManagerEditor.tsx | 39 +++++++++++++++---- 5 files changed, 57 insertions(+), 12 deletions(-) diff --git a/src/backend/database/routes/ssh.ts b/src/backend/database/routes/ssh.ts index 3cb76e67..fcec0541 100644 --- a/src/backend/database/routes/ssh.ts +++ b/src/backend/database/routes/ssh.ts @@ -281,7 +281,14 @@ router.post( sshDataObj.keyPassword = keyPassword || null; sshDataObj.keyType = keyType; sshDataObj.password = null; + } else if (effectiveAuthType === "none") { + // No authentication credentials - set all to null + sshDataObj.password = null; + sshDataObj.key = null; + sshDataObj.keyPassword = null; + sshDataObj.keyType = null; } else { + // credential type or fallback - set all to null except credentialId sshDataObj.password = null; sshDataObj.key = null; sshDataObj.keyPassword = null; @@ -471,7 +478,14 @@ router.put( sshDataObj.keyType = keyType; } sshDataObj.password = null; + } else if (effectiveAuthType === "none") { + // No authentication credentials - set all to null + sshDataObj.password = null; + sshDataObj.key = null; + sshDataObj.keyPassword = null; + sshDataObj.keyType = null; } else { + // credential type or fallback - set all to null except credentialId sshDataObj.password = null; sshDataObj.key = null; sshDataObj.keyPassword = null; @@ -1356,10 +1370,12 @@ router.post( continue; } - if (!["password", "key", "credential"].includes(hostData.authType)) { + if ( + !["password", "key", "credential", "none"].includes(hostData.authType) + ) { results.failed++; results.errors.push( - `Host ${i + 1}: Invalid authType. Must be 'password', 'key', or 'credential'`, + `Host ${i + 1}: Invalid authType. Must be 'password', 'key', 'credential', or 'none'`, ); continue; } @@ -1391,6 +1407,8 @@ router.post( continue; } + // "none" authType requires no validation - no credentials needed + const sshDataObj: any = { userId: userId, name: hostData.name || `${hostData.username}@${hostData.ip}`, diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 0e6d735e..7b2b4063 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -639,6 +639,8 @@ "password": "Password", "key": "Key", "credential": "Credential", + "none": "None", + "noneDescription": "No authentication credentials required. You can add credentials later or use SSH agent for authentication.", "selectCredential": "Select Credential", "selectCredentialPlaceholder": "Choose a credential...", "credentialRequired": "Credential is required when using credential authentication", diff --git a/src/locales/zh/translation.json b/src/locales/zh/translation.json index 62069e11..d573da45 100644 --- a/src/locales/zh/translation.json +++ b/src/locales/zh/translation.json @@ -635,6 +635,8 @@ "password": "密码", "key": "密钥", "credential": "凭证", + "none": "无", + "noneDescription": "无需认证凭证。您可以稍后添加凭证或使用 SSH 代理进行认证。", "selectCredential": "选择凭证", "selectCredentialPlaceholder": "选择一个凭证...", "credentialRequired": "使用凭证认证时需要选择凭证", diff --git a/src/types/index.ts b/src/types/index.ts index ee7cedb2..05a3405b 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -18,7 +18,7 @@ export interface SSHHost { folder: string; tags: string[]; pin: boolean; - authType: "password" | "key" | "credential"; + authType: "password" | "key" | "credential" | "none"; password?: string; key?: string; keyPassword?: string; @@ -47,7 +47,7 @@ export interface SSHHostData { folder?: string; tags?: string[]; pin?: boolean; - authType: "password" | "key" | "credential"; + authType: "password" | "key" | "credential" | "none"; password?: string; key?: File | null; keyPassword?: string; diff --git a/src/ui/Desktop/Apps/Host Manager/HostManagerEditor.tsx b/src/ui/Desktop/Apps/Host Manager/HostManagerEditor.tsx index 08a41724..b660b051 100644 --- a/src/ui/Desktop/Apps/Host Manager/HostManagerEditor.tsx +++ b/src/ui/Desktop/Apps/Host Manager/HostManagerEditor.tsx @@ -48,7 +48,7 @@ interface SSHHost { folder: string; tags: string[]; pin: boolean; - authType: string; + authType: "password" | "key" | "credential" | "none"; password?: string; key?: string; keyPassword?: string; @@ -79,9 +79,9 @@ export function HostManagerEditor({ const [credentials, setCredentials] = useState([]); const [loading, setLoading] = useState(true); - const [authTab, setAuthTab] = useState<"password" | "key" | "credential">( - "password", - ); + const [authTab, setAuthTab] = useState< + "password" | "key" | "credential" | "none" + >("password"); const [keyInputMethod, setKeyInputMethod] = useState<"upload" | "paste">( "upload", ); @@ -174,7 +174,7 @@ export function HostManagerEditor({ folder: z.string().optional(), tags: z.array(z.string().min(1)).default([]), pin: z.boolean().default(false), - authType: z.enum(["password", "key", "credential"]), + authType: z.enum(["password", "key", "credential", "none"]), credentialId: z.number().optional().nullable(), password: z.string().optional(), key: z.any().optional().nullable(), @@ -210,6 +210,11 @@ export function HostManagerEditor({ defaultPath: z.string().optional(), }) .superRefine((data, ctx) => { + // Skip authentication validation for "none" type + if (data.authType === "none") { + return; + } + if (data.authType === "key") { if ( !data.key || @@ -313,7 +318,9 @@ export function HostManagerEditor({ ? "credential" : cleanedHost.key ? "key" - : "password"; + : cleanedHost.password + ? "password" + : "none"; setAuthTab(defaultAuthType); const formData = { @@ -324,7 +331,7 @@ export function HostManagerEditor({ folder: cleanedHost.folder || "", tags: cleanedHost.tags || [], pin: Boolean(cleanedHost.pin), - authType: defaultAuthType as "password" | "key" | "credential", + authType: defaultAuthType as "password" | "key" | "credential" | "none", credentialId: null, password: "", key: null, @@ -863,7 +870,8 @@ export function HostManagerEditor({ const newAuthType = value as | "password" | "key" - | "credential"; + | "credential" + | "none"; setAuthTab(newAuthType); form.setValue("authType", newAuthType); @@ -880,6 +888,13 @@ export function HostManagerEditor({ form.setValue("key", null); form.setValue("keyPassword", ""); form.setValue("keyType", "auto"); + } else if (newAuthType === "none") { + // Clear all authentication fields for "none" type + form.setValue("password", ""); + form.setValue("key", null); + form.setValue("keyPassword", ""); + form.setValue("keyType", "auto"); + form.setValue("credentialId", null); } }} className="flex-1 flex flex-col h-full min-h-0" @@ -892,6 +907,7 @@ export function HostManagerEditor({ {t("hosts.credential")} + {t("hosts.none")} + + + + {t("hosts.noneDescription")} + + +