Multi-user: email sign-in, invite/open registration, admin calls
- kv-proto: email on setup, AccountInfo.email/is_admin, ServerInfo.registration, admin DTOs. - kv-sync: prelogin by email or account id (setup QR / Emergency Kit), register with email, set_email, /v1/admin/* client calls. - kv-ffi: register_account(code, email), login(.., email), account_details, set_account_email, admin_create_invite. kv-wasm: the same plus the full admin API for the web vault. - Live test: two users on one server stay isolated; a new device signs in by email. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -70,7 +70,7 @@ async fn two_devices_against_local_server() {
|
||||
let sk = a.create_account(pw.into()).unwrap();
|
||||
connect(&a, &url, "device A");
|
||||
assert!(a.server_info().await.unwrap().setup_open, "server must be fresh (setup open)");
|
||||
a.register_account(token).await.unwrap();
|
||||
a.register_account(token, None).await.unwrap();
|
||||
let id = a.save_item(item("Bank", "pw-a")).unwrap();
|
||||
let r = a.sync_now().await.unwrap();
|
||||
assert_eq!(r.pushed, 1);
|
||||
@@ -79,13 +79,13 @@ async fn two_devices_against_local_server() {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(61)).await;
|
||||
let b = VaultCore::new_in_memory();
|
||||
connect(&b, &url, "device B");
|
||||
if b.login(pw.into(), sk.clone(), None).await.unwrap() == LoginResult::PendingApproval {
|
||||
if b.login(pw.into(), sk.clone(), None, None).await.unwrap() == LoginResult::PendingApproval {
|
||||
let devices = a.list_devices().await.unwrap();
|
||||
let pending = devices.iter().find(|d| !d.approved).expect("B listed as pending");
|
||||
assert_eq!(pending.key_fingerprint, b.device_key_fingerprint().unwrap());
|
||||
a.approve_device(pending.device_id.clone()).await.unwrap();
|
||||
tokio::time::sleep(std::time::Duration::from_secs(61)).await; // rate limit again
|
||||
assert_eq!(b.login(pw.into(), sk.clone(), None).await.unwrap(), LoginResult::Ok);
|
||||
assert_eq!(b.login(pw.into(), sk.clone(), None, None).await.unwrap(), LoginResult::Ok);
|
||||
}
|
||||
b.sync_now().await.unwrap();
|
||||
assert_eq!(b.get_item(id.clone()).unwrap().password.as_deref(), Some("pw-a"));
|
||||
@@ -167,7 +167,7 @@ async fn removed_device_reenrols_against_local_server() {
|
||||
let web = VaultCore::new_in_memory();
|
||||
web.create_account(pw.into()).unwrap();
|
||||
connect(&web, &url, "web vault");
|
||||
web.register_account(token).await.unwrap();
|
||||
web.register_account(token, None).await.unwrap();
|
||||
let id = web.save_item(item("Bank", "pw-a")).unwrap();
|
||||
web.sync_now().await.unwrap();
|
||||
|
||||
@@ -206,7 +206,7 @@ async fn delete_account_against_local_server() {
|
||||
let app = VaultCore::new_in_memory();
|
||||
app.create_account(pw.into()).unwrap();
|
||||
connect(&app, &url, "phone");
|
||||
app.register_account(token.clone()).await.unwrap();
|
||||
app.register_account(token.clone(), None).await.unwrap();
|
||||
app.save_item(item("Bank", "pw-a")).unwrap();
|
||||
app.sync_now().await.unwrap();
|
||||
|
||||
@@ -217,6 +217,55 @@ async fn delete_account_against_local_server() {
|
||||
let fresh = VaultCore::new_in_memory();
|
||||
fresh.create_account(pw.into()).unwrap();
|
||||
connect(&fresh, &url, "new phone");
|
||||
fresh.register_account(token).await.unwrap();
|
||||
fresh.register_account(token, None).await.unwrap();
|
||||
println!("account deleted and server reusable OK against {url}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "needs a fresh local kestrove-server; see the module docs"]
|
||||
async fn two_users_on_one_server() {
|
||||
let url = std::env::var("KESTROVE_SERVER_URL").unwrap_or_else(|_| "http://127.0.0.1:8080".into());
|
||||
let token =
|
||||
std::env::var("KESTROVE_SETUP_TOKEN").expect("set KESTROVE_SETUP_TOKEN to the token the server printed");
|
||||
let (pw_a, pw_b) = ("the admin's long master password", "the second user's long master password");
|
||||
|
||||
// The admin: first account, with the setup code.
|
||||
let admin = VaultCore::new_in_memory();
|
||||
admin.create_account(pw_a.into()).unwrap();
|
||||
connect(&admin, &url, "admin laptop");
|
||||
admin.register_account(token, Some("[email protected]".into())).await.unwrap();
|
||||
let details = admin.account_details().await.unwrap();
|
||||
assert_eq!((details.email.as_deref(), details.is_admin), (Some("[email protected]"), true));
|
||||
admin.save_item(item("Admin bank", "admin-secret")).unwrap();
|
||||
admin.sync_now().await.unwrap();
|
||||
|
||||
// A second user, invited by the admin.
|
||||
let invite = admin.admin_create_invite(Some("[email protected]".into()), None).await.unwrap();
|
||||
let b = VaultCore::new_in_memory();
|
||||
let sk_b = b.create_account(pw_b.into()).unwrap();
|
||||
connect(&b, &url, "b phone");
|
||||
b.register_account(invite.code.clone(), Some("[email protected]".into())).await.unwrap();
|
||||
assert!(!b.account_details().await.unwrap().is_admin);
|
||||
let b_item = b.save_item(item("B mail", "b-secret")).unwrap();
|
||||
b.sync_now().await.unwrap();
|
||||
// The invite is used up.
|
||||
let c = VaultCore::new_in_memory();
|
||||
c.create_account("someone else's password!!".into()).unwrap();
|
||||
connect(&c, &url, "c");
|
||||
assert!(c.register_account(invite.code, Some("[email protected]".into())).await.is_err());
|
||||
|
||||
// Neither sees the other's items after syncing.
|
||||
admin.sync_now().await.unwrap();
|
||||
let titles = |core: &VaultCore| core.list_items().unwrap().into_iter().map(|i| i.title).collect::<Vec<_>>();
|
||||
assert_eq!(titles(&admin), vec!["Admin bank".to_string()]);
|
||||
assert_eq!(titles(&b), vec!["B mail".to_string()]);
|
||||
|
||||
// B's new device signs in with email + password + Secret Key and gets only B's vault (after approval).
|
||||
let laptop = VaultCore::new_in_memory();
|
||||
connect(&laptop, &url, "b laptop");
|
||||
let r = laptop.login(pw_b.into(), sk_b, None, Some("[email protected]".into())).await.unwrap();
|
||||
assert_eq!(r, LoginResult::PendingApproval);
|
||||
let pending = b.list_devices().await.unwrap().into_iter().find(|d| !d.approved).unwrap();
|
||||
b.approve_device(pending.device_id).await.unwrap();
|
||||
println!("two users on {url}: isolated OK ({b_item})");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user