Add optional password requirement for SSH sessions (Issue #118)

Users can now choose whether to require a password when saving SSH sessions.
A new "Require Password" toggle has been added to the password authentication
tab, allowing sessions to be saved without entering a password when disabled.

- Add requirePassword boolean field to SSH host schema (defaults to true)
- Update form validation to conditionally require password based on setting
- Add "Require Password" toggle with description in Host Manager UI
- Update all backend SSH routes to handle requirePassword field correctly
- Add translations for new UI elements in English and Chinese
- Maintain full backward compatibility with existing hosts

Resolves #118
This commit is contained in:
ZacharyZcR
2025-09-16 11:21:50 +08:00
parent c2545f9279
commit 182b60a428
5 changed files with 56 additions and 1 deletions

View File

@@ -45,6 +45,9 @@ export const sshData = sqliteTable("ssh_data", {
authType: text("auth_type").notNull(), authType: text("auth_type").notNull(),
password: text("password"), password: text("password"),
requirePassword: integer("require_password", { mode: "boolean" })
.notNull()
.default(true),
key: text("key", { length: 8192 }), key: text("key", { length: 8192 }),
keyPassword: text("key_password"), keyPassword: text("key_password"),
keyType: text("key_type"), keyType: text("key_type"),

View File

@@ -77,6 +77,7 @@ router.get("/db/host/internal", async (req: Request, res: Response) => {
: [] : []
: [], : [],
pin: !!row.pin, pin: !!row.pin,
requirePassword: !!row.requirePassword,
enableTerminal: !!row.enableTerminal, enableTerminal: !!row.enableTerminal,
enableTunnel: !!row.enableTunnel, enableTunnel: !!row.enableTunnel,
tunnelConnections: row.tunnelConnections tunnelConnections: row.tunnelConnections
@@ -137,6 +138,7 @@ router.post(
port, port,
username, username,
password, password,
requirePassword,
authMethod, authMethod,
authType, authType,
credentialId, credentialId,
@@ -188,6 +190,7 @@ router.post(
if (effectiveAuthType === "password") { if (effectiveAuthType === "password") {
sshDataObj.password = password || null; sshDataObj.password = password || null;
sshDataObj.requirePassword = requirePassword !== false ? 1 : 0;
sshDataObj.key = null; sshDataObj.key = null;
sshDataObj.keyPassword = null; sshDataObj.keyPassword = null;
sshDataObj.keyType = null; sshDataObj.keyType = null;
@@ -196,6 +199,14 @@ router.post(
sshDataObj.keyPassword = keyPassword || null; sshDataObj.keyPassword = keyPassword || null;
sshDataObj.keyType = keyType; sshDataObj.keyType = keyType;
sshDataObj.password = null; sshDataObj.password = null;
sshDataObj.requirePassword = 1; // Default to true for non-password auth
} else {
// For credential auth
sshDataObj.password = null;
sshDataObj.key = null;
sshDataObj.keyPassword = null;
sshDataObj.keyType = null;
sshDataObj.requirePassword = 1; // Default to true for non-password auth
} }
try { try {
@@ -222,6 +233,7 @@ router.post(
: [] : []
: [], : [],
pin: !!createdHost.pin, pin: !!createdHost.pin,
requirePassword: !!createdHost.requirePassword,
enableTerminal: !!createdHost.enableTerminal, enableTerminal: !!createdHost.enableTerminal,
enableTunnel: !!createdHost.enableTunnel, enableTunnel: !!createdHost.enableTunnel,
tunnelConnections: createdHost.tunnelConnections tunnelConnections: createdHost.tunnelConnections
@@ -308,6 +320,7 @@ router.put(
port, port,
username, username,
password, password,
requirePassword,
authMethod, authMethod,
authType, authType,
credentialId, credentialId,
@@ -362,6 +375,7 @@ router.put(
if (password) { if (password) {
sshDataObj.password = password; sshDataObj.password = password;
} }
sshDataObj.requirePassword = requirePassword !== false ? 1 : 0;
sshDataObj.key = null; sshDataObj.key = null;
sshDataObj.keyPassword = null; sshDataObj.keyPassword = null;
sshDataObj.keyType = null; sshDataObj.keyType = null;
@@ -376,6 +390,14 @@ router.put(
sshDataObj.keyType = keyType; sshDataObj.keyType = keyType;
} }
sshDataObj.password = null; sshDataObj.password = null;
sshDataObj.requirePassword = 1; // Default to true for non-password auth
} else {
// For credential auth
sshDataObj.password = null;
sshDataObj.key = null;
sshDataObj.keyPassword = null;
sshDataObj.keyType = null;
sshDataObj.requirePassword = 1; // Default to true for non-password auth
} }
try { try {
@@ -408,6 +430,7 @@ router.put(
: [] : []
: [], : [],
pin: !!updatedHost.pin, pin: !!updatedHost.pin,
requirePassword: !!updatedHost.requirePassword,
enableTerminal: !!updatedHost.enableTerminal, enableTerminal: !!updatedHost.enableTerminal,
enableTunnel: !!updatedHost.enableTunnel, enableTunnel: !!updatedHost.enableTunnel,
tunnelConnections: updatedHost.tunnelConnections tunnelConnections: updatedHost.tunnelConnections
@@ -475,6 +498,7 @@ router.get("/db/host", authenticateJWT, async (req: Request, res: Response) => {
: [] : []
: [], : [],
pin: !!row.pin, pin: !!row.pin,
requirePassword: !!row.requirePassword,
enableTerminal: !!row.enableTerminal, enableTerminal: !!row.enableTerminal,
enableTunnel: !!row.enableTunnel, enableTunnel: !!row.enableTunnel,
tunnelConnections: row.tunnelConnections tunnelConnections: row.tunnelConnections

View File

@@ -502,6 +502,8 @@
"upload": "Upload", "upload": "Upload",
"authentication": "Authentication", "authentication": "Authentication",
"password": "Password", "password": "Password",
"requirePassword": "Require Password",
"requirePasswordDescription": "When disabled, sessions can be saved without entering a password",
"key": "Key", "key": "Key",
"credential": "Credential", "credential": "Credential",
"selectCredential": "Select Credential", "selectCredential": "Select Credential",

View File

@@ -502,6 +502,8 @@
"upload": "上传", "upload": "上传",
"authentication": "认证方式", "authentication": "认证方式",
"password": "密码", "password": "密码",
"requirePassword": "要求密码",
"requirePasswordDescription": "禁用时,可以在不输入密码的情况下保存会话",
"key": "密钥", "key": "密钥",
"credential": "凭证", "credential": "凭证",
"selectCredential": "选择凭证", "selectCredential": "选择凭证",

View File

@@ -45,6 +45,7 @@ interface SSHHost {
pin: boolean; pin: boolean;
authType: string; authType: string;
password?: string; password?: string;
requirePassword?: boolean;
key?: string; key?: string;
keyPassword?: string; keyPassword?: string;
keyType?: string; keyType?: string;
@@ -172,6 +173,7 @@ export function HostManagerEditor({
authType: z.enum(["password", "key", "credential"]), authType: z.enum(["password", "key", "credential"]),
credentialId: z.number().optional().nullable(), credentialId: z.number().optional().nullable(),
password: z.string().optional(), password: z.string().optional(),
requirePassword: z.boolean().default(true),
key: z.any().optional().nullable(), key: z.any().optional().nullable(),
keyPassword: z.string().optional(), keyPassword: z.string().optional(),
keyType: z keyType: z
@@ -206,7 +208,7 @@ export function HostManagerEditor({
}) })
.superRefine((data, ctx) => { .superRefine((data, ctx) => {
if (data.authType === "password") { if (data.authType === "password") {
if (!data.password || data.password.trim() === "") { if (data.requirePassword && (!data.password || data.password.trim() === "")) {
ctx.addIssue({ ctx.addIssue({
code: z.ZodIssueCode.custom, code: z.ZodIssueCode.custom,
message: t("hosts.passwordRequired"), message: t("hosts.passwordRequired"),
@@ -274,6 +276,7 @@ export function HostManagerEditor({
authType: "password" as const, authType: "password" as const,
credentialId: null, credentialId: null,
password: "", password: "",
requirePassword: true,
key: null, key: null,
keyPassword: "", keyPassword: "",
keyType: "auto" as const, keyType: "auto" as const,
@@ -330,6 +333,7 @@ export function HostManagerEditor({
authType: defaultAuthType as "password" | "key" | "credential", authType: defaultAuthType as "password" | "key" | "credential",
credentialId: null, credentialId: null,
password: "", password: "",
requirePassword: cleanedHost.requirePassword ?? true,
key: null, key: null,
keyPassword: "", keyPassword: "",
keyType: "auto" as const, keyType: "auto" as const,
@@ -365,6 +369,7 @@ export function HostManagerEditor({
authType: "password" as const, authType: "password" as const,
credentialId: null, credentialId: null,
password: "", password: "",
requirePassword: true,
key: null, key: null,
keyPassword: "", keyPassword: "",
keyType: "auto" as const, keyType: "auto" as const,
@@ -867,6 +872,24 @@ export function HostManagerEditor({
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="password"> <TabsContent value="password">
<FormField
control={form.control}
name="requirePassword"
render={({ field }) => (
<FormItem className="mb-4">
<FormLabel>{t("hosts.requirePassword")}</FormLabel>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormDescription>
{t("hosts.requirePasswordDescription")}
</FormDescription>
</FormItem>
)}
/>
<FormField <FormField
control={form.control} control={form.control}
name="password" name="password"
@@ -876,6 +899,7 @@ export function HostManagerEditor({
<FormControl> <FormControl>
<PasswordInput <PasswordInput
placeholder={t("placeholders.password")} placeholder={t("placeholders.password")}
disabled={!form.watch("requirePassword")}
{...field} {...field}
/> />
</FormControl> </FormControl>