From 8c1a75d3d500f0d472d944302d9e5e0896a6cdde Mon Sep 17 00:00:00 2001 From: SuLea-IT <105108570+SuLea-IT@users.noreply.github.com> Date: Wed, 6 May 2026 13:10:46 +0800 Subject: [PATCH 1/2] fix: improve mongodb legacy connection params --- crates/dbx-core/src/models/connection.rs | 83 ++++++++++++++++++- .../connection/ConnectionDialog.vue | 28 ++++++- src/i18n/locales/en.ts | 2 + src/i18n/locales/zh-CN.ts | 2 + 4 files changed, 109 insertions(+), 6 deletions(-) diff --git a/crates/dbx-core/src/models/connection.rs b/crates/dbx-core/src/models/connection.rs index d726b7ab04..7535984142 100644 --- a/crates/dbx-core/src/models/connection.rs +++ b/crates/dbx-core/src/models/connection.rs @@ -121,7 +121,8 @@ impl ConnectionConfig { if let Some(cs) = self.connection_string.as_deref().filter(|s| !s.is_empty()) { return cs.to_string(); } - format!("mongodb://{host}:{port}{db_part}") + let suffix = if params.is_empty() { String::new() } else { format!("?{params}") }; + format!("mongodb://{host}:{port}{db_part}{suffix}") } DatabaseType::Oracle => format!("oracle://{host}:{port}{db_part}"), DatabaseType::Elasticsearch => format!("http://{host}:{port}"), @@ -172,10 +173,11 @@ impl ConnectionConfig { if let Some(cs) = self.connection_string.as_deref().filter(|s| !s.is_empty()) { return cs.to_string(); } + let suffix = if params.is_empty() { String::new() } else { format!("?{params}") }; if self.username.is_empty() { - format!("mongodb://{host}:{port}{db_part}") + format!("mongodb://{host}:{port}{db_part}{suffix}") } else { - format!("mongodb://{username}:{password}@{host}:{port}{db_part}") + format!("mongodb://{username}:{password}@{host}:{port}{db_part}{suffix}") } } DatabaseType::Oracle => { @@ -233,9 +235,33 @@ impl ConnectionConfig { } } DatabaseType::Postgres | DatabaseType::Redshift => value.trim_start_matches('?').to_string(), + DatabaseType::MongoDb => self.normalized_mongodb_url_params(value), _ => value.trim_start_matches('?').to_string(), } } + + fn normalized_mongodb_url_params(&self, value: &str) -> String { + let mut params: Vec = value + .trim_start_matches('?') + .split('&') + .filter(|param| !param.trim().is_empty()) + .map(str::to_string) + .collect(); + + push_query_param_if_missing(&mut params, "directConnection", "directConnection=true".to_string()); + + if !self.username.is_empty() { + let auth_source = self.database.as_deref().filter(|db| !db.is_empty()).unwrap_or("admin"); + push_query_param_if_missing( + &mut params, + "authSource", + format!("authSource={}", encode_url_part(auth_source)), + ); + push_query_param_if_missing(&mut params, "authMechanism", "authMechanism=SCRAM-SHA-1".to_string()); + } + + params.join("&") + } } fn encode_url_part(value: &str) -> String { @@ -250,6 +276,15 @@ fn bracket_ipv6(host: &str) -> String { } } +fn push_query_param_if_missing(params: &mut Vec, key: &str, value: String) { + if !params + .iter() + .any(|param| param.split_once('=').map(|(name, _)| name).unwrap_or(param.as_str()).eq_ignore_ascii_case(key)) + { + params.push(value); + } +} + #[cfg(test)] mod tests { use super::{ConnectionConfig, DatabaseType}; @@ -281,6 +316,13 @@ mod tests { } } + fn mongodb_config(username: &str, password: &str, database: Option<&str>) -> ConnectionConfig { + let mut config = mysql_config(username, password, database); + config.db_type = DatabaseType::MongoDb; + config.port = 17000; + config + } + #[test] fn mysql_url_encodes_oceanbase_username() { let config = mysql_config("user@tenant#cluster", "secret", None); @@ -321,6 +363,27 @@ mod tests { assert_eq!(config.connection_url(), "postgres://postgres:secret@10.1.2.3:2883/test?sslmode=disable"); } + #[test] + fn mongodb_form_url_adds_legacy_direct_connection_params() { + let config = mongodb_config("root", "secret", Some("admin")); + + assert_eq!( + config.connection_url(), + "mongodb://root:secret@10.1.2.3:17000/admin?directConnection=true&authSource=admin&authMechanism=SCRAM-SHA-1" + ); + } + + #[test] + fn mongodb_form_url_respects_custom_auth_params() { + let mut config = mongodb_config("root", "secret", Some("app")); + config.url_params = Some("authSource=admin&authMechanism=SCRAM-SHA-256&retryWrites=false".to_string()); + + assert_eq!( + config.connection_url(), + "mongodb://root:secret@10.1.2.3:17000/app?authSource=admin&authMechanism=SCRAM-SHA-256&retryWrites=false&directConnection=true" + ); + } + #[test] fn redacted_mysql_url_omits_credentials() { let config = mysql_config("user@tenant#cluster", "p@ss:word#1", Some("db/name")); @@ -357,4 +420,18 @@ mod tests { assert!(!url.contains("default")); assert!(!url.contains("redis-secret")); } + + #[test] + fn redacted_mongodb_url_keeps_compatibility_params_without_credentials() { + let config = mongodb_config("root", "secret", Some("admin")); + + let url = config.redacted_connection_url(); + + assert_eq!( + url, + "mongodb://10.1.2.3:17000/admin?directConnection=true&authSource=admin&authMechanism=SCRAM-SHA-1" + ); + assert!(!url.contains("root")); + assert!(!url.contains("secret")); + } } diff --git a/src/components/connection/ConnectionDialog.vue b/src/components/connection/ConnectionDialog.vue index e745668a6f..93e449e09e 100644 --- a/src/components/connection/ConnectionDialog.vue +++ b/src/components/connection/ConnectionDialog.vue @@ -401,7 +401,7 @@ async function testConnection() { isTesting.value = true; testResult.value = null; try { - const config: ConnectionConfig = { ...form.value, id: editingId.value || uuid() }; + const config = connectionConfigForSubmit(editingId.value || uuid()); const msg = await api.testConnection(config); if (runId !== testRunId) return; testResult.value = { ok: true, message: msg }; @@ -415,6 +415,14 @@ async function testConnection() { } } +function connectionConfigForSubmit(id: string): ConnectionConfig { + const config: ConnectionConfig = { ...form.value, id }; + if (config.db_type === "mongodb" && !mongoUseUrl.value) { + config.connection_string = undefined; + } + return config; +} + function resetTestState() { testRunId += 1; isTesting.value = false; @@ -462,11 +470,11 @@ async function save() { resetTestState(); try { if (editingId.value) { - const updated: ConnectionConfig = { ...form.value, id: editingId.value }; + const updated = connectionConfigForSubmit(editingId.value); await store.updateConnection(updated); store.stopEditing(); } else { - const config: ConnectionConfig = { ...form.value, id: uuid() }; + const config = connectionConfigForSubmit(uuid()); await store.addConnection(config); open.value = false; await nextTick(); @@ -757,6 +765,20 @@ async function browseSshKeyPath() { :placeholder="t('connection.databasePlaceholder')" /> +
+ + +
+
+ +

+ {{ t("connection.mongoLegacyHint") }} +

+
diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index aae68e7528..1df26c9755 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -112,6 +112,8 @@ export default { sshKeyPathBrowse: "Browse", sshExposeLan: "Expose tunnel to LAN", dmCompatHint: "Requires DM PG compatibility mode (set COMPATIBLE_MODE=7 in dm.ini and restart)", + mongoLegacyHint: + "MongoDB form connections add directConnection automatically; authenticated connections default to the selected database as auth source (admin when blank) and SCRAM-SHA-1, which you can override here.", compatible: "Compatible", mainstream: "Popular", color: "Color", diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index a2885f03c8..65afdf5e2d 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -111,6 +111,8 @@ export default { sshKeyPathBrowse: "浏览", sshExposeLan: "允许局域网访问隧道", dmCompatHint: "需要开启达梦 PG 兼容模式(dm.ini 中设置 COMPATIBLE_MODE=7 并重启服务)", + mongoLegacyHint: + "MongoDB 表单连接会自动补充 directConnection;带用户名时默认使用当前数据库认证源(留空为 admin)和 SCRAM-SHA-1,可在此覆盖。", compatible: "兼容", mainstream: "主流", color: "颜色", From 47f507f9ba38e7aed7ed6f574e79690aa03470af Mon Sep 17 00:00:00 2001 From: SuLea-IT <105108570+SuLea-IT@users.noreply.github.com> Date: Wed, 6 May 2026 13:42:35 +0800 Subject: [PATCH 2/2] fix: address mongodb params review --- crates/dbx-core/src/models/connection.rs | 60 +++++------------------- src/i18n/locales/en.ts | 2 +- src/i18n/locales/zh-CN.ts | 2 +- 3 files changed, 14 insertions(+), 50 deletions(-) diff --git a/crates/dbx-core/src/models/connection.rs b/crates/dbx-core/src/models/connection.rs index 7535984142..6a3ddd9f3b 100644 --- a/crates/dbx-core/src/models/connection.rs +++ b/crates/dbx-core/src/models/connection.rs @@ -234,34 +234,12 @@ impl ConnectionConfig { format!("ssl-mode=disabled&{}", filtered.join("&")) } } - DatabaseType::Postgres | DatabaseType::Redshift => value.trim_start_matches('?').to_string(), - DatabaseType::MongoDb => self.normalized_mongodb_url_params(value), + DatabaseType::Postgres | DatabaseType::Redshift | DatabaseType::MongoDb => { + value.trim_start_matches('?').to_string() + } _ => value.trim_start_matches('?').to_string(), } } - - fn normalized_mongodb_url_params(&self, value: &str) -> String { - let mut params: Vec = value - .trim_start_matches('?') - .split('&') - .filter(|param| !param.trim().is_empty()) - .map(str::to_string) - .collect(); - - push_query_param_if_missing(&mut params, "directConnection", "directConnection=true".to_string()); - - if !self.username.is_empty() { - let auth_source = self.database.as_deref().filter(|db| !db.is_empty()).unwrap_or("admin"); - push_query_param_if_missing( - &mut params, - "authSource", - format!("authSource={}", encode_url_part(auth_source)), - ); - push_query_param_if_missing(&mut params, "authMechanism", "authMechanism=SCRAM-SHA-1".to_string()); - } - - params.join("&") - } } fn encode_url_part(value: &str) -> String { @@ -276,15 +254,6 @@ fn bracket_ipv6(host: &str) -> String { } } -fn push_query_param_if_missing(params: &mut Vec, key: &str, value: String) { - if !params - .iter() - .any(|param| param.split_once('=').map(|(name, _)| name).unwrap_or(param.as_str()).eq_ignore_ascii_case(key)) - { - params.push(value); - } -} - #[cfg(test)] mod tests { use super::{ConnectionConfig, DatabaseType}; @@ -364,23 +333,20 @@ mod tests { } #[test] - fn mongodb_form_url_adds_legacy_direct_connection_params() { + fn mongodb_form_url_without_params_does_not_force_topology_or_auth() { let config = mongodb_config("root", "secret", Some("admin")); - assert_eq!( - config.connection_url(), - "mongodb://root:secret@10.1.2.3:17000/admin?directConnection=true&authSource=admin&authMechanism=SCRAM-SHA-1" - ); + assert_eq!(config.connection_url(), "mongodb://root:secret@10.1.2.3:17000/admin"); } #[test] - fn mongodb_form_url_respects_custom_auth_params() { + fn mongodb_form_url_appends_custom_params() { let mut config = mongodb_config("root", "secret", Some("app")); - config.url_params = Some("authSource=admin&authMechanism=SCRAM-SHA-256&retryWrites=false".to_string()); + config.url_params = Some("?authSource=admin&authMechanism=SCRAM-SHA-1&directConnection=true".to_string()); assert_eq!( config.connection_url(), - "mongodb://root:secret@10.1.2.3:17000/app?authSource=admin&authMechanism=SCRAM-SHA-256&retryWrites=false&directConnection=true" + "mongodb://root:secret@10.1.2.3:17000/app?authSource=admin&authMechanism=SCRAM-SHA-1&directConnection=true" ); } @@ -422,15 +388,13 @@ mod tests { } #[test] - fn redacted_mongodb_url_keeps_compatibility_params_without_credentials() { - let config = mongodb_config("root", "secret", Some("admin")); + fn redacted_mongodb_url_keeps_custom_params_without_credentials() { + let mut config = mongodb_config("root", "secret", Some("admin")); + config.url_params = Some("authSource=admin&authMechanism=SCRAM-SHA-1".to_string()); let url = config.redacted_connection_url(); - assert_eq!( - url, - "mongodb://10.1.2.3:17000/admin?directConnection=true&authSource=admin&authMechanism=SCRAM-SHA-1" - ); + assert_eq!(url, "mongodb://10.1.2.3:17000/admin?authSource=admin&authMechanism=SCRAM-SHA-1"); assert!(!url.contains("root")); assert!(!url.contains("secret")); } diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 1df26c9755..3e67c68983 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -113,7 +113,7 @@ export default { sshExposeLan: "Expose tunnel to LAN", dmCompatHint: "Requires DM PG compatibility mode (set COMPATIBLE_MODE=7 in dm.ini and restart)", mongoLegacyHint: - "MongoDB form connections add directConnection automatically; authenticated connections default to the selected database as auth source (admin when blank) and SCRAM-SHA-1, which you can override here.", + "For older MongoDB servers, enter authSource=admin&authMechanism=SCRAM-SHA-1 here; add directConnection=true only when needed for direct standalone connections.", compatible: "Compatible", mainstream: "Popular", color: "Color", diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index 65afdf5e2d..6ceb76fde4 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -112,7 +112,7 @@ export default { sshExposeLan: "允许局域网访问隧道", dmCompatHint: "需要开启达梦 PG 兼容模式(dm.ini 中设置 COMPATIBLE_MODE=7 并重启服务)", mongoLegacyHint: - "MongoDB 表单连接会自动补充 directConnection;带用户名时默认使用当前数据库认证源(留空为 admin)和 SCRAM-SHA-1,可在此覆盖。", + "连接旧版 MongoDB 时,可在此填写 authSource=admin&authMechanism=SCRAM-SHA-1;直连单节点时可按需追加 directConnection=true。", compatible: "兼容", mainstream: "主流", color: "颜色",