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:
2026-10-24 07:35:23 +07:00
co-authored by Claude Opus 5.5
parent ead95bb605
commit c84015f67b
16 changed files with 563 additions and 64 deletions
+6 -6
View File
@@ -70,7 +70,7 @@ async fn web_vault_enrolls_and_syncs_with_an_app_device() {
app.configure_server(server.url(), vec![]).unwrap();
app.set_device_signer(Arc::new(Signer(SoftwareSigner::generate(Uuid::new_v4()))), "phone".into(), PlatformDto::Ios)
.unwrap();
app.register_account(SETUP_TOKEN.into()).await.unwrap();
app.register_account(SETUP_TOKEN.into(), None).await.unwrap();
let mut on_app = kv_ffi::ItemDto {
id: String::new(),
kind: kv_ffi::ItemKindDto::Login,
@@ -93,12 +93,12 @@ async fn web_vault_enrolls_and_syncs_with_an_app_device() {
let signer = Arc::new(SoftwareSigner::generate(Uuid::new_v4()));
let web = web_core(store.clone(), &server.url(), signer.clone());
assert!(matches!(web.sync_now().await, Err(CoreError::NoAccount | CoreError::Locked)));
assert_eq!(web.login(PW, &sk, None).await.unwrap(), LoginResult::PendingApproval);
assert_eq!(web.login(PW, &sk, None, None).await.unwrap(), LoginResult::PendingApproval);
assert!(!web.has_account());
let pending = app.list_devices().await.unwrap().into_iter().find(|d| !d.approved).unwrap();
assert_eq!(pending.key_fingerprint, web.device_key_fingerprint().unwrap());
app.approve_device(pending.device_id).await.unwrap();
assert_eq!(web.login(PW, &sk, None).await.unwrap(), LoginResult::Ok);
assert_eq!(web.login(PW, &sk, None, None).await.unwrap(), LoginResult::Ok);
let r = web.sync_now().await.unwrap();
assert_eq!((r.pulled, r.vaults_synced), (1, 1));
assert_eq!(web.get_item(&bank).unwrap().password.as_deref(), Some("from the app"));
@@ -158,7 +158,7 @@ async fn concurrent_sync_is_refused_not_interleaved() {
let server = TestServer::start().await;
let web = web_core(Arc::new(WebStore::new()), &server.url(), Arc::new(SoftwareSigner::generate(Uuid::new_v4())));
let sk = web.create_account(PW).unwrap();
web.register_account(SETUP_TOKEN).await.unwrap();
web.register_account(SETUP_TOKEN, None).await.unwrap();
web.save_item(web_item("A", "1")).unwrap();
let (a, b) = tokio::join!(web.sync_now(), web.sync_now());
assert!(matches!((&a, &b), (Ok(_), Err(CoreError::Busy)) | (Err(CoreError::Busy), Ok(_))), "{a:?} {b:?}");
@@ -176,7 +176,7 @@ async fn app_with_item(server: &TestServer) -> (Arc<kv_ffi::VaultCore>, String,
app.configure_server(server.url(), vec![]).unwrap();
app.set_device_signer(Arc::new(Signer(SoftwareSigner::generate(Uuid::new_v4()))), "phone".into(), PlatformDto::Ios)
.unwrap();
app.register_account(SETUP_TOKEN.into()).await.unwrap();
app.register_account(SETUP_TOKEN.into(), None).await.unwrap();
let id = app
.save_item(kv_ffi::ItemDto {
id: String::new(),
@@ -254,7 +254,7 @@ async fn web_vault_enrolls_by_ticket_both_ways() {
assert!(matches!(web.unlock("wrong password!!", &sk), Err(CoreError::BadCredentials)));
assert!(matches!(web.setup_payload_for_kit(), Err(CoreError::Locked)));
web.unlock(PW, &sk).unwrap();
assert!(web.login("wrong password!!", &sk, None).await.is_err());
assert!(web.login("wrong password!!", &sk, None, None).await.is_err());
assert!(matches!(web.setup_payload_for_kit(), Err(CoreError::Locked)));
web.unlock(PW, &sk).unwrap();