From 4fee45cf8d513127880077b3f97b3aff8fa5b978 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Fri, 12 Jun 2026 11:11:16 +0800 Subject: [PATCH] fix(db): update redis auth and mongo document refresh --- .../components/document/DocumentBrowser.vue | 30 +++ crates/dbx-core/src/connection.rs | 2 +- crates/dbx-core/src/db/redis_driver.rs | 244 +++++++++++++++--- 3 files changed, 235 insertions(+), 41 deletions(-) diff --git a/apps/desktop/src/components/document/DocumentBrowser.vue b/apps/desktop/src/components/document/DocumentBrowser.vue index 87b6c63867..9028c97caa 100644 --- a/apps/desktop/src/components/document/DocumentBrowser.vue +++ b/apps/desktop/src/components/document/DocumentBrowser.vue @@ -365,6 +365,8 @@ const customSaveHandler = computed(() => ({ async function load() { loading.value = true; error.value = ""; + const previousSelectedIdx = selectedIdx.value; + const previousSelectedId = previousSelectedIdx === null ? null : documentIdentity(documents.value[previousSelectedIdx]); try { const filter = currentDocumentFilter(); const sort = sortInput.value.trim() || undefined; @@ -382,6 +384,7 @@ async function load() { lastGridColumns.value = [...keySet]; } total.value = result.total; + syncSelectedDocumentAfterLoad(previousSelectedIdx, previousSelectedId); } catch (e: unknown) { error.value = e instanceof Error ? e.message : String(e); } finally { @@ -414,6 +417,33 @@ function asRecord(value: unknown): JsonRecord { return {}; } +function documentIdentity(doc: JsonRecord | undefined): string | null { + const id = doc?._id; + if (id === null || id === undefined) return null; + return typeof id === "object" ? JSON.stringify(id) : String(id); +} + +function syncSelectedDocumentAfterLoad(previousSelectedIdx: number | null, previousSelectedId: string | null) { + if (isNew.value || previousSelectedIdx === null) return; + if (!documents.value.length) { + selectedIdx.value = null; + if (!isEditing.value) editJson.value = ""; + return; + } + + const nextIdx = previousSelectedId ? documents.value.findIndex((doc) => documentIdentity(doc) === previousSelectedId) : previousSelectedIdx < documents.value.length ? previousSelectedIdx : -1; + if (nextIdx < 0) { + selectedIdx.value = null; + if (!isEditing.value) editJson.value = ""; + return; + } + + selectedIdx.value = nextIdx; + if (!isEditing.value) { + editJson.value = JSON.stringify(documents.value[nextIdx], null, 2); + } +} + function selectDoc(idx: number) { selectedIdx.value = idx; editJson.value = JSON.stringify(documents.value[idx], null, 2); diff --git a/crates/dbx-core/src/connection.rs b/crates/dbx-core/src/connection.rs index c234d586ba..bb8467671e 100644 --- a/crates/dbx-core/src/connection.rs +++ b/crates/dbx-core/src/connection.rs @@ -412,7 +412,7 @@ impl AppState { )) } else { db::redis_driver::RedisConnection::Direct(tokio::sync::Mutex::new( - db::redis_driver::connect(&url, connect_timeout).await?, + db::redis_driver::connect_standalone(&db_config, &host, port, connect_timeout).await?, )) }; PoolKind::Redis(con) diff --git a/crates/dbx-core/src/db/redis_driver.rs b/crates/dbx-core/src/db/redis_driver.rs index f76d68fd35..4abcc15128 100644 --- a/crates/dbx-core/src/db/redis_driver.rs +++ b/crates/dbx-core/src/db/redis_driver.rs @@ -82,6 +82,12 @@ pub struct RedisClusterPool { pub password: String, } +#[derive(Debug, Clone, PartialEq, Eq)] +struct RedisAuthCandidate { + username: String, + password: String, +} + #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct RedisNodeEndpoint { pub host: String, @@ -90,15 +96,56 @@ pub struct RedisNodeEndpoint { pub async fn connect(url: &str, timeout: std::time::Duration) -> Result { let client = redis::Client::open(url).map_err(|e| format!("Redis connection failed: {e}"))?; + connect_client_with_timeout(client, timeout, "Redis").await +} + +pub async fn connect_standalone( + config: &ConnectionConfig, + host: &str, + port: u16, + timeout: std::time::Duration, +) -> Result { + let mut last_error = None; + for auth in redis_auth_candidates(&config.username, &config.password) { + let client = redis::Client::open(connection_info( + host, + port, + config.ssl, + config.redis_tls_insecure(), + &auth.username, + &auth.password, + redis_database_index(config), + )) + .map_err(|e| format!("Redis connection failed: {e}"))?; + match connect_client_with_timeout(client, timeout, "Redis").await { + Ok(con) => return Ok(con), + Err(err) if last_error.is_none() || is_redis_auth_error(&err) => { + let should_retry = is_redis_auth_error(&err); + last_error = Some(err); + if !should_retry { + break; + } + } + Err(err) => return Err(err), + } + } + Err(last_error.unwrap_or_else(|| "Redis connection failed".to_string())) +} + +async fn connect_client_with_timeout( + client: redis::Client, + timeout: std::time::Duration, + label: &str, +) -> Result { let mut con = tokio::time::timeout(timeout, client.get_multiplexed_async_connection()) .await - .map_err(|_| format!("Redis connection timed out ({}s)", timeout.as_secs()))? - .map_err(|e| format!("Redis connection failed: {e}"))?; + .map_err(|_| format!("{label} connection timed out ({}s)", timeout.as_secs()))? + .map_err(|e| format!("{label} connection failed: {e}"))?; tokio::time::timeout(timeout, redis::cmd("PING").query_async::(&mut con)) .await - .map_err(|_| format!("Redis ping timed out ({}s)", timeout.as_secs()))? - .map_err(|e| format!("Redis authentication failed or command rejected: {e}"))?; + .map_err(|_| format!("{label} ping timed out ({}s)", timeout.as_secs()))? + .map_err(|e| format!("{label} authentication failed or command rejected: {e}"))?; Ok(con) } @@ -128,39 +175,66 @@ pub async fn connect_sentinel(config: &ConnectionConfig) -> Result Result { let seed_nodes = redis_cluster_seed_nodes(config)?; - let cluster_nodes: Vec = seed_nodes - .iter() - .map(|endpoint| { - connection_info( - &endpoint.host, - endpoint.port, - config.ssl, - config.redis_tls_insecure(), - &config.username, - &config.password, - 0, - ) - }) - .collect(); - let client = ClusterClient::new(cluster_nodes).map_err(|e| format!("Redis cluster connection failed: {e}"))?; - let mut con = tokio::time::timeout(super::connection_timeout(), client.get_async_connection()) - .await - .map_err(|_| format!("Redis cluster connection timed out ({}s)", super::CONNECTION_TIMEOUT_SECS))? - .map_err(|e| format!("Redis cluster connection failed: {e}"))?; + let mut last_error = None; + for auth in redis_auth_candidates(&config.username, &config.password) { + let cluster_nodes: Vec = seed_nodes + .iter() + .map(|endpoint| { + connection_info( + &endpoint.host, + endpoint.port, + config.ssl, + config.redis_tls_insecure(), + &auth.username, + &auth.password, + 0, + ) + }) + .collect(); + let client = ClusterClient::new(cluster_nodes).map_err(|e| format!("Redis cluster connection failed: {e}"))?; + let mut con = match tokio::time::timeout(super::connection_timeout(), client.get_async_connection()) + .await + .map_err(|_| format!("Redis cluster connection timed out ({}s)", super::CONNECTION_TIMEOUT_SECS))? + .map_err(|e| format!("Redis cluster connection failed: {e}")) + { + Ok(con) => con, + Err(err) if last_error.is_none() || is_redis_auth_error(&err) => { + let should_retry = is_redis_auth_error(&err); + last_error = Some(err); + if should_retry { + continue; + } + break; + } + Err(err) => return Err(err), + }; - tokio::time::timeout(super::connection_timeout(), redis::cmd("PING").query_async::(&mut con)) - .await - .map_err(|_| format!("Redis cluster ping timed out ({}s)", super::CONNECTION_TIMEOUT_SECS))? - .map_err(|e| format!("Redis cluster authentication failed or command rejected: {e}"))?; - - Ok(RedisClusterPool { - connection: Mutex::new(con), - seed_nodes, - tls: config.ssl, - tls_insecure: config.redis_tls_insecure(), - username: config.username.clone(), - password: config.password.clone(), - }) + match tokio::time::timeout(super::connection_timeout(), redis::cmd("PING").query_async::(&mut con)) + .await + .map_err(|_| format!("Redis cluster ping timed out ({}s)", super::CONNECTION_TIMEOUT_SECS))? + .map_err(|e| format!("Redis cluster authentication failed or command rejected: {e}")) + { + Ok(_) => { + return Ok(RedisClusterPool { + connection: Mutex::new(con), + seed_nodes, + tls: config.ssl, + tls_insecure: config.redis_tls_insecure(), + username: auth.username, + password: auth.password, + }); + } + Err(err) if last_error.is_none() || is_redis_auth_error(&err) => { + let should_retry = is_redis_auth_error(&err); + last_error = Some(err); + if !should_retry { + break; + } + } + Err(err) => return Err(err), + } + } + Err(last_error.unwrap_or_else(|| "Redis cluster connection failed".to_string())) } fn redis_sentinel_nodes(config: &ConnectionConfig) -> Result, String> { @@ -273,6 +347,25 @@ fn redis_connection_info(username: &str, password: &str, db: i64) -> RedisConnec } } +fn redis_auth_candidates(username: &str, password: &str) -> Vec { + let username = username.trim(); + let password = password.trim(); + let mut candidates = vec![RedisAuthCandidate { username: username.to_string(), password: password.to_string() }]; + if !username.is_empty() && !password.is_empty() { + candidates.push(RedisAuthCandidate { username: String::new(), password: format!("{username}@{password}") }); + } + candidates +} + +fn redis_database_index(config: &ConnectionConfig) -> i64 { + config.effective_database().and_then(|database| database.parse::().ok()).unwrap_or(0) +} + +fn is_redis_auth_error(error: &str) -> bool { + let error = error.to_ascii_lowercase(); + error.contains("auth") || error.contains("wrongpass") || error.contains("invalid username-password") +} + fn non_empty_string(value: &str) -> Option { let value = value.trim(); if value.is_empty() { @@ -1530,11 +1623,12 @@ mod tests { use super::{ classify_command, connection_info, decode_cluster_cursor, encode_cluster_cursor, is_redis_json_type, parse_cluster_slots, parse_command_argv, parse_database_count, parse_redis_endpoint, parse_scan_keys, - parse_stream_entries, redis_command_raw_to_json, redis_json_raw_to_json, redis_json_value_preview, - redis_key_bytes_to_display, redis_key_bytes_to_raw, redis_key_raw_to_bytes, redis_key_value_preview, - redis_raw_to_json, redis_value_contains_binary, redis_value_matches_query, RedisCommandSafety, - RedisNodeEndpoint, RedisRawValue, + parse_stream_entries, redis_auth_candidates, redis_command_raw_to_json, redis_database_index, + redis_json_raw_to_json, redis_json_value_preview, redis_key_bytes_to_display, redis_key_bytes_to_raw, + redis_key_raw_to_bytes, redis_key_value_preview, redis_raw_to_json, redis_value_contains_binary, + redis_value_matches_query, RedisAuthCandidate, RedisCommandSafety, RedisNodeEndpoint, RedisRawValue, }; + use crate::models::connection::ConnectionConfig; use redis::ConnectionAddr; fn bulk(value: &str) -> RedisRawValue { @@ -1754,6 +1848,76 @@ mod tests { assert!(matches!(info.addr, ConnectionAddr::TcpTls { insecure: true, .. })); } + #[test] + fn redis_connection_info_preserves_acl_username_and_password() { + let info = connection_info("cache.example.com", 6379, false, false, "app-user", "secret", 0); + + assert_eq!(info.redis.username.as_deref(), Some("app-user")); + assert_eq!(info.redis.password.as_deref(), Some("secret")); + } + + #[test] + fn redis_auth_candidates_try_username_at_password_fallback() { + let candidates = redis_auth_candidates("app-user", "secret"); + + assert_eq!( + candidates, + vec![ + RedisAuthCandidate { username: "app-user".to_string(), password: "secret".to_string() }, + RedisAuthCandidate { username: String::new(), password: "app-user@secret".to_string() }, + ] + ); + } + + #[test] + fn redis_database_index_uses_numeric_database_only() { + let mut config = ConnectionConfig { + id: "redis".to_string(), + name: "Redis".to_string(), + db_type: crate::models::connection::DatabaseType::Redis, + driver_profile: None, + driver_label: None, + url_params: None, + host: "cache.example.com".to_string(), + port: 6379, + username: String::new(), + password: String::new(), + database: Some("4".to_string()), + visible_databases: None, + attached_databases: Vec::new(), + color: None, + transport_layers: Vec::new(), + connect_timeout_secs: crate::models::connection::default_connect_timeout_secs(), + query_timeout_secs: crate::models::connection::default_query_timeout_secs(), + idle_timeout_secs: crate::models::connection::default_idle_timeout_secs(), + ssl: false, + ca_cert_path: String::new(), + client_cert_path: String::new(), + client_key_path: String::new(), + sysdba: false, + oracle_connection_type: None, + connection_string: None, + redis_connection_mode: None, + redis_sentinel_master: String::new(), + redis_sentinel_nodes: String::new(), + redis_sentinel_username: String::new(), + redis_sentinel_password: String::new(), + redis_sentinel_tls: false, + redis_cluster_nodes: String::new(), + redis_key_separator: crate::models::connection::default_redis_key_separator(), + etcd_endpoints: String::new(), + external_config: None, + jdbc_driver_class: None, + jdbc_driver_paths: Vec::new(), + one_time: false, + read_only: false, + }; + + assert_eq!(redis_database_index(&config), 4); + config.database = Some("not-a-number".to_string()); + assert_eq!(redis_database_index(&config), 0); + } + #[test] fn encodes_and_decodes_cluster_scan_cursor() { let encoded = encode_cluster_cursor(12, 3456).unwrap();