Refactor and extend offline synchronization logic across components and services
- Integrated sync queue mechanisms with `LocalSyncQueueContext` for offline data handling. - Updated key sync-related services (e.g., book, chapter, series) to support offline-first functionality. - Removed redundant database fetch methods to optimize repository logic and improve maintainability. - Enhanced Tauri IPC usage for sync operations and removed legacy methods in Rust services.
This commit is contained in:
@@ -44,6 +44,9 @@ const KEYRING_USER: &str = "vault-key";
|
||||
/// 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] {
|
||||
if cfg!(debug_assertions) {
|
||||
return derive_machine_key_legacy();
|
||||
}
|
||||
let entry = keyring::Entry::new(SERVICE_NAME, KEYRING_USER);
|
||||
if let Ok(entry) = &entry {
|
||||
if let Ok(stored) = entry.get_password() {
|
||||
@@ -56,7 +59,6 @@ fn get_vault_key() -> [u8; 32] {
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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);
|
||||
@@ -120,7 +122,6 @@ fn read_vault() -> AppResult<SecureVault> {
|
||||
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) {
|
||||
@@ -128,16 +129,15 @@ fn read_vault() -> AppResult<SecureVault> {
|
||||
}
|
||||
}
|
||||
|
||||
// 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)))?;
|
||||
if let Ok(decrypted) = decrypt_vault(&raw, &legacy_key) {
|
||||
if let Ok(vault) = serde_json::from_slice::<SecureVault>(&decrypted) {
|
||||
let _ = write_vault_with_key(&vault, &key);
|
||||
return Ok(vault);
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate: re-encrypt with the new keyring key
|
||||
let _ = write_vault_with_key(&vault, &key);
|
||||
Ok(vault)
|
||||
Ok(SecureVault::default())
|
||||
}
|
||||
|
||||
fn write_vault_with_key(vault: &SecureVault, key: &[u8; 32]) -> AppResult<()> {
|
||||
|
||||
@@ -404,29 +404,6 @@ pub fn fetch_book_chapters(conn: &Connection, user_id: &str, book_id: &str, lang
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
/// Retrieves chapter information for a specific chapter.
|
||||
pub fn fetch_book_chapter_infos(conn: &Connection, user_id: &str, chapter_id: &str, lang: Lang) -> AppResult<Vec<BookChapterInfosTable>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT chapter_info_id, chapter_id, act_id, incident_id, plot_point_id, book_id, author_id, summary, goal, last_update FROM book_chapter_infos WHERE author_id=?1 AND chapter_id=?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les infos des chapitres.".to_string() } else { "Unable to retrieve chapter infos.".to_string() }))?;
|
||||
|
||||
let rows = statement
|
||||
.query_map(params![user_id, chapter_id], |query_row| {
|
||||
Ok(BookChapterInfosTable {
|
||||
_chapter_info_id: query_row.get(0)?, chapter_id: query_row.get(1)?,
|
||||
_act_id: query_row.get(2)?, _incident_id: query_row.get(3)?,
|
||||
_plot_point_id: query_row.get(4)?, _book_id: query_row.get(5)?,
|
||||
author_id: query_row.get(6)?, summary: query_row.get(7)?,
|
||||
goal: query_row.get(8)?, last_update: query_row.get(9)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les infos des chapitres.".to_string() } else { "Unable to retrieve chapter infos.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les infos des chapitres.".to_string() } else { "Unable to retrieve chapter infos.".to_string() }))?;
|
||||
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
pub fn fetch_all_chapter_infos_by_book(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Vec<BookChapterInfosTable>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT chapter_info_id, chapter_id, act_id, incident_id, plot_point_id, book_id, author_id, summary, goal, last_update FROM book_chapter_infos WHERE author_id=?1 AND book_id=?2")
|
||||
|
||||
@@ -172,33 +172,6 @@ pub fn is_chapter_content_exist(conn: &Connection, user_id: &str, content_id: &s
|
||||
Ok(exists)
|
||||
}
|
||||
|
||||
/// Fetches all chapter contents for a specific chapter belonging to a user.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The ID of the user/author
|
||||
/// * `chapter_id` - The ID of the chapter
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns an array of book chapter content records.
|
||||
pub fn fetch_book_chapter_contents(conn: &Connection, user_id: &str, chapter_id: &str, lang: Lang) -> AppResult<Vec<BookChapterContentTable>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT content_id, chapter_id, author_id, version, content, words_count, time_on_it, last_update FROM book_chapter_content WHERE author_id=?1 AND chapter_id=?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le contenu des chapitres.".to_string() } else { "Unable to retrieve chapter contents.".to_string() }))?;
|
||||
|
||||
let rows = statement
|
||||
.query_map(params![user_id, chapter_id], |query_row| {
|
||||
Ok(BookChapterContentTable {
|
||||
content_id: query_row.get(0)?, chapter_id: query_row.get(1)?,
|
||||
author_id: query_row.get(2)?, version: query_row.get(3)?,
|
||||
content: query_row.get(4)?, _words_count: query_row.get(5)?,
|
||||
_time_on_it: query_row.get(6)?, last_update: query_row.get(7)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le contenu des chapitres.".to_string() } else { "Unable to retrieve chapter contents.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le contenu des chapitres.".to_string() } else { "Unable to retrieve chapter contents.".to_string() }))?;
|
||||
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
pub fn fetch_all_chapter_contents_by_book(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Vec<BookChapterContentTable>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT bcc.content_id, bcc.chapter_id, bcc.author_id, bcc.version, bcc.content, bcc.words_count, bcc.time_on_it, bcc.last_update FROM book_chapter_content bcc INNER JOIN book_chapters bc ON bcc.chapter_id = bc.chapter_id WHERE bcc.author_id=?1 AND bc.book_id=?2")
|
||||
|
||||
@@ -416,32 +416,6 @@ pub fn fetch_book_characters(conn: &Connection, user_id: &str, book_id: &str, la
|
||||
Ok(characters)
|
||||
}
|
||||
|
||||
/// Fetches all attributes for a specific character.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `character_id` - The unique identifier of the character
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of character attributes.
|
||||
pub fn fetch_book_characters_attributes(conn: &Connection, user_id: &str, character_id: &str, lang: Lang) -> AppResult<Vec<BookCharactersAttributesTable>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT attr_id, character_id, user_id, attribute_name, attribute_value, last_update FROM book_characters_attributes WHERE user_id=?1 AND character_id=?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les attributs des personnages.".to_string() } else { "Unable to retrieve character attributes.".to_string() }))?;
|
||||
|
||||
let attributes = statement
|
||||
.query_map(params![user_id, character_id], |query_row| {
|
||||
Ok(BookCharactersAttributesTable {
|
||||
attr_id: query_row.get(0)?, character_id: query_row.get(1)?,
|
||||
user_id: query_row.get(2)?, attribute_name: query_row.get(3)?,
|
||||
attribute_value: query_row.get(4)?, last_update: query_row.get(5)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les attributs des personnages.".to_string() } else { "Unable to retrieve character attributes.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les attributs des personnages.".to_string() } else { "Unable to retrieve character attributes.".to_string() }))?;
|
||||
|
||||
Ok(attributes)
|
||||
}
|
||||
|
||||
pub fn fetch_all_character_attributes_by_book(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Vec<BookCharactersAttributesTable>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT bca.attr_id, bca.character_id, bca.user_id, bca.attribute_name, bca.attribute_value, bca.last_update FROM book_characters_attributes bca INNER JOIN book_characters bc ON bca.character_id = bc.character_id WHERE bca.user_id=?1 AND bc.book_id=?2")
|
||||
|
||||
@@ -425,33 +425,6 @@ pub fn fetch_book_locations(conn: &Connection, user_id: &str, book_id: &str, lan
|
||||
Ok(book_locations)
|
||||
}
|
||||
|
||||
/// Fetches all elements for a specific location.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The user's unique identifier
|
||||
/// * `location_id` - The location's unique identifier
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of location element records.
|
||||
pub fn fetch_location_elements(conn: &Connection, user_id: &str, location_id: &str, lang: Lang) -> AppResult<Vec<LocationElementTable>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT element_id, location, user_id, element_name, original_name, element_description, last_update FROM location_element WHERE user_id = ?1 AND location = ?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de lieu.".to_string() } else { "Unable to retrieve location elements.".to_string() }))?;
|
||||
|
||||
let location_elements = statement
|
||||
.query_map(params![user_id, location_id], |query_row| {
|
||||
Ok(LocationElementTable {
|
||||
element_id: query_row.get(0)?, location: query_row.get(1)?,
|
||||
user_id: query_row.get(2)?, element_name: query_row.get(3)?,
|
||||
original_name: query_row.get(4)?, element_description: query_row.get(5)?,
|
||||
last_update: query_row.get(6)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de lieu.".to_string() } else { "Unable to retrieve location elements.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de lieu.".to_string() } else { "Unable to retrieve location elements.".to_string() }))?;
|
||||
|
||||
Ok(location_elements)
|
||||
}
|
||||
|
||||
pub fn fetch_all_location_elements_by_book(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Vec<LocationElementTable>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT e.element_id, e.location, e.user_id, e.element_name, e.original_name, e.element_description, e.last_update FROM location_element e INNER JOIN book_location l ON e.location = l.loc_id AND e.user_id = l.user_id WHERE e.user_id = ?1 AND l.book_id = ?2")
|
||||
@@ -494,33 +467,6 @@ pub fn fetch_all_location_sub_elements_by_book(conn: &Connection, user_id: &str,
|
||||
Ok(location_sub_elements)
|
||||
}
|
||||
|
||||
/// Fetches all sub-elements for a specific location element.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The user's unique identifier
|
||||
/// * `element_id` - The element's unique identifier
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of location sub-element records.
|
||||
pub fn fetch_location_sub_elements(conn: &Connection, user_id: &str, element_id: &str, lang: Lang) -> AppResult<Vec<LocationSubElementTable>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT sub_element_id, element_id, user_id, sub_elem_name, original_name, sub_elem_description, last_update FROM location_sub_element WHERE user_id = ?1 AND element_id = ?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sous-éléments de lieu.".to_string() } else { "Unable to retrieve location sub-elements.".to_string() }))?;
|
||||
|
||||
let location_sub_elements = statement
|
||||
.query_map(params![user_id, element_id], |query_row| {
|
||||
Ok(LocationSubElementTable {
|
||||
sub_element_id: query_row.get(0)?, element_id: query_row.get(1)?,
|
||||
user_id: query_row.get(2)?, sub_elem_name: query_row.get(3)?,
|
||||
original_name: query_row.get(4)?, sub_elem_description: query_row.get(5)?,
|
||||
last_update: query_row.get(6)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sous-éléments de lieu.".to_string() } else { "Unable to retrieve location sub-elements.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sous-éléments de lieu.".to_string() } else { "Unable to retrieve location sub-elements.".to_string() }))?;
|
||||
|
||||
Ok(location_sub_elements)
|
||||
}
|
||||
|
||||
/// Fetches all synced locations for a user (used for synchronization).
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The user's unique identifier
|
||||
|
||||
@@ -281,33 +281,6 @@ pub fn fetch_book_worlds(conn: &Connection, user_id: &str, book_id: &str, lang:
|
||||
Ok(worlds)
|
||||
}
|
||||
|
||||
/// Fetches all elements for a specific world.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `world_id` - The unique identifier of the world
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of book world elements table records.
|
||||
pub fn fetch_book_world_elements(conn: &Connection, user_id: &str, world_id: &str, lang: Lang) -> AppResult<Vec<BookWorldElementsTable>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT element_id, world_id, user_id, element_type, name, original_name, description, last_update FROM book_world_elements WHERE user_id=?1 AND world_id=?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les \u{00e9}l\u{00e9}ments du monde.".to_string() } else { "Unable to retrieve world elements.".to_string() }))?;
|
||||
|
||||
let elements = statement
|
||||
.query_map(params![user_id, world_id], |query_row| {
|
||||
Ok(BookWorldElementsTable {
|
||||
element_id: query_row.get(0)?, world_id: query_row.get(1)?,
|
||||
user_id: query_row.get(2)?, element_type: query_row.get(3)?,
|
||||
name: query_row.get(4)?, original_name: query_row.get(5)?,
|
||||
description: query_row.get(6)?, last_update: query_row.get(7)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les \u{00e9}l\u{00e9}ments du monde.".to_string() } else { "Unable to retrieve world elements.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les \u{00e9}l\u{00e9}ments du monde.".to_string() } else { "Unable to retrieve world elements.".to_string() }))?;
|
||||
|
||||
Ok(elements)
|
||||
}
|
||||
|
||||
pub fn fetch_all_world_elements_by_book(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Vec<BookWorldElementsTable>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT e.element_id, e.world_id, e.user_id, e.element_type, e.name, e.original_name, e.description, e.last_update FROM book_world_elements e INNER JOIN book_world w ON e.world_id = w.world_id AND e.user_id = w.author_id WHERE e.user_id=?1 AND w.book_id=?2")
|
||||
|
||||
Reference in New Issue
Block a user