fix: bind sync state to active vault
This commit is contained in:
+81
-12
@@ -108,6 +108,9 @@ func NewApp(
|
||||
debug: debugEnabled,
|
||||
activityEvents: make(map[string]bool),
|
||||
}
|
||||
if app.syncSvc == nil {
|
||||
app.rebindSyncService()
|
||||
}
|
||||
app.ensureActivityProviderSubscriptions()
|
||||
app.startFileWatcherForOpenVault()
|
||||
return app
|
||||
@@ -714,6 +717,7 @@ func (a *App) CreateVault(path string) error {
|
||||
if err := a.vault.CreateVault(path); err != nil {
|
||||
return err
|
||||
}
|
||||
a.rebindSyncService()
|
||||
a.secretsSession = nil
|
||||
a.startFileWatcherForOpenVault()
|
||||
return nil
|
||||
@@ -727,6 +731,7 @@ func (a *App) OpenVault(path string) error {
|
||||
if err := a.vault.OpenVault(path); err != nil {
|
||||
return err
|
||||
}
|
||||
a.rebindSyncService()
|
||||
a.secretsSession = nil
|
||||
a.startFileWatcherForOpenVault()
|
||||
return nil
|
||||
@@ -741,6 +746,7 @@ func (a *App) CloseVault() error {
|
||||
a.fileWatcher.Stop()
|
||||
}
|
||||
a.vault.CloseVault()
|
||||
a.syncSvc = nil
|
||||
a.secretsSession = nil
|
||||
return nil
|
||||
}
|
||||
@@ -1656,6 +1662,7 @@ func (a *App) SetCurrentVault(path string) string {
|
||||
a.secretsSession = nil
|
||||
// Save the actual vault path (normalized by OpenVault, includes VerstakVault/)
|
||||
vaultPath := a.vault.GetVaultPath()
|
||||
a.rebindSyncService()
|
||||
if err := a.appSettings.SetCurrentVault(vaultPath); err != nil {
|
||||
return fmt.Sprintf("failed to save app settings: %v", err)
|
||||
}
|
||||
@@ -1685,6 +1692,16 @@ func (a *App) SetCurrentVault(path string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (a *App) rebindSyncService() {
|
||||
if a == nil || a.vault == nil || a.vault.GetVaultStatus() != vault.StatusOpen {
|
||||
if a != nil {
|
||||
a.syncSvc = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
a.syncSvc = syncsvc.NewService(a.vault.GetVaultPath(), "")
|
||||
}
|
||||
|
||||
func (a *App) startFileWatcherForOpenVault() {
|
||||
if a == nil || a.vault == nil || a.eventBus == nil {
|
||||
return
|
||||
@@ -2160,7 +2177,9 @@ func (a *App) syncStatus() (*SyncStatusDTO, error) {
|
||||
LastError: cfg.Sync.LastError,
|
||||
}
|
||||
|
||||
if cfg.Sync.DeviceID != "" {
|
||||
if deviceID := a.syncSvc.GetDeviceID(); deviceID != "" {
|
||||
dto.DeviceID = deviceID
|
||||
} else if cfg.Sync.DeviceID != "" {
|
||||
dto.DeviceID = cfg.Sync.DeviceID
|
||||
}
|
||||
|
||||
@@ -2170,10 +2189,13 @@ func (a *App) syncStatus() (*SyncStatusDTO, error) {
|
||||
if deviceToken != "" {
|
||||
client := newSyncClient(serverURL, "", "", vaultPath)
|
||||
client.DeviceToken = deviceToken
|
||||
if cfg.Sync.DeviceID != "" {
|
||||
client.DeviceID = cfg.Sync.DeviceID
|
||||
if dto.DeviceID != "" {
|
||||
client.DeviceID = dto.DeviceID
|
||||
}
|
||||
if info, err := client.GetMe(); err == nil {
|
||||
if info.DeviceID != "" {
|
||||
_ = a.syncSvc.SetDeviceID(info.DeviceID)
|
||||
}
|
||||
dto.DeviceName = info.DeviceName
|
||||
dto.DeviceID = info.DeviceID
|
||||
dto.Connected = true
|
||||
@@ -2224,19 +2246,24 @@ func (a *App) syncConfigure(serverURL, username, password string) error {
|
||||
if err := a.requireVault(); err != nil {
|
||||
return err
|
||||
}
|
||||
meta := a.vault.GetVaultMeta()
|
||||
if meta == nil || strings.TrimSpace(meta.VaultID) == "" {
|
||||
return fmt.Errorf("vault ID is unavailable")
|
||||
}
|
||||
vaultPath := a.vaultPath()
|
||||
hostname, _ := os.Hostname()
|
||||
if hostname == "" {
|
||||
hostname = "unknown"
|
||||
}
|
||||
client := newSyncClient(serverURL, "", "", vaultPath)
|
||||
deviceID, deviceToken, err := client.PairDevice(serverURL, username, password, hostname, "verstak-desktop/v2")
|
||||
deviceID, deviceToken, err := client.PairDevice(serverURL, username, password, hostname, "verstak-desktop/v2", meta.VaultID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("pair: %w", err)
|
||||
}
|
||||
if err := syncsvc.SaveDeviceToken(vaultPath, deviceToken); err != nil {
|
||||
return fmt.Errorf("save token: %w", err)
|
||||
}
|
||||
a.syncSvc = syncsvc.NewService(vaultPath, deviceID)
|
||||
if err := a.syncSvc.SetState(serverURL, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -2271,9 +2298,15 @@ func (a *App) syncDisconnect() error {
|
||||
vaultPath := a.vaultPath()
|
||||
deviceToken := syncsvc.LoadDeviceToken(vaultPath)
|
||||
cfg := a.appSettings.Get()
|
||||
serverURL := cfg.Sync.ServerURL
|
||||
if a.syncSvc != nil {
|
||||
if currentServerURL, _, _, _, err := a.syncSvc.GetState(); err == nil && currentServerURL != "" {
|
||||
serverURL = currentServerURL
|
||||
}
|
||||
}
|
||||
|
||||
if deviceToken != "" {
|
||||
client := newSyncClient(cfg.Sync.ServerURL, "", "", vaultPath)
|
||||
if deviceToken != "" && serverURL != "" {
|
||||
client := newSyncClient(serverURL, "", "", vaultPath)
|
||||
client.DeviceToken = deviceToken
|
||||
_ = client.RevokeCurrent()
|
||||
}
|
||||
@@ -2288,6 +2321,9 @@ func (a *App) syncDisconnect() error {
|
||||
if err := a.appSettings.UpdateSync(cfg.Sync); err != nil {
|
||||
return err
|
||||
}
|
||||
if a.syncSvc == nil {
|
||||
return nil
|
||||
}
|
||||
return a.syncSvc.SetState("", "")
|
||||
}
|
||||
|
||||
@@ -2350,6 +2386,7 @@ func (a *App) syncResetKey() error {
|
||||
return err
|
||||
}
|
||||
vaultPath := a.vaultPath()
|
||||
deviceToken := syncsvc.LoadDeviceToken(vaultPath)
|
||||
if err := syncsvc.RemoveDeviceToken(vaultPath); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
@@ -2361,7 +2398,14 @@ func (a *App) syncResetKey() error {
|
||||
cfg.Sync.LastStatus = "disconnected"
|
||||
cfg.Sync.LastError = ""
|
||||
if a.syncSvc != nil {
|
||||
if err := a.syncSvc.SetState(cfg.Sync.ServerURL, ""); err != nil {
|
||||
serverURL, _, _, _, err := a.syncSvc.GetState()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if serverURL == "" && deviceToken != "" {
|
||||
serverURL = cfg.Sync.ServerURL
|
||||
}
|
||||
if err := a.syncSvc.SetState(serverURL, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -2384,6 +2428,12 @@ func (a *App) syncNow() (map[string]interface{}, error) {
|
||||
return nil, err
|
||||
}
|
||||
vaultPath := a.vaultPath()
|
||||
if a.syncSvc == nil {
|
||||
a.rebindSyncService()
|
||||
}
|
||||
if a.syncSvc == nil {
|
||||
return nil, fmt.Errorf("sync service not initialized")
|
||||
}
|
||||
|
||||
serverURL, apiKey, lastPullSeq, _, err := a.syncSvc.GetState()
|
||||
deviceToken := syncsvc.LoadDeviceToken(vaultPath)
|
||||
@@ -2391,14 +2441,28 @@ func (a *App) syncNow() (map[string]interface{}, error) {
|
||||
return nil, fmt.Errorf("sync not configured")
|
||||
}
|
||||
|
||||
deviceID := ""
|
||||
deviceID := a.syncSvc.GetDeviceID()
|
||||
cfg := a.appSettings.Get()
|
||||
if cfg.Sync.DeviceID != "" {
|
||||
if deviceID == "" && deviceToken == "" && cfg.Sync.DeviceID != "" {
|
||||
deviceID = cfg.Sync.DeviceID
|
||||
}
|
||||
|
||||
client := newSyncClient(serverURL, apiKey, deviceID, vaultPath)
|
||||
client.DeviceToken = deviceToken
|
||||
if deviceID == "" && deviceToken != "" {
|
||||
info, err := client.GetMe()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sync identity: %w", err)
|
||||
}
|
||||
if info.DeviceID == "" {
|
||||
return nil, fmt.Errorf("sync identity: server returned an empty device ID")
|
||||
}
|
||||
if err := a.syncSvc.SetDeviceID(info.DeviceID); err != nil {
|
||||
return nil, fmt.Errorf("save sync identity: %w", err)
|
||||
}
|
||||
deviceID = info.DeviceID
|
||||
client.DeviceID = deviceID
|
||||
}
|
||||
|
||||
unpushed, err := a.syncSvc.GetUnpushedOps()
|
||||
if err != nil {
|
||||
@@ -2637,14 +2701,19 @@ func syncPayloadPath(op syncsvc.Op, payload syncFilePayload) string {
|
||||
}
|
||||
|
||||
func (a *App) localSyncDeviceID() string {
|
||||
if a.syncSvc != nil {
|
||||
if deviceID := a.syncSvc.GetDeviceID(); deviceID != "" {
|
||||
return deviceID
|
||||
}
|
||||
}
|
||||
if a.vault != nil && a.vault.GetVaultStatus() == vault.StatusOpen && syncsvc.LoadDeviceToken(a.vaultPath()) != "" {
|
||||
return ""
|
||||
}
|
||||
if a.appSettings != nil {
|
||||
if deviceID := a.appSettings.Get().Sync.DeviceID; deviceID != "" {
|
||||
return deviceID
|
||||
}
|
||||
}
|
||||
if a.syncSvc != nil {
|
||||
return a.syncSvc.GetDeviceID()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
|
||||
@@ -1104,6 +1104,174 @@ func TestSyncNowPushesLocalOpsAndAppliesPulledFileOps(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncConfigurePairsCurrentVaultID(t *testing.T) {
|
||||
app, _ := newSyncFilesTestApp(t, []string{"files.read", "files.write", "files.delete"}, "local-device")
|
||||
meta := app.vault.GetVaultMeta()
|
||||
if meta == nil || meta.VaultID == "" {
|
||||
t.Fatal("test vault must have an ID")
|
||||
}
|
||||
|
||||
var pairedVaultID string
|
||||
server := newLocalHTTPTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/api/client/pair" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
var request struct {
|
||||
VaultID string `json:"vault_id"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
pairedVaultID = request.VaultID
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{
|
||||
"device_id": "paired-device",
|
||||
"device_token": "paired-token",
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
if err := app.syncConfigure(server.URL, "alice", "secret"); err != nil {
|
||||
t.Fatalf("syncConfigure: %v", err)
|
||||
}
|
||||
if pairedVaultID != meta.VaultID {
|
||||
t.Fatalf("paired vault ID = %q, want %q", pairedVaultID, meta.VaultID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncNowHydratesLegacyVaultDeviceID(t *testing.T) {
|
||||
app, root := newSyncFilesTestApp(t, []string{"files.read", "files.write", "files.delete"}, "wrong-global-device")
|
||||
var pushedDeviceID string
|
||||
server := newLocalHTTPTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get("Authorization") != "Bearer vault-token" {
|
||||
http.Error(w, "missing auth", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
switch r.URL.Path {
|
||||
case "/api/client/me":
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{"device_id": "vault-device"})
|
||||
case "/api/v1/sync/push":
|
||||
var request struct {
|
||||
DeviceID string `json:"device_id"`
|
||||
Ops []struct {
|
||||
OpID string `json:"op_id"`
|
||||
} `json:"ops"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
pushedDeviceID = request.DeviceID
|
||||
accepted := make([]string, 0, len(request.Ops))
|
||||
for _, op := range request.Ops {
|
||||
accepted = append(accepted, op.OpID)
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"accepted": accepted,
|
||||
"count": len(accepted),
|
||||
"conflicts": []map[string]interface{}{},
|
||||
})
|
||||
case "/api/v1/sync/pull":
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"server_sequence": 0,
|
||||
"ops": []map[string]interface{}{},
|
||||
})
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
app.syncSvc = syncsvc.NewService(root, "")
|
||||
if err := app.syncSvc.SetState(server.URL, ""); err != nil {
|
||||
t.Fatalf("SetState: %v", err)
|
||||
}
|
||||
if err := syncsvc.SaveDeviceToken(root, "vault-token"); err != nil {
|
||||
t.Fatalf("SaveDeviceToken: %v", err)
|
||||
}
|
||||
if err := app.syncSvc.RecordOp(syncsvc.EntityFile, "Docs/one.txt", syncsvc.OpCreate, map[string]string{"path": "Docs/one.txt"}); err != nil {
|
||||
t.Fatalf("RecordOp: %v", err)
|
||||
}
|
||||
|
||||
if _, err := app.syncNow(); err != nil {
|
||||
t.Fatalf("syncNow: %v", err)
|
||||
}
|
||||
if pushedDeviceID != "vault-device" {
|
||||
t.Fatalf("pushed device ID = %q, want vault-device", pushedDeviceID)
|
||||
}
|
||||
if app.syncSvc.GetDeviceID() != "vault-device" {
|
||||
t.Fatalf("persisted device ID = %q, want vault-device", app.syncSvc.GetDeviceID())
|
||||
}
|
||||
}
|
||||
|
||||
func TestVaultTransitionsRebindSyncService(t *testing.T) {
|
||||
settings := appsettings.NewManager(filepath.Join(t.TempDir(), "config.json"))
|
||||
if err := settings.Load(); err != nil {
|
||||
t.Fatalf("settings Load: %v", err)
|
||||
}
|
||||
app := &App{
|
||||
capRegistry: capability.NewRegistry(),
|
||||
vault: vault.NewVault(nil),
|
||||
appSettings: settings,
|
||||
}
|
||||
|
||||
if err := app.CreateVault(t.TempDir()); err != nil {
|
||||
t.Fatalf("CreateVault: %v", err)
|
||||
}
|
||||
if app.syncSvc == nil {
|
||||
t.Fatal("CreateVault must initialize the sync service")
|
||||
}
|
||||
|
||||
vaultB := vault.NewVault(nil)
|
||||
if err := vaultB.CreateVault(t.TempDir()); err != nil {
|
||||
t.Fatalf("Create vault B: %v", err)
|
||||
}
|
||||
primeSyncState(t, vaultB.GetVaultPath(), "device-b", "https://sync-b.example.test", 22)
|
||||
if err := app.OpenVault(vaultB.GetVaultPath()); err != nil {
|
||||
t.Fatalf("OpenVault B: %v", err)
|
||||
}
|
||||
assertSyncState(t, app.syncSvc, "device-b", "https://sync-b.example.test", 22)
|
||||
|
||||
vaultC := vault.NewVault(nil)
|
||||
if err := vaultC.CreateVault(t.TempDir()); err != nil {
|
||||
t.Fatalf("Create vault C: %v", err)
|
||||
}
|
||||
primeSyncState(t, vaultC.GetVaultPath(), "device-c", "https://sync-c.example.test", 33)
|
||||
if errStr := app.SetCurrentVault(vaultC.GetVaultPath()); errStr != "" {
|
||||
t.Fatalf("SetCurrentVault C: %s", errStr)
|
||||
}
|
||||
assertSyncState(t, app.syncSvc, "device-c", "https://sync-c.example.test", 33)
|
||||
}
|
||||
|
||||
func primeSyncState(t *testing.T, vaultRoot, deviceID, serverURL string, lastPullSeq int) {
|
||||
t.Helper()
|
||||
service := syncsvc.NewService(vaultRoot, deviceID)
|
||||
if err := service.SetState(serverURL, ""); err != nil {
|
||||
t.Fatalf("SetState: %v", err)
|
||||
}
|
||||
if err := service.SetLastPullSeq(lastPullSeq); err != nil {
|
||||
t.Fatalf("SetLastPullSeq: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func assertSyncState(t *testing.T, service *syncsvc.Service, wantDeviceID, wantServerURL string, wantLastPullSeq int) {
|
||||
t.Helper()
|
||||
if service == nil {
|
||||
t.Fatal("sync service is nil")
|
||||
}
|
||||
serverURL, _, lastPullSeq, _, err := service.GetState()
|
||||
if err != nil {
|
||||
t.Fatalf("GetState: %v", err)
|
||||
}
|
||||
if service.GetDeviceID() != wantDeviceID || serverURL != wantServerURL || lastPullSeq != wantLastPullSeq {
|
||||
t.Fatalf("sync state = device=%q server=%q cursor=%d, want device=%q server=%q cursor=%d",
|
||||
service.GetDeviceID(), serverURL, lastPullSeq, wantDeviceID, wantServerURL, wantLastPullSeq)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetCurrentVaultInitializesWorkspaceWhenMissingAtStartup(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
vaultParent := filepath.Join(tmpDir, "vault-parent")
|
||||
|
||||
Reference in New Issue
Block a user