eidetica/user/session/mod.rs
1//! User session management
2//!
3//! Represents an authenticated user session with decrypted keys.
4//!
5//! # API Overview
6//!
7//! The User API is organized into three areas for managing Databases:
8//!
9//! ## Database Lifecycle
10//!
11//! - **`create_database()`** - Create a new database
12//! - **`open_database()`** - Open an existing database
13//! - **`find_database()`** - Search for databases by name
14//!
15//! ## Tracked Databases
16//!
17//! Manage your personal list of tracked databases:
18//!
19//! - **`databases()`** - List all tracked databases
20//! - **`database()`** - Get a specific tracked database
21//! - **`track_database()`** - Add or update a tracked database (upsert)
22//! - **`untrack_database()`** - Remove a database from your tracked list
23//!
24//! ## Key-Database Mappings
25//!
26//! Control which keys access which databases:
27//!
28//! - **`map_key()`** - Map a key to a SigKey identifier for a database
29//! - **`key_mapping()`** - Get the SigKey mapping for a key-database pair
30//! - **`find_key()`** - Find which key can access a database
31//!
32//! This explicit approach ensures predictable behavior and avoids ambiguity about which
33//! keys have access to which databases.
34
35use handle_trait::Handle;
36use std::collections::HashMap;
37
38use super::{UserKeyManager, types::UserInfo};
39use crate::{
40 Database, Error, Instance, Result, Transaction,
41 auth::{Permission, SigKey, crypto::PublicKey},
42 crdt::Doc,
43 database::DatabaseKey,
44 entry::ID,
45 instance::{InstanceError, backend::Backend},
46 store::Table,
47 sync::{BootstrapRequest, DatabaseTicket, Sync},
48 user::{SyncSettings, TrackedDatabase, UserError},
49};
50
51#[cfg(test)]
52mod tests;
53
54/// User session object, returned after successful login
55///
56/// Represents an authenticated user with decrypted private keys loaded in memory.
57/// The User struct provides access to key management, tracked databases, and
58/// bootstrap approval operations.
59pub struct User {
60 /// Stable internal user UUID (Table primary key)
61 user_uuid: String,
62
63 /// Username (login identifier)
64 username: String,
65
66 /// User's private database (contains encrypted keys and tracked databases)
67 user_database: Database,
68
69 /// Instance reference for database operations
70 instance: Instance,
71
72 /// Decrypted user keys (in memory only during session)
73 key_manager: UserKeyManager,
74
75 /// User info (cached from _users database)
76 user_info: UserInfo,
77}
78
79impl std::fmt::Debug for User {
80 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81 f.debug_struct("User")
82 .field("user_uuid", &self.user_uuid)
83 .field("username", &self.username)
84 .field("user_database", &self.user_database)
85 .field("instance", &self.instance)
86 .field("key_manager", &"<KeyManager [sensitive]>")
87 .field("user_info", &self.user_info)
88 .finish()
89 }
90}
91
92impl User {
93 /// Create a new User session
94 ///
95 /// This is an internal constructor used after successful login.
96 /// Use `Instance::login_user()` to create a User session.
97 ///
98 /// # Arguments
99 /// * `user_uuid` - Internal UUID (Table primary key)
100 /// * `user_info` - User information from _users database
101 /// * `user_database` - The user's private database
102 /// * `instance` - Instance reference
103 /// * `key_manager` - Initialized key manager with decrypted keys
104 #[allow(dead_code)]
105 pub(crate) fn new(
106 user_uuid: String,
107 user_info: UserInfo,
108 user_database: Database,
109 instance: Instance,
110 key_manager: UserKeyManager,
111 ) -> Self {
112 Self {
113 user_uuid,
114 username: user_info.username.clone(),
115 user_database,
116 instance,
117 key_manager,
118 user_info,
119 }
120 }
121
122 // === Basic Session Methods ===
123
124 /// Get the internal user UUID (stable identifier)
125 pub fn user_uuid(&self) -> &str {
126 &self.user_uuid
127 }
128
129 /// Get the username (login identifier)
130 pub fn username(&self) -> &str {
131 &self.username
132 }
133
134 /// Get a reference to the user's database
135 pub fn user_database(&self) -> &Database {
136 &self.user_database
137 }
138
139 /// Get a reference to the backend
140 pub fn backend(&self) -> &Backend {
141 self.instance.backend()
142 }
143
144 /// Get a reference to the user info
145 pub fn user_info(&self) -> &UserInfo {
146 &self.user_info
147 }
148
149 /// Logout (consumes self and clears decrypted keys from memory)
150 ///
151 /// After logout, all decrypted keys are zeroized and the session is ended.
152 /// Keys are automatically cleared when the User is dropped.
153 pub fn logout(self) -> Result<()> {
154 // Consume self, all keys are stored in other Types that zeroize themselves on drop
155 Ok(())
156 }
157
158 // === Key Manager Access (Internal) ===
159
160 /// Get a reference to the key manager (for internal use)
161 #[allow(dead_code)]
162 pub(crate) fn key_manager(&self) -> &UserKeyManager {
163 &self.key_manager
164 }
165
166 /// Get a mutable reference to the key manager (for internal use)
167 #[allow(dead_code)]
168 pub(crate) fn key_manager_mut(&mut self) -> &mut UserKeyManager {
169 &mut self.key_manager
170 }
171
172 // === Database Operations (User Context) ===
173
174 /// Create a new database with explicit key selection.
175 ///
176 /// This method requires you to specify which key should be used to create and manage
177 /// the database, providing explicit control over key-database relationships.
178 ///
179 /// # Arguments
180 /// * `settings` - Initial database settings (metadata, name, etc.)
181 /// * `key_id` - The ID of the key to use for this database (public key string)
182 ///
183 /// # Returns
184 /// The created Database
185 ///
186 /// # Errors
187 /// - Returns an error if the specified key_id doesn't exist
188 /// - Returns an error if the key cannot be retrieved
189 ///
190 /// # Example
191 /// ```rust,ignore
192 /// // Get available keys
193 /// let keys = user.list_keys()?;
194 /// let key_id = &keys[1]; // Use the second key
195 ///
196 /// // Create database with explicit key selection
197 /// let mut settings = Doc::new();
198 /// settings.set("name", "My Database");
199 /// let database = user.new_database(settings, key_id)?;
200 /// ```
201 pub async fn create_database(&mut self, settings: Doc, key_id: &PublicKey) -> Result<Database> {
202 use crate::user::types::{SyncSettings, UserKey};
203
204 // Get the signing key from UserKeyManager
205 let signing_key = self
206 .key_manager
207 .get_signing_key(key_id)
208 .ok_or_else(|| UserError::KeyNotFound {
209 key_id: key_id.to_string(),
210 })?
211 .clone();
212
213 // Create the database with the provided key directly
214 let database = Database::create(&self.instance, signing_key, settings).await?;
215
216 // Store the mapping in UserKey and track the database
217 let tx = self.user_database.new_transaction().await?;
218 let keys_table = tx.get_store::<Table<UserKey>>("keys").await?;
219
220 // Find the key metadata in the database
221 let (uuid_primary_key, mut metadata) = keys_table
222 .search(|uk| &uk.key_id == key_id)
223 .await?
224 .into_iter()
225 .next()
226 .ok_or_else(|| UserError::KeyNotFound {
227 key_id: key_id.to_string(),
228 })?;
229
230 // Add the database sigkey mapping (None = default pubkey identity)
231 metadata
232 .database_sigkeys
233 .insert(database.root_id().clone(), None);
234
235 // Update the key in user database using the UUID primary key
236 keys_table.set(&uuid_primary_key, metadata.clone()).await?;
237
238 // Also track the database in the databases table
239 let databases_table = tx.get_store::<Table<TrackedDatabase>>("databases").await?;
240 let tracked = TrackedDatabase {
241 database_id: database.root_id().clone(),
242 key_id: key_id.clone(),
243 sync_settings: SyncSettings::disabled(),
244 };
245 databases_table.set(database.root_id(), tracked).await?;
246
247 tx.commit().await?;
248
249 // Update the in-memory key manager with the updated metadata
250 self.key_manager.add_key(metadata)?;
251
252 Ok(database)
253 }
254
255 /// Open an existing database by its root ID using this user's keys.
256 ///
257 /// This method automatically:
258 /// 1. Finds an appropriate key that has access to the database
259 /// 2. Retrieves the decrypted SigningKey from the UserKeyManager
260 /// 3. Gets the SigKey mapping for this database
261 /// 4. Creates a Database instance configured with the user's key
262 ///
263 /// The returned Database will use the user's provided key for all operations,
264 /// without requiring backend key lookups.
265 ///
266 /// # Arguments
267 /// * `root_id` - The root entry ID of the database
268 ///
269 /// # Returns
270 /// The opened Database configured to use this user's keys
271 ///
272 /// # Errors
273 /// - Returns an error if no key is found for the database
274 /// - Returns an error if no SigKey mapping exists
275 /// - Returns an error if the key is not in the UserKeyManager
276 pub async fn open_database(&self, root_id: &ID) -> Result<Database> {
277 // Validate the root exists
278 self.instance.backend().get(root_id).await?;
279
280 // Find an appropriate key for this database
281 let key_id =
282 self.find_key(root_id)?
283 .ok_or_else(|| super::errors::UserError::NoKeyForDatabase {
284 database_id: root_id.clone(),
285 })?;
286
287 // Get the SigningKey from UserKeyManager
288 let signing_key = self.key_manager.get_signing_key(&key_id).ok_or_else(|| {
289 super::errors::UserError::KeyNotFound {
290 key_id: key_id.to_string(),
291 }
292 })?;
293
294 // Get the SigKey mapping for this database
295 let sigkey = self.key_mapping(&key_id, root_id)?.ok_or_else(|| {
296 super::errors::UserError::NoSigKeyMapping {
297 key_id: key_id.to_string(),
298 database_id: root_id.clone(),
299 }
300 })?;
301
302 // Create Database with user-provided key using resolved SigKey identity
303 let key = DatabaseKey::with_identity(signing_key.clone(), sigkey);
304 Database::open(self.instance.handle(), root_id, key).await
305 }
306
307 /// Find databases by name among the user's tracked databases.
308 ///
309 /// Searches only the databases this user has tracked for those matching the given name.
310 ///
311 /// # Arguments
312 /// * `name` - Database name to search for
313 ///
314 /// # Returns
315 /// Vector of matching databases from the user's tracked list
316 pub async fn find_database(&self, name: impl AsRef<str>) -> Result<Vec<Database>> {
317 let name = name.as_ref();
318 let tracked = self.databases().await?;
319 let mut matching = Vec::new();
320
321 for tracked_db in tracked {
322 if let Ok(database) = self.open_database(&tracked_db.database_id).await
323 && let Ok(db_name) = database.get_name().await
324 && db_name == name
325 {
326 matching.push(database);
327 }
328 }
329
330 if matching.is_empty() {
331 Err(UserError::DatabaseNotTracked {
332 database_id: format!("name:{name}"),
333 }
334 .into())
335 } else {
336 Ok(matching)
337 }
338 }
339
340 /// Find which key can access a database.
341 ///
342 /// Searches this user's keys to find one that can access the specified database.
343 /// Considers the SigKey mappings stored in user key metadata.
344 ///
345 /// Returns the key_id of a suitable key, preferring keys with mappings for this database.
346 ///
347 /// # Arguments
348 /// * `database_id` - The ID of the database
349 ///
350 /// # Returns
351 /// Some(key_id) if a suitable key is found, None if no keys can access this database
352 pub fn find_key(&self, database_id: &ID) -> Result<Option<PublicKey>> {
353 // Iterate through all keys and find ones with SigKey mappings for this database
354 for key_id in self.key_manager.list_key_ids() {
355 if let Some(metadata) = self.key_manager.get_key_metadata(&key_id)
356 && metadata.database_sigkeys.contains_key(database_id)
357 {
358 return Ok(Some(key_id));
359 }
360 }
361
362 // No key found with mapping for this database
363 Ok(None)
364 }
365
366 /// Get the resolved SigKey mapping for a key in a specific database.
367 ///
368 /// Users map their private keys to SigKey identifiers on a per-database basis.
369 /// This retrieves the resolved SigKey that a specific key uses in
370 /// a specific database's authentication settings.
371 ///
372 /// Internally, `None` in the stored mapping means "default pubkey identity",
373 /// which this method resolves to the concrete `SigKey::from_pubkey(...)` value.
374 ///
375 /// # Arguments
376 /// * `key_id` - The user's key identifier
377 /// * `database_id` - The database ID
378 ///
379 /// # Returns
380 /// `Ok(Some(sigkey))` if a mapping exists (resolved to concrete SigKey),
381 /// `Ok(None)` if no mapping is configured for this database
382 ///
383 /// # Errors
384 /// Returns an error if the key_id doesn't exist in the UserKeyManager
385 pub fn key_mapping(&self, key_id: &PublicKey, database_id: &ID) -> Result<Option<SigKey>> {
386 let metadata = self.key_manager.get_key_metadata(key_id).ok_or_else(|| {
387 super::errors::UserError::KeyNotFound {
388 key_id: key_id.to_string(),
389 }
390 })?;
391
392 match metadata.database_sigkeys.get(database_id) {
393 None => Ok(None), // no mapping exists
394 Some(None) => {
395 // Default: pubkey identity derived directly from key_id
396 Ok(Some(SigKey::from_pubkey(key_id)))
397 }
398 Some(Some(sigkey)) => Ok(Some(sigkey.clone())),
399 }
400 }
401
402 /// Map a key to a SigKey identity for a specific database.
403 ///
404 /// Registers that this user's key should be used with a specific SigKey identity
405 /// when interacting with a database. This is typically used when a user has been
406 /// granted access to a database and needs to configure their local key to work with it.
407 ///
408 /// If the provided SigKey matches the default pubkey identity for this key,
409 /// it is normalized to `None` internally (compact storage for the common case).
410 ///
411 /// # Multi-Key Support
412 ///
413 /// **Note**: A database may have mappings to multiple keys. This is useful for
414 /// multi-device scenarios where the same user wants to access a database from
415 /// different devices, each with their own key.
416 ///
417 /// # Arguments
418 /// * `key_id` - The user's key identifier (public key)
419 /// * `database_id` - The database ID
420 /// * `sigkey` - The SigKey identity to use for this database
421 ///
422 /// # Errors
423 /// Returns an error if the key_id doesn't exist in the user database
424 pub async fn map_key(
425 &mut self,
426 key_id: &PublicKey,
427 database_id: &ID,
428 sigkey: SigKey,
429 ) -> Result<()> {
430 let tx = self.user_database.new_transaction().await?;
431 self.map_key_in_txn(&tx, key_id, database_id, sigkey)
432 .await?;
433 tx.commit().await?;
434 Ok(())
435 }
436
437 /// Internal helper: Add a SigKey mapping within an existing transaction
438 ///
439 /// This is used internally by methods that manage their own transactions.
440 /// For external use, call `map_key()` instead.
441 ///
442 /// Normalizes the stored value: if the sigkey matches the default pubkey
443 /// identity for this key, stores `None` instead of `Some(sigkey)`.
444 async fn map_key_in_txn(
445 &mut self,
446 tx: &Transaction,
447 key_id: &PublicKey,
448 database_id: &ID,
449 sigkey: SigKey,
450 ) -> Result<()> {
451 use crate::store::Table;
452 use crate::user::types::UserKey;
453
454 let keys_table = tx.get_store::<Table<UserKey>>("keys").await?;
455
456 // Find the key metadata in the database
457 let (uuid_primary_key, mut metadata) = keys_table
458 .search(|uk| &uk.key_id == key_id)
459 .await?
460 .into_iter()
461 .next()
462 .ok_or_else(|| super::errors::UserError::KeyNotFound {
463 key_id: key_id.to_string(),
464 })?;
465
466 // Normalize: if the sigkey matches the default pubkey identity, store None
467 let default_sigkey = SigKey::from_pubkey(key_id);
468 let stored = if sigkey == default_sigkey {
469 None
470 } else {
471 Some(sigkey)
472 };
473
474 // Add the database sigkey mapping
475 metadata
476 .database_sigkeys
477 .insert(database_id.clone(), stored);
478
479 // Update the key in user database using the UUID primary key
480 keys_table.set(&uuid_primary_key, metadata.clone()).await?;
481
482 // Update the in-memory key manager with the updated metadata
483 self.key_manager.add_key(metadata)?;
484
485 Ok(())
486 }
487
488 /// Internal helper: Validate key and set up SigKey mapping within an existing transaction
489 ///
490 /// This validates that a key exists and has access to a database, discovers the appropriate
491 /// SigKey, and creates the mapping. Used by track_database (which has upsert behavior).
492 async fn validate_and_map_key_in_txn(
493 &mut self,
494 tx: &Transaction,
495 database_id: &ID,
496 key_id: &PublicKey,
497 ) -> Result<()> {
498 // Verify the key exists
499 if self.key_manager.get_signing_key(key_id).is_none() {
500 return Err(UserError::KeyNotFound {
501 key_id: key_id.to_string(),
502 }
503 .into());
504 }
505
506 // Discover available SigKeys for this public key
507 let available_sigkeys = Database::find_sigkeys(&self.instance, database_id, key_id).await?;
508
509 if available_sigkeys.is_empty() {
510 return Err(UserError::NoSigKeyFound {
511 key_id: key_id.to_string(),
512 database_id: database_id.clone(),
513 }
514 .into());
515 }
516
517 // Select the first SigKey (highest permission, since find_sigkeys returns sorted list)
518 let (sigkey, _permission) = &available_sigkeys[0];
519
520 // Store the discovered SigKey directly (map_key_in_txn normalizes to None if default)
521 self.map_key_in_txn(tx, key_id, database_id, sigkey.clone())
522 .await?;
523
524 Ok(())
525 }
526
527 // === Key Management (User Context) ===
528
529 /// Add a new private key to this user's keyring.
530 ///
531 /// Generates a new Ed25519 keypair, encrypts it (for password-protected users)
532 /// or stores it unencrypted (for passwordless users), and adds it to the user's
533 /// key database.
534 ///
535 /// # Arguments
536 /// * `display_name` - Optional display name for the key
537 ///
538 /// # Returns
539 /// The key ID (public key string)
540 pub async fn add_private_key(&mut self, display_name: Option<&str>) -> Result<PublicKey> {
541 use crate::auth::crypto::generate_keypair;
542 use crate::store::Table;
543 use crate::user::types::{KeyStorage, UserKey};
544
545 // Generate new keypair
546 let (private_key, public_key) = generate_keypair();
547
548 // Get current timestamp using the instance's clock
549 let timestamp = self.instance.clock().now_secs();
550
551 // Prepare UserKey based on encryption type
552 let user_key = if let Some(encryption_key) = self.key_manager.encryption_key() {
553 // Password-protected user: encrypt the key
554 use crate::user::crypto::encrypt_private_key;
555 let (ciphertext, nonce) = encrypt_private_key(&private_key, encryption_key)?;
556
557 UserKey {
558 key_id: public_key.clone(),
559 storage: KeyStorage::Encrypted {
560 algorithm: "aes-256-gcm".to_string(),
561 ciphertext,
562 nonce,
563 },
564 display_name: display_name.map(|s| s.to_string()),
565 created_at: timestamp,
566 last_used: None,
567 is_default: false, // New keys are not default
568 database_sigkeys: HashMap::new(),
569 }
570 } else {
571 // Passwordless user: store unencrypted
572 UserKey {
573 key_id: public_key.clone(),
574 storage: KeyStorage::Unencrypted { key: private_key },
575 display_name: display_name.map(|s| s.to_string()),
576 created_at: timestamp,
577 last_used: None,
578 is_default: false, // New keys are not default
579 database_sigkeys: HashMap::new(),
580 }
581 };
582
583 // Store in user database
584 let tx = self.user_database.new_transaction().await?;
585 let keys_table = tx.get_store::<Table<UserKey>>("keys").await?;
586 keys_table.insert(user_key.clone()).await?;
587 tx.commit().await?;
588
589 // Add to in-memory key manager
590 self.key_manager.add_key(user_key)?;
591
592 Ok(public_key)
593 }
594
595 /// List all key IDs owned by this user.
596 ///
597 /// Keys are returned sorted by creation timestamp (oldest first), making the
598 /// first key in the list the "default" key created when the user was set up.
599 ///
600 /// # Returns
601 /// Vector of PublicKeys sorted by creation time
602 pub fn list_keys(&self) -> Result<Vec<PublicKey>> {
603 Ok(self.key_manager.list_key_ids())
604 }
605
606 /// Get the default key.
607 ///
608 /// Returns the key marked as is_default=true, or falls back to the oldest key
609 /// by creation timestamp if no default is explicitly set.
610 ///
611 /// # Returns
612 /// The PublicKey of the default key
613 ///
614 /// # Errors
615 /// Returns an error if no keys exist
616 pub fn get_default_key(&self) -> Result<PublicKey> {
617 self.key_manager
618 .get_default_key_id()
619 .ok_or_else(|| Error::from(InstanceError::AuthenticationRequired))
620 }
621
622 /// Get a signing key by its ID.
623 ///
624 /// # Arguments
625 /// * `key_id` - The public key identifier
626 ///
627 /// # Returns
628 /// The PrivateKey if found
629 #[cfg(any(test, feature = "testing"))]
630 pub fn get_signing_key(&self, key_id: &PublicKey) -> Result<crate::auth::crypto::PrivateKey> {
631 self.key_manager
632 .get_signing_key(key_id)
633 .cloned()
634 .ok_or_else(|| {
635 UserError::KeyNotFound {
636 key_id: key_id.to_string(),
637 }
638 .into()
639 })
640 }
641
642 // === Bootstrap Request Management (User Context) ===
643
644 /// Get all pending bootstrap requests from the sync system.
645 ///
646 /// This is a convenience method that requires the Instance's Sync to be initialized.
647 ///
648 /// # Arguments
649 /// * `sync` - Reference to the Instance's Sync object
650 ///
651 /// # Returns
652 /// A vector of (request_id, bootstrap_request) pairs for pending requests
653 pub async fn pending_bootstrap_requests(
654 &self,
655 sync: &Sync,
656 ) -> Result<Vec<(String, BootstrapRequest)>> {
657 sync.pending_bootstrap_requests().await
658 }
659
660 /// Approve a bootstrap request and add the requesting key to the target database.
661 ///
662 /// The approving key must have Admin permission on the target database.
663 ///
664 /// # Arguments
665 /// * `sync` - Mutable reference to the Instance's Sync object
666 /// * `request_id` - The unique identifier of the request to approve
667 /// * `approving_key_id` - The ID of this user's key to use for approval (must have Admin permission)
668 ///
669 /// # Returns
670 /// Result indicating success or failure of the approval operation
671 ///
672 /// # Errors
673 /// - Returns an error if the user doesn't own the specified approving key
674 /// - Returns an error if the approving key doesn't have Admin permission on the target database
675 /// - Returns an error if the request doesn't exist or isn't pending
676 /// - Returns an error if the key addition to the database fails
677 pub async fn approve_bootstrap_request(
678 &self,
679 sync: &Sync,
680 request_id: &str,
681 approving_key_id: &PublicKey,
682 ) -> Result<()> {
683 // Get the signing key from the key manager
684 let signing_key = self
685 .key_manager
686 .get_signing_key(approving_key_id)
687 .ok_or_else(|| super::errors::UserError::KeyNotFound {
688 key_id: approving_key_id.to_string(),
689 })?;
690
691 // Delegate to Sync layer with the user-provided key
692 // The Sync layer will validate permissions when committing the transaction
693 let key = DatabaseKey::new(signing_key.clone());
694 sync.approve_bootstrap_request_with_key(request_id, &key)
695 .await?;
696
697 Ok(())
698 }
699
700 /// Reject a bootstrap request.
701 ///
702 /// This method marks the request as rejected. The requesting device will not
703 /// be granted access to the target database. Requires Admin permission on the
704 /// target database to prevent unauthorized users from disrupting the bootstrap protocol.
705 ///
706 /// # Arguments
707 /// * `sync` - Mutable reference to the Instance's Sync object
708 /// * `request_id` - The unique identifier of the request to reject
709 /// * `rejecting_key_id` - The ID of this user's key (for permission validation and audit trail)
710 ///
711 /// # Returns
712 /// Result indicating success or failure of the rejection operation
713 ///
714 /// # Errors
715 /// - Returns an error if the user doesn't own the specified rejecting key
716 /// - Returns an error if the request doesn't exist or isn't pending
717 /// - Returns an error if the rejecting key lacks Admin permission on the target database
718 pub async fn reject_bootstrap_request(
719 &self,
720 sync: &Sync,
721 request_id: &str,
722 rejecting_key_id: &PublicKey,
723 ) -> Result<()> {
724 // Get the signing key from the key manager
725 let signing_key = self
726 .key_manager
727 .get_signing_key(rejecting_key_id)
728 .ok_or_else(|| super::errors::UserError::KeyNotFound {
729 key_id: rejecting_key_id.to_string(),
730 })?;
731
732 // Delegate to Sync layer with the user-provided key
733 // The Sync layer will validate Admin permission on the target database
734 let key = DatabaseKey::new(signing_key.clone());
735 sync.reject_bootstrap_request_with_key(request_id, &key)
736 .await?;
737
738 Ok(())
739 }
740
741 /// Request access to a database from a peer (bootstrap sync).
742 ///
743 /// This convenience method initiates a bootstrap sync request to access a database
744 /// that this user doesn't have locally yet. The user's key will be sent to the peer
745 /// to request the specified permission level.
746 ///
747 /// This is useful for multi-device scenarios where a user wants to access their
748 /// existing database from a new device, or when requesting access to a database
749 /// shared by another user.
750 ///
751 /// # Arguments
752 /// * `sync` - Reference to the Instance's Sync object
753 /// * `ticket` - A ticket containing the database ID and address hints
754 /// * `key_id` - The ID of this user's key to use for the request
755 /// * `requested_permission` - The permission level being requested
756 ///
757 /// # Returns
758 /// Result indicating success or failure of the bootstrap request
759 ///
760 /// # Errors
761 /// - Returns an error if the user doesn't own the specified key
762 /// - Returns an error if all addresses in the ticket fail
763 /// - Returns an error if the bootstrap sync fails
764 ///
765 /// # Example
766 /// ```rust,ignore
767 /// // Request write access to a shared database
768 /// let user_key_id = user.get_default_key()?;
769 /// let ticket: DatabaseTicket = "eidetica:?db=sha256:abc...&pr=http:192.168.1.1:8080".parse()?;
770 /// user.request_database_access(
771 /// &sync,
772 /// &ticket,
773 /// &user_key_id,
774 /// Permission::Write(5),
775 /// ).await?;
776 ///
777 /// // After approval, the database can be opened
778 /// let database = user.open_database(ticket.database_id())?;
779 /// ```
780 pub async fn request_database_access(
781 &self,
782 sync: &Sync,
783 ticket: &DatabaseTicket,
784 key_id: &PublicKey,
785 requested_permission: Permission,
786 ) -> Result<()> {
787 if self.key_manager.get_signing_key(key_id).is_none() {
788 return Err(super::errors::UserError::KeyNotFound {
789 key_id: key_id.to_string(),
790 }
791 .into());
792 }
793
794 let key_name = key_id.to_string();
795
796 sync.bootstrap_with_ticket(ticket, key_id, &key_name, requested_permission)
797 .await
798 }
799
800 // === Tracked Databases ===
801
802 /// Track a database, adding it to this user's list with auto-discovery of SigKeys.
803 ///
804 /// This method adds an existing database to your tracked list, or updates it if
805 /// already tracked (upsert behavior).
806 ///
807 /// When tracking:
808 /// 1. Uses Database::find_sigkeys() to discover which SigKey the user can use
809 /// 2. Automatically selects the SigKey with highest permission
810 /// 3. Stores the key mapping and sync settings
811 ///
812 /// The sync_settings indicate your sync preferences, but do not automatically
813 /// configure sync. Use the Sync module's peer and tree methods to set up actual
814 /// sync relationships.
815 ///
816 /// # Arguments
817 /// * `database_id` - ID of the database to track
818 /// * `key_id` - Which user key to use for this database
819 /// * `sync_settings` - Sync preferences for this database
820 ///
821 /// # Returns
822 /// Result indicating success or failure
823 ///
824 /// # Errors
825 /// - Returns `NoSigKeyFound` if no SigKey can be found for the specified key
826 /// - Returns `KeyNotFound` if the specified key_id doesn't exist
827 pub async fn track_database(
828 &mut self,
829 database_id: impl Into<ID>,
830 key_id: &PublicKey,
831 sync_settings: SyncSettings,
832 ) -> Result<()> {
833 let tracked = TrackedDatabase {
834 database_id: database_id.into(),
835 key_id: key_id.clone(),
836 sync_settings,
837 };
838 // Single transaction for all operations
839 let tx = self.user_database.new_transaction().await?;
840 let databases_table = tx.get_store::<Table<TrackedDatabase>>("databases").await?;
841
842 // Use database ID as the key - check if it already exists (O(1))
843 let db_id_key = tracked.database_id.to_string();
844 let existing = databases_table.get(&db_id_key).await.ok();
845
846 // Determine if we need to validate and setup key mapping
847 let needs_key_validation = match &existing {
848 Some(existing) => existing.key_id != tracked.key_id, // Key changed
849 None => true, // New database
850 };
851
852 // Validate key and set up mapping if needed
853 if needs_key_validation {
854 self.validate_and_map_key_in_txn(&tx, &tracked.database_id, &tracked.key_id)
855 .await?;
856 }
857
858 // Store using database ID as explicit key (not using insert's auto-generated UUID)
859 databases_table.set(&db_id_key, tracked).await?;
860
861 // Single commit for all changes
862 tx.commit().await?;
863
864 // Update sync system to immediately recompute combined settings
865 // This ensures automatic sync works right away, without waiting for background worker
866 if let Some(sync) = self.instance.sync() {
867 // Auto-sync user tracking if not already synced
868 // This is idempotent - safe to call multiple times
869 sync.sync_user(&self.user_uuid, self.user_database.root_id())
870 .await?;
871 }
872
873 Ok(())
874 }
875
876 /// List all tracked databases.
877 ///
878 /// Returns all databases this user has added to their tracked list.
879 ///
880 /// # Returns
881 /// Vector of TrackedDatabase entries
882 pub async fn databases(&self) -> Result<Vec<TrackedDatabase>> {
883 let databases_table = self
884 .user_database
885 .get_store_viewer::<Table<TrackedDatabase>>("databases")
886 .await?;
887
888 // Get all entries from the table (returns Vec<(key, value)>)
889 let all_entries = databases_table.search(|_| true).await?;
890
891 // Extract just the values
892 let tracked: Vec<TrackedDatabase> = all_entries.into_iter().map(|(_key, db)| db).collect();
893
894 Ok(tracked)
895 }
896
897 /// Get a specific tracked database by ID.
898 ///
899 /// # Arguments
900 /// * `database_id` - The ID of the database
901 ///
902 /// # Returns
903 /// The TrackedDatabase if it's in the user's tracked list
904 ///
905 /// # Errors
906 /// Returns `DatabaseNotTracked` if the database is not in the user's list
907 pub async fn database(&self, database_id: &ID) -> Result<TrackedDatabase> {
908 let databases_table = self
909 .user_database()
910 .get_store_viewer::<Table<TrackedDatabase>>("databases")
911 .await?;
912
913 // Direct O(1) lookup using database ID as key
914 let db_id_key = database_id.to_string();
915 databases_table.get(&db_id_key).await.map_err(|_| {
916 UserError::DatabaseNotTracked {
917 database_id: database_id.to_string(),
918 }
919 .into()
920 })
921 }
922
923 /// Stop tracking a database.
924 ///
925 /// This removes the database from the user's tracked list.
926 /// It does not delete the database itself, remove key mappings, or delete any data.
927 ///
928 /// # Arguments
929 /// * `database_id` - The ID of the database to stop tracking
930 ///
931 /// # Errors
932 /// Returns `DatabaseNotTracked` if the database is not in the user's list
933 pub async fn untrack_database(&mut self, database_id: &ID) -> Result<()> {
934 let tx = self.user_database.new_transaction().await?;
935 let databases_table = tx.get_store::<Table<TrackedDatabase>>("databases").await?;
936
937 // Direct O(1) delete using database ID as key
938 let db_id_key = database_id.to_string();
939
940 // Verify it exists before deleting
941 if databases_table.get(&db_id_key).await.is_err() {
942 return Err(UserError::DatabaseNotTracked {
943 database_id: database_id.to_string(),
944 }
945 .into());
946 }
947
948 // Delete using database ID as key
949 databases_table.delete(&db_id_key).await?;
950 tx.commit().await?;
951
952 Ok(())
953 }
954}