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.
This commit is contained in:
natreex
2026-03-24 23:05:56 -04:00
parent cfd08e3261
commit 25c7f25a0e
11 changed files with 203 additions and 79 deletions

View File

@@ -24,6 +24,10 @@ struct SecureVault {
encryption_keys: HashMap<String, String>,
last_user_id: Option<String>,
pin_hashes: HashMap<String, String>,
#[serde(default)]
pin_failed_attempts: i32,
#[serde(default)]
pin_locked_until: Option<i64>,
}
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<Vec<u8>> {
Ok(decrypted.to_vec())
}
fn read_vault() -> SecureVault {
fn read_vault() -> AppResult<SecureVault> {
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 !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::<SecureVault>(&decrypted) {
return vault;
}
}
}
SecureVault::default()
}
Err(_) => SecureVault::default(),
return Ok(vault);
}
}
fn write_vault(vault: &SecureVault) -> AppResult<()> {
// 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_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<String> {
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<bool> {
let vault = read_vault()?;
Ok(vault.encryption_keys.contains_key(user_id))
}
pub fn get_token() -> AppResult<Option<String>> {
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<Option<String>> {
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<Option<String>> {
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)
}

View File

@@ -61,7 +61,7 @@ pub fn update_act_summary(
) -> AppResult<bool> {
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() }))?;

View File

@@ -110,7 +110,7 @@ pub fn fetch_books(conn: &Connection, user_id: &str, lang: Lang) -> AppResult<Ve
/// Returns the book information.
pub fn fetch_book(conn: &Connection, book_id: &str, user_id: &str, lang: Lang) -> AppResult<BookQuery> {
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 {

View File

@@ -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<bool> {
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.

View File

@@ -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<bool> {
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<String> = 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<String> = if let Some(ref summary) = book_data.summary { Some(encrypt_data_with_user_key(summary, &user_encryption_key)?) } else { None };

View File

@@ -127,19 +127,21 @@ pub fn transform_to_pdf(book_data: &CompleteBookData) -> AppResult<ExportResult>
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;
}
}

View File

@@ -57,6 +57,8 @@ pub fn offline_pin_set(data: PinData, session: State<SessionState>) -> Result<Of
#[tauri::command]
pub fn offline_pin_verify(data: PinData, db: State<DbManager>, session: State<SessionState>) -> Result<OfflineResult, AppError> {
key_manager::check_pin_rate_limit()?;
let last_user_id: Option<String> = 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<DbManager>, session: State<Se
.map_err(|e| AppError::Encryption(format!("Failed to verify PIN: {}", e)))?;
if !is_valid {
key_manager::record_pin_failure()?;
return Ok(OfflineResult { success: false, error: Some("Invalid PIN".to_string()), user_id: None });
}
let has_key: bool = key_manager::has_user_encryption_key(&last_user_id);
key_manager::reset_pin_attempts()?;
let has_key: bool = key_manager::has_user_encryption_key(&last_user_id)?;
if !has_key {
return Ok(OfflineResult { success: false, error: Some("No encryption key found".to_string()), user_id: None });
}

View File

@@ -222,7 +222,7 @@ pub fn get_spell_list(conn: &Connection, user_id: &str, book_id: &str, lang: Lan
.collect();
let truncated_description: String = match decrypted_description {
Some(ref desc) if desc.len() > 150 => format!("{}...", &desc[..150]),
Some(ref desc) if desc.chars().count() > 150 => format!("{}...", desc.chars().take(150).collect::<String>()),
Some(ref desc) => desc.clone(),
None => String::new(),
};

View File

@@ -626,7 +626,10 @@ pub fn sync_book_from_server_to_client(conn: &Connection, user_id: &str, complet
let server_guide_lines: &Vec<BookGuideLineTable> = &complete_book.guide_line;
let server_ai_guide_lines: &Vec<BookAIGuideLineTable> = &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 {

View File

@@ -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<F: FnOnce() -> AppResult<bool>>(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<SessionState>) -> 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<TombstoneInput>, db: State<DbManage
for tombstone in &tombstones {
let book_id = tombstone.book_id.as_deref().unwrap_or("");
let entity_id = tombstone.entity_id.as_str();
let deleted_at = tombstone.deleted_at;
match tombstone.table_name.as_str() {
"erit_books" => { 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<TombstoneInput>, db: State<DbMana
let conn = db_manager.get_connection(&user_id)?;
for tombstone in &tombstones {
let entity_id = tombstone.entity_id.as_str();
let deleted_at = tombstone.deleted_at;
match tombstone.table_name.as_str() {
"erit_series" => { 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))),
}
}

View File

@@ -111,13 +111,12 @@ pub fn set_token(token: String) -> Result<(), AppError> {
#[tauri::command]
pub fn remove_token(db: State<DbManager>, session: State<SessionState>) -> Result<(), AppError> {
if let Ok(session_guard) = session.lock() {
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() {
if let Ok(mut db_manager) = db.lock() {
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<DbManager>, session: State<SessionState>) -> Resul
pub fn get_user_encryption_key(user_id: String) -> Result<Option<String>, 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),
}
}