diff --git a/ROADMAP.md b/ROADMAP.md index ed72da6..0778905 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -6,8 +6,8 @@ ## P0 — 阻塞级(连接功能不完整) -- [ ] **SSH 隧道** — 类型已定义,NewConnectionDialog 需添加 host/port/user/password/privateKey/passphrase 表单 -- [ ] **SSL/TLS 证书路径** — CA / Cert / Key 文件路径 + SNI servername,当前仅 TLS 复选框 +- [x] **SSH 隧道** — 类型已定义,NewConnectionDialog 需添加 host/port/user/password/privateKey/passphrase 表单 +- [x] **SSL/TLS 证书路径** — CA / Cert / Key 文件路径 + SNI servername,当前仅 TLS 复选框 - [ ] **Sentinel 模式** — masterName + nodePassword 表单 - [ ] **Cluster 开关** — UI 暴露 cluster 模式切换 + FAQ 说明 diff --git a/electron/main/redis/connection.ts b/electron/main/redis/connection.ts index 5f793f0..33f976f 100644 --- a/electron/main/redis/connection.ts +++ b/electron/main/redis/connection.ts @@ -13,6 +13,10 @@ export interface ConnectionOptions { username?: string password?: string tls?: boolean + tlsCaPath?: string + tlsCertPath?: string + tlsKeyPath?: string + tlsRejectUnauthorized?: boolean sshEnabled?: boolean sshHost?: string sshPort?: number @@ -121,10 +125,20 @@ function buildRedisOptions(opts: ConnectionOptions): RedisOptions { } if (opts.tls) { - options.tls = { - rejectUnauthorized: false, + const tlsOpts: any = { + rejectUnauthorized: opts.tlsRejectUnauthorized || false, checkServerIdentity: () => undefined, } + if (opts.tlsCaPath) { + try { tlsOpts.ca = readFileSync(expandHomePath(opts.tlsCaPath)) } catch { /* ignore */ } + } + if (opts.tlsCertPath) { + try { tlsOpts.cert = readFileSync(expandHomePath(opts.tlsCertPath)) } catch { /* ignore */ } + } + if (opts.tlsKeyPath) { + try { tlsOpts.key = readFileSync(expandHomePath(opts.tlsKeyPath)) } catch { /* ignore */ } + } + options.tls = tlsOpts } return options diff --git a/electron/main/store.ts b/electron/main/store.ts index fc2f888..fe6da43 100644 --- a/electron/main/store.ts +++ b/electron/main/store.ts @@ -12,6 +12,7 @@ interface ConnectionConfig { tlsCaPath: string tlsCertPath: string tlsKeyPath: string + tlsRejectUnauthorized: boolean sshEnabled: boolean sshHost: string sshPort: number diff --git a/electron/preload/index.d.ts b/electron/preload/index.d.ts index ad26a4b..308ba48 100644 --- a/electron/preload/index.d.ts +++ b/electron/preload/index.d.ts @@ -10,6 +10,7 @@ export interface ConnectionConfig { tlsCaPath: string tlsCertPath: string tlsKeyPath: string + tlsRejectUnauthorized: boolean sshEnabled: boolean sshHost: string sshPort: number diff --git a/src/components/NewConnectionDialog.vue b/src/components/NewConnectionDialog.vue index 709364f..3e567c6 100644 --- a/src/components/NewConnectionDialog.vue +++ b/src/components/NewConnectionDialog.vue @@ -18,6 +18,10 @@ const form = reactive({ auth: '', username: '', tls: false, + tlsCaPath: '', + tlsCertPath: '', + tlsKeyPath: '', + tlsRejectUnauthorized: false, separator: ':', sshEnabled: false, sshHost: '', @@ -44,6 +48,9 @@ watch(() => props.visible, async (v) => { auth: decryptedAuth, username: props.connection.username, tls: props.connection.tls, + tlsCaPath: props.connection.tlsCaPath || '', + tlsCertPath: props.connection.tlsCertPath || '', + tlsKeyPath: props.connection.tlsKeyPath || '', separator: props.connection.separator, sshEnabled: props.connection.sshEnabled || false, sshHost: props.connection.sshHost || '', @@ -57,7 +64,8 @@ watch(() => props.visible, async (v) => { isEdit.value = false Object.assign(form, { name: '', host: '127.0.0.1', port: 6379, - auth: '', username: '', tls: false, separator: ':', + auth: '', username: '', tls: false, tlsCaPath: '', tlsCertPath: '', tlsKeyPath: '', + separator: ':', sshEnabled: false, sshHost: '', sshPort: 22, sshUsername: '', sshPassword: '', sshPrivateKeyPath: '', sshPassphrase: '', }) @@ -79,6 +87,30 @@ async function selectFile() { } } +async function selectTlsCaFile() { + const result = await window.electronAPI.dialog.openFile({ + title: t('connection.tlsCaPath'), + filters: [{ name: 'Certificate Files', extensions: ['pem', 'crt', 'cer', 'key'] }], + }) + if (result) { form.tlsCaPath = result.path } +} + +async function selectTlsCertFile() { + const result = await window.electronAPI.dialog.openFile({ + title: t('connection.tlsCertPath'), + filters: [{ name: 'Certificate Files', extensions: ['pem', 'crt', 'cer'] }], + }) + if (result) { form.tlsCertPath = result.path } +} + +async function selectTlsKeyFile() { + const result = await window.electronAPI.dialog.openFile({ + title: t('connection.tlsKeyPath'), + filters: [{ name: 'Key Files', extensions: ['pem', 'key'] }], + }) + if (result) { form.tlsKeyPath = result.path } +} + async function handleTest() { if (!form.host.trim()) { ElMessage.warning('Host is required') @@ -92,6 +124,10 @@ async function handleTest() { auth: form.auth || undefined, username: form.username || undefined, tls: form.tls, + tlsCaPath: form.tlsCaPath, + tlsCertPath: form.tlsCertPath, + tlsKeyPath: form.tlsKeyPath, + tlsRejectUnauthorized: form.tlsRejectUnauthorized, sshEnabled: form.sshEnabled, sshHost: form.sshHost.trim(), sshPort: Number(form.sshPort) || 22, @@ -125,9 +161,10 @@ async function handleSave() { auth: form.auth, username: form.username.trim(), tls: form.tls, - tlsCaPath: '', - tlsCertPath: '', - tlsKeyPath: '', + tlsCaPath: form.tlsCaPath, + tlsCertPath: form.tlsCertPath, + tlsKeyPath: form.tlsKeyPath, + tlsRejectUnauthorized: form.tlsRejectUnauthorized, sshEnabled: form.sshEnabled, sshHost: form.sshHost.trim(), sshPort: Number(form.sshPort) || 22, @@ -188,6 +225,29 @@ async function handleSave() { {{ t('connection.tls') }} + + + + + {{ t('connection.selectFile') }} + + + + + + {{ t('connection.selectFile') }} + + + + + + {{ t('connection.selectFile') }} + + + + {{ t('connection.tlsRejectUnauthorized') }} + + diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 7733536..f9386bb 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -30,6 +30,10 @@ export default { password: 'Password', username: 'Username', tls: 'Enable TLS', + tlsCaPath: 'CA Certificate', + tlsCertPath: 'Client Certificate', + tlsKeyPath: 'Client Private Key', + tlsRejectUnauthorized: 'Verify Server Certificate', ssh: 'SSH Tunnel', sshHost: 'SSH Host', sshPort: 'SSH Port', diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index c48833c..9e616dc 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -30,6 +30,10 @@ export default { password: '密码', username: '用户名', tls: '启用 TLS', + tlsCaPath: 'CA 证书', + tlsCertPath: '客户端证书', + tlsKeyPath: '客户端私钥', + tlsRejectUnauthorized: '验证服务器证书', ssh: 'SSH 隧道', sshHost: 'SSH 主机', sshPort: 'SSH 端口', diff --git a/src/stores/connection.ts b/src/stores/connection.ts index f4f16b8..1aeaf29 100644 --- a/src/stores/connection.ts +++ b/src/stores/connection.ts @@ -13,6 +13,7 @@ export interface ConnectionConfig { tlsCaPath: string tlsCertPath: string tlsKeyPath: string + tlsRejectUnauthorized: boolean sshEnabled: boolean sshHost: string sshPort: number @@ -83,6 +84,10 @@ export const useConnectionStore = defineStore('connection', () => { password: auth || undefined, username: conn.username || undefined, tls: conn.tls, + tlsCaPath: conn.tlsCaPath, + tlsCertPath: conn.tlsCertPath, + tlsKeyPath: conn.tlsKeyPath, + tlsRejectUnauthorized: conn.tlsRejectUnauthorized, sshEnabled: conn.sshEnabled, sshHost: conn.sshHost, sshPort: conn.sshPort, @@ -151,6 +156,7 @@ export const useConnectionStore = defineStore('connection', () => { async function testConnection(opts: { host: string; port: number; auth?: string; username?: string; tls?: boolean + tlsCaPath?: string; tlsCertPath?: string; tlsKeyPath?: string sshEnabled?: boolean; sshHost?: string; sshPort?: number; sshUsername?: string sshPassword?: string; sshPrivateKeyPath?: string; sshPassphrase?: string }): Promise<{ success: boolean; error?: string }> { @@ -165,6 +171,10 @@ export const useConnectionStore = defineStore('connection', () => { password: opts.auth || undefined, username: opts.username || undefined, tls: opts.tls, + tlsCaPath: opts.tlsCaPath, + tlsCertPath: opts.tlsCertPath, + tlsKeyPath: opts.tlsKeyPath, + tlsRejectUnauthorized: (opts as any).tlsRejectUnauthorized, sshEnabled: opts.sshEnabled, sshHost: opts.sshHost, sshPort: opts.sshPort,