feat: TLS certificate file support
- NewConnectionDialog: TLS cert file selectors (CA/Cert/Key) + rejectUnauthorized checkbox - connection.ts: read cert files and pass to ioredis tls options - Add tlsRejectUnauthorized to all ConnectionConfig types - i18n keys for tlsCaPath/tlsCertPath/tlsKeyPath/tlsRejectUnauthorized
This commit is contained in:
+2
-2
@@ -6,8 +6,8 @@
|
|||||||
|
|
||||||
## P0 — 阻塞级(连接功能不完整)
|
## P0 — 阻塞级(连接功能不完整)
|
||||||
|
|
||||||
- [ ] **SSH 隧道** — 类型已定义,NewConnectionDialog 需添加 host/port/user/password/privateKey/passphrase 表单
|
- [x] **SSH 隧道** — 类型已定义,NewConnectionDialog 需添加 host/port/user/password/privateKey/passphrase 表单
|
||||||
- [ ] **SSL/TLS 证书路径** — CA / Cert / Key 文件路径 + SNI servername,当前仅 TLS 复选框
|
- [x] **SSL/TLS 证书路径** — CA / Cert / Key 文件路径 + SNI servername,当前仅 TLS 复选框
|
||||||
- [ ] **Sentinel 模式** — masterName + nodePassword 表单
|
- [ ] **Sentinel 模式** — masterName + nodePassword 表单
|
||||||
- [ ] **Cluster 开关** — UI 暴露 cluster 模式切换 + FAQ 说明
|
- [ ] **Cluster 开关** — UI 暴露 cluster 模式切换 + FAQ 说明
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ export interface ConnectionOptions {
|
|||||||
username?: string
|
username?: string
|
||||||
password?: string
|
password?: string
|
||||||
tls?: boolean
|
tls?: boolean
|
||||||
|
tlsCaPath?: string
|
||||||
|
tlsCertPath?: string
|
||||||
|
tlsKeyPath?: string
|
||||||
|
tlsRejectUnauthorized?: boolean
|
||||||
sshEnabled?: boolean
|
sshEnabled?: boolean
|
||||||
sshHost?: string
|
sshHost?: string
|
||||||
sshPort?: number
|
sshPort?: number
|
||||||
@@ -121,10 +125,20 @@ function buildRedisOptions(opts: ConnectionOptions): RedisOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (opts.tls) {
|
if (opts.tls) {
|
||||||
options.tls = {
|
const tlsOpts: any = {
|
||||||
rejectUnauthorized: false,
|
rejectUnauthorized: opts.tlsRejectUnauthorized || false,
|
||||||
checkServerIdentity: () => undefined,
|
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
|
return options
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ interface ConnectionConfig {
|
|||||||
tlsCaPath: string
|
tlsCaPath: string
|
||||||
tlsCertPath: string
|
tlsCertPath: string
|
||||||
tlsKeyPath: string
|
tlsKeyPath: string
|
||||||
|
tlsRejectUnauthorized: boolean
|
||||||
sshEnabled: boolean
|
sshEnabled: boolean
|
||||||
sshHost: string
|
sshHost: string
|
||||||
sshPort: number
|
sshPort: number
|
||||||
|
|||||||
Vendored
+1
@@ -10,6 +10,7 @@ export interface ConnectionConfig {
|
|||||||
tlsCaPath: string
|
tlsCaPath: string
|
||||||
tlsCertPath: string
|
tlsCertPath: string
|
||||||
tlsKeyPath: string
|
tlsKeyPath: string
|
||||||
|
tlsRejectUnauthorized: boolean
|
||||||
sshEnabled: boolean
|
sshEnabled: boolean
|
||||||
sshHost: string
|
sshHost: string
|
||||||
sshPort: number
|
sshPort: number
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ const form = reactive({
|
|||||||
auth: '',
|
auth: '',
|
||||||
username: '',
|
username: '',
|
||||||
tls: false,
|
tls: false,
|
||||||
|
tlsCaPath: '',
|
||||||
|
tlsCertPath: '',
|
||||||
|
tlsKeyPath: '',
|
||||||
|
tlsRejectUnauthorized: false,
|
||||||
separator: ':',
|
separator: ':',
|
||||||
sshEnabled: false,
|
sshEnabled: false,
|
||||||
sshHost: '',
|
sshHost: '',
|
||||||
@@ -44,6 +48,9 @@ watch(() => props.visible, async (v) => {
|
|||||||
auth: decryptedAuth,
|
auth: decryptedAuth,
|
||||||
username: props.connection.username,
|
username: props.connection.username,
|
||||||
tls: props.connection.tls,
|
tls: props.connection.tls,
|
||||||
|
tlsCaPath: props.connection.tlsCaPath || '',
|
||||||
|
tlsCertPath: props.connection.tlsCertPath || '',
|
||||||
|
tlsKeyPath: props.connection.tlsKeyPath || '',
|
||||||
separator: props.connection.separator,
|
separator: props.connection.separator,
|
||||||
sshEnabled: props.connection.sshEnabled || false,
|
sshEnabled: props.connection.sshEnabled || false,
|
||||||
sshHost: props.connection.sshHost || '',
|
sshHost: props.connection.sshHost || '',
|
||||||
@@ -57,7 +64,8 @@ watch(() => props.visible, async (v) => {
|
|||||||
isEdit.value = false
|
isEdit.value = false
|
||||||
Object.assign(form, {
|
Object.assign(form, {
|
||||||
name: '', host: '127.0.0.1', port: 6379,
|
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,
|
sshEnabled: false, sshHost: '', sshPort: 22,
|
||||||
sshUsername: '', sshPassword: '', sshPrivateKeyPath: '', sshPassphrase: '',
|
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() {
|
async function handleTest() {
|
||||||
if (!form.host.trim()) {
|
if (!form.host.trim()) {
|
||||||
ElMessage.warning('Host is required')
|
ElMessage.warning('Host is required')
|
||||||
@@ -92,6 +124,10 @@ async function handleTest() {
|
|||||||
auth: form.auth || undefined,
|
auth: form.auth || undefined,
|
||||||
username: form.username || undefined,
|
username: form.username || undefined,
|
||||||
tls: form.tls,
|
tls: form.tls,
|
||||||
|
tlsCaPath: form.tlsCaPath,
|
||||||
|
tlsCertPath: form.tlsCertPath,
|
||||||
|
tlsKeyPath: form.tlsKeyPath,
|
||||||
|
tlsRejectUnauthorized: form.tlsRejectUnauthorized,
|
||||||
sshEnabled: form.sshEnabled,
|
sshEnabled: form.sshEnabled,
|
||||||
sshHost: form.sshHost.trim(),
|
sshHost: form.sshHost.trim(),
|
||||||
sshPort: Number(form.sshPort) || 22,
|
sshPort: Number(form.sshPort) || 22,
|
||||||
@@ -125,9 +161,10 @@ async function handleSave() {
|
|||||||
auth: form.auth,
|
auth: form.auth,
|
||||||
username: form.username.trim(),
|
username: form.username.trim(),
|
||||||
tls: form.tls,
|
tls: form.tls,
|
||||||
tlsCaPath: '',
|
tlsCaPath: form.tlsCaPath,
|
||||||
tlsCertPath: '',
|
tlsCertPath: form.tlsCertPath,
|
||||||
tlsKeyPath: '',
|
tlsKeyPath: form.tlsKeyPath,
|
||||||
|
tlsRejectUnauthorized: form.tlsRejectUnauthorized,
|
||||||
sshEnabled: form.sshEnabled,
|
sshEnabled: form.sshEnabled,
|
||||||
sshHost: form.sshHost.trim(),
|
sshHost: form.sshHost.trim(),
|
||||||
sshPort: Number(form.sshPort) || 22,
|
sshPort: Number(form.sshPort) || 22,
|
||||||
@@ -188,6 +225,29 @@ async function handleSave() {
|
|||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-checkbox v-model="form.tls">{{ t('connection.tls') }}</el-checkbox>
|
<el-checkbox v-model="form.tls">{{ t('connection.tls') }}</el-checkbox>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<template v-if="form.tls">
|
||||||
|
<el-form-item :label="t('connection.tlsCaPath')">
|
||||||
|
<div style="display:flex;gap:8px;">
|
||||||
|
<el-input v-model="form.tlsCaPath" placeholder="/path/to/ca.pem" />
|
||||||
|
<el-button @click="selectTlsCaFile">{{ t('connection.selectFile') }}</el-button>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item :label="t('connection.tlsCertPath')">
|
||||||
|
<div style="display:flex;gap:8px;">
|
||||||
|
<el-input v-model="form.tlsCertPath" placeholder="/path/to/client.pem" />
|
||||||
|
<el-button @click="selectTlsCertFile">{{ t('connection.selectFile') }}</el-button>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item :label="t('connection.tlsKeyPath')">
|
||||||
|
<div style="display:flex;gap:8px;">
|
||||||
|
<el-input v-model="form.tlsKeyPath" placeholder="/path/to/client-key.pem" />
|
||||||
|
<el-button @click="selectTlsKeyFile">{{ t('connection.selectFile') }}</el-button>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-checkbox v-model="form.tlsRejectUnauthorized">{{ t('connection.tlsRejectUnauthorized') }}</el-checkbox>
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- SSH Tunnel -->
|
<!-- SSH Tunnel -->
|
||||||
<el-divider />
|
<el-divider />
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ export default {
|
|||||||
password: 'Password',
|
password: 'Password',
|
||||||
username: 'Username',
|
username: 'Username',
|
||||||
tls: 'Enable TLS',
|
tls: 'Enable TLS',
|
||||||
|
tlsCaPath: 'CA Certificate',
|
||||||
|
tlsCertPath: 'Client Certificate',
|
||||||
|
tlsKeyPath: 'Client Private Key',
|
||||||
|
tlsRejectUnauthorized: 'Verify Server Certificate',
|
||||||
ssh: 'SSH Tunnel',
|
ssh: 'SSH Tunnel',
|
||||||
sshHost: 'SSH Host',
|
sshHost: 'SSH Host',
|
||||||
sshPort: 'SSH Port',
|
sshPort: 'SSH Port',
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ export default {
|
|||||||
password: '密码',
|
password: '密码',
|
||||||
username: '用户名',
|
username: '用户名',
|
||||||
tls: '启用 TLS',
|
tls: '启用 TLS',
|
||||||
|
tlsCaPath: 'CA 证书',
|
||||||
|
tlsCertPath: '客户端证书',
|
||||||
|
tlsKeyPath: '客户端私钥',
|
||||||
|
tlsRejectUnauthorized: '验证服务器证书',
|
||||||
ssh: 'SSH 隧道',
|
ssh: 'SSH 隧道',
|
||||||
sshHost: 'SSH 主机',
|
sshHost: 'SSH 主机',
|
||||||
sshPort: 'SSH 端口',
|
sshPort: 'SSH 端口',
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export interface ConnectionConfig {
|
|||||||
tlsCaPath: string
|
tlsCaPath: string
|
||||||
tlsCertPath: string
|
tlsCertPath: string
|
||||||
tlsKeyPath: string
|
tlsKeyPath: string
|
||||||
|
tlsRejectUnauthorized: boolean
|
||||||
sshEnabled: boolean
|
sshEnabled: boolean
|
||||||
sshHost: string
|
sshHost: string
|
||||||
sshPort: number
|
sshPort: number
|
||||||
@@ -83,6 +84,10 @@ export const useConnectionStore = defineStore('connection', () => {
|
|||||||
password: auth || undefined,
|
password: auth || undefined,
|
||||||
username: conn.username || undefined,
|
username: conn.username || undefined,
|
||||||
tls: conn.tls,
|
tls: conn.tls,
|
||||||
|
tlsCaPath: conn.tlsCaPath,
|
||||||
|
tlsCertPath: conn.tlsCertPath,
|
||||||
|
tlsKeyPath: conn.tlsKeyPath,
|
||||||
|
tlsRejectUnauthorized: conn.tlsRejectUnauthorized,
|
||||||
sshEnabled: conn.sshEnabled,
|
sshEnabled: conn.sshEnabled,
|
||||||
sshHost: conn.sshHost,
|
sshHost: conn.sshHost,
|
||||||
sshPort: conn.sshPort,
|
sshPort: conn.sshPort,
|
||||||
@@ -151,6 +156,7 @@ export const useConnectionStore = defineStore('connection', () => {
|
|||||||
|
|
||||||
async function testConnection(opts: {
|
async function testConnection(opts: {
|
||||||
host: string; port: number; auth?: string; username?: string; tls?: boolean
|
host: string; port: number; auth?: string; username?: string; tls?: boolean
|
||||||
|
tlsCaPath?: string; tlsCertPath?: string; tlsKeyPath?: string
|
||||||
sshEnabled?: boolean; sshHost?: string; sshPort?: number; sshUsername?: string
|
sshEnabled?: boolean; sshHost?: string; sshPort?: number; sshUsername?: string
|
||||||
sshPassword?: string; sshPrivateKeyPath?: string; sshPassphrase?: string
|
sshPassword?: string; sshPrivateKeyPath?: string; sshPassphrase?: string
|
||||||
}): Promise<{ success: boolean; error?: string }> {
|
}): Promise<{ success: boolean; error?: string }> {
|
||||||
@@ -165,6 +171,10 @@ export const useConnectionStore = defineStore('connection', () => {
|
|||||||
password: opts.auth || undefined,
|
password: opts.auth || undefined,
|
||||||
username: opts.username || undefined,
|
username: opts.username || undefined,
|
||||||
tls: opts.tls,
|
tls: opts.tls,
|
||||||
|
tlsCaPath: opts.tlsCaPath,
|
||||||
|
tlsCertPath: opts.tlsCertPath,
|
||||||
|
tlsKeyPath: opts.tlsKeyPath,
|
||||||
|
tlsRejectUnauthorized: (opts as any).tlsRejectUnauthorized,
|
||||||
sshEnabled: opts.sshEnabled,
|
sshEnabled: opts.sshEnabled,
|
||||||
sshHost: opts.sshHost,
|
sshHost: opts.sshHost,
|
||||||
sshPort: opts.sshPort,
|
sshPort: opts.sshPort,
|
||||||
|
|||||||
Reference in New Issue
Block a user