sign_out_device: remove the vault from this device, keep the account on the server
Syncs first (fails if that sync fails, unless discard_unsynced), ends the session best effort and wipes the local store, keeping the device identity. Refused for a vault with no server, which would be lost. Test: unsynced item survives sign-out and returns after signing in again. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -991,11 +991,28 @@ impl VaultCore {
|
||||
let api = self.api()?;
|
||||
self.revoked(api.delete_account().await)?;
|
||||
}
|
||||
self.store.wipe()?;
|
||||
let mut inner = self.lock_inner();
|
||||
let device = inner.device.take();
|
||||
*inner = Inner { device, ..Default::default() };
|
||||
Ok(())
|
||||
self.forget_local()
|
||||
}
|
||||
|
||||
/// Signs this device out of Kestrove: the vault is removed from this device, while the account and its data
|
||||
/// stay on the server, so signing in again (setup QR, or password + Secret Key) brings everything back.
|
||||
/// Unless `discard_unsynced`, it syncs first and fails if that sync fails, so local changes aren't lost; the
|
||||
/// app then asks before calling again with `discard_unsynced = true`. A vault with no server can't be signed
|
||||
/// out of (it would be lost): `Invalid`; Delete Account is the way to remove it.
|
||||
pub async fn sign_out_device(&self, discard_unsynced: bool) -> Result<()> {
|
||||
if self.lock_inner().server.is_none() {
|
||||
return Err(CoreError::Invalid {
|
||||
msg: "this vault is only on this device; signing out would lose it (use Delete Account instead)".into(),
|
||||
});
|
||||
}
|
||||
if !discard_unsynced && self.is_logged_in() && !self.is_locked() {
|
||||
self.sync_now_inner().await?;
|
||||
}
|
||||
if self.is_logged_in() {
|
||||
// Best effort: the session expires on the server anyway.
|
||||
let _ = self.logout().await;
|
||||
}
|
||||
self.forget_local()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1021,6 +1038,16 @@ impl VaultCore {
|
||||
}
|
||||
|
||||
/// Clones what an operation needs so no lock is held while it runs.
|
||||
/// Wipes the local store and forgets the account and server; only the device identity is kept, so the next
|
||||
/// setup enrols it again (or, after a sign-out, signs it back in as the same device).
|
||||
fn forget_local(&self) -> Result<()> {
|
||||
self.store.wipe()?;
|
||||
let mut inner = self.lock_inner();
|
||||
let device = inner.device.take();
|
||||
*inner = Inner { device, ..Default::default() };
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Re-derives the keys from `password` and the stored Secret Key; `BadCredentials` if they don't open the keyset.
|
||||
fn verify_master_password(&self, password: &str) -> Result<()> {
|
||||
let (acct, u) = self.unlocked_snapshot()?;
|
||||
|
||||
@@ -664,3 +664,30 @@ async fn delete_account_checks_the_password_and_wipes_the_device() {
|
||||
assert!(!reopened.has_account());
|
||||
reopened.create_account("a new master password".into()).unwrap();
|
||||
}
|
||||
|
||||
/// Sign out of this device: the vault leaves the device, the account stays on the server, signing in brings it back.
|
||||
#[tokio::test]
|
||||
async fn sign_out_device_keeps_the_account_on_the_server() {
|
||||
let server = TestServer::start().await;
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("a.sqlite").to_string_lossy().into_owned();
|
||||
|
||||
let local = VaultCore::open(dir.path().join("local.sqlite").to_string_lossy().into_owned()).unwrap();
|
||||
local.create_account(PW.into()).unwrap();
|
||||
assert!(matches!(local.sign_out_device(false).await, Err(CoreError::Invalid { .. })));
|
||||
assert!(local.has_account(), "a vault with no server is never signed out of");
|
||||
|
||||
let a = VaultCore::open(path.clone()).unwrap();
|
||||
let sk = a.create_account(PW.into()).unwrap();
|
||||
connect(&a, &server);
|
||||
a.register_account(SETUP_TOKEN.into()).await.unwrap();
|
||||
let id = a.save_item(login_item("Bank", "pw-a")).unwrap(); // not synced yet: sign-out syncs it first
|
||||
a.sign_out_device(false).await.unwrap();
|
||||
assert!(!a.has_account() && !a.is_logged_in());
|
||||
|
||||
// Same device, signed in again: the item came back from the server.
|
||||
connect(&a, &server);
|
||||
assert_eq!(a.login(PW.into(), sk, None).await.unwrap(), LoginResult::Ok);
|
||||
a.sync_now().await.unwrap();
|
||||
assert_eq!(a.get_item(id).unwrap().password.as_deref(), Some("pw-a"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user