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.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}`,
+2
View File
@@ -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",
+2
View File
@@ -635,6 +635,8 @@
"password": "密码",
"key": "密钥",
"credential": "凭证",
"none": "无",
"noneDescription": "无需认证凭证。您可以稍后添加凭证或使用 SSH 代理进行认证。",
"selectCredential": "选择凭证",
"selectCredentialPlaceholder": "选择一个凭证...",
"credentialRequired": "使用凭证认证时需要选择凭证",
+2 -2
View File
@@ -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;
@@ -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<any[]>([]);
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({
<TabsTrigger value="credential">
{t("hosts.credential")}
</TabsTrigger>
<TabsTrigger value="none">{t("hosts.none")}</TabsTrigger>
</TabsList>
<TabsContent value="password">
<FormField
@@ -1110,6 +1126,13 @@ export function HostManagerEditor({
)}
/>
</TabsContent>
<TabsContent value="none">
<FormItem>
<FormDescription>
{t("hosts.noneDescription")}
</FormDescription>
</FormItem>
</TabsContent>
</Tabs>
</TabsContent>
<TabsContent value="terminal">