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
This commit is contained in:
ZacharyZcR
2025-10-04 04:17:07 +08:00
parent 937e04fa5c
commit 522fe3e571
5 changed files with 57 additions and 12 deletions
+20 -2
View File
@@ -281,7 +281,14 @@ router.post(
sshDataObj.keyPassword = keyPassword || null; sshDataObj.keyPassword = keyPassword || null;
sshDataObj.keyType = keyType; sshDataObj.keyType = keyType;
sshDataObj.password = null; 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 { } else {
// credential type or fallback - set all to null except credentialId
sshDataObj.password = null; sshDataObj.password = null;
sshDataObj.key = null; sshDataObj.key = null;
sshDataObj.keyPassword = null; sshDataObj.keyPassword = null;
@@ -471,7 +478,14 @@ router.put(
sshDataObj.keyType = keyType; sshDataObj.keyType = keyType;
} }
sshDataObj.password = null; 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 { } else {
// credential type or fallback - set all to null except credentialId
sshDataObj.password = null; sshDataObj.password = null;
sshDataObj.key = null; sshDataObj.key = null;
sshDataObj.keyPassword = null; sshDataObj.keyPassword = null;
@@ -1356,10 +1370,12 @@ router.post(
continue; continue;
} }
if (!["password", "key", "credential"].includes(hostData.authType)) { if (
!["password", "key", "credential", "none"].includes(hostData.authType)
) {
results.failed++; results.failed++;
results.errors.push( 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; continue;
} }
@@ -1391,6 +1407,8 @@ router.post(
continue; continue;
} }
// "none" authType requires no validation - no credentials needed
const sshDataObj: any = { const sshDataObj: any = {
userId: userId, userId: userId,
name: hostData.name || `${hostData.username}@${hostData.ip}`, name: hostData.name || `${hostData.username}@${hostData.ip}`,
+2
View File
@@ -639,6 +639,8 @@
"password": "Password", "password": "Password",
"key": "Key", "key": "Key",
"credential": "Credential", "credential": "Credential",
"none": "None",
"noneDescription": "No authentication credentials required. You can add credentials later or use SSH agent for authentication.",
"selectCredential": "Select Credential", "selectCredential": "Select Credential",
"selectCredentialPlaceholder": "Choose a credential...", "selectCredentialPlaceholder": "Choose a credential...",
"credentialRequired": "Credential is required when using credential authentication", "credentialRequired": "Credential is required when using credential authentication",
+2
View File
@@ -635,6 +635,8 @@
"password": "密码", "password": "密码",
"key": "密钥", "key": "密钥",
"credential": "凭证", "credential": "凭证",
"none": "无",
"noneDescription": "无需认证凭证。您可以稍后添加凭证或使用 SSH 代理进行认证。",
"selectCredential": "选择凭证", "selectCredential": "选择凭证",
"selectCredentialPlaceholder": "选择一个凭证...", "selectCredentialPlaceholder": "选择一个凭证...",
"credentialRequired": "使用凭证认证时需要选择凭证", "credentialRequired": "使用凭证认证时需要选择凭证",
+2 -2
View File
@@ -18,7 +18,7 @@ export interface SSHHost {
folder: string; folder: string;
tags: string[]; tags: string[];
pin: boolean; pin: boolean;
authType: "password" | "key" | "credential"; authType: "password" | "key" | "credential" | "none";
password?: string; password?: string;
key?: string; key?: string;
keyPassword?: string; keyPassword?: string;
@@ -47,7 +47,7 @@ export interface SSHHostData {
folder?: string; folder?: string;
tags?: string[]; tags?: string[];
pin?: boolean; pin?: boolean;
authType: "password" | "key" | "credential"; authType: "password" | "key" | "credential" | "none";
password?: string; password?: string;
key?: File | null; key?: File | null;
keyPassword?: string; keyPassword?: string;
@@ -48,7 +48,7 @@ interface SSHHost {
folder: string; folder: string;
tags: string[]; tags: string[];
pin: boolean; pin: boolean;
authType: string; authType: "password" | "key" | "credential" | "none";
password?: string; password?: string;
key?: string; key?: string;
keyPassword?: string; keyPassword?: string;
@@ -79,9 +79,9 @@ export function HostManagerEditor({
const [credentials, setCredentials] = useState<any[]>([]); const [credentials, setCredentials] = useState<any[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [authTab, setAuthTab] = useState<"password" | "key" | "credential">( const [authTab, setAuthTab] = useState<
"password", "password" | "key" | "credential" | "none"
); >("password");
const [keyInputMethod, setKeyInputMethod] = useState<"upload" | "paste">( const [keyInputMethod, setKeyInputMethod] = useState<"upload" | "paste">(
"upload", "upload",
); );
@@ -174,7 +174,7 @@ export function HostManagerEditor({
folder: z.string().optional(), folder: z.string().optional(),
tags: z.array(z.string().min(1)).default([]), tags: z.array(z.string().min(1)).default([]),
pin: z.boolean().default(false), pin: z.boolean().default(false),
authType: z.enum(["password", "key", "credential"]), authType: z.enum(["password", "key", "credential", "none"]),
credentialId: z.number().optional().nullable(), credentialId: z.number().optional().nullable(),
password: z.string().optional(), password: z.string().optional(),
key: z.any().optional().nullable(), key: z.any().optional().nullable(),
@@ -210,6 +210,11 @@ export function HostManagerEditor({
defaultPath: z.string().optional(), defaultPath: z.string().optional(),
}) })
.superRefine((data, ctx) => { .superRefine((data, ctx) => {
// Skip authentication validation for "none" type
if (data.authType === "none") {
return;
}
if (data.authType === "key") { if (data.authType === "key") {
if ( if (
!data.key || !data.key ||
@@ -313,7 +318,9 @@ export function HostManagerEditor({
? "credential" ? "credential"
: cleanedHost.key : cleanedHost.key
? "key" ? "key"
: "password"; : cleanedHost.password
? "password"
: "none";
setAuthTab(defaultAuthType); setAuthTab(defaultAuthType);
const formData = { const formData = {
@@ -324,7 +331,7 @@ export function HostManagerEditor({
folder: cleanedHost.folder || "", folder: cleanedHost.folder || "",
tags: cleanedHost.tags || [], tags: cleanedHost.tags || [],
pin: Boolean(cleanedHost.pin), pin: Boolean(cleanedHost.pin),
authType: defaultAuthType as "password" | "key" | "credential", authType: defaultAuthType as "password" | "key" | "credential" | "none",
credentialId: null, credentialId: null,
password: "", password: "",
key: null, key: null,
@@ -863,7 +870,8 @@ export function HostManagerEditor({
const newAuthType = value as const newAuthType = value as
| "password" | "password"
| "key" | "key"
| "credential"; | "credential"
| "none";
setAuthTab(newAuthType); setAuthTab(newAuthType);
form.setValue("authType", newAuthType); form.setValue("authType", newAuthType);
@@ -880,6 +888,13 @@ export function HostManagerEditor({
form.setValue("key", null); form.setValue("key", null);
form.setValue("keyPassword", ""); form.setValue("keyPassword", "");
form.setValue("keyType", "auto"); 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" className="flex-1 flex flex-col h-full min-h-0"
@@ -892,6 +907,7 @@ export function HostManagerEditor({
<TabsTrigger value="credential"> <TabsTrigger value="credential">
{t("hosts.credential")} {t("hosts.credential")}
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="none">{t("hosts.none")}</TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="password"> <TabsContent value="password">
<FormField <FormField
@@ -1110,6 +1126,13 @@ export function HostManagerEditor({
)} )}
/> />
</TabsContent> </TabsContent>
<TabsContent value="none">
<FormItem>
<FormDescription>
{t("hosts.noneDescription")}
</FormDescription>
</FormItem>
</TabsContent>
</Tabs> </Tabs>
</TabsContent> </TabsContent>
<TabsContent value="terminal"> <TabsContent value="terminal">