feat: SSH tunnel support
- Add ssh2 dependency for SSH port forwarding - NewConnectionDialog: SSH toggle + host/port/user/password/privateKey/passphrase form - connection.ts: createSshTunnel() with local port forwarding - Connect ioredis through SSH tunnel when sshEnabled - Clean up SSH client + tunnel server on disconnect - i18n keys for all SSH fields (zh-CN + en) - Add sshPassphrase to all ConnectionConfig types
This commit is contained in:
@@ -19,6 +19,13 @@ const form = reactive({
|
||||
username: '',
|
||||
tls: false,
|
||||
separator: ':',
|
||||
sshEnabled: false,
|
||||
sshHost: '',
|
||||
sshPort: 22,
|
||||
sshUsername: '',
|
||||
sshPassword: '',
|
||||
sshPrivateKeyPath: '',
|
||||
sshPassphrase: '',
|
||||
})
|
||||
|
||||
const saving = ref(false)
|
||||
@@ -38,12 +45,21 @@ watch(() => props.visible, async (v) => {
|
||||
username: props.connection.username,
|
||||
tls: props.connection.tls,
|
||||
separator: props.connection.separator,
|
||||
sshEnabled: props.connection.sshEnabled || false,
|
||||
sshHost: props.connection.sshHost || '',
|
||||
sshPort: props.connection.sshPort || 22,
|
||||
sshUsername: props.connection.sshUsername || '',
|
||||
sshPassword: props.connection.sshPassword || '',
|
||||
sshPrivateKeyPath: props.connection.sshPrivateKeyPath || '',
|
||||
sshPassphrase: props.connection.sshPassphrase || '',
|
||||
})
|
||||
} else {
|
||||
isEdit.value = false
|
||||
Object.assign(form, {
|
||||
name: '', host: '127.0.0.1', port: 6379,
|
||||
auth: '', username: '', tls: false, separator: ':',
|
||||
sshEnabled: false, sshHost: '', sshPort: 22,
|
||||
sshUsername: '', sshPassword: '', sshPrivateKeyPath: '', sshPassphrase: '',
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -53,6 +69,16 @@ function close() {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
async function selectFile() {
|
||||
const result = await window.electronAPI.dialog.openFile({
|
||||
title: t('connection.sshPrivateKey'),
|
||||
filters: [{ name: 'All Files', extensions: ['*'] }],
|
||||
})
|
||||
if (result) {
|
||||
form.sshPrivateKeyPath = result.path
|
||||
}
|
||||
}
|
||||
|
||||
async function handleTest() {
|
||||
if (!form.host.trim()) {
|
||||
ElMessage.warning('Host is required')
|
||||
@@ -66,7 +92,14 @@ async function handleTest() {
|
||||
auth: form.auth || undefined,
|
||||
username: form.username || undefined,
|
||||
tls: form.tls,
|
||||
})
|
||||
sshEnabled: form.sshEnabled,
|
||||
sshHost: form.sshHost.trim(),
|
||||
sshPort: Number(form.sshPort) || 22,
|
||||
sshUsername: form.sshUsername.trim(),
|
||||
sshPassword: form.sshPassword,
|
||||
sshPrivateKeyPath: form.sshPrivateKeyPath,
|
||||
sshPassphrase: form.sshPassphrase,
|
||||
} as any)
|
||||
if (result.success) {
|
||||
ElMessage.success('Connection successful')
|
||||
} else {
|
||||
@@ -95,12 +128,13 @@ async function handleSave() {
|
||||
tlsCaPath: '',
|
||||
tlsCertPath: '',
|
||||
tlsKeyPath: '',
|
||||
sshEnabled: false,
|
||||
sshHost: '',
|
||||
sshPort: 22,
|
||||
sshUsername: '',
|
||||
sshPassword: '',
|
||||
sshPrivateKeyPath: '',
|
||||
sshEnabled: form.sshEnabled,
|
||||
sshHost: form.sshHost.trim(),
|
||||
sshPort: Number(form.sshPort) || 22,
|
||||
sshUsername: form.sshUsername.trim(),
|
||||
sshPassword: form.sshPassword,
|
||||
sshPrivateKeyPath: form.sshPrivateKeyPath,
|
||||
sshPassphrase: form.sshPassphrase,
|
||||
cluster: false,
|
||||
sentinelMasterName: '',
|
||||
separator: form.separator || ':',
|
||||
@@ -122,7 +156,7 @@ async function handleSave() {
|
||||
<el-dialog
|
||||
:model-value="visible"
|
||||
:title="isEdit ? t('connection.edit') : t('connection.new')"
|
||||
width="480px"
|
||||
width="520px"
|
||||
:close-on-click-modal="false"
|
||||
@update:model-value="close"
|
||||
>
|
||||
@@ -154,6 +188,41 @@ async function handleSave() {
|
||||
<el-form-item>
|
||||
<el-checkbox v-model="form.tls">{{ t('connection.tls') }}</el-checkbox>
|
||||
</el-form-item>
|
||||
|
||||
<!-- SSH Tunnel -->
|
||||
<el-divider />
|
||||
<el-form-item>
|
||||
<el-checkbox v-model="form.sshEnabled">{{ t('connection.ssh') }}</el-checkbox>
|
||||
</el-form-item>
|
||||
<template v-if="form.sshEnabled">
|
||||
<el-row :gutter="12">
|
||||
<el-col :span="16">
|
||||
<el-form-item :label="t('connection.sshHost')">
|
||||
<el-input v-model="form.sshHost" placeholder="192.168.1.1" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item :label="t('connection.sshPort')">
|
||||
<el-input-number v-model="form.sshPort" :min="1" :max="65535" controls-position="right" style="width:100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item :label="t('connection.sshUsername')">
|
||||
<el-input v-model="form.sshUsername" placeholder="root" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('connection.sshPassword')">
|
||||
<el-input v-model="form.sshPassword" type="password" show-password :placeholder="t('connection.sshPassword')" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('connection.sshPrivateKey')">
|
||||
<div style="display:flex;gap:8px;">
|
||||
<el-input v-model="form.sshPrivateKeyPath" placeholder="~/.ssh/id_rsa" />
|
||||
<el-button @click="selectFile">{{ t('connection.selectFile') }}</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('connection.sshPassphrase')">
|
||||
<el-input v-model="form.sshPassphrase" type="password" show-password :placeholder="t('connection.sshPassphrase')" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
|
||||
@@ -31,6 +31,13 @@ export default {
|
||||
username: 'Username',
|
||||
tls: 'Enable TLS',
|
||||
ssh: 'SSH Tunnel',
|
||||
sshHost: 'SSH Host',
|
||||
sshPort: 'SSH Port',
|
||||
sshUsername: 'SSH Username',
|
||||
sshPassword: 'SSH Password',
|
||||
sshPrivateKey: 'Private Key Path',
|
||||
sshPassphrase: 'Passphrase',
|
||||
selectFile: 'Select File',
|
||||
connect: 'Connect',
|
||||
disconnect: 'Disconnect',
|
||||
deleteConfirm: 'Delete this connection?',
|
||||
|
||||
@@ -31,6 +31,13 @@ export default {
|
||||
username: '用户名',
|
||||
tls: '启用 TLS',
|
||||
ssh: 'SSH 隧道',
|
||||
sshHost: 'SSH 主机',
|
||||
sshPort: 'SSH 端口',
|
||||
sshUsername: 'SSH 用户名',
|
||||
sshPassword: 'SSH 密码',
|
||||
sshPrivateKey: '私钥路径',
|
||||
sshPassphrase: '私钥密码',
|
||||
selectFile: '选择文件',
|
||||
connect: '连接',
|
||||
disconnect: '断开',
|
||||
deleteConfirm: '删除此连接?',
|
||||
|
||||
@@ -19,6 +19,7 @@ export interface ConnectionConfig {
|
||||
sshUsername: string
|
||||
sshPassword: string
|
||||
sshPrivateKeyPath: string
|
||||
sshPassphrase: string
|
||||
cluster: boolean
|
||||
sentinelMasterName: string
|
||||
separator: string
|
||||
@@ -82,6 +83,13 @@ export const useConnectionStore = defineStore('connection', () => {
|
||||
password: auth || undefined,
|
||||
username: conn.username || undefined,
|
||||
tls: conn.tls,
|
||||
sshEnabled: conn.sshEnabled,
|
||||
sshHost: conn.sshHost,
|
||||
sshPort: conn.sshPort,
|
||||
sshUsername: conn.sshUsername,
|
||||
sshPassword: conn.sshPassword,
|
||||
sshPrivateKeyPath: conn.sshPrivateKeyPath,
|
||||
sshPassphrase: conn.sshPassphrase,
|
||||
})
|
||||
if (result.success) {
|
||||
activeId.value = conn.id
|
||||
@@ -143,6 +151,8 @@ export const useConnectionStore = defineStore('connection', () => {
|
||||
|
||||
async function testConnection(opts: {
|
||||
host: string; port: number; auth?: string; username?: string; tls?: boolean
|
||||
sshEnabled?: boolean; sshHost?: string; sshPort?: number; sshUsername?: string
|
||||
sshPassword?: string; sshPrivateKeyPath?: string; sshPassphrase?: string
|
||||
}): Promise<{ success: boolean; error?: string }> {
|
||||
if (!window.electronAPI) {
|
||||
return { success: false, error: 'Electron API not ready' }
|
||||
@@ -155,6 +165,13 @@ export const useConnectionStore = defineStore('connection', () => {
|
||||
password: opts.auth || undefined,
|
||||
username: opts.username || undefined,
|
||||
tls: opts.tls,
|
||||
sshEnabled: opts.sshEnabled,
|
||||
sshHost: opts.sshHost,
|
||||
sshPort: opts.sshPort,
|
||||
sshUsername: opts.sshUsername,
|
||||
sshPassword: opts.sshPassword,
|
||||
sshPrivateKeyPath: opts.sshPrivateKeyPath,
|
||||
sshPassphrase: opts.sshPassphrase,
|
||||
})
|
||||
if (result.success) {
|
||||
await window.electronAPI.redis.disconnect(testId)
|
||||
|
||||
Reference in New Issue
Block a user