From 25c7f25a0e9b5b4e893671a5811359f6b5910af4 Mon Sep 17 00:00:00 2001 From: natreex Date: Tue, 24 Mar 2026 23:05:56 -0400 Subject: [PATCH] Migrate vault encryption to OS keyring, handle PIN rate limiting, and improve tombstone handling logic. - Replaced legacy vault key derivation with OS keyring-backed storage for improved security and platform integration. - Introduced PIN rate limiting with configurable lockout durations to mitigate brute-force attacks. - Enhanced tombstone services to use unified error-handling logic via `apply_tombstone`. - Refactored logic for book and series data synchronization, fixing null checks and improving flow consistency. - Added comprehensive error handling to all database and vault operations in key manager services. --- src-tauri/src/crypto/key_manager.rs | 148 ++++++++++++++++---- src-tauri/src/domains/act/repo.rs | 2 +- src-tauri/src/domains/book/repo.rs | 4 +- src-tauri/src/domains/book/service.rs | 14 +- src-tauri/src/domains/download/service.rs | 5 +- src-tauri/src/domains/export/service.rs | 12 +- src-tauri/src/domains/offline/commands.rs | 7 +- src-tauri/src/domains/spell/service.rs | 2 +- src-tauri/src/domains/sync/service.rs | 5 +- src-tauri/src/domains/tombstone/commands.rs | 69 +++++---- src-tauri/src/domains/user/commands.rs | 14 +- 11 files changed, 203 insertions(+), 79 deletions(-) diff --git a/src-tauri/src/crypto/key_manager.rs b/src-tauri/src/crypto/key_manager.rs index d7b17cc..3d2ca6a 100644 --- a/src-tauri/src/crypto/key_manager.rs +++ b/src-tauri/src/crypto/key_manager.rs @@ -24,6 +24,10 @@ struct SecureVault { encryption_keys: HashMap, last_user_id: Option, pin_hashes: HashMap, + #[serde(default)] + pin_failed_attempts: i32, + #[serde(default)] + pin_locked_until: Option, } fn vault_path() -> PathBuf { @@ -33,7 +37,37 @@ fn vault_path() -> PathBuf { .join("secure-config.json") } -fn derive_machine_key() -> [u8; 32] { +const KEYRING_USER: &str = "vault-key"; + +/// Retrieves (or generates and stores) the vault encryption key via the OS keyring +/// (macOS Keychain, Windows DPAPI, Linux Secret Service). +/// Falls back to the old derivation method if the keyring is unavailable, +/// and attempts to migrate the key into the keyring for next time. +fn get_vault_key() -> [u8; 32] { + let entry = keyring::Entry::new(SERVICE_NAME, KEYRING_USER); + if let Ok(entry) = &entry { + if let Ok(stored) = entry.get_password() { + if let Ok(decoded) = BASE64.decode(stored.trim()) { + if decoded.len() == 32 { + let mut key = [0u8; 32]; + key.copy_from_slice(&decoded); + return key; + } + } + } + } + // No key in keyring yet — generate a random one + let mut key = [0u8; 32]; + rand::rng().fill_bytes(&mut key); + let encoded = BASE64.encode(key); + if let Ok(entry) = &entry { + let _ = entry.set_password(&encoded); + } + key +} + +/// Legacy derivation for migrating vaults created before keyring support. +fn derive_machine_key_legacy() -> [u8; 32] { let mut hasher = Sha256::new(); hasher.update(SERVICE_NAME.as_bytes()); if let Ok(name) = hostname::get() { @@ -76,96 +110,150 @@ fn decrypt_vault(data: &[u8], key: &[u8; 32]) -> AppResult> { Ok(decrypted.to_vec()) } -fn read_vault() -> SecureVault { +fn read_vault() -> AppResult { let path = vault_path(); - let key = derive_machine_key(); - match fs::read_to_string(&path) { - Ok(content) => { - if let Ok(raw) = BASE64.decode(content.trim()) { - if let Ok(decrypted) = decrypt_vault(&raw, &key) { - if let Ok(vault) = serde_json::from_slice::(&decrypted) { - return vault; - } - } - } - SecureVault::default() - } - Err(_) => SecureVault::default(), + if !path.exists() { + return Ok(SecureVault::default()); } + let content = fs::read_to_string(&path) + .map_err(|e| AppError::Keyring(format!("Failed to read vault: {}", e)))?; + let raw = BASE64.decode(content.trim()) + .map_err(|e| AppError::Keyring(format!("Vault corrupted (base64): {}", e)))?; + + // Try the new keyring-backed key first + let key = get_vault_key(); + if let Ok(decrypted) = decrypt_vault(&raw, &key) { + if let Ok(vault) = serde_json::from_slice::(&decrypted) { + return Ok(vault); + } + } + + // Fallback: try legacy key and migrate if successful + let legacy_key = derive_machine_key_legacy(); + let decrypted = decrypt_vault(&raw, &legacy_key) + .map_err(|_| AppError::Keyring("Vault corrupted: unable to decrypt with any key.".to_string()))?; + let vault: SecureVault = serde_json::from_slice(&decrypted) + .map_err(|e| AppError::Keyring(format!("Vault corrupted (json): {}", e)))?; + + // Migrate: re-encrypt with the new keyring key + let _ = write_vault_with_key(&vault, &key); + Ok(vault) } -fn write_vault(vault: &SecureVault) -> AppResult<()> { +fn write_vault_with_key(vault: &SecureVault, key: &[u8; 32]) -> AppResult<()> { let path = vault_path(); if let Some(parent) = path.parent() { fs::create_dir_all(parent).map_err(|e| AppError::Internal(format!("Failed to create vault dir: {}", e)))?; } let json = serde_json::to_string(vault) .map_err(|e| AppError::Internal(format!("Failed to serialize vault: {}", e)))?; - let key = derive_machine_key(); - let encrypted = encrypt_vault(json.as_bytes(), &key)?; + let encrypted = encrypt_vault(json.as_bytes(), key)?; let encoded = BASE64.encode(&encrypted); fs::write(&path, encoded).map_err(|e| AppError::Internal(format!("Failed to write vault: {}", e))) } +fn write_vault(vault: &SecureVault) -> AppResult<()> { + let key = get_vault_key(); + write_vault_with_key(vault, &key) +} + // ===== Public API (same signatures as before) ===== pub fn get_user_encryption_key(user_id: &str) -> AppResult { - let vault = read_vault(); + let vault = read_vault()?; vault.encryption_keys.get(user_id).cloned() .ok_or_else(|| AppError::Keyring(format!("No encryption key for user {}", user_id))) } pub fn set_user_encryption_key(user_id: &str, encryption_key: &str) -> AppResult<()> { - let mut vault = read_vault(); + let mut vault = read_vault()?; vault.encryption_keys.insert(user_id.to_string(), encryption_key.to_string()); write_vault(&vault) } -pub fn has_user_encryption_key(user_id: &str) -> bool { - let vault = read_vault(); - vault.encryption_keys.contains_key(user_id) +pub fn has_user_encryption_key(user_id: &str) -> AppResult { + let vault = read_vault()?; + Ok(vault.encryption_keys.contains_key(user_id)) } pub fn get_token() -> AppResult> { - let vault = read_vault(); + let vault = read_vault()?; Ok(vault.token) } pub fn set_token(token: &str) -> AppResult<()> { - let mut vault = read_vault(); + let mut vault = read_vault()?; vault.token = Some(token.to_string()); write_vault(&vault) } pub fn remove_token() -> AppResult<()> { - let mut vault = read_vault(); + let mut vault = read_vault()?; vault.token = None; write_vault(&vault) } pub fn set_pin_hash(user_id: &str, pin_hash: &str) -> AppResult<()> { - let mut vault = read_vault(); + let mut vault = read_vault()?; vault.pin_hashes.insert(user_id.to_string(), pin_hash.to_string()); write_vault(&vault) } pub fn get_pin_hash(user_id: &str) -> AppResult> { - let vault = read_vault(); + let vault = read_vault()?; Ok(vault.pin_hashes.get(user_id).cloned()) } pub fn set_last_user_id(user_id: &str) -> AppResult<()> { - let mut vault = read_vault(); + let mut vault = read_vault()?; vault.last_user_id = Some(user_id.to_string()); write_vault(&vault) } pub fn get_last_user_id() -> AppResult> { - let vault = read_vault(); + let vault = read_vault()?; Ok(vault.last_user_id) } pub fn clear_vault() -> AppResult<()> { write_vault(&SecureVault::default()) } + +const MAX_PIN_ATTEMPTS: i32 = 5; +const PIN_LOCKOUT_SECONDS: i64 = 300; // 5 minutes + +pub fn check_pin_rate_limit() -> AppResult<()> { + let vault = read_vault()?; + if let Some(locked_until) = vault.pin_locked_until { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_secs() as i64) + .unwrap_or(0); + if now < locked_until { + let remaining = locked_until - now; + return Err(AppError::Auth(format!("Too many attempts. Try again in {} seconds.", remaining))); + } + } + Ok(()) +} + +pub fn record_pin_failure() -> AppResult<()> { + let mut vault = read_vault()?; + vault.pin_failed_attempts += 1; + if vault.pin_failed_attempts >= MAX_PIN_ATTEMPTS { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_secs() as i64) + .unwrap_or(0); + vault.pin_locked_until = Some(now + PIN_LOCKOUT_SECONDS); + } + write_vault(&vault) +} + +pub fn reset_pin_attempts() -> AppResult<()> { + let mut vault = read_vault()?; + vault.pin_failed_attempts = 0; + vault.pin_locked_until = None; + write_vault(&vault) +} diff --git a/src-tauri/src/domains/act/repo.rs b/src-tauri/src/domains/act/repo.rs index e9437f9..32ed347 100644 --- a/src-tauri/src/domains/act/repo.rs +++ b/src-tauri/src/domains/act/repo.rs @@ -61,7 +61,7 @@ pub fn update_act_summary( ) -> AppResult { let update_result = conn .execute( - "UPDATE book_act_summaries SET summary=?1, last_update=?2 WHERE user_id=?3 AND book_id=?4 AND act_sum_id=?5", + "UPDATE book_act_summaries SET summary=?1, last_update=?2 WHERE user_id=?3 AND book_id=?4 AND act_index=?5", params![summary, last_update, user_id, book_id, act_id], ) .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour le résumé de l'acte.".to_string() } else { "Unable to update act summary.".to_string() }))?; diff --git a/src-tauri/src/domains/book/repo.rs b/src-tauri/src/domains/book/repo.rs index 19bc1ab..f451fba 100644 --- a/src-tauri/src/domains/book/repo.rs +++ b/src-tauri/src/domains/book/repo.rs @@ -110,7 +110,7 @@ pub fn fetch_books(conn: &Connection, user_id: &str, lang: Lang) -> AppResult AppResult { let mut statement = conn - .prepare("SELECT book_id, author_id, title, summary, sub_title, cover_image, desired_release_date, desired_word_count, words_count, serie_id FROM erit_books WHERE book_id=?1 AND author_id=?2") + .prepare("SELECT book_id, author_id, title, summary, sub_title, cover_image, desired_release_date, desired_word_count, words_count, serie_id, type FROM erit_books WHERE book_id=?1 AND author_id=?2") .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les informations du livre.".to_string() } else { "Unable to retrieve book information.".to_string() }))?; let book = statement @@ -121,7 +121,7 @@ pub fn fetch_book(conn: &Connection, book_id: &str, user_id: &str, lang: Lang) - sub_title: query_row.get(4)?, cover_image: query_row.get(5)?, desired_release_date: query_row.get(6)?, desired_word_count: query_row.get(7)?, words_count: query_row.get(8)?, serie_id: query_row.get(9)?, - book_type: String::new(), + book_type: query_row.get(10)?, }) }) .map_err(|error| match error { diff --git a/src-tauri/src/domains/book/service.rs b/src-tauri/src/domains/book/service.rs index 4904dbf..f23331b 100644 --- a/src-tauri/src/domains/book/service.rs +++ b/src-tauri/src/domains/book/service.rs @@ -870,9 +870,19 @@ pub fn remove_book(conn: &Connection, user_id: &str, book_id: &str, deleted_at: /// * `lang` - The language for error messages /// Returns true if the update was successful. pub fn update_book_tool_setting(conn: &Connection, user_id: &str, book_id: &str, tool_name: &str, enabled: bool, lang: Lang) -> AppResult { - let column_name: String = format!("{}_enabled", tool_name); + let column_name: &str = match tool_name { + "characters" => "characters_enabled", + "worlds" => "worlds_enabled", + "locations" => "locations_enabled", + "spells" => "spells_enabled", + _ => return Err(AppError::Validation(if lang == Lang::Fr { + format!("Outil inconnu: {}", tool_name) + } else { + format!("Unknown tool: {}", tool_name) + })), + }; let last_update: i64 = timestamp_in_seconds(); - repo::update_book_tool_setting(conn, user_id, book_id, &column_name, enabled, last_update, lang) + repo::update_book_tool_setting(conn, user_id, book_id, column_name, enabled, last_update, lang) } /// Retrieves complete book data including chapters and user information. diff --git a/src-tauri/src/domains/download/service.rs b/src-tauri/src/domains/download/service.rs index 1705134..9b1c525 100644 --- a/src-tauri/src/domains/download/service.rs +++ b/src-tauri/src/domains/download/service.rs @@ -17,7 +17,7 @@ use crate::domains::spell::repo as spell_repo; use crate::domains::spell_tag::repo as spell_tag_repo; use crate::domains::chapter_content::repo as chapter_content_repo; use crate::domains::world::repo as world_repo; -use crate::error::AppResult; +use crate::error::{AppError, AppResult}; use crate::shared::types::Lang; /// Saves a complete book with all its associated data to the local database. @@ -33,7 +33,8 @@ use crate::shared::types::Lang; pub fn save_complete_book(conn: &Connection, user_id: &str, data: &CompleteBook, lang: Lang) -> AppResult { let user_encryption_key: String = get_user_encryption_key(user_id)?; - let book_data = &data.erit_books[0]; + let book_data = data.erit_books.first() + .ok_or_else(|| AppError::Validation("No book data to download.".to_string()))?; let encrypted_book_title: String = encrypt_data_with_user_key(&book_data.title, &user_encryption_key)?; let encrypted_book_sub_title: Option = if let Some(ref sub_title) = book_data.sub_title { Some(encrypt_data_with_user_key(sub_title, &user_encryption_key)?) } else { None }; let encrypted_book_summary: Option = if let Some(ref summary) = book_data.summary { Some(encrypt_data_with_user_key(summary, &user_encryption_key)?) } else { None }; diff --git a/src-tauri/src/domains/export/service.rs b/src-tauri/src/domains/export/service.rs index 4f5fa89..b06046a 100644 --- a/src-tauri/src/domains/export/service.rs +++ b/src-tauri/src/domains/export/service.rs @@ -127,19 +127,21 @@ pub fn transform_to_pdf(book_data: &CompleteBookData) -> AppResult continue; } - let (new_page_index, new_layer_index) = pdf_document.add_page(Mm(210.0), Mm(297.0), &chapter.title); - let chapter_layer = pdf_document.get_page(new_page_index).get_layer(new_layer_index); + let (first_page_index, first_layer_index) = pdf_document.add_page(Mm(210.0), Mm(297.0), &chapter.title); + let mut current_layer = pdf_document.get_page(first_page_index).get_layer(first_layer_index); let mut chapter_y: f32 = 270.0; - chapter_layer.use_text(&chapter.title, 16.0, Mm(20.0), Mm(chapter_y), &font); + current_layer.use_text(&chapter.title, 16.0, Mm(20.0), Mm(chapter_y), &font); chapter_y -= 10.0; let lines: Vec<&str> = chapter.content.split('\n').collect(); for line in lines { if chapter_y < 20.0 { - break; + let (next_page_index, next_layer_index) = pdf_document.add_page(Mm(210.0), Mm(297.0), &chapter.title); + current_layer = pdf_document.get_page(next_page_index).get_layer(next_layer_index); + chapter_y = 270.0; } - chapter_layer.use_text(line, 12.0, Mm(20.0), Mm(chapter_y), &font); + current_layer.use_text(line, 12.0, Mm(20.0), Mm(chapter_y), &font); chapter_y -= 6.0; } } diff --git a/src-tauri/src/domains/offline/commands.rs b/src-tauri/src/domains/offline/commands.rs index 41b105f..dc9cfba 100644 --- a/src-tauri/src/domains/offline/commands.rs +++ b/src-tauri/src/domains/offline/commands.rs @@ -57,6 +57,8 @@ pub fn offline_pin_set(data: PinData, session: State) -> Result, session: State) -> Result { + key_manager::check_pin_rate_limit()?; + let last_user_id: Option = key_manager::get_last_user_id()?; let last_user_id: String = match last_user_id { Some(id) => id, @@ -73,10 +75,13 @@ pub fn offline_pin_verify(data: PinData, db: State, session: State 150 => format!("{}...", &desc[..150]), + Some(ref desc) if desc.chars().count() > 150 => format!("{}...", desc.chars().take(150).collect::()), Some(ref desc) => desc.clone(), None => String::new(), }; diff --git a/src-tauri/src/domains/sync/service.rs b/src-tauri/src/domains/sync/service.rs index c2ec061..8435244 100644 --- a/src-tauri/src/domains/sync/service.rs +++ b/src-tauri/src/domains/sync/service.rs @@ -626,7 +626,10 @@ pub fn sync_book_from_server_to_client(conn: &Connection, user_id: &str, complet let server_guide_lines: &Vec = &complete_book.guide_line; let server_ai_guide_lines: &Vec = &complete_book.ai_guide_line; - let book_id: String = if !complete_book.erit_books.is_empty() { complete_book.erit_books[0].book_id.clone() } else { String::new() }; + let book_id: String = match complete_book.erit_books.first() { + Some(book) => book.book_id.clone(), + None => return Ok(false), + }; if !server_chapters.is_empty() { for server_chapter in server_chapters { diff --git a/src-tauri/src/domains/tombstone/commands.rs b/src-tauri/src/domains/tombstone/commands.rs index fbb70cf..8450a29 100644 --- a/src-tauri/src/domains/tombstone/commands.rs +++ b/src-tauri/src/domains/tombstone/commands.rs @@ -17,10 +17,21 @@ use crate::domains::series_character::service as series_character_service; use crate::domains::series_location::service as series_location_service; use crate::domains::series_world::service as series_world_service; use crate::domains::series_spell::service as series_spell_service; -use crate::error::AppError; +use crate::error::{AppError, AppResult}; use crate::shared::session::SessionState; use crate::shared::types::Lang; +/// Runs a tombstone deletion, ignoring NotFound errors (entity already deleted). +/// Propagates all other errors (DB corruption, lock failure, etc.). +fn apply_tombstone AppResult>(action: F) -> AppResult<()> { + match action() { + Ok(_) => Ok(()), + Err(AppError::NotFound(_)) => Ok(()), + Err(AppError::Internal(msg)) if msg.contains("non trouvé") || msg.contains("not found") => Ok(()), + Err(e) => Err(e), + } +} + fn get_session(session: &State) -> Result<(String, Lang), AppError> { let session_guard = session.lock().map_err(|e| AppError::Internal(format!("Session lock failed: {}", e)))?; let user_id = session_guard.get_user_id().map_err(|e| AppError::Auth(e))?.to_string(); @@ -68,22 +79,24 @@ pub fn apply_book_tombstones(tombstones: Vec, db: State { let _ = book_service::remove_book(conn, &user_id, &tombstone.entity_id, tombstone.deleted_at, lang); } - "book_chapters" => { let _ = chapter_service::remove_chapter(conn, &user_id, book_id, &tombstone.entity_id, tombstone.deleted_at, lang); } - "book_chapter_infos" => { let _ = chapter_service::remove_chapter_information(conn, &user_id, book_id, &tombstone.entity_id, tombstone.deleted_at, lang); } - "book_characters" => { let _ = character_service::delete_character(conn, &user_id, book_id, &tombstone.entity_id, tombstone.deleted_at, lang); } - "book_characters_attributes" => { let _ = character_service::delete_attribute(conn, &user_id, book_id, &tombstone.entity_id, tombstone.deleted_at, lang); } - "book_location" => { let _ = location_service::delete_location_section(conn, &user_id, book_id, &tombstone.entity_id, tombstone.deleted_at, lang); } - "location_element" => { let _ = location_service::delete_location_element(conn, &user_id, book_id, &tombstone.entity_id, tombstone.deleted_at, lang); } - "location_sub_element" => { let _ = location_service::delete_location_sub_element(conn, &user_id, book_id, &tombstone.entity_id, tombstone.deleted_at, lang); } - "book_world_elements" => { let _ = world_service::remove_element_from_world(conn, &user_id, book_id, &tombstone.entity_id, tombstone.deleted_at, lang); } - "book_incidents" => { let _ = incident_service::remove_incident(conn, &user_id, book_id, &tombstone.entity_id, tombstone.deleted_at, lang); } - "book_plot_points" => { let _ = plotpoint_service::remove_plot_point(conn, &user_id, book_id, &tombstone.entity_id, tombstone.deleted_at, lang); } - "book_issues" => { let _ = issue_service::remove_issue(conn, &user_id, book_id, &tombstone.entity_id, tombstone.deleted_at, lang); } - "book_spells" => { let _ = spell_service::delete_spell(conn, &user_id, book_id, &tombstone.entity_id, tombstone.deleted_at, lang); } - "book_spell_tags" => { let _ = spell_service::delete_spell_tag(conn, &user_id, book_id, &tombstone.entity_id, tombstone.deleted_at, lang); } - _ => {} + "erit_books" => apply_tombstone(|| book_service::remove_book(conn, &user_id, entity_id, deleted_at, lang))?, + "book_chapters" => apply_tombstone(|| chapter_service::remove_chapter(conn, &user_id, book_id, entity_id, deleted_at, lang))?, + "book_chapter_infos" => apply_tombstone(|| chapter_service::remove_chapter_information(conn, &user_id, book_id, entity_id, deleted_at, lang))?, + "book_characters" => apply_tombstone(|| character_service::delete_character(conn, &user_id, book_id, entity_id, deleted_at, lang))?, + "book_characters_attributes" => apply_tombstone(|| character_service::delete_attribute(conn, &user_id, book_id, entity_id, deleted_at, lang))?, + "book_location" => apply_tombstone(|| location_service::delete_location_section(conn, &user_id, book_id, entity_id, deleted_at, lang))?, + "location_element" => apply_tombstone(|| location_service::delete_location_element(conn, &user_id, book_id, entity_id, deleted_at, lang))?, + "location_sub_element" => apply_tombstone(|| location_service::delete_location_sub_element(conn, &user_id, book_id, entity_id, deleted_at, lang))?, + "book_world_elements" => apply_tombstone(|| world_service::remove_element_from_world(conn, &user_id, book_id, entity_id, deleted_at, lang))?, + "book_incidents" => apply_tombstone(|| incident_service::remove_incident(conn, &user_id, book_id, entity_id, deleted_at, lang))?, + "book_plot_points" => apply_tombstone(|| plotpoint_service::remove_plot_point(conn, &user_id, book_id, entity_id, deleted_at, lang))?, + "book_issues" => apply_tombstone(|| issue_service::remove_issue(conn, &user_id, book_id, entity_id, deleted_at, lang))?, + "book_spells" => apply_tombstone(|| spell_service::delete_spell(conn, &user_id, book_id, entity_id, deleted_at, lang))?, + "book_spell_tags" => apply_tombstone(|| spell_service::delete_spell_tag(conn, &user_id, book_id, entity_id, deleted_at, lang))?, + _ => return Err(AppError::Validation(format!("Unknown tombstone table: {}", tombstone.table_name))), } } @@ -97,22 +110,24 @@ pub fn apply_series_tombstones(tombstones: Vec, db: State { let _ = series_service::delete_series(conn, &user_id, &tombstone.entity_id, tombstone.deleted_at, lang); } + "erit_series" => apply_tombstone(|| series_service::delete_series(conn, &user_id, entity_id, deleted_at, lang))?, "series_books" => { if let Some(ref book_id) = tombstone.book_id { - let _ = series_service::remove_book_from_series(conn, &user_id, book_id, &tombstone.entity_id, tombstone.deleted_at, lang); + apply_tombstone(|| series_service::remove_book_from_series(conn, &user_id, book_id, entity_id, deleted_at, lang))?; } } - "series_characters" => { let _ = series_character_service::delete_character(conn, &user_id, &tombstone.entity_id, tombstone.deleted_at, lang); } - "series_characters_attributes" => { let _ = series_character_service::delete_attribute(conn, &user_id, &tombstone.entity_id, tombstone.deleted_at, lang); } - "series_locations" => { let _ = series_location_service::delete_location(conn, &user_id, &tombstone.entity_id, tombstone.deleted_at, lang); } - "series_location_elements" => { let _ = series_location_service::delete_element(conn, &user_id, &tombstone.entity_id, tombstone.deleted_at, lang); } - "series_location_sub_elements" => { let _ = series_location_service::delete_sub_element(conn, &user_id, &tombstone.entity_id, tombstone.deleted_at, lang); } - "series_world_elements" => { let _ = series_world_service::delete_element(conn, &user_id, &tombstone.entity_id, tombstone.deleted_at, lang); } - "series_spells" => { let _ = series_spell_service::delete_spell(conn, &user_id, &tombstone.entity_id, tombstone.deleted_at, lang); } - "series_spell_tags" => { let _ = series_spell_service::delete_tag(conn, &user_id, &tombstone.entity_id, tombstone.deleted_at, lang); } - _ => {} + "series_characters" => apply_tombstone(|| series_character_service::delete_character(conn, &user_id, entity_id, deleted_at, lang))?, + "series_characters_attributes" => apply_tombstone(|| series_character_service::delete_attribute(conn, &user_id, entity_id, deleted_at, lang))?, + "series_locations" => apply_tombstone(|| series_location_service::delete_location(conn, &user_id, entity_id, deleted_at, lang))?, + "series_location_elements" => apply_tombstone(|| series_location_service::delete_element(conn, &user_id, entity_id, deleted_at, lang))?, + "series_location_sub_elements" => apply_tombstone(|| series_location_service::delete_sub_element(conn, &user_id, entity_id, deleted_at, lang))?, + "series_world_elements" => apply_tombstone(|| series_world_service::delete_element(conn, &user_id, entity_id, deleted_at, lang))?, + "series_spells" => apply_tombstone(|| series_spell_service::delete_spell(conn, &user_id, entity_id, deleted_at, lang))?, + "series_spell_tags" => apply_tombstone(|| series_spell_service::delete_tag(conn, &user_id, entity_id, deleted_at, lang))?, + _ => return Err(AppError::Validation(format!("Unknown tombstone table: {}", tombstone.table_name))), } } diff --git a/src-tauri/src/domains/user/commands.rs b/src-tauri/src/domains/user/commands.rs index 07c3a0e..492d019 100644 --- a/src-tauri/src/domains/user/commands.rs +++ b/src-tauri/src/domains/user/commands.rs @@ -111,13 +111,12 @@ pub fn set_token(token: String) -> Result<(), AppError> { #[tauri::command] pub fn remove_token(db: State, session: State) -> Result<(), AppError> { - if let Ok(session_guard) = session.lock() { - if let Ok(user_id) = session_guard.get_user_id() { - if let Ok(mut db_manager) = db.lock() { - db_manager.close(user_id); - } - } + let session_guard = session.lock().map_err(|e| AppError::Internal(format!("Session lock failed: {}", e)))?; + if let Ok(user_id) = session_guard.get_user_id() { + let mut db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))?; + db_manager.close(user_id); } + drop(session_guard); key_manager::remove_token() } @@ -125,7 +124,8 @@ pub fn remove_token(db: State, session: State) -> Resul pub fn get_user_encryption_key(user_id: String) -> Result, AppError> { match key_manager::get_user_encryption_key(&user_id) { Ok(key) => Ok(Some(key)), - Err(_) => Ok(None), + Err(AppError::Keyring(msg)) if msg.contains("No encryption key") => Ok(None), + Err(e) => Err(e), } }