From b27e2f168c9aedf84838e59dfe2b287a078b6220 Mon Sep 17 00:00:00 2001 From: Joe Julian Date: Mon, 6 Apr 2026 16:05:12 -0700 Subject: [PATCH] Persist vault binding refs in recent remotes --- main.go | 76 ++++++++++++++++++++++++++++++++-------------------- main_test.go | 58 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 29 deletions(-) diff --git a/main.go b/main.go index 1acac2f..1391f42 100644 --- a/main.go +++ b/main.go @@ -152,10 +152,13 @@ type recentVaultRecord struct { } type recentRemoteRecord struct { - BaseURL string `json:"baseUrl"` - Path string `json:"path"` - LastGroup []string `json:"lastGroup,omitempty"` - UsedAt string `json:"usedAt,omitempty"` + BaseURL string `json:"baseUrl"` + Path string `json:"path"` + LocalVaultPath string `json:"localVaultPath,omitempty"` + RemoteProfileID string `json:"remoteProfileId,omitempty"` + CredentialEntryID string `json:"credentialEntryId,omitempty"` + LastGroup []string `json:"lastGroup,omitempty"` + UsedAt string `json:"usedAt,omitempty"` } type uiPreferences struct { @@ -1077,21 +1080,23 @@ func (u *ui) openRemoteAction() error { if err != nil { return err } - if binding, resolved, ok, err := u.resolvedSelectedVaultRemoteBinding(); err != nil { - return err - } else if ok { - if err := u.state.OpenBoundRemoteVault(binding, key); err != nil { + if u.hasOpenVault() { + if binding, resolved, ok, err := u.resolvedSelectedVaultRemoteBinding(); err != nil { return err + } else if ok { + if err := u.state.OpenBoundRemoteVault(binding, key); err != nil { + return err + } + u.remoteBaseURL.SetText(resolved.Profile.BaseURL) + u.remotePath.SetText(resolved.Profile.Path) + u.noteRecentRemote(resolved.Profile.BaseURL, resolved.Profile.Path) + u.resetPasswordPeek() + u.restoreRecentRemoteGroup(resolved.Profile.BaseURL, resolved.Profile.Path) + u.loadSecuritySettingsFromSession() + u.editingEntry = false + u.filter() + return nil } - u.remoteBaseURL.SetText(resolved.Profile.BaseURL) - u.remotePath.SetText(resolved.Profile.Path) - u.noteRecentRemote(resolved.Profile.BaseURL, resolved.Profile.Path) - u.resetPasswordPeek() - u.restoreRecentRemoteGroup(resolved.Profile.BaseURL, resolved.Profile.Path) - u.loadSecuritySettingsFromSession() - u.editingEntry = false - u.filter() - return nil } client := webdav.Client{ BaseURL: strings.TrimSpace(u.remoteBaseURL.Text()), @@ -1132,19 +1137,21 @@ func (u *ui) startOpenRemoteAction() { Password: u.remotePassword.Text(), } remotePath := strings.TrimSpace(u.remotePath.Text()) - if _, resolved, ok, err := u.resolvedSelectedVaultRemoteBinding(); err != nil { - u.state.ErrorMessage = u.describeActionError("open remote vault", err) - u.requestMasterPassFocus = true - return - } else if ok { - client = webdav.Client{ - BaseURL: resolved.Profile.BaseURL, - Username: resolved.Credentials.Username, - Password: resolved.Credentials.Password, + if u.hasOpenVault() { + if _, resolved, ok, err := u.resolvedSelectedVaultRemoteBinding(); err != nil { + u.state.ErrorMessage = u.describeActionError("open remote vault", err) + u.requestMasterPassFocus = true + return + } else if ok { + client = webdav.Client{ + BaseURL: resolved.Profile.BaseURL, + Username: resolved.Credentials.Username, + Password: resolved.Credentials.Password, + } + remotePath = resolved.Profile.Path + u.remoteBaseURL.SetText(resolved.Profile.BaseURL) + u.remotePath.SetText(resolved.Profile.Path) } - remotePath = resolved.Profile.Path - u.remoteBaseURL.SetText(resolved.Profile.BaseURL) - u.remotePath.SetText(resolved.Profile.Path) } u.lastLifecycleAction = "open remote vault" u.runBackgroundAction("open remote vault", func() (func() error, error) { @@ -1518,6 +1525,9 @@ func (u *ui) loadRecentRemotes() { for _, record := range records { record.BaseURL = strings.TrimSpace(record.BaseURL) record.Path = strings.TrimSpace(record.Path) + record.LocalVaultPath = strings.TrimSpace(record.LocalVaultPath) + record.RemoteProfileID = strings.TrimSpace(record.RemoteProfileID) + record.CredentialEntryID = strings.TrimSpace(record.CredentialEntryID) if record.BaseURL == "" || record.Path == "" { continue } @@ -1750,6 +1760,11 @@ func (u *ui) noteRecentRemote(baseURL, path string) { LastGroup: append([]string(nil), u.currentPath...), UsedAt: u.now().Format(time.RFC3339Nano), } + if binding, ok := u.selectedVaultRemoteBinding(); ok { + record.LocalVaultPath = binding.LocalVaultPath + record.RemoteProfileID = binding.RemoteProfileID + record.CredentialEntryID = binding.CredentialEntryID + } if len(record.LastGroup) == 0 { record.LastGroup = u.recentRemoteGroup(baseURL, path) } @@ -1905,6 +1920,9 @@ func (u *ui) selectedRecentRemoteRecord() (recentRemoteRecord, bool) { func (u *ui) applyRecentRemoteRecord(record recentRemoteRecord) { u.remoteBaseURL.SetText(record.BaseURL) u.remotePath.SetText(record.Path) + u.vaultPath.SetText(strings.TrimSpace(record.LocalVaultPath)) + u.selectedVaultRemoteProfileID = strings.TrimSpace(record.RemoteProfileID) + u.selectedVaultRemoteCredentialEntryID = strings.TrimSpace(record.CredentialEntryID) u.remotePassword.Mask = '•' u.selectedRemoteConnection = true } diff --git a/main_test.go b/main_test.go index 1bdf8f3..34df635 100644 --- a/main_test.go +++ b/main_test.go @@ -4018,6 +4018,40 @@ func TestUIRecentRemoteConnectionsPersistAndReload(t *testing.T) { } } +func TestUIRecentRemoteConnectionsPersistVaultBindingMetadata(t *testing.T) { + t.Parallel() + + configPath := filepath.Join(t.TempDir(), "recent-remotes.json") + + first := newUIWithSession("desktop", &session.Manager{}) + first.recentRemotesPath = configPath + first.recentRemotes = nil + first.currentPath = []string{"Root", "Internet"} + first.vaultPath.SetText("/vaults/family.kdbx") + first.selectedVaultRemoteProfileID = "remote-profile-1" + first.selectedVaultRemoteCredentialEntryID = "remote-creds-1" + first.noteRecentRemote("https://dav.example.com", "vaults/home.kdbx") + + second := newUIWithSession("desktop", &session.Manager{}) + second.recentRemotesPath = configPath + second.recentRemotes = nil + second.loadRecentRemotes() + + if got := len(second.recentRemotes); got != 1 { + t.Fatalf("len(recentRemotes) = %d, want 1", got) + } + record := second.recentRemotes[0] + if record.LocalVaultPath != "/vaults/family.kdbx" { + t.Fatalf("recentRemotes[0].LocalVaultPath = %q, want /vaults/family.kdbx", record.LocalVaultPath) + } + if record.RemoteProfileID != "remote-profile-1" { + t.Fatalf("recentRemotes[0].RemoteProfileID = %q, want remote-profile-1", record.RemoteProfileID) + } + if record.CredentialEntryID != "remote-creds-1" { + t.Fatalf("recentRemotes[0].CredentialEntryID = %q, want remote-creds-1", record.CredentialEntryID) + } +} + func TestUILoadRecentRemotesIgnoresLegacySavedCredentials(t *testing.T) { t.Parallel() @@ -4048,6 +4082,30 @@ func TestUILoadRecentRemotesIgnoresLegacySavedCredentials(t *testing.T) { } } +func TestUIApplyRecentRemoteRecordRestoresVaultBindingSelection(t *testing.T) { + t.Parallel() + + u := newUIWithSession("desktop", &session.Manager{}) + + u.applyRecentRemoteRecord(recentRemoteRecord{ + BaseURL: "https://dav.example.com", + Path: "vaults/home.kdbx", + LocalVaultPath: "/vaults/family.kdbx", + RemoteProfileID: "remote-profile-1", + CredentialEntryID: "remote-creds-1", + }) + + if got := u.vaultPath.Text(); got != "/vaults/family.kdbx" { + t.Fatalf("vaultPath = %q, want /vaults/family.kdbx", got) + } + if got := u.selectedVaultRemoteProfileID; got != "remote-profile-1" { + t.Fatalf("selectedVaultRemoteProfileID = %q, want remote-profile-1", got) + } + if got := u.selectedVaultRemoteCredentialEntryID; got != "remote-creds-1" { + t.Fatalf("selectedVaultRemoteCredentialEntryID = %q, want remote-creds-1", got) + } +} + func TestUIStartupPreselectsNewestTargetAcrossLocalAndRemote(t *testing.T) { t.Parallel()