From a0298887251415233c9cc0430ece5f2625474748 Mon Sep 17 00:00:00 2001 From: Abeautifulsnow Date: Wed, 12 Aug 2026 19:34:00 +0800 Subject: [PATCH] feat(connection): allow opting out of password storage --- .../connection/ConnectionDialog.vue | 22 +++ .../ConnectionPasswordPromptDialog.vue | 70 +++++++++ .../src/components/layout/AppDialogs.vue | 2 + apps/desktop/src/i18n/locales/en.ts | 7 + apps/desktop/src/i18n/locales/es.ts | 7 + apps/desktop/src/i18n/locales/it.ts | 7 + apps/desktop/src/i18n/locales/ja.ts | 7 + apps/desktop/src/i18n/locales/ko.ts | 7 + apps/desktop/src/i18n/locales/pt-BR.ts | 7 + apps/desktop/src/i18n/locales/zh-CN.ts | 7 + apps/desktop/src/i18n/locales/zh-TW.ts | 7 + .../connectionStore.savePassword.spec.ts | 148 ++++++++++++++++++ .../stores/connectionPasswordPromptStore.ts | 60 +++++++ apps/desktop/src/stores/connectionStore.ts | 31 ++++ apps/desktop/src/types/database.ts | 6 + .../examples/table_import_live_bench.rs | 1 + crates/dbx-core/src/agent_connection.rs | 1 + crates/dbx-core/src/agent_tools.rs | 1 + crates/dbx-core/src/ai_qoder_cli.rs | 4 +- crates/dbx-core/src/cloud_sync.rs | 69 +++++++- crates/dbx-core/src/connection.rs | 1 + crates/dbx-core/src/connection_secrets.rs | 1 + crates/dbx-core/src/db/redis_driver.rs | 1 + crates/dbx-core/src/models/connection.rs | 12 ++ crates/dbx-core/src/mq/config.rs | 1 + crates/dbx-core/src/mq/service.rs | 1 + crates/dbx-core/src/nacos/config.rs | 1 + crates/dbx-core/src/nacos/service.rs | 2 + crates/dbx-core/src/production_safety.rs | 1 + crates/dbx-core/src/query.rs | 2 + crates/dbx-core/src/schema.rs | 1 + crates/dbx-core/src/schema/normalization.rs | 1 + crates/dbx-core/src/storage.rs | 84 +++++++++- crates/dbx-core/src/table_import.rs | 1 + crates/dbx-core/src/transfer.rs | 3 +- .../tests/database_export_prefetch.rs | 1 + crates/dbx-core/tests/dump_docs_fixture.rs | 1 + .../tests/live_postgres_docs_annotations.rs | 1 + .../tests/live_postgres_docs_snapshot.rs | 1 + .../live_postgres_query_result_export.rs | 1 + .../dbx-core/tests/live_postgres_transfer.rs | 1 + .../tests/live_sqlserver_completion.rs | 1 + .../live_sqlserver_query_result_export.rs | 1 + crates/dbx-mcp/tests/local.rs | 2 +- crates/dbx-web/src/routes/connection.rs | 1 + crates/dbx-web/src/routes/transfer.rs | 1 + src-tauri/src/commands/connection.rs | 1 + 47 files changed, 585 insertions(+), 12 deletions(-) create mode 100644 apps/desktop/src/components/connection/ConnectionPasswordPromptDialog.vue create mode 100644 apps/desktop/src/stores/__tests__/connectionStore.savePassword.spec.ts create mode 100644 apps/desktop/src/stores/connectionPasswordPromptStore.ts diff --git a/apps/desktop/src/components/connection/ConnectionDialog.vue b/apps/desktop/src/components/connection/ConnectionDialog.vue index b04c7e66b6..086bd5aed4 100644 --- a/apps/desktop/src/components/connection/ConnectionDialog.vue +++ b/apps/desktop/src/components/connection/ConnectionDialog.vue @@ -320,6 +320,7 @@ const defaultForm = (): ConnectionForm => ({ is_production: false, production_databases: [], visible_databases: undefined, + save_password: true, }); const elasticsearchConnectionMode = ref("direct"); @@ -2470,6 +2471,7 @@ watch( production_databases: config.production_databases || [], visible_databases: config.visible_databases, visible_schemas: config.visible_schemas, + save_password: config.save_password !== false, }; oracleTnsAdminPath.value = parseOracleTnsConnectionString(config.connection_string)?.tnsAdmin || ""; productionProtectionEnabled.value = !!config.is_production || (config.production_databases?.length ?? 0) > 0; @@ -3755,6 +3757,9 @@ function connectionConfigForSubmit(id: string, generatedName = ""): ConnectionCo } if (!config.one_time) config.one_time = undefined; if (!config.read_only) config.read_only = undefined; + // Save-password is a positive default: only an explicit unchecked state (false) + // is persisted; anything else keeps the current behavior. + config.save_password = config.save_password !== false; if ((isSingleDatabase(config.db_type) || config.db_type === "mq" || config.db_type === "mqtt") && config.production_databases?.length) { // Single-database / MQ drivers expose no independently selectable database list for PROD scope. config.is_production = true; @@ -5814,6 +5819,14 @@ function openExternalUrl(url: string) { +
+ + +
@@ -6953,6 +6966,15 @@ function openExternalUrl(url: string) {
+
+ + +
+
diff --git a/apps/desktop/src/components/connection/ConnectionPasswordPromptDialog.vue b/apps/desktop/src/components/connection/ConnectionPasswordPromptDialog.vue new file mode 100644 index 0000000000..3856992f91 --- /dev/null +++ b/apps/desktop/src/components/connection/ConnectionPasswordPromptDialog.vue @@ -0,0 +1,70 @@ + + + diff --git a/apps/desktop/src/components/layout/AppDialogs.vue b/apps/desktop/src/components/layout/AppDialogs.vue index 7ebb11d262..6fa9fe68a8 100644 --- a/apps/desktop/src/components/layout/AppDialogs.vue +++ b/apps/desktop/src/components/layout/AppDialogs.vue @@ -17,6 +17,7 @@ const FieldLineageDialog = defineAsyncComponent(() => import("@/components/linea const ConfigPassphraseDialog = defineAsyncComponent(() => import("@/components/config/ConfigPassphraseDialog.vue")); const DatabaseSearchDialog = defineAsyncComponent(() => import("@/components/search/DatabaseSearchDialog.vue")); const SshHostKeyPromptDialog = defineAsyncComponent(() => import("@/components/ssh/SshHostKeyPromptDialog.vue")); +const ConnectionPasswordPromptDialog = defineAsyncComponent(() => import("@/components/connection/ConnectionPasswordPromptDialog.vue")); const DatabaseExportDialog = defineAsyncComponent(() => import("@/components/export/DatabaseExportDialog.vue")); const DataGenerateDialog = defineAsyncComponent(() => import("@/components/generate/DataGenerateDialog.vue")); import { useConnectionStore } from "@/stores/connectionStore"; @@ -300,4 +301,5 @@ watch( + diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index fa4fe5c408..a64a1259f4 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -430,6 +430,13 @@ export default { sqliteExtensionBrowse: "Select SQLite extension", user: "User", password: "Password", + savePassword: "Save password", + savePasswordHint: "Uncheck to require entering it on every connect", + promptPasswordTitle: "Enter password", + promptPasswordMessage: "Enter the password for connection “{connection}”. It will not be saved locally.", + promptPasswordPlaceholder: "Password", + promptPasswordSubmit: "Connect", + promptPasswordCancel: "Cancel", database: "Database", databasePlaceholder: "Optional", databasePlaceholderRequired: "Required, enter an existing database", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index 11bdc30f0b..f2b3627121 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -431,6 +431,13 @@ export default withEnglishFallback({ sqliteExtensionBrowse: "Seleccionar extensión de SQLite", user: "Usuario", password: "Contraseña", + savePassword: "Guardar contraseña", + savePasswordHint: "Desmarcar para solicitarla en cada conexión", + promptPasswordTitle: "Introducir contraseña", + promptPasswordMessage: "Introduzca la contraseña de la conexión “{connection}”. No se guardará localmente.", + promptPasswordPlaceholder: "Contraseña", + promptPasswordSubmit: "Conectar", + promptPasswordCancel: "Cancelar", database: "Base de datos", databasePlaceholder: "Opcional", databasePlaceholderRequired: "Obligatorio, ingresa una base de datos existente", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index a0bcd75175..7d825b0d34 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -430,6 +430,13 @@ export default withEnglishFallback({ sqliteExtensionBrowse: "Seleziona estensione SQLite", user: "Utente", password: "Password", + savePassword: "Salva password", + savePasswordHint: "Deseleziona per richiederla a ogni connessione", + promptPasswordTitle: "Inserisci password", + promptPasswordMessage: "Inserisci la password per la connessione “{connection}”. Non verrà salvata localmente.", + promptPasswordPlaceholder: "Password", + promptPasswordSubmit: "Connetti", + promptPasswordCancel: "Annulla", database: "Database", databasePlaceholder: "Opzionale", databasePlaceholderRequired: "Obbligatorio, inserisci un database esistente", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index 2698720455..1cab150621 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -430,6 +430,13 @@ export default withEnglishFallback({ sqliteExtensionBrowse: "SQLite拡張を選択", user: "ユーザー", password: "パスワード", + savePassword: "パスワードを保存", + savePasswordHint: "チェックを外すと、接続のたびに入力が必要です", + promptPasswordTitle: "パスワードを入力", + promptPasswordMessage: "接続「{connection}」のパスワードを入力してください。ローカルには保存されません。", + promptPasswordPlaceholder: "パスワード", + promptPasswordSubmit: "接続", + promptPasswordCancel: "キャンセル", database: "データベース", databasePlaceholder: "任意", databasePlaceholderRequired: "必須、既存のデータベースを入力", diff --git a/apps/desktop/src/i18n/locales/ko.ts b/apps/desktop/src/i18n/locales/ko.ts index 63c16b8a4a..da5dd391f4 100644 --- a/apps/desktop/src/i18n/locales/ko.ts +++ b/apps/desktop/src/i18n/locales/ko.ts @@ -424,6 +424,13 @@ export default withEnglishFallback({ sqliteExtensionBrowse: "SQLite 확장 선택", user: "사용자", password: "비밀번호", + savePassword: "비밀번호 저장", + savePasswordHint: "체크 해제 시 연결할 때마다 입력해야 합니다", + promptPasswordTitle: "비밀번호 입력", + promptPasswordMessage: "연결 “{connection}”의 비밀번호를 입력하세요. 로컬에 저장되지 않습니다.", + promptPasswordPlaceholder: "비밀번호", + promptPasswordSubmit: "연결", + promptPasswordCancel: "취소", database: "데이터베이스", databasePlaceholder: "선택", databasePlaceholderRequired: "필수, 기존 데이터베이스 입력", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index 373193b7a1..31c436134b 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -431,6 +431,13 @@ export default withEnglishFallback({ sqliteExtensionBrowse: "Selecionar extensão SQLite", user: "Usuário", password: "Senha", + savePassword: "Salvar senha", + savePasswordHint: "Desmarque para exigir o preenchimento em cada conexão", + promptPasswordTitle: "Digite a senha", + promptPasswordMessage: "Digite a senha da conexão “{connection}”. Ela não será salva localmente.", + promptPasswordPlaceholder: "Senha", + promptPasswordSubmit: "Conectar", + promptPasswordCancel: "Cancelar", database: "Banco de dados", databasePlaceholder: "Opcional", databasePlaceholderRequired: "Obrigatório, informe um banco de dados existente", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index 320e9e6802..5ba41fe445 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -356,6 +356,13 @@ export default withEnglishFallback({ sqliteExtensionBrowse: "选择 SQLite 扩展库", user: "用户名", password: "密码", + savePassword: "本地保存密码", + savePasswordHint: "取消勾选后,每次连接都需要输入密码", + promptPasswordTitle: "输入密码", + promptPasswordMessage: "请输入连接“{connection}”的密码,该密码不会保存在本地。", + promptPasswordPlaceholder: "密码", + promptPasswordSubmit: "连接", + promptPasswordCancel: "取消", database: "数据库", databasePlaceholder: "可选", databasePlaceholderRequired: "必填,请填写已存在的数据库", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index 53e4a99d56..85fdcafd1a 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -431,6 +431,13 @@ export default withEnglishFallback({ sqliteExtensionBrowse: "選擇 SQLite 擴充庫", user: "使用者名稱", password: "密碼", + savePassword: "本機儲存密碼", + savePasswordHint: "取消勾選後,每次連線都需要輸入密碼", + promptPasswordTitle: "輸入密碼", + promptPasswordMessage: "請輸入連線「{connection}」的密碼,該密碼不會儲存在本機。", + promptPasswordPlaceholder: "密碼", + promptPasswordSubmit: "連線", + promptPasswordCancel: "取消", database: "資料庫", databasePlaceholder: "可選", databasePlaceholderRequired: "必填,請填寫已存在的資料庫", diff --git a/apps/desktop/src/stores/__tests__/connectionStore.savePassword.spec.ts b/apps/desktop/src/stores/__tests__/connectionStore.savePassword.spec.ts new file mode 100644 index 0000000000..a4c90216d0 --- /dev/null +++ b/apps/desktop/src/stores/__tests__/connectionStore.savePassword.spec.ts @@ -0,0 +1,148 @@ +import { createPinia, setActivePinia } from "pinia"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import type { ConnectionConfig } from "@/types/database"; +import { CONNECTION_PASSWORD_REQUIRED_MESSAGE } from "@/stores/connectionStore"; + +function installLocalStorage() { + const data = new Map(); + vi.stubGlobal("localStorage", { + getItem: vi.fn((key: string) => data.get(key) ?? null), + setItem: vi.fn((key: string, value: string) => data.set(key, value)), + removeItem: vi.fn((key: string) => data.delete(key)), + }); +} + +function postgresConnection(overrides: Partial = {}): ConnectionConfig { + return { + id: "pg-1", + name: "Postgres", + db_type: "postgres", + host: "127.0.0.1", + port: 5432, + username: "postgres", + password: "", + database: "app", + read_only: false, + ...overrides, + } as ConnectionConfig; +} + +let requestPassword: ReturnType; + +function installApiMocks(extra: Record = {}) { + vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false })); + vi.doMock("@/lib/backend/api", () => ({ + checkConnectionHealth: vi.fn().mockResolvedValue(undefined), + connectDb: vi.fn().mockResolvedValue("pg-1"), + deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined), + saveConnections: vi.fn().mockResolvedValue(undefined), + saveSidebarLayout: vi.fn().mockResolvedValue(undefined), + connectionDatabaseInfo: vi.fn().mockResolvedValue(undefined), + listInstalledAgents: vi.fn().mockResolvedValue([]), + ...extra, + })); +} + +function installPasswordPromptMock() { + vi.doMock("@/stores/connectionPasswordPromptStore", () => ({ + useConnectionPasswordPromptStore: () => ({ + pending: null, + requestPassword, + }), + })); +} + +describe("connectionStore save_password opt-out", () => { + beforeEach(() => { + vi.resetModules(); + vi.unstubAllGlobals(); + installLocalStorage(); + setActivePinia(createPinia()); + requestPassword = vi.fn(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("addConnection blanks the in-memory password when save_password is false", async () => { + installApiMocks(); + const { useConnectionStore } = await import("@/stores/connectionStore"); + const store = useConnectionStore(); + + await store.addConnection(postgresConnection({ id: "no-save", save_password: false, password: "hunter2" })); + + expect(store.getConfig("no-save")?.password).toBe(""); + const { saveConnections } = await import("@/lib/backend/api"); + expect(saveConnections).toHaveBeenCalledWith(expect.arrayContaining([expect.objectContaining({ id: "no-save", password: "", save_password: false })])); + }); + + it("addConnection keeps the password for legacy configs without save_password", async () => { + installApiMocks(); + const { useConnectionStore } = await import("@/stores/connectionStore"); + const store = useConnectionStore(); + + await store.addConnection(postgresConnection({ id: "legacy", password: "s3cret" })); + + expect(store.getConfig("legacy")?.password).toBe("s3cret"); + }); + + it("updateConnection blanks the in-memory password when save_password is false", async () => { + installApiMocks(); + const { useConnectionStore } = await import("@/stores/connectionStore"); + const store = useConnectionStore(); + await store.addConnection(postgresConnection({ id: "pg-1", save_password: true, password: "old" })); + + await store.updateConnection(postgresConnection({ id: "pg-1", save_password: false, password: "hunter2" })); + + expect(store.getConfig("pg-1")?.password).toBe(""); + }); + + it("connect prompts for the password and uses it only for connectDb", async () => { + installApiMocks(); + installPasswordPromptMock(); + requestPassword.mockResolvedValue("typed-pw"); + const { useConnectionStore } = await import("@/stores/connectionStore"); + const store = useConnectionStore(); + const connection = postgresConnection({ id: "pg-1", save_password: false, password: "" }); + store.connections = [connection]; + + await store.connect(connection); + + expect(requestPassword).toHaveBeenCalledWith({ connectionId: "pg-1", connectionName: "Postgres" }); + const { connectDb } = await import("@/lib/backend/api"); + expect(connectDb).toHaveBeenCalledWith(expect.objectContaining({ id: "pg-1", password: "typed-pw" }), expect.any(Number)); + // The typed password is never written back to the store. + expect(store.getConfig("pg-1")?.password).toBe(""); + }); + + it("connect cancels when the prompt is dismissed", async () => { + installApiMocks(); + installPasswordPromptMock(); + requestPassword.mockResolvedValue(null); + const { useConnectionStore } = await import("@/stores/connectionStore"); + const store = useConnectionStore(); + const connection = postgresConnection({ id: "pg-1", save_password: false, password: "" }); + store.connections = [connection]; + + await expect(store.connect(connection)).rejects.toThrow(CONNECTION_PASSWORD_REQUIRED_MESSAGE); + + const { connectDb } = await import("@/lib/backend/api"); + expect(connectDb).not.toHaveBeenCalled(); + }); + + it("connect does not prompt for a saved-password connection or one with a password present", async () => { + installApiMocks(); + installPasswordPromptMock(); + const { useConnectionStore } = await import("@/stores/connectionStore"); + const store = useConnectionStore(); + const saved = postgresConnection({ id: "saved", save_password: true, password: "s3cret" }); + const fresh = postgresConnection({ id: "fresh", save_password: false, password: "typed" }); + store.connections = [saved, fresh]; + + await store.connect(saved); + await store.connect(fresh); + + expect(requestPassword).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/desktop/src/stores/connectionPasswordPromptStore.ts b/apps/desktop/src/stores/connectionPasswordPromptStore.ts new file mode 100644 index 0000000000..6aae800971 --- /dev/null +++ b/apps/desktop/src/stores/connectionPasswordPromptStore.ts @@ -0,0 +1,60 @@ +import { ref } from "vue"; +import { defineStore } from "pinia"; + +export interface ConnectionPasswordPromptRequest { + connectionId: string; + connectionName: string; +} + +interface QueuedPasswordPrompt { + request: ConnectionPasswordPromptRequest; + resolve: (password: string | null) => void; +} + +/** + * Coordinates the single "enter password for a connection that is set to not + * save it locally" dialog shared by every connect surface. Mirrors the + * production-safety confirmation store: a request is transient, never + * persisted, and concurrent connects are queued so each prompt is shown and + * answered exactly once. A `null` resolution means the user cancelled. + */ +export const useConnectionPasswordPromptStore = defineStore("connectionPasswordPrompt", () => { + const pending = ref(); + const queue: QueuedPasswordPrompt[] = []; + let resolvePending: ((password: string | null) => void) | undefined; + + function requestPassword(request: ConnectionPasswordPromptRequest): Promise { + return new Promise((resolve) => { + if (pending.value) { + queue.push({ request, resolve }); + return; + } + beginRequest(request, resolve); + }); + } + + function beginRequest(request: ConnectionPasswordPromptRequest, resolve: (password: string | null) => void) { + pending.value = request; + resolvePending = resolve; + } + + function settle(password: string | null) { + const resolve = resolvePending; + resolvePending = undefined; + pending.value = undefined; + resolve?.(password); + + const next = queue.shift(); + if (next) beginRequest(next.request, next.resolve); + } + + function submit(password: string) { + settle(password); + } + + function cancel() { + settle(null); + } + + return { pending, requestPassword, submit, cancel }; +}); diff --git a/apps/desktop/src/stores/connectionStore.ts b/apps/desktop/src/stores/connectionStore.ts index cb05edd46b..abe0a596d7 100644 --- a/apps/desktop/src/stores/connectionStore.ts +++ b/apps/desktop/src/stores/connectionStore.ts @@ -302,6 +302,8 @@ type MetadataListPageResult = TableInfo[] | ObjectInfo[]; type BeforeConnectHandler = (config: ConnectionConfig) => Promise; export const CONNECTION_ATTEMPT_CANCELLED_MESSAGE = "Connection attempt was cancelled"; +/** Thrown when a no-save-password connection is connected without a typed password. */ +export const CONNECTION_PASSWORD_REQUIRED_MESSAGE = "Password is required for this connection"; function metadataDriverProfile(config?: ConnectionConfig): string | undefined { return config?.driver_profile || config?.db_type; @@ -2339,6 +2341,7 @@ export const useConnectionStore = defineStore("connection", () => { async function addConnection(config: ConnectionConfig, targetGroupId?: string | null) { const normalized = normalizeConnection(config); + if (normalized.save_password === false) normalized.password = ""; await persistTimeoutInheritance(normalized.id, normalized.connect_timeout_inherit === true, normalized.query_timeout_inherit === true); const existing = connections.value.findIndex((c) => c.id === normalized.id); const nextConnections = [...connections.value]; @@ -2502,6 +2505,7 @@ export const useConnectionStore = defineStore("connection", () => { async function updateConnection(config: ConnectionConfig) { config = normalizeConnection(config); + if (config.save_password === false) config.password = ""; const idx = connections.value.findIndex((c) => c.id === config.id); if (idx < 0) return; const runtimeConfigChanged = connectionConfigFingerprint(connections.value[idx]) !== connectionConfigFingerprint(config); @@ -2827,11 +2831,32 @@ export const useConnectionStore = defineStore("connection", () => { } } + /** + * For a connection configured with `save_password === false` (password not + * persisted), prompt the user for the password and return a copy of the + * config carrying the typed value. The returned config is used only for the + * immediate `connectDb` call — it is never written back to the store, so the + * typed password is not persisted and the next connect prompts again. + */ + async function ensureConnectionPassword(config: ConnectionConfig): Promise { + if (config.save_password !== false || config.password) return config; + const { useConnectionPasswordPromptStore } = await import("@/stores/connectionPasswordPromptStore"); + const password = await useConnectionPasswordPromptStore().requestPassword({ + connectionId: config.id, + connectionName: config.name, + }); + if (!password) throw new Error(CONNECTION_PASSWORD_REQUIRED_MESSAGE); + return { ...config, password }; + } + async function connect(config: ConnectionConfig) { config = normalizeConnection(config); if (getBlockingDisconnectInFlight(config.id)) await waitForBlockingDisconnectInFlight(config.id); const localAttempt = beginLocalConnectionAttempt(config.id); try { + if (config.save_password === false && !config.password) { + config = await ensureConnectionPassword(config); + } await beforeConnectHandler?.(config); if (config.db_type === "sqlserver") { await ensureSqlServerLegacyCompatibilityComponentInstalled(config); @@ -3011,6 +3036,12 @@ export const useConnectionStore = defineStore("connection", () => { } const localAttempt = beginLocalConnectionAttempt(connectionId); const connectPromise = (async () => { + // Fast-path the common case (password saved or no password needed) so the + // in-flight dedup above keeps its exact microtask cadence; only await the + // interactive prompt when the connection actually needs a typed password. + if (config.save_password === false && !config.password) { + config = await ensureConnectionPassword(config); + } await beforeConnectHandler?.(config); if (config.db_type === "sqlserver") { await ensureSqlServerLegacyCompatibilityComponentInstalled(config); diff --git a/apps/desktop/src/types/database.ts b/apps/desktop/src/types/database.ts index 19ce7bd92b..bd7082baa3 100644 --- a/apps/desktop/src/types/database.ts +++ b/apps/desktop/src/types/database.ts @@ -185,6 +185,12 @@ export interface ConnectionConfig { informix_server?: string; external_config?: unknown; one_time?: boolean; + /** + * Whether the database password may be persisted locally. When false, the + * password is never written to local storage and the user is prompted on + * every connect. Absent/true keeps current behavior (password saved). + */ + save_password?: boolean; read_only?: boolean; /** Explicit production marker for every database reachable through this connection. */ is_production?: boolean; diff --git a/crates/dbx-core/examples/table_import_live_bench.rs b/crates/dbx-core/examples/table_import_live_bench.rs index 651107286d..0868777484 100644 --- a/crates/dbx-core/examples/table_import_live_bench.rs +++ b/crates/dbx-core/examples/table_import_live_bench.rs @@ -204,6 +204,7 @@ fn connection_config(id: &str, database: BenchDatabase) -> Result Result { let mut connection_secrets = Vec::new(); for config in connections { - push_secret(&mut connection_secrets, &config.id, "password", &config.password); + // A transient password must not become durable through a sync snapshot. + if config.save_password { + push_secret(&mut connection_secrets, &config.id, "password", &config.password); + } push_secret(&mut connection_secrets, &config.id, "init_script", config.init_script.as_deref().unwrap_or("")); for (index, layer) in config.transport_layers.iter().enumerate() { match layer { @@ -1173,7 +1176,11 @@ fn scrub_json_secret(object: &mut serde_json::Map, fi } } -async fn apply_sensitive_payload(storage: &Storage, payload: &SensitiveSyncPayload) -> Result<(), String> { +async fn apply_sensitive_payload( + storage: &Storage, + payload: &SensitiveSyncPayload, + connections: &[ConnectionConfig], +) -> Result<(), String> { for secret in &payload.connection_secrets { if !SECRET_KEYS.contains(&secret.key.as_str()) && !secret.key.starts_with(SSH_TUNNEL_SECRET_PREFIX) @@ -1181,6 +1188,13 @@ async fn apply_sensitive_payload(storage: &Storage, payload: &SensitiveSyncPaylo { continue; } + // Metadata is applied before secrets. Never let an older encrypted snapshot + // restore a password after the user chose not to retain it locally. + if secret.key == "password" + && connections.iter().any(|config| config.id == secret.connection_id && !config.save_password) + { + continue; + } storage.set_secret(&secret.connection_id, &secret.key, &secret.secret).await?; } if let Some(configs) = &payload.ai_configs { @@ -1744,6 +1758,7 @@ mod tests { jdbc_driver_class: None, jdbc_driver_paths: Vec::new(), one_time: false, + save_password: true, read_only: false, is_production: false, production_databases: vec![], @@ -1811,6 +1826,7 @@ mod tests { jdbc_driver_class: None, jdbc_driver_paths: Vec::new(), one_time: false, + save_password: true, read_only: false, is_production: false, production_databases: vec![], @@ -1962,6 +1978,7 @@ mod tests { jdbc_driver_class: None, jdbc_driver_paths: Vec::new(), one_time: false, + save_password: true, read_only: false, is_production: false, production_databases: vec![], @@ -2396,6 +2413,42 @@ mod tests { })); } + #[tokio::test] + async fn sync_restore_does_not_revive_password_when_connection_disables_saving() { + let source = Storage::open(&temp_db_path("sync-no-save-password-source")).await.unwrap(); + source.save_connections(&[postgres_connection("pg", "remote-secret")]).await.unwrap(); + let mut snapshot = build_sync_snapshot(&source, "test-version", None, Some("sync-pass")).await.unwrap(); + snapshot.connections[0].save_password = false; + + let target = Storage::open(&temp_db_path("sync-no-save-password-target")).await.unwrap(); + apply_sync_snapshot( + &target, + &snapshot, + ApplySnapshotOptions { secrets_passphrase: Some("sync-pass"), restore_secrets: true }, + ) + .await + .unwrap(); + + assert_eq!(target.get_secret("pg", "password").await.unwrap(), None); + assert!(target.load_connections().await.unwrap()[0].password.is_empty()); + } + + #[tokio::test] + async fn metadata_only_sync_removes_existing_password_when_connection_disables_saving() { + let source = Storage::open(&temp_db_path("sync-no-save-password-metadata-source")).await.unwrap(); + let mut source_connection = postgres_connection("pg", "unused"); + source_connection.save_password = false; + source.save_connections(&[source_connection]).await.unwrap(); + let snapshot = build_sync_snapshot(&source, "test-version", None, None).await.unwrap(); + + let target = Storage::open(&temp_db_path("sync-no-save-password-metadata-target")).await.unwrap(); + target.save_connections(&[postgres_connection("pg", "local-secret")]).await.unwrap(); + apply_sync_snapshot(&target, &snapshot, ApplySnapshotOptions::default()).await.unwrap(); + + assert_eq!(target.get_secret("pg", "password").await.unwrap(), None); + assert!(target.load_connections().await.unwrap()[0].password.is_empty()); + } + #[tokio::test] async fn saved_sync_passphrase_encrypts_nacos_auth_password_without_exposing_it() { let storage = Storage::open(&temp_db_path("saved-sync-nacos-snapshot")).await.unwrap(); @@ -2470,7 +2523,7 @@ mod tests { ai_config: None, tunnel_profiles: None, }; - apply_sensitive_payload(&storage, &payload).await.unwrap(); + apply_sensitive_payload(&storage, &payload, &[]).await.unwrap(); let loaded = storage.load_ai_configs().await.unwrap(); assert!(loaded.is_empty(), "None → no configs written"); } @@ -2490,7 +2543,7 @@ mod tests { ai_config: None, tunnel_profiles: None, }; - apply_sensitive_payload(&storage, &payload).await.unwrap(); + apply_sensitive_payload(&storage, &payload, &[]).await.unwrap(); let loaded = storage.load_ai_configs().await.unwrap(); assert!(loaded.is_empty(), "Some([]) → table cleared"); } @@ -2515,7 +2568,7 @@ mod tests { ai_config: None, tunnel_profiles: None, }; - apply_sensitive_payload(&storage, &payload).await.unwrap(); + apply_sensitive_payload(&storage, &payload, &[]).await.unwrap(); let loaded = storage.load_ai_configs().await.unwrap(); assert_eq!(loaded.len(), 2); let opencode = loaded.iter().find(|item| item.name == "synced").unwrap(); @@ -2548,9 +2601,9 @@ mod tests { tunnel_profiles: None, }; - apply_sensitive_payload(&storage, &payload).await.unwrap(); + apply_sensitive_payload(&storage, &payload, &[]).await.unwrap(); // Reapplying the same old snapshot must not collide with the generated ID. - apply_sensitive_payload(&storage, &payload).await.unwrap(); + apply_sensitive_payload(&storage, &payload, &[]).await.unwrap(); let loaded = storage.load_ai_configs().await.unwrap(); assert_eq!(loaded.len(), 1); diff --git a/crates/dbx-core/src/connection.rs b/crates/dbx-core/src/connection.rs index 238ac37424..aafe2cca94 100644 --- a/crates/dbx-core/src/connection.rs +++ b/crates/dbx-core/src/connection.rs @@ -5202,6 +5202,7 @@ mod tests { jdbc_driver_class: None, jdbc_driver_paths: Vec::new(), one_time: false, + save_password: true, read_only: false, is_production: false, production_databases: vec![], diff --git a/crates/dbx-core/src/connection_secrets.rs b/crates/dbx-core/src/connection_secrets.rs index fd59097983..5e7d7c28a2 100644 --- a/crates/dbx-core/src/connection_secrets.rs +++ b/crates/dbx-core/src/connection_secrets.rs @@ -874,6 +874,7 @@ mod tests { jdbc_driver_class: None, jdbc_driver_paths: Vec::new(), one_time: false, + save_password: true, read_only: false, is_production: false, production_databases: vec![], diff --git a/crates/dbx-core/src/db/redis_driver.rs b/crates/dbx-core/src/db/redis_driver.rs index e47342651b..b114e94d36 100644 --- a/crates/dbx-core/src/db/redis_driver.rs +++ b/crates/dbx-core/src/db/redis_driver.rs @@ -5398,6 +5398,7 @@ mod tests { jdbc_driver_class: None, jdbc_driver_paths: Vec::new(), one_time: false, + save_password: true, read_only: false, is_production: false, production_databases: vec![], diff --git a/crates/dbx-core/src/models/connection.rs b/crates/dbx-core/src/models/connection.rs index 274546f89f..96565ffb0f 100644 --- a/crates/dbx-core/src/models/connection.rs +++ b/crates/dbx-core/src/models/connection.rs @@ -170,6 +170,14 @@ pub struct ConnectionConfig { pub jdbc_driver_paths: Vec, #[serde(default, skip_serializing_if = "is_false")] pub one_time: bool, + /// Whether the database password may be persisted locally (SQLite + /// `connection_secrets`). When false, the `"password"` secret is never + /// written (or is deleted) and the user must type it on every connect. + /// Defaults to `true` so pre-existing saved connections keep current + /// behavior; a bare `#[serde(default)]` would upgrade them to "don't save" + /// and delete every stored password on the next save. + #[serde(default = "default_true")] + pub save_password: bool, #[serde(default, skip_serializing_if = "is_false")] pub read_only: bool, /// Explicitly marks every database reachable through this connection as production. @@ -691,6 +699,8 @@ struct ConnectionConfigData { pub jdbc_driver_paths: Vec, #[serde(default)] pub one_time: bool, + #[serde(default = "default_true")] + pub save_password: bool, #[serde(default)] pub read_only: bool, #[serde(default)] @@ -754,6 +764,7 @@ impl From for ConnectionConfig { jdbc_driver_class: data.jdbc_driver_class, jdbc_driver_paths: data.jdbc_driver_paths, one_time: data.one_time, + save_password: data.save_password, read_only: data.read_only, is_production: data.is_production, production_databases: data.production_databases, @@ -2425,6 +2436,7 @@ mod tests { jdbc_driver_class: None, jdbc_driver_paths: Vec::new(), one_time: false, + save_password: true, read_only: false, is_production: false, production_databases: vec![], diff --git a/crates/dbx-core/src/mq/config.rs b/crates/dbx-core/src/mq/config.rs index ae3fcf643c..b279fe3cb4 100644 --- a/crates/dbx-core/src/mq/config.rs +++ b/crates/dbx-core/src/mq/config.rs @@ -229,6 +229,7 @@ mod tests { jdbc_driver_class: None, jdbc_driver_paths: Vec::new(), one_time: false, + save_password: true, read_only: false, is_production: false, production_databases: vec![], diff --git a/crates/dbx-core/src/mq/service.rs b/crates/dbx-core/src/mq/service.rs index 3bfcacedf7..f58af5f5a7 100644 --- a/crates/dbx-core/src/mq/service.rs +++ b/crates/dbx-core/src/mq/service.rs @@ -920,6 +920,7 @@ mod tests { jdbc_driver_class: None, jdbc_driver_paths: Vec::new(), one_time: false, + save_password: true, read_only, is_production: false, production_databases: Vec::new(), diff --git a/crates/dbx-core/src/nacos/config.rs b/crates/dbx-core/src/nacos/config.rs index 42e415ee73..c126c6bb77 100644 --- a/crates/dbx-core/src/nacos/config.rs +++ b/crates/dbx-core/src/nacos/config.rs @@ -333,6 +333,7 @@ mod tests { jdbc_driver_class: None, jdbc_driver_paths: Vec::new(), one_time: false, + save_password: true, read_only: false, is_production: false, production_databases: vec![], diff --git a/crates/dbx-core/src/nacos/service.rs b/crates/dbx-core/src/nacos/service.rs index f24ac2d9c5..22714f67cd 100644 --- a/crates/dbx-core/src/nacos/service.rs +++ b/crates/dbx-core/src/nacos/service.rs @@ -403,6 +403,7 @@ mod tests { jdbc_driver_class: None, jdbc_driver_paths: Vec::new(), one_time: false, + save_password: true, read_only: true, is_production: false, production_databases: Vec::new(), @@ -477,6 +478,7 @@ mod tests { jdbc_driver_class: None, jdbc_driver_paths: Vec::new(), one_time: false, + save_password: true, read_only: true, is_production: false, production_databases: Vec::new(), diff --git a/crates/dbx-core/src/production_safety.rs b/crates/dbx-core/src/production_safety.rs index 35d828db5e..7e393d7d38 100644 --- a/crates/dbx-core/src/production_safety.rs +++ b/crates/dbx-core/src/production_safety.rs @@ -688,6 +688,7 @@ mod tests { informix_server: String::new(), external_config: None, one_time: false, + save_password: true, read_only: false, is_production: false, production_databases: vec!["prod_app".to_string()], diff --git a/crates/dbx-core/src/query.rs b/crates/dbx-core/src/query.rs index 82a85ec524..65d528bacc 100644 --- a/crates/dbx-core/src/query.rs +++ b/crates/dbx-core/src/query.rs @@ -4788,6 +4788,7 @@ for line in sys.stdin: jdbc_driver_class: None, jdbc_driver_paths: Vec::new(), one_time: false, + save_password: true, read_only: false, is_production: false, production_databases: vec![], @@ -6439,6 +6440,7 @@ for line in sys.stdin: jdbc_driver_class: None, jdbc_driver_paths: Vec::new(), one_time: false, + save_password: true, read_only: false, is_production: false, production_databases: vec![], diff --git a/crates/dbx-core/src/schema.rs b/crates/dbx-core/src/schema.rs index 429ed6ea27..8ab822f1ea 100644 --- a/crates/dbx-core/src/schema.rs +++ b/crates/dbx-core/src/schema.rs @@ -2819,6 +2819,7 @@ mod tests { jdbc_driver_class: None, jdbc_driver_paths: Vec::new(), one_time: false, + save_password: true, read_only: false, is_production: false, production_databases: vec![], diff --git a/crates/dbx-core/src/schema/normalization.rs b/crates/dbx-core/src/schema/normalization.rs index 6987f3fd28..3dda30601b 100644 --- a/crates/dbx-core/src/schema/normalization.rs +++ b/crates/dbx-core/src/schema/normalization.rs @@ -190,6 +190,7 @@ mod tests { jdbc_driver_class: None, jdbc_driver_paths: Vec::new(), one_time: false, + save_password: true, read_only: false, is_production: false, production_databases: vec![], diff --git a/crates/dbx-core/src/storage.rs b/crates/dbx-core/src/storage.rs index b02431a332..415261561c 100644 --- a/crates/dbx-core/src/storage.rs +++ b/crates/dbx-core/src/storage.rs @@ -2222,7 +2222,14 @@ fn persist_connection_in_tx(tx: &rusqlite::Transaction<'_>, config: &ConnectionC tx.execute("INSERT INTO connections (id, config_json) VALUES (?1, ?2)", params![config_id, json]) .map_err(|e| e.to_string())?; - persist_secret_in_tx(tx, &config.id, "password", &config.password)?; + if config.save_password { + persist_secret_in_tx(tx, &config.id, "password", &config.password)?; + } else { + // "Don't save password": write an empty value, which persist_secret_in_tx + // turns into a DELETE — the password secret is never persisted (and any + // previously stored secret is removed on this save). + persist_secret_in_tx(tx, &config.id, "password", "")?; + } delete_secret_prefix_in_tx(tx, &config.id, TRANSPORT_LAYER_SECRET_PREFIX)?; for (index, layer) in config.transport_layers.iter().enumerate() { match layer { @@ -2343,6 +2350,12 @@ impl Storage { for config in &configs { let config = config.canonicalized(); let config_id = config.id.clone(); + if !config.save_password { + // Metadata-only imports/sync preserve existing secrets by default. + // This preference is an exception: retaining the old password would + // make a no-save connection silently authenticate without prompting. + persist_secret_in_tx(&tx, &config.id, "password", "")?; + } let mut sanitized = config; sanitized.password = String::new(); scrub_transport_layer_secrets(&mut sanitized); @@ -4181,6 +4194,73 @@ mod tests { let _ = std::fs::remove_file(path); } + fn plain_connection(id: &str, password: &str) -> ConnectionConfig { + serde_json::from_value::(serde_json::json!({ + "id": id, + "name": format!("conn {id}"), + "db_type": "postgres", + "host": "127.0.0.1", + "port": 5432, + "username": "postgres", + "password": password, + "database": "app" + })) + .unwrap() + } + + #[tokio::test] + async fn save_connections_does_not_persist_password_when_save_password_false() { + let path = temp_db_path("save-password-false"); + let storage = Storage::open(&path).await.unwrap(); + + let mut config = plain_connection("no-save", "hunter2"); + config.save_password = false; + storage.save_connections(std::slice::from_ref(&config)).await.unwrap(); + + assert_eq!(storage.get_secret(&config.id, "password").await.unwrap(), None); + let loaded = storage.load_connections().await.unwrap(); + assert_eq!(loaded.len(), 1); + assert_eq!(loaded[0].password, ""); + assert!(!loaded[0].save_password); + + let _ = std::fs::remove_file(path); + } + + #[tokio::test] + async fn save_connections_persists_password_when_save_password_true() { + let path = temp_db_path("save-password-true"); + let storage = Storage::open(&path).await.unwrap(); + + let config = plain_connection("save-yes", "hunter2"); + storage.save_connections(std::slice::from_ref(&config)).await.unwrap(); + + assert_eq!(storage.get_secret(&config.id, "password").await.unwrap().as_deref(), Some("hunter2")); + let loaded = storage.load_connections().await.unwrap(); + assert_eq!(loaded[0].password, "hunter2"); + assert!(loaded[0].save_password); + + let _ = std::fs::remove_file(path); + } + + #[tokio::test] + async fn switching_save_password_off_removes_stored_password() { + let path = temp_db_path("save-password-switch-off"); + let storage = Storage::open(&path).await.unwrap(); + + let mut config = plain_connection("switch", "hunter2"); + storage.save_connections(std::slice::from_ref(&config)).await.unwrap(); + assert_eq!(storage.get_secret(&config.id, "password").await.unwrap().as_deref(), Some("hunter2")); + + config.save_password = false; + storage.save_connections(std::slice::from_ref(&config)).await.unwrap(); + assert_eq!(storage.get_secret(&config.id, "password").await.unwrap(), None); + let loaded = storage.load_connections().await.unwrap(); + assert_eq!(loaded[0].password, ""); + assert!(!loaded[0].save_password); + + let _ = std::fs::remove_file(path); + } + fn mq_connection(id: &str, token: &str) -> ConnectionConfig { ConnectionConfig { docs_notes_path: None, @@ -4240,6 +4320,7 @@ mod tests { jdbc_driver_class: None, jdbc_driver_paths: Vec::new(), one_time: false, + save_password: true, read_only: false, is_production: false, production_databases: vec![], @@ -4307,6 +4388,7 @@ mod tests { jdbc_driver_class: None, jdbc_driver_paths: Vec::new(), one_time: false, + save_password: true, read_only: false, is_production: false, production_databases: vec![], diff --git a/crates/dbx-core/src/table_import.rs b/crates/dbx-core/src/table_import.rs index 98f3044a90..ff7eeb034b 100644 --- a/crates/dbx-core/src/table_import.rs +++ b/crates/dbx-core/src/table_import.rs @@ -4296,6 +4296,7 @@ async fn flush_sqlite_append_transaction( Ok(rows_imported.saturating_add(rows)) } +#[allow(clippy::too_many_arguments)] async fn finish_sqlite_append_transaction( state: &AppState, pool_key: &str, diff --git a/crates/dbx-core/src/transfer.rs b/crates/dbx-core/src/transfer.rs index 4dc2e739e0..0c8ab66398 100644 --- a/crates/dbx-core/src/transfer.rs +++ b/crates/dbx-core/src/transfer.rs @@ -4098,7 +4098,7 @@ fn transfer_ddl_statements(sql: &str, db_type: &DatabaseType) -> Vec { .collect() } } else if matches!(db_type, DatabaseType::Dameng) { - let statements = split_sql_statements_for_database(sql, db_type.clone()); + let statements = split_sql_statements_for_database(sql, *db_type); if statements.is_empty() { vec![sql.trim().to_string()] } else { @@ -10338,6 +10338,7 @@ SELECT 1 FROM dual"# jdbc_driver_class: None, jdbc_driver_paths: Vec::new(), one_time: false, + save_password: true, read_only: false, is_production: false, production_databases: vec![], diff --git a/crates/dbx-core/tests/database_export_prefetch.rs b/crates/dbx-core/tests/database_export_prefetch.rs index 5884ecfbdd..099eb1ba5d 100644 --- a/crates/dbx-core/tests/database_export_prefetch.rs +++ b/crates/dbx-core/tests/database_export_prefetch.rs @@ -134,6 +134,7 @@ fn postgres_test_config(id: &str, port: u16) -> ConnectionConfig { jdbc_driver_class: None, jdbc_driver_paths: Vec::new(), one_time: false, + save_password: true, read_only: false, is_production: false, production_databases: vec![], diff --git a/crates/dbx-core/tests/dump_docs_fixture.rs b/crates/dbx-core/tests/dump_docs_fixture.rs index b86893045f..d8b0c5c9d5 100644 --- a/crates/dbx-core/tests/dump_docs_fixture.rs +++ b/crates/dbx-core/tests/dump_docs_fixture.rs @@ -71,6 +71,7 @@ fn live_postgres_config( jdbc_driver_class: None, jdbc_driver_paths: Vec::new(), one_time: false, + save_password: true, read_only: false, is_production: false, production_databases: vec![], diff --git a/crates/dbx-core/tests/live_postgres_docs_annotations.rs b/crates/dbx-core/tests/live_postgres_docs_annotations.rs index 2e9c2f798c..2f41154d7f 100644 --- a/crates/dbx-core/tests/live_postgres_docs_annotations.rs +++ b/crates/dbx-core/tests/live_postgres_docs_annotations.rs @@ -67,6 +67,7 @@ fn live_postgres_config( jdbc_driver_class: None, jdbc_driver_paths: Vec::new(), one_time: false, + save_password: true, read_only: false, is_production: false, production_databases: vec![], diff --git a/crates/dbx-core/tests/live_postgres_docs_snapshot.rs b/crates/dbx-core/tests/live_postgres_docs_snapshot.rs index 24f4452a26..42d6879885 100644 --- a/crates/dbx-core/tests/live_postgres_docs_snapshot.rs +++ b/crates/dbx-core/tests/live_postgres_docs_snapshot.rs @@ -62,6 +62,7 @@ fn live_postgres_config( jdbc_driver_class: None, jdbc_driver_paths: Vec::new(), one_time: false, + save_password: true, read_only: false, is_production: false, production_databases: vec![], diff --git a/crates/dbx-core/tests/live_postgres_query_result_export.rs b/crates/dbx-core/tests/live_postgres_query_result_export.rs index 4355167ee9..4312a693ba 100644 --- a/crates/dbx-core/tests/live_postgres_query_result_export.rs +++ b/crates/dbx-core/tests/live_postgres_query_result_export.rs @@ -67,6 +67,7 @@ fn live_postgres_config( jdbc_driver_class: None, jdbc_driver_paths: Vec::new(), one_time: false, + save_password: true, read_only: false, is_production: false, production_databases: vec![], diff --git a/crates/dbx-core/tests/live_postgres_transfer.rs b/crates/dbx-core/tests/live_postgres_transfer.rs index 47f25bee30..a65494747e 100644 --- a/crates/dbx-core/tests/live_postgres_transfer.rs +++ b/crates/dbx-core/tests/live_postgres_transfer.rs @@ -59,6 +59,7 @@ fn postgres_test_config(id: &str, database: &str) -> ConnectionConfig { jdbc_driver_class: None, jdbc_driver_paths: Vec::new(), one_time: false, + save_password: true, read_only: false, is_production: false, production_databases: vec![], diff --git a/crates/dbx-core/tests/live_sqlserver_completion.rs b/crates/dbx-core/tests/live_sqlserver_completion.rs index 3efa9f0b9a..77e6890136 100644 --- a/crates/dbx-core/tests/live_sqlserver_completion.rs +++ b/crates/dbx-core/tests/live_sqlserver_completion.rs @@ -68,6 +68,7 @@ fn live_sqlserver_config(id: &str, database: &str) -> dbx_core::models::connecti jdbc_driver_class: None, jdbc_driver_paths: Vec::new(), one_time: false, + save_password: true, read_only: false, is_production: false, production_databases: vec![], diff --git a/crates/dbx-core/tests/live_sqlserver_query_result_export.rs b/crates/dbx-core/tests/live_sqlserver_query_result_export.rs index c5ec0deebc..d7d078f38f 100644 --- a/crates/dbx-core/tests/live_sqlserver_query_result_export.rs +++ b/crates/dbx-core/tests/live_sqlserver_query_result_export.rs @@ -57,6 +57,7 @@ fn live_sqlserver_config(id: &str, database: &str) -> dbx_core::models::connecti jdbc_driver_class: None, jdbc_driver_paths: Vec::new(), one_time: false, + save_password: true, read_only: false, is_production: false, production_databases: vec![], diff --git a/crates/dbx-mcp/tests/local.rs b/crates/dbx-mcp/tests/local.rs index 9d3ab3d6c6..0a6f0ab506 100644 --- a/crates/dbx-mcp/tests/local.rs +++ b/crates/dbx-mcp/tests/local.rs @@ -1,7 +1,7 @@ use std::{ffi::OsString, sync::Arc}; use dbx_core::{models::connection::ConnectionConfig, storage::Storage}; -use dbx_mcp::{DbxMcpServer, LocalBackend, McpScope}; +use dbx_mcp::{DbxBackend, DbxMcpServer, LocalBackend, McpScope}; use rmcp::{model::CallToolRequestParams, ServiceExt}; use serde_json::{json, Map, Value}; use tempfile::tempdir; diff --git a/crates/dbx-web/src/routes/connection.rs b/crates/dbx-web/src/routes/connection.rs index 556c0c6ca9..f5a5831a22 100644 --- a/crates/dbx-web/src/routes/connection.rs +++ b/crates/dbx-web/src/routes/connection.rs @@ -589,6 +589,7 @@ mod tests { jdbc_driver_class: None, jdbc_driver_paths: Vec::new(), one_time: false, + save_password: true, read_only: false, is_production: false, production_databases: vec![], diff --git a/crates/dbx-web/src/routes/transfer.rs b/crates/dbx-web/src/routes/transfer.rs index 52c0ffeb56..f3b70cf62e 100644 --- a/crates/dbx-web/src/routes/transfer.rs +++ b/crates/dbx-web/src/routes/transfer.rs @@ -577,6 +577,7 @@ mod tests { jdbc_driver_class: None, jdbc_driver_paths: Vec::new(), one_time: false, + save_password: true, read_only: false, is_production: false, production_databases: vec![], diff --git a/src-tauri/src/commands/connection.rs b/src-tauri/src/commands/connection.rs index c2ef4be664..c1cc97a2ad 100644 --- a/src-tauri/src/commands/connection.rs +++ b/src-tauri/src/commands/connection.rs @@ -230,6 +230,7 @@ mod tests { jdbc_driver_class: None, jdbc_driver_paths: Vec::new(), one_time: false, + save_password: true, read_only: false, is_production: false, production_databases: vec![],