mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 10:28:44 +00:00
feat(api): 添加WebDAV同步秘密状态和偏好设置功能 (#2560)
- 新增获取WebDAV同步秘密状态的接口 - 新增保存WebDAV同步秘密偏好的接口 - 新增忘记WebDAV同步秘密密码的接口 - 更新相关的存储和加密逻辑以支持新的功能 Co-authored-by: t8y2 <1156263951@qq.com>
This commit is contained in:
@@ -52,11 +52,14 @@ import {
|
||||
aiTestConnection,
|
||||
checkMcpServerStatus,
|
||||
installMcpServer,
|
||||
forgetWebdavSyncSecretsPassphrase,
|
||||
forgetWebdavSavedPassword,
|
||||
listSystemFonts,
|
||||
saveWebdavSyncSecretsPreference,
|
||||
saveWebdavSavedPassword,
|
||||
webdavPasswordStatus,
|
||||
webdavSyncDownload,
|
||||
webdavSyncSecretsStatus,
|
||||
webdavSyncTest,
|
||||
webdavSyncUpload,
|
||||
type AiModelInfo,
|
||||
@@ -1278,13 +1281,14 @@ const webdavHasSavedPassword = ref(false);
|
||||
const webdavRemotePath = ref(localStorage.getItem("dbx-webdav-remote-path") || DEFAULT_WEB_DAV_REMOTE_PATH);
|
||||
const webdavSyncSecrets = ref(false);
|
||||
const webdavSecretsPassphrase = ref("");
|
||||
const webdavHasSavedSecretsPassphrase = ref(false);
|
||||
const webdavAutoUploadEnabled = ref(localStorage.getItem("dbx-webdav-auto-upload-enabled") === "true");
|
||||
const webdavAutoUploadIntervalMinutes = ref(Number(localStorage.getItem("dbx-webdav-auto-upload-interval-minutes") || String(DEFAULT_WEB_DAV_AUTO_UPLOAD_INTERVAL_MINUTES)));
|
||||
const webdavBusy = ref<"" | "test" | "upload" | "download">("");
|
||||
const webdavMessage = ref("");
|
||||
const webdavError = ref(false);
|
||||
|
||||
const webdavReady = computed(() => !!webdavEndpoint.value.trim() && !webdavBusy.value && (!webdavSyncSecrets.value || !!webdavSecretsPassphrase.value.trim()));
|
||||
const webdavReady = computed(() => !!webdavEndpoint.value.trim() && !webdavBusy.value && (!webdavSyncSecrets.value || !!webdavSecretsPassphrase.value.trim() || webdavHasSavedSecretsPassphrase.value));
|
||||
|
||||
function currentWebDavConfig(): WebDavConfig {
|
||||
return {
|
||||
@@ -1320,6 +1324,7 @@ async function runWebDavAction(kind: "test" | "upload" | "download", action: ()
|
||||
try {
|
||||
rememberWebDavFields();
|
||||
await applyWebDavPasswordPreference();
|
||||
await applyWebDavSyncSecretsPreference();
|
||||
setWebDavResult(await action());
|
||||
} catch (e: any) {
|
||||
setWebDavResult(e?.message || String(e), true);
|
||||
@@ -1356,6 +1361,40 @@ async function applyWebDavPasswordPreference() {
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshWebDavSyncSecretsStatus() {
|
||||
try {
|
||||
const status = await webdavSyncSecretsStatus();
|
||||
webdavSyncSecrets.value = status.enabled;
|
||||
webdavHasSavedSecretsPassphrase.value = status.hasSavedPassphrase;
|
||||
} catch {
|
||||
webdavSyncSecrets.value = false;
|
||||
webdavHasSavedSecretsPassphrase.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function applyWebDavSyncSecretsPreference() {
|
||||
const passphrase = webdavSecretsPassphrase.value.trim();
|
||||
if (!webdavSyncSecrets.value) {
|
||||
await saveWebdavSyncSecretsPreference(false);
|
||||
return;
|
||||
}
|
||||
await saveWebdavSyncSecretsPreference(true, passphrase || undefined);
|
||||
if (passphrase) {
|
||||
webdavHasSavedSecretsPassphrase.value = true;
|
||||
webdavSecretsPassphrase.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
async function clearWebDavSyncSecretsPassphrase() {
|
||||
try {
|
||||
await forgetWebdavSyncSecretsPassphrase();
|
||||
webdavHasSavedSecretsPassphrase.value = false;
|
||||
webdavSecretsPassphrase.value = "";
|
||||
} catch (e: any) {
|
||||
setWebDavResult(e?.message || String(e), true);
|
||||
}
|
||||
}
|
||||
|
||||
async function testWebDav() {
|
||||
await runWebDavAction("test", async () => {
|
||||
await webdavSyncTest(currentWebDavConfig());
|
||||
@@ -1425,7 +1464,9 @@ watch(
|
||||
editDebugLoggingEnabled.value = settingsStore.desktopSettings.debug_logging_enabled;
|
||||
editSidebarTablePageSize.value = settingsStore.desktopSettings.sidebar_table_page_size ?? DEFAULT_SIDEBAR_TABLE_PAGE_SIZE;
|
||||
webdavPassword.value = "";
|
||||
webdavSecretsPassphrase.value = "";
|
||||
await refreshWebDavPasswordStatus();
|
||||
await refreshWebDavSyncSecretsStatus();
|
||||
syncAiEditState();
|
||||
if (!isWeb && activeSettingsTab.value === "mcp") void refreshMcpStatus();
|
||||
if (!isWeb && activeSettingsTab.value === "ai" && aiIsCodexCli.value) void ensureCodexMcpStatus();
|
||||
@@ -3173,7 +3214,21 @@ onUnmounted(cleanupPreviewEditor);
|
||||
</div>
|
||||
<div v-if="webdavSyncSecrets" class="space-y-2">
|
||||
<Label for="webdav-secrets-passphrase">{{ t("settings.syncSecretsPassphrase") }}</Label>
|
||||
<PasswordInput id="webdav-secrets-passphrase" v-model="webdavSecretsPassphrase" autocomplete="new-password" />
|
||||
<div class="flex items-center gap-2">
|
||||
<PasswordInput id="webdav-secrets-passphrase" v-model="webdavSecretsPassphrase" class="min-w-0 flex-1" :placeholder="webdavHasSavedSecretsPassphrase ? '••••••••' : ''" :show-toggle="!webdavHasSavedSecretsPassphrase || !!webdavSecretsPassphrase" autocomplete="new-password" />
|
||||
<Button
|
||||
v-if="webdavHasSavedSecretsPassphrase"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-9 w-9 shrink-0 text-muted-foreground hover:text-foreground"
|
||||
:title="t('settings.syncClearSavedPassword')"
|
||||
:aria-label="t('settings.syncClearSavedPassword')"
|
||||
@click="clearWebDavSyncSecretsPassphrase"
|
||||
>
|
||||
<X class="size-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">{{ t("settings.syncSecretsPassphraseDescription") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -223,6 +223,9 @@ export const webdavSyncTest = forward("webdavSyncTest");
|
||||
export const webdavPasswordStatus = forward("webdavPasswordStatus");
|
||||
export const saveWebdavSavedPassword = forward("saveWebdavSavedPassword");
|
||||
export const forgetWebdavSavedPassword = forward("forgetWebdavSavedPassword");
|
||||
export const webdavSyncSecretsStatus = forward("webdavSyncSecretsStatus");
|
||||
export const saveWebdavSyncSecretsPreference = forward("saveWebdavSyncSecretsPreference");
|
||||
export const forgetWebdavSyncSecretsPassphrase = forward("forgetWebdavSyncSecretsPassphrase");
|
||||
export const webdavSyncUpload = forward("webdavSyncUpload");
|
||||
export const webdavSyncDownload = forward("webdavSyncDownload");
|
||||
export const saveAiConversation = forward("saveAiConversation");
|
||||
|
||||
@@ -1095,6 +1095,11 @@ export interface WebDavPasswordStatus {
|
||||
hasSavedPassword: boolean;
|
||||
}
|
||||
|
||||
export interface WebDavSyncSecretsStatus {
|
||||
enabled: boolean;
|
||||
hasSavedPassphrase: boolean;
|
||||
}
|
||||
|
||||
export async function webdavSyncTest(config: WebDavConfig): Promise<void> {
|
||||
return post("/api/cloud-sync/webdav/test", { config });
|
||||
}
|
||||
@@ -1111,6 +1116,18 @@ export async function forgetWebdavSavedPassword(config: WebDavConfig): Promise<v
|
||||
return post("/api/cloud-sync/webdav/forget-password", { config });
|
||||
}
|
||||
|
||||
export async function webdavSyncSecretsStatus(): Promise<WebDavSyncSecretsStatus> {
|
||||
return post("/api/cloud-sync/webdav/sync-secrets-status", {});
|
||||
}
|
||||
|
||||
export async function saveWebdavSyncSecretsPreference(enabled: boolean, passphrase?: string): Promise<void> {
|
||||
return post("/api/cloud-sync/webdav/save-sync-secrets-preference", { enabled, passphrase });
|
||||
}
|
||||
|
||||
export async function forgetWebdavSyncSecretsPassphrase(): Promise<void> {
|
||||
return post("/api/cloud-sync/webdav/forget-sync-secrets-passphrase", {});
|
||||
}
|
||||
|
||||
export async function webdavSyncUpload(config: WebDavConfig, editorSettings?: unknown, secretsPassphrase?: string): Promise<WebDavSyncSummary> {
|
||||
return post("/api/cloud-sync/webdav/upload", { config, editorSettings, secretsPassphrase });
|
||||
}
|
||||
|
||||
@@ -190,6 +190,11 @@ export interface WebDavPasswordStatus {
|
||||
hasSavedPassword: boolean;
|
||||
}
|
||||
|
||||
export interface WebDavSyncSecretsStatus {
|
||||
enabled: boolean;
|
||||
hasSavedPassphrase: boolean;
|
||||
}
|
||||
|
||||
export interface QueryPagination {
|
||||
limit: number;
|
||||
offset: number;
|
||||
@@ -428,6 +433,18 @@ export async function forgetWebdavSavedPassword(config: WebDavConfig): Promise<v
|
||||
return invoke("forget_webdav_saved_password", { config });
|
||||
}
|
||||
|
||||
export async function webdavSyncSecretsStatus(): Promise<WebDavSyncSecretsStatus> {
|
||||
return invoke("webdav_sync_secrets_status");
|
||||
}
|
||||
|
||||
export async function saveWebdavSyncSecretsPreference(enabled: boolean, passphrase?: string): Promise<void> {
|
||||
return invoke("save_webdav_sync_secrets_preference", { enabled, passphrase });
|
||||
}
|
||||
|
||||
export async function forgetWebdavSyncSecretsPassphrase(): Promise<void> {
|
||||
return invoke("forget_webdav_sync_secrets_passphrase");
|
||||
}
|
||||
|
||||
export async function webdavSyncUpload(config: WebDavConfig, editorSettings?: unknown, secretsPassphrase?: string): Promise<WebDavSyncSummary> {
|
||||
return invoke("webdav_sync_upload", { config, editorSettings, secretsPassphrase });
|
||||
}
|
||||
|
||||
@@ -11,9 +11,10 @@ use sha2::{Digest, Sha256};
|
||||
|
||||
use crate::ai::AiConfig;
|
||||
use crate::connection_secrets::{
|
||||
MQ_AUTH_API_KEY_VALUE_KEY, MQ_AUTH_CLIENT_SECRET_KEY, MQ_AUTH_PASSWORD_KEY, MQ_AUTH_TOKEN_KEY, MQ_TOKEN_SIGNING_KEY,
|
||||
MQ_AUTH_API_KEY_VALUE_KEY, MQ_AUTH_CLIENT_SECRET_KEY, MQ_AUTH_PASSWORD_KEY, MQ_AUTH_TOKEN_KEY,
|
||||
MQ_TOKEN_SIGNING_KEY, NACOS_AUTH_PASSWORD_KEY,
|
||||
};
|
||||
use crate::models::connection::{ConnectionConfig, TransportLayerConfig};
|
||||
use crate::models::connection::{ConnectionConfig, DatabaseType, TransportLayerConfig};
|
||||
use crate::saved_sql::SavedSqlLibrary;
|
||||
use crate::storage::{DesktopSettings, Storage};
|
||||
|
||||
@@ -31,6 +32,7 @@ const SECRET_KEYS: &[&str] = &[
|
||||
MQ_AUTH_API_KEY_VALUE_KEY,
|
||||
MQ_AUTH_CLIENT_SECRET_KEY,
|
||||
MQ_TOKEN_SIGNING_KEY,
|
||||
NACOS_AUTH_PASSWORD_KEY,
|
||||
];
|
||||
const SSH_TUNNEL_SECRET_PREFIX: &str = "ssh_tunnels.";
|
||||
const TRANSPORT_LAYER_SECRET_PREFIX: &str = "transport_layers.";
|
||||
@@ -50,6 +52,13 @@ pub struct WebDavPasswordStatus {
|
||||
pub has_saved_password: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct WebDavSyncSecretsStatus {
|
||||
pub enabled: bool,
|
||||
pub has_saved_passphrase: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SyncSnapshot {
|
||||
@@ -143,6 +152,21 @@ pub async fn build_sync_snapshot(
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn build_sync_snapshot_with_saved_secrets(
|
||||
storage: &Storage,
|
||||
app_version: impl Into<String>,
|
||||
editor_settings: Option<serde_json::Value>,
|
||||
secrets_passphrase: Option<&str>,
|
||||
) -> Result<SyncSnapshot, String> {
|
||||
match normalized_passphrase(secrets_passphrase) {
|
||||
Some(passphrase) => build_sync_snapshot(storage, app_version, editor_settings, Some(passphrase)).await,
|
||||
None => {
|
||||
let saved_passphrase = resolve_webdav_sync_secrets_passphrase(storage).await?;
|
||||
build_sync_snapshot(storage, app_version, editor_settings, saved_passphrase.as_deref()).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn apply_sync_snapshot(
|
||||
storage: &Storage,
|
||||
snapshot: &SyncSnapshot,
|
||||
@@ -214,6 +238,46 @@ pub async fn resolve_webdav_password(storage: &Storage, config: &mut WebDavConfi
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn webdav_sync_secrets_status(storage: &Storage) -> Result<WebDavSyncSecretsStatus, String> {
|
||||
Ok(WebDavSyncSecretsStatus {
|
||||
enabled: storage.load_webdav_sync_secrets_enabled().await?,
|
||||
has_saved_passphrase: storage.load_webdav_sync_secrets_passphrase_blob().await?.is_some(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn save_webdav_sync_secrets_preference(
|
||||
storage: &Storage,
|
||||
enabled: bool,
|
||||
passphrase: Option<&str>,
|
||||
) -> Result<(), String> {
|
||||
let normalized = normalized_passphrase(passphrase);
|
||||
let blob = match normalized {
|
||||
Some(passphrase) => {
|
||||
let secret = storage.load_or_create_local_device_secret().await?;
|
||||
let blob = encrypt_text_with_secret(passphrase, &secret)?;
|
||||
Some(serde_json::to_value(blob).map_err(|e| e.to_string())?)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
storage.save_webdav_sync_secrets_preference(enabled, blob.as_ref()).await
|
||||
}
|
||||
|
||||
pub async fn forget_webdav_sync_secrets_passphrase(storage: &Storage) -> Result<(), String> {
|
||||
storage.delete_webdav_sync_secrets_passphrase_blob().await
|
||||
}
|
||||
|
||||
pub async fn resolve_webdav_sync_secrets_passphrase(storage: &Storage) -> Result<Option<String>, String> {
|
||||
if !storage.load_webdav_sync_secrets_enabled().await? {
|
||||
return Ok(None);
|
||||
}
|
||||
let Some(value) = storage.load_webdav_sync_secrets_passphrase_blob().await? else {
|
||||
return Ok(None);
|
||||
};
|
||||
let blob: EncryptedSecretsBlob = serde_json::from_value(value).map_err(|e| e.to_string())?;
|
||||
let secret = storage.load_or_create_local_device_secret().await?;
|
||||
decrypt_text_with_secret(&blob, &secret).map(Some)
|
||||
}
|
||||
|
||||
impl WebDavClient {
|
||||
pub fn new(config: WebDavConfig) -> Self {
|
||||
Self { http: Client::new(), config }
|
||||
@@ -326,6 +390,8 @@ fn scrub_connection_secrets(config: &mut ConnectionConfig) {
|
||||
}
|
||||
config.redis_sentinel_password.clear();
|
||||
config.connection_string = None;
|
||||
scrub_mq_external_config_secrets(config);
|
||||
scrub_nacos_auth_secrets(config);
|
||||
}
|
||||
|
||||
fn webdav_password_account(config: &WebDavConfig) -> String {
|
||||
@@ -382,6 +448,7 @@ async fn build_sensitive_payload(
|
||||
push_secret(&mut connection_secrets, &config.id, "connection_string", connection_string);
|
||||
}
|
||||
push_mq_external_config_secrets(&mut connection_secrets, config);
|
||||
push_nacos_external_config_secrets(&mut connection_secrets, config);
|
||||
}
|
||||
|
||||
Ok(SensitiveSyncPayload { connection_secrets, ai_config: storage.load_ai_config().await? })
|
||||
@@ -407,6 +474,44 @@ fn push_mq_external_config_secrets(secrets: &mut Vec<ConnectionSecretSnapshot>,
|
||||
}
|
||||
}
|
||||
|
||||
fn scrub_mq_external_config_secrets(config: &mut ConnectionConfig) {
|
||||
if config.db_type != DatabaseType::MessageQueue {
|
||||
return;
|
||||
}
|
||||
let Some(external_config) = config.external_config.as_mut() else {
|
||||
return;
|
||||
};
|
||||
if let Some(auth) = external_config.get_mut("auth").and_then(serde_json::Value::as_object_mut) {
|
||||
match auth.get("kind").and_then(serde_json::Value::as_str) {
|
||||
Some("token") => scrub_json_secret(auth, "token"),
|
||||
Some("basic") => scrub_json_secret(auth, "password"),
|
||||
Some("apiKey") | Some("api_key") | Some("apikey") => scrub_json_secret(auth, "value"),
|
||||
Some("oauth2") => scrub_json_secret(auth, "clientSecret"),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if let Some(signing) = external_config.get_mut("tokenSigning").and_then(serde_json::Value::as_object_mut) {
|
||||
scrub_json_secret(signing, "key");
|
||||
}
|
||||
}
|
||||
|
||||
fn push_nacos_external_config_secrets(secrets: &mut Vec<ConnectionSecretSnapshot>, config: &ConnectionConfig) {
|
||||
if config.db_type != DatabaseType::Nacos {
|
||||
return;
|
||||
}
|
||||
let Some(auth) = config
|
||||
.external_config
|
||||
.as_ref()
|
||||
.and_then(|external_config| external_config.get("auth"))
|
||||
.and_then(serde_json::Value::as_object)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if auth.get("kind").and_then(serde_json::Value::as_str) == Some("usernamePassword") {
|
||||
push_json_secret(secrets, &config.id, NACOS_AUTH_PASSWORD_KEY, auth, "password");
|
||||
}
|
||||
}
|
||||
|
||||
fn push_json_secret(
|
||||
secrets: &mut Vec<ConnectionSecretSnapshot>,
|
||||
connection_id: &str,
|
||||
@@ -430,6 +535,30 @@ fn push_secret(secrets: &mut Vec<ConnectionSecretSnapshot>, connection_id: &str,
|
||||
});
|
||||
}
|
||||
|
||||
fn scrub_nacos_auth_secrets(config: &mut ConnectionConfig) {
|
||||
if config.db_type != DatabaseType::Nacos {
|
||||
return;
|
||||
}
|
||||
let Some(auth) = config
|
||||
.external_config
|
||||
.as_mut()
|
||||
.and_then(|external_config| external_config.get_mut("auth"))
|
||||
.and_then(serde_json::Value::as_object_mut)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if auth.get("kind").and_then(serde_json::Value::as_str) == Some("usernamePassword") && auth.contains_key("password")
|
||||
{
|
||||
scrub_json_secret(auth, "password");
|
||||
}
|
||||
}
|
||||
|
||||
fn scrub_json_secret(object: &mut serde_json::Map<String, serde_json::Value>, field: &str) {
|
||||
if object.contains_key(field) {
|
||||
object.insert(field.to_string(), serde_json::Value::String(String::new()));
|
||||
}
|
||||
}
|
||||
|
||||
async fn apply_sensitive_payload(storage: &Storage, payload: &SensitiveSyncPayload) -> Result<(), String> {
|
||||
for secret in &payload.connection_secrets {
|
||||
if !SECRET_KEYS.contains(&secret.key.as_str())
|
||||
@@ -586,12 +715,130 @@ fn parent_collection_paths(remote_path: &str) -> Vec<String> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
decrypt_sensitive_payload, encrypt_sensitive_payload, normalized_remote_path, parent_collection_paths,
|
||||
scrub_connection_secrets, ConnectionSecretSnapshot, SensitiveSyncPayload,
|
||||
build_sync_snapshot_with_saved_secrets, decrypt_sensitive_payload, encrypt_sensitive_payload,
|
||||
forget_webdav_sync_secrets_passphrase, normalized_remote_path, parent_collection_paths,
|
||||
resolve_webdav_sync_secrets_passphrase, save_webdav_sync_secrets_preference, scrub_connection_secrets,
|
||||
webdav_sync_secrets_status, ConnectionSecretSnapshot, SensitiveSyncPayload,
|
||||
};
|
||||
use crate::connection_secrets::NACOS_AUTH_PASSWORD_KEY;
|
||||
use crate::models::connection::{
|
||||
default_redis_key_separator, ConnectionConfig, DatabaseType, TransportLayerConfig,
|
||||
};
|
||||
use crate::storage::Storage;
|
||||
|
||||
fn temp_db_path(name: &str) -> std::path::PathBuf {
|
||||
std::env::temp_dir().join(format!("dbx-cloud-sync-{name}-{}.db", uuid::Uuid::new_v4()))
|
||||
}
|
||||
|
||||
fn postgres_connection(id: &str, password: &str) -> ConnectionConfig {
|
||||
ConnectionConfig {
|
||||
id: id.to_string(),
|
||||
name: "Postgres".to_string(),
|
||||
db_type: DatabaseType::Postgres,
|
||||
driver_profile: None,
|
||||
driver_label: None,
|
||||
url_params: None,
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: 5432,
|
||||
username: "app".to_string(),
|
||||
password: password.to_string(),
|
||||
database: Some("app_db".to_string()),
|
||||
visible_databases: None,
|
||||
visible_schemas: None,
|
||||
attached_databases: Vec::new(),
|
||||
color: None,
|
||||
transport_layers: Vec::new(),
|
||||
connect_timeout_secs: 5,
|
||||
query_timeout_secs: 30,
|
||||
idle_timeout_secs: 60,
|
||||
keepalive_interval_secs: 0,
|
||||
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: default_redis_key_separator(),
|
||||
redis_scan_page_size: None,
|
||||
etcd_endpoints: String::new(),
|
||||
gbase_server: String::new(),
|
||||
informix_server: String::new(),
|
||||
external_config: None,
|
||||
jdbc_driver_class: None,
|
||||
jdbc_driver_paths: Vec::new(),
|
||||
one_time: false,
|
||||
read_only: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn nacos_connection(id: &str, password: &str) -> ConnectionConfig {
|
||||
ConnectionConfig {
|
||||
id: id.to_string(),
|
||||
name: "Nacos".to_string(),
|
||||
db_type: DatabaseType::Nacos,
|
||||
driver_profile: None,
|
||||
driver_label: None,
|
||||
url_params: None,
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: 8848,
|
||||
username: "nacos".to_string(),
|
||||
password: String::new(),
|
||||
database: None,
|
||||
visible_databases: None,
|
||||
visible_schemas: None,
|
||||
attached_databases: Vec::new(),
|
||||
color: None,
|
||||
transport_layers: Vec::new(),
|
||||
connect_timeout_secs: 5,
|
||||
query_timeout_secs: 30,
|
||||
idle_timeout_secs: 60,
|
||||
keepalive_interval_secs: 0,
|
||||
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: default_redis_key_separator(),
|
||||
redis_scan_page_size: None,
|
||||
etcd_endpoints: String::new(),
|
||||
gbase_server: String::new(),
|
||||
informix_server: String::new(),
|
||||
external_config: Some(serde_json::json!({
|
||||
"namespace": "public",
|
||||
"group": "DEFAULT_GROUP",
|
||||
"auth": {
|
||||
"kind": "usernamePassword",
|
||||
"username": "nacos",
|
||||
"password": password
|
||||
}
|
||||
})),
|
||||
jdbc_driver_class: None,
|
||||
jdbc_driver_paths: Vec::new(),
|
||||
one_time: false,
|
||||
read_only: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn nacos_auth_password(config: &ConnectionConfig) -> Option<&str> {
|
||||
config.external_config.as_ref()?.get("auth")?.get("password")?.as_str()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalizes_empty_remote_path_to_default() {
|
||||
@@ -731,4 +978,68 @@ mod tests {
|
||||
let encrypted = encrypt_sensitive_payload(&payload, "sync-pass").unwrap();
|
||||
assert!(decrypt_sensitive_payload(&encrypted, "wrong-pass").is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn webdav_sync_secrets_preference_round_trips_and_clears_passphrase() {
|
||||
let storage = Storage::open(&temp_db_path("sync-secrets-preference")).await.unwrap();
|
||||
|
||||
let status = webdav_sync_secrets_status(&storage).await.unwrap();
|
||||
assert!(!status.enabled);
|
||||
assert!(!status.has_saved_passphrase);
|
||||
assert_eq!(resolve_webdav_sync_secrets_passphrase(&storage).await.unwrap(), None);
|
||||
|
||||
save_webdav_sync_secrets_preference(&storage, true, Some("sync-pass")).await.unwrap();
|
||||
|
||||
let status = webdav_sync_secrets_status(&storage).await.unwrap();
|
||||
assert!(status.enabled);
|
||||
assert!(status.has_saved_passphrase);
|
||||
assert_eq!(resolve_webdav_sync_secrets_passphrase(&storage).await.unwrap().as_deref(), Some("sync-pass"));
|
||||
|
||||
forget_webdav_sync_secrets_passphrase(&storage).await.unwrap();
|
||||
let status = webdav_sync_secrets_status(&storage).await.unwrap();
|
||||
assert!(status.enabled);
|
||||
assert!(!status.has_saved_passphrase);
|
||||
assert_eq!(resolve_webdav_sync_secrets_passphrase(&storage).await.unwrap(), None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn saved_sync_passphrase_encrypts_snapshot_secrets_without_exposing_connection_passwords() {
|
||||
let storage = Storage::open(&temp_db_path("saved-sync-snapshot")).await.unwrap();
|
||||
storage.save_connections(&[postgres_connection("pg", "db-secret")]).await.unwrap();
|
||||
|
||||
let plain_snapshot =
|
||||
build_sync_snapshot_with_saved_secrets(&storage, "test-version", None, None).await.unwrap();
|
||||
assert!(plain_snapshot.encrypted_secrets.is_none());
|
||||
assert_eq!(plain_snapshot.connections[0].password, "");
|
||||
|
||||
save_webdav_sync_secrets_preference(&storage, true, Some("sync-pass")).await.unwrap();
|
||||
let encrypted_snapshot =
|
||||
build_sync_snapshot_with_saved_secrets(&storage, "test-version", None, None).await.unwrap();
|
||||
|
||||
assert_eq!(encrypted_snapshot.connections[0].password, "");
|
||||
let encrypted = encrypted_snapshot.encrypted_secrets.as_ref().expect("encrypted secrets");
|
||||
let decrypted = decrypt_sensitive_payload(encrypted, "sync-pass").unwrap();
|
||||
assert!(decrypted.connection_secrets.iter().any(|secret| {
|
||||
secret.connection_id == "pg" && secret.key == "password" && secret.secret == "db-secret"
|
||||
}));
|
||||
}
|
||||
|
||||
#[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();
|
||||
storage.save_connections(&[nacos_connection("nacos", "nacos-secret")]).await.unwrap();
|
||||
|
||||
save_webdav_sync_secrets_preference(&storage, true, Some("sync-pass")).await.unwrap();
|
||||
let encrypted_snapshot =
|
||||
build_sync_snapshot_with_saved_secrets(&storage, "test-version", None, None).await.unwrap();
|
||||
|
||||
assert_eq!(nacos_auth_password(&encrypted_snapshot.connections[0]), Some(""));
|
||||
let public_json = serde_json::to_string(&encrypted_snapshot.connections).unwrap();
|
||||
assert!(!public_json.contains("nacos-secret"));
|
||||
let encrypted = encrypted_snapshot.encrypted_secrets.as_ref().expect("encrypted secrets");
|
||||
let decrypted = decrypt_sensitive_payload(encrypted, "sync-pass").unwrap();
|
||||
assert!(decrypted.connection_secrets.iter().any(|secret| {
|
||||
secret.connection_id == "nacos" && secret.key == NACOS_AUTH_PASSWORD_KEY && secret.secret == "nacos-secret"
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@ pub const MQ_AUTH_API_KEY_VALUE_KEY: &str = "mq.auth.api_key_value";
|
||||
pub const MQ_AUTH_CLIENT_SECRET_KEY: &str = "mq.auth.client_secret";
|
||||
pub const MQ_TOKEN_SIGNING_SECRET_PREFIX: &str = "mq.token_signing.";
|
||||
pub const MQ_TOKEN_SIGNING_KEY: &str = "mq.token_signing.key";
|
||||
pub const NACOS_AUTH_SECRET_PREFIX: &str = "nacos.auth.";
|
||||
pub const NACOS_AUTH_PASSWORD_KEY: &str = "nacos.auth.password";
|
||||
|
||||
pub trait ConnectionSecretStore {
|
||||
fn set_secret(&self, connection_id: &str, key: &str, secret: &str) -> Result<(), String>;
|
||||
|
||||
@@ -9,7 +9,8 @@ use uuid::Uuid;
|
||||
use crate::ai::{AiChatMessage, AiConfig, AiConversation, AiProvider};
|
||||
use crate::connection_secrets::{
|
||||
MQ_AUTH_API_KEY_VALUE_KEY, MQ_AUTH_CLIENT_SECRET_KEY, MQ_AUTH_PASSWORD_KEY, MQ_AUTH_SECRET_PREFIX,
|
||||
MQ_AUTH_TOKEN_KEY, MQ_TOKEN_SIGNING_KEY, MQ_TOKEN_SIGNING_SECRET_PREFIX,
|
||||
MQ_AUTH_TOKEN_KEY, MQ_TOKEN_SIGNING_KEY, MQ_TOKEN_SIGNING_SECRET_PREFIX, NACOS_AUTH_PASSWORD_KEY,
|
||||
NACOS_AUTH_SECRET_PREFIX,
|
||||
};
|
||||
use crate::db::sqlite::{connect_path_create_if_missing, SqliteHandle};
|
||||
use crate::history::{HistoryEntry, MAX_HISTORY};
|
||||
@@ -505,6 +506,18 @@ fn scrub_mq_token_signing_secret(config: &mut ConnectionConfig) {
|
||||
scrub_json_secret(signing, "key");
|
||||
}
|
||||
|
||||
fn scrub_nacos_auth_secrets(config: &mut ConnectionConfig) {
|
||||
if config.db_type != DatabaseType::Nacos {
|
||||
return;
|
||||
}
|
||||
let Some(auth) = nacos_auth_object_mut(config.external_config.as_mut()) else {
|
||||
return;
|
||||
};
|
||||
if auth.get("kind").and_then(serde_json::Value::as_str) == Some("usernamePassword") {
|
||||
scrub_json_secret(auth, "password");
|
||||
}
|
||||
}
|
||||
|
||||
fn delete_secret_prefix_in_tx(
|
||||
tx: &rusqlite::Transaction<'_>,
|
||||
connection_id: &str,
|
||||
@@ -935,6 +948,35 @@ impl Storage {
|
||||
settings.insert("webdav_passwords".to_string(), serde_json::Value::Object(credentials));
|
||||
self.save_app_settings_json(&settings).await
|
||||
}
|
||||
|
||||
pub async fn save_webdav_sync_secrets_preference(
|
||||
&self,
|
||||
enabled: bool,
|
||||
blob: Option<&serde_json::Value>,
|
||||
) -> Result<(), String> {
|
||||
let mut settings = self.load_app_settings_json().await?;
|
||||
settings.insert("webdav_sync_secrets_enabled".to_string(), serde_json::Value::Bool(enabled));
|
||||
if let Some(blob) = blob {
|
||||
settings.insert("webdav_sync_secrets_passphrase".to_string(), blob.clone());
|
||||
}
|
||||
self.save_app_settings_json(&settings).await
|
||||
}
|
||||
|
||||
pub async fn load_webdav_sync_secrets_enabled(&self) -> Result<bool, String> {
|
||||
let settings = self.load_app_settings_json().await?;
|
||||
Ok(settings.get("webdav_sync_secrets_enabled").and_then(serde_json::Value::as_bool).unwrap_or(false))
|
||||
}
|
||||
|
||||
pub async fn load_webdav_sync_secrets_passphrase_blob(&self) -> Result<Option<serde_json::Value>, String> {
|
||||
let settings = self.load_app_settings_json().await?;
|
||||
Ok(settings.get("webdav_sync_secrets_passphrase").cloned())
|
||||
}
|
||||
|
||||
pub async fn delete_webdav_sync_secrets_passphrase_blob(&self) -> Result<(), String> {
|
||||
let mut settings = self.load_app_settings_json().await?;
|
||||
settings.remove("webdav_sync_secrets_passphrase");
|
||||
self.save_app_settings_json(&settings).await
|
||||
}
|
||||
}
|
||||
|
||||
// AI Conversations
|
||||
@@ -1031,6 +1073,7 @@ impl Storage {
|
||||
sanitized.connection_string = None;
|
||||
scrub_mq_auth_secrets(&mut sanitized);
|
||||
scrub_mq_token_signing_secret(&mut sanitized);
|
||||
scrub_nacos_auth_secrets(&mut sanitized);
|
||||
let json = serde_json::to_string(&sanitized).map_err(|e| e.to_string())?;
|
||||
|
||||
tx.execute("INSERT INTO connections (id, config_json) VALUES (?1, ?2)", params![config_id, json])
|
||||
@@ -1067,6 +1110,7 @@ impl Storage {
|
||||
sanitized.connection_string = None;
|
||||
scrub_mq_auth_secrets(&mut sanitized);
|
||||
scrub_mq_token_signing_secret(&mut sanitized);
|
||||
scrub_nacos_auth_secrets(&mut sanitized);
|
||||
let json = serde_json::to_string(&sanitized).map_err(|e| e.to_string())?;
|
||||
|
||||
tx.execute("INSERT INTO connections (id, config_json) VALUES (?1, ?2)", params![config_id, json])
|
||||
@@ -1124,6 +1168,7 @@ impl Storage {
|
||||
}
|
||||
persist_mq_auth_secrets_in_tx(&tx, &config)?;
|
||||
persist_mq_token_signing_secret_in_tx(&tx, &config)?;
|
||||
persist_nacos_auth_secrets_in_tx(&tx, &config)?;
|
||||
}
|
||||
|
||||
if configs.is_empty() {
|
||||
@@ -1210,11 +1255,14 @@ impl Storage {
|
||||
config.connection_string = self.get_secret(&id, "connection_string").await?;
|
||||
let needs_mq_auth_rewrite = self.hydrate_mq_auth_secrets(&id, &mut config).await?;
|
||||
let needs_mq_token_signing_rewrite = self.hydrate_mq_token_signing_secret(&id, &mut config).await?;
|
||||
let needs_mq_secret_rewrite = needs_mq_auth_rewrite || needs_mq_token_signing_rewrite;
|
||||
if needs_mq_secret_rewrite {
|
||||
let needs_nacos_auth_rewrite = self.hydrate_nacos_auth_secret(&id, &mut config).await?;
|
||||
let needs_external_secret_rewrite =
|
||||
needs_mq_auth_rewrite || needs_mq_token_signing_rewrite || needs_nacos_auth_rewrite;
|
||||
if needs_external_secret_rewrite {
|
||||
let mut sanitized = config.clone().canonicalized();
|
||||
scrub_mq_auth_secrets(&mut sanitized);
|
||||
scrub_mq_token_signing_secret(&mut sanitized);
|
||||
scrub_nacos_auth_secrets(&mut sanitized);
|
||||
let sanitized_json = serde_json::to_string(&sanitized).map_err(|e| e.to_string())?;
|
||||
let update_id = id.clone();
|
||||
self.with_conn(move |conn| {
|
||||
@@ -1275,6 +1323,24 @@ impl Storage {
|
||||
|
||||
hydrate_mq_json_secret(self, connection_id, MQ_TOKEN_SIGNING_KEY, signing, "key").await
|
||||
}
|
||||
|
||||
async fn hydrate_nacos_auth_secret(
|
||||
&self,
|
||||
connection_id: &str,
|
||||
config: &mut ConnectionConfig,
|
||||
) -> Result<bool, String> {
|
||||
if config.db_type != DatabaseType::Nacos {
|
||||
return Ok(false);
|
||||
}
|
||||
let Some(auth) = nacos_auth_object_mut(config.external_config.as_mut()) else {
|
||||
return Ok(false);
|
||||
};
|
||||
if auth.get("kind").and_then(serde_json::Value::as_str) != Some("usernamePassword") {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
hydrate_mq_json_secret(self, connection_id, NACOS_AUTH_PASSWORD_KEY, auth, "password").await
|
||||
}
|
||||
}
|
||||
|
||||
// Saved SQL
|
||||
@@ -2092,6 +2158,45 @@ fn persist_mq_token_signing_secret_in_tx(
|
||||
persist_json_secret_if_present_in_tx(tx, &config.id, MQ_TOKEN_SIGNING_KEY, signing, "key")
|
||||
}
|
||||
|
||||
fn persist_nacos_auth_secrets_in_tx(tx: &rusqlite::Transaction<'_>, config: &ConnectionConfig) -> Result<(), String> {
|
||||
if config.db_type != DatabaseType::Nacos {
|
||||
delete_secret_prefix_in_tx(tx, &config.id, NACOS_AUTH_SECRET_PREFIX)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let Some(auth) = nacos_auth_object(config.external_config.as_ref()) else {
|
||||
delete_secret_prefix_in_tx(tx, &config.id, NACOS_AUTH_SECRET_PREFIX)?;
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if auth.get("kind").and_then(serde_json::Value::as_str) == Some("usernamePassword") {
|
||||
replace_nacos_auth_secret_in_tx(tx, &config.id, NACOS_AUTH_PASSWORD_KEY, auth, "password")?;
|
||||
} else {
|
||||
delete_secret_prefix_in_tx(tx, &config.id, NACOS_AUTH_SECRET_PREFIX)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn replace_nacos_auth_secret_in_tx(
|
||||
tx: &rusqlite::Transaction<'_>,
|
||||
connection_id: &str,
|
||||
key: &str,
|
||||
auth: &serde_json::Map<String, serde_json::Value>,
|
||||
field: &str,
|
||||
) -> Result<(), String> {
|
||||
let current = auth.get(field).and_then(serde_json::Value::as_str).filter(|secret| !secret.is_empty());
|
||||
let existing = if current.is_none() { get_secret_in_tx(tx, connection_id, key)? } else { None };
|
||||
delete_secret_prefix_in_tx(tx, connection_id, NACOS_AUTH_SECRET_PREFIX)?;
|
||||
match current {
|
||||
Some(secret) => persist_secret_in_tx(tx, connection_id, key, secret),
|
||||
None => match existing {
|
||||
Some(secret) => persist_secret_in_tx(tx, connection_id, key, &secret),
|
||||
None => Ok(()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn persist_json_secret_if_present_in_tx(
|
||||
tx: &rusqlite::Transaction<'_>,
|
||||
connection_id: &str,
|
||||
@@ -2153,6 +2258,16 @@ fn mq_token_signing_object_mut(
|
||||
value?.get_mut("tokenSigning")?.as_object_mut()
|
||||
}
|
||||
|
||||
fn nacos_auth_object(value: Option<&serde_json::Value>) -> Option<&serde_json::Map<String, serde_json::Value>> {
|
||||
value?.get("auth")?.as_object()
|
||||
}
|
||||
|
||||
fn nacos_auth_object_mut(
|
||||
value: Option<&mut serde_json::Value>,
|
||||
) -> Option<&mut serde_json::Map<String, serde_json::Value>> {
|
||||
value?.get_mut("auth")?.as_object_mut()
|
||||
}
|
||||
|
||||
fn is_api_key_auth_kind(kind: &str) -> bool {
|
||||
matches!(kind, "apiKey" | "api_key" | "apikey")
|
||||
}
|
||||
@@ -2183,7 +2298,9 @@ fn map_from_sql_err(err: serde_json::Error) -> rusqlite::Error {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{maybe_import_user_data_db, DataDbImportResult, DesktopIconTheme, DesktopSettings, Storage};
|
||||
use crate::connection_secrets::{MQ_AUTH_PASSWORD_KEY, MQ_AUTH_TOKEN_KEY, MQ_TOKEN_SIGNING_KEY};
|
||||
use crate::connection_secrets::{
|
||||
MQ_AUTH_PASSWORD_KEY, MQ_AUTH_TOKEN_KEY, MQ_TOKEN_SIGNING_KEY, NACOS_AUTH_PASSWORD_KEY,
|
||||
};
|
||||
use crate::models::connection::{ConnectionConfig, DatabaseType};
|
||||
use crate::saved_sql::SavedSqlFile;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
@@ -2254,6 +2371,63 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn nacos_connection(id: &str, password: &str) -> ConnectionConfig {
|
||||
ConnectionConfig {
|
||||
id: id.to_string(),
|
||||
name: "Nacos".to_string(),
|
||||
db_type: DatabaseType::Nacos,
|
||||
driver_profile: None,
|
||||
driver_label: None,
|
||||
url_params: None,
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: 8848,
|
||||
username: "nacos".to_string(),
|
||||
password: String::new(),
|
||||
database: None,
|
||||
visible_databases: None,
|
||||
visible_schemas: None,
|
||||
attached_databases: Vec::new(),
|
||||
color: None,
|
||||
transport_layers: Vec::new(),
|
||||
connect_timeout_secs: 30,
|
||||
query_timeout_secs: 300,
|
||||
idle_timeout_secs: 600,
|
||||
keepalive_interval_secs: crate::models::connection::default_keepalive_interval_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: ":".to_string(),
|
||||
redis_scan_page_size: None,
|
||||
etcd_endpoints: String::new(),
|
||||
gbase_server: String::new(),
|
||||
informix_server: String::new(),
|
||||
external_config: Some(serde_json::json!({
|
||||
"namespace": "public",
|
||||
"group": "DEFAULT_GROUP",
|
||||
"auth": {
|
||||
"kind": "usernamePassword",
|
||||
"username": "nacos",
|
||||
"password": password
|
||||
}
|
||||
})),
|
||||
jdbc_driver_class: None,
|
||||
jdbc_driver_paths: Vec::new(),
|
||||
one_time: false,
|
||||
read_only: false,
|
||||
}
|
||||
}
|
||||
|
||||
async fn raw_connection_json(storage: &Storage, id: &str) -> String {
|
||||
let id = id.to_string();
|
||||
storage
|
||||
@@ -2286,6 +2460,10 @@ mod tests {
|
||||
config.external_config.as_ref()?.get("tokenSigning")?.get("key")?.as_str()
|
||||
}
|
||||
|
||||
fn nacos_auth_password(config: &ConnectionConfig) -> Option<&str> {
|
||||
config.external_config.as_ref()?.get("auth")?.get("password")?.as_str()
|
||||
}
|
||||
|
||||
async fn create_data_dir_with_connection(name: &str, connection_id: &str, token: &str) -> std::path::PathBuf {
|
||||
let data_dir = temp_data_dir(name);
|
||||
let storage = Storage::open(&data_dir.join("dbx.db")).await.unwrap();
|
||||
@@ -2485,6 +2663,46 @@ mod tests {
|
||||
assert_eq!(mq_token_signing_key(&loaded[0]), Some("broker-signing-secret"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn save_connections_moves_nacos_auth_password_to_secret_table_and_restores_it() {
|
||||
let path = temp_db_path("nacos-auth-secret");
|
||||
let storage = Storage::open(&path).await.unwrap();
|
||||
|
||||
storage.save_connections(&[nacos_connection("nacos", "nacos-secret")]).await.unwrap();
|
||||
|
||||
let raw_json = raw_connection_json(&storage, "nacos").await;
|
||||
assert!(!raw_json.contains("nacos-secret"));
|
||||
let persisted: ConnectionConfig = serde_json::from_str(&raw_json).unwrap();
|
||||
assert_eq!(nacos_auth_password(&persisted), Some(""));
|
||||
assert_eq!(
|
||||
storage.get_secret("nacos", NACOS_AUTH_PASSWORD_KEY).await.unwrap().as_deref(),
|
||||
Some("nacos-secret")
|
||||
);
|
||||
|
||||
let loaded = storage.load_connections().await.unwrap();
|
||||
assert_eq!(loaded.len(), 1);
|
||||
assert_eq!(nacos_auth_password(&loaded[0]), Some("nacos-secret"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn load_connections_migrates_legacy_nacos_auth_password_out_of_config_json() {
|
||||
let path = temp_db_path("nacos-auth-legacy-migration");
|
||||
let storage = Storage::open(&path).await.unwrap();
|
||||
insert_raw_connection(&storage, &nacos_connection("nacos", "legacy-nacos-secret")).await;
|
||||
|
||||
let loaded = storage.load_connections().await.unwrap();
|
||||
|
||||
assert_eq!(nacos_auth_password(&loaded[0]), Some("legacy-nacos-secret"));
|
||||
assert_eq!(
|
||||
storage.get_secret("nacos", NACOS_AUTH_PASSWORD_KEY).await.unwrap().as_deref(),
|
||||
Some("legacy-nacos-secret")
|
||||
);
|
||||
let raw_json = raw_connection_json(&storage, "nacos").await;
|
||||
assert!(!raw_json.contains("legacy-nacos-secret"));
|
||||
let persisted: ConnectionConfig = serde_json::from_str(&raw_json).unwrap();
|
||||
assert_eq!(nacos_auth_password(&persisted), Some(""));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn desktop_settings_default_to_background_enabled() {
|
||||
let path = temp_db_path("desktop-settings-default");
|
||||
|
||||
@@ -551,6 +551,15 @@ async fn main() {
|
||||
.route("/cloud-sync/webdav/password-status", post(routes::cloud_sync::webdav_password_status))
|
||||
.route("/cloud-sync/webdav/save-password", post(routes::cloud_sync::save_webdav_saved_password))
|
||||
.route("/cloud-sync/webdav/forget-password", post(routes::cloud_sync::forget_webdav_saved_password))
|
||||
.route("/cloud-sync/webdav/sync-secrets-status", post(routes::cloud_sync::webdav_sync_secrets_status))
|
||||
.route(
|
||||
"/cloud-sync/webdav/save-sync-secrets-preference",
|
||||
post(routes::cloud_sync::save_webdav_sync_secrets_preference),
|
||||
)
|
||||
.route(
|
||||
"/cloud-sync/webdav/forget-sync-secrets-passphrase",
|
||||
post(routes::cloud_sync::forget_webdav_sync_secrets_passphrase),
|
||||
)
|
||||
.route("/cloud-sync/webdav/upload", post(routes::cloud_sync::webdav_sync_upload))
|
||||
.route("/cloud-sync/webdav/download", post(routes::cloud_sync::webdav_sync_download));
|
||||
|
||||
|
||||
@@ -3,9 +3,12 @@ use std::sync::Arc;
|
||||
use axum::extract::State;
|
||||
use axum::Json;
|
||||
use dbx_core::cloud_sync::{
|
||||
apply_sync_snapshot, build_sync_snapshot, forget_webdav_password, resolve_webdav_password, save_webdav_password,
|
||||
webdav_saved_password_status, ApplySnapshotOptions, ApplySnapshotSummary, WebDavClient, WebDavConfig,
|
||||
WebDavPasswordStatus, WebDavSyncSummary,
|
||||
apply_sync_snapshot, build_sync_snapshot_with_saved_secrets, forget_webdav_password,
|
||||
forget_webdav_sync_secrets_passphrase as core_forget_webdav_sync_secrets_passphrase, resolve_webdav_password,
|
||||
resolve_webdav_sync_secrets_passphrase, save_webdav_password,
|
||||
save_webdav_sync_secrets_preference as core_save_webdav_sync_secrets_preference, webdav_saved_password_status,
|
||||
webdav_sync_secrets_status as core_webdav_sync_secrets_status, ApplySnapshotOptions, ApplySnapshotSummary,
|
||||
WebDavClient, WebDavConfig, WebDavPasswordStatus, WebDavSyncSecretsStatus, WebDavSyncSummary,
|
||||
};
|
||||
use dbx_core::storage::DesktopSettings;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -50,6 +53,13 @@ pub struct WebDavDownloadRequest {
|
||||
pub secrets_passphrase: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct WebDavSyncSecretsPreferenceRequest {
|
||||
pub enabled: bool,
|
||||
pub passphrase: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn webdav_sync_test(
|
||||
State(state): State<Arc<WebState>>,
|
||||
Json(mut req): Json<WebDavConfigRequest>,
|
||||
@@ -82,12 +92,33 @@ pub async fn forget_webdav_saved_password(
|
||||
Ok(Json(()))
|
||||
}
|
||||
|
||||
pub async fn webdav_sync_secrets_status(
|
||||
State(state): State<Arc<WebState>>,
|
||||
) -> Result<Json<WebDavSyncSecretsStatus>, AppError> {
|
||||
core_webdav_sync_secrets_status(&state.app.storage).await.map(Json).map_err(AppError)
|
||||
}
|
||||
|
||||
pub async fn save_webdav_sync_secrets_preference(
|
||||
State(state): State<Arc<WebState>>,
|
||||
Json(req): Json<WebDavSyncSecretsPreferenceRequest>,
|
||||
) -> Result<Json<()>, AppError> {
|
||||
core_save_webdav_sync_secrets_preference(&state.app.storage, req.enabled, req.passphrase.as_deref())
|
||||
.await
|
||||
.map_err(AppError)?;
|
||||
Ok(Json(()))
|
||||
}
|
||||
|
||||
pub async fn forget_webdav_sync_secrets_passphrase(State(state): State<Arc<WebState>>) -> Result<Json<()>, AppError> {
|
||||
core_forget_webdav_sync_secrets_passphrase(&state.app.storage).await.map_err(AppError)?;
|
||||
Ok(Json(()))
|
||||
}
|
||||
|
||||
pub async fn webdav_sync_upload(
|
||||
State(state): State<Arc<WebState>>,
|
||||
Json(mut req): Json<WebDavUploadRequest>,
|
||||
) -> Result<Json<WebDavSyncSummary>, AppError> {
|
||||
resolve_webdav_password(&state.app.storage, &mut req.config).await.map_err(AppError)?;
|
||||
let snapshot = build_sync_snapshot(
|
||||
let snapshot = build_sync_snapshot_with_saved_secrets(
|
||||
&state.app.storage,
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
req.editor_settings,
|
||||
@@ -104,10 +135,16 @@ pub async fn webdav_sync_download(
|
||||
) -> Result<Json<WebDavDownloadResult>, AppError> {
|
||||
resolve_webdav_password(&state.app.storage, &mut req.config).await.map_err(AppError)?;
|
||||
let (snapshot, summary) = WebDavClient::new(req.config).get_snapshot().await.map_err(AppError)?;
|
||||
let explicit_passphrase = req.secrets_passphrase.as_deref().map(str::trim).filter(|value| !value.is_empty());
|
||||
let saved_passphrase = if explicit_passphrase.is_some() {
|
||||
None
|
||||
} else {
|
||||
resolve_webdav_sync_secrets_passphrase(&state.app.storage).await.map_err(AppError)?
|
||||
};
|
||||
let apply_summary = apply_sync_snapshot(
|
||||
&state.app.storage,
|
||||
&snapshot,
|
||||
ApplySnapshotOptions { secrets_passphrase: req.secrets_passphrase.as_deref() },
|
||||
ApplySnapshotOptions { secrets_passphrase: explicit_passphrase.or(saved_passphrase.as_deref()) },
|
||||
)
|
||||
.await
|
||||
.map_err(AppError)?;
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use dbx_core::cloud_sync::{
|
||||
apply_sync_snapshot, build_sync_snapshot, forget_webdav_password, resolve_webdav_password, save_webdav_password,
|
||||
webdav_saved_password_status, ApplySnapshotOptions, ApplySnapshotSummary, WebDavClient, WebDavConfig,
|
||||
WebDavPasswordStatus, WebDavSyncSummary,
|
||||
apply_sync_snapshot, build_sync_snapshot_with_saved_secrets, forget_webdav_password,
|
||||
forget_webdav_sync_secrets_passphrase as core_forget_webdav_sync_secrets_passphrase, resolve_webdav_password,
|
||||
resolve_webdav_sync_secrets_passphrase, save_webdav_password,
|
||||
save_webdav_sync_secrets_preference as core_save_webdav_sync_secrets_preference, webdav_saved_password_status,
|
||||
webdav_sync_secrets_status as core_webdav_sync_secrets_status, ApplySnapshotOptions, ApplySnapshotSummary,
|
||||
WebDavClient, WebDavConfig, WebDavPasswordStatus, WebDavSyncSecretsStatus, WebDavSyncSummary,
|
||||
};
|
||||
use dbx_core::storage::DesktopSettings;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -48,6 +51,25 @@ pub async fn forget_webdav_saved_password(state: State<'_, Arc<AppState>>, confi
|
||||
forget_webdav_password(&state.storage, &config).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn webdav_sync_secrets_status(state: State<'_, Arc<AppState>>) -> Result<WebDavSyncSecretsStatus, String> {
|
||||
core_webdav_sync_secrets_status(&state.storage).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn save_webdav_sync_secrets_preference(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
enabled: bool,
|
||||
passphrase: Option<String>,
|
||||
) -> Result<(), String> {
|
||||
core_save_webdav_sync_secrets_preference(&state.storage, enabled, passphrase.as_deref()).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn forget_webdav_sync_secrets_passphrase(state: State<'_, Arc<AppState>>) -> Result<(), String> {
|
||||
core_forget_webdav_sync_secrets_passphrase(&state.storage).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn webdav_sync_upload(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
@@ -56,9 +78,13 @@ pub async fn webdav_sync_upload(
|
||||
secrets_passphrase: Option<String>,
|
||||
) -> Result<WebDavSyncSummary, String> {
|
||||
resolve_webdav_password(&state.storage, &mut config).await?;
|
||||
let snapshot =
|
||||
build_sync_snapshot(&state.storage, env!("CARGO_PKG_VERSION"), editor_settings, secrets_passphrase.as_deref())
|
||||
.await?;
|
||||
let snapshot = build_sync_snapshot_with_saved_secrets(
|
||||
&state.storage,
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
editor_settings,
|
||||
secrets_passphrase.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
WebDavClient::new(config).put_snapshot(&snapshot).await
|
||||
}
|
||||
|
||||
@@ -70,10 +96,16 @@ pub async fn webdav_sync_download(
|
||||
) -> Result<WebDavDownloadResult, String> {
|
||||
resolve_webdav_password(&state.storage, &mut config).await?;
|
||||
let (snapshot, summary) = WebDavClient::new(config).get_snapshot().await?;
|
||||
let explicit_passphrase = secrets_passphrase.as_deref().map(str::trim).filter(|value| !value.is_empty());
|
||||
let saved_passphrase = if explicit_passphrase.is_some() {
|
||||
None
|
||||
} else {
|
||||
resolve_webdav_sync_secrets_passphrase(&state.storage).await?
|
||||
};
|
||||
let apply_summary = apply_sync_snapshot(
|
||||
&state.storage,
|
||||
&snapshot,
|
||||
ApplySnapshotOptions { secrets_passphrase: secrets_passphrase.as_deref() },
|
||||
ApplySnapshotOptions { secrets_passphrase: explicit_passphrase.or(saved_passphrase.as_deref()) },
|
||||
)
|
||||
.await?;
|
||||
Ok(WebDavDownloadResult {
|
||||
|
||||
@@ -739,6 +739,9 @@ pub fn run() {
|
||||
commands::cloud_sync::webdav_password_status,
|
||||
commands::cloud_sync::save_webdav_saved_password,
|
||||
commands::cloud_sync::forget_webdav_saved_password,
|
||||
commands::cloud_sync::webdav_sync_secrets_status,
|
||||
commands::cloud_sync::save_webdav_sync_secrets_preference,
|
||||
commands::cloud_sync::forget_webdav_sync_secrets_passphrase,
|
||||
commands::cloud_sync::webdav_sync_upload,
|
||||
commands::cloud_sync::webdav_sync_download,
|
||||
commands::connection::test_connection,
|
||||
|
||||
Reference in New Issue
Block a user