use rusqlite::Connection; use serde::Serialize; use crate::crypto::encryption::{encrypt_data_with_user_key, decrypt_data_with_user_key, hash_element}; use crate::crypto::key_manager::get_user_encryption_key; use crate::domains::series_spell::repo; use crate::domains::tombstone::repo as tombstone_repo; use crate::error::{AppError, AppResult}; use crate::helpers::{create_unique_id, timestamp_in_seconds}; use crate::shared::types::Lang; #[derive(Serialize)] pub struct SeriesSpellTagProps { pub id: String, pub name: String, pub color: Option, } #[derive(Serialize)] pub struct SeriesSpellListProps { pub id: String, pub name: String, pub description: String, pub tags: Vec, } #[derive(Serialize)] pub struct SeriesSpellListResponse { pub spells: Vec, pub tags: Vec, } #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub struct SeriesSpellDetailProps { pub id: String, pub name: String, pub description: String, pub appearance: String, pub tags: Vec, pub power_level: Option, pub components: Option, pub limitations: Option, pub notes: Option, } /// Retrieves all spells and tags for a series. /// * `conn` - Database connection /// * `user_id` - The unique identifier of the user /// * `series_id` - The unique identifier of the series /// * `lang` - The language for error messages /// Returns the list of spells and tags. pub fn get_spell_list(conn: &Connection, user_id: &str, series_id: &str, lang: Lang) -> AppResult { let user_key: String = get_user_encryption_key(user_id)?; let spells_result: Vec = repo::fetch_spells(conn, user_id, series_id, lang)?; let tags_result: Vec = repo::fetch_tags(conn, user_id, series_id, lang)?; let mut spells: Vec = Vec::with_capacity(spells_result.len()); for spell in spells_result { let decrypted_name: String = if spell.name.is_empty() { String::new() } else { decrypt_data_with_user_key(&spell.name, &user_key)? }; let decrypted_description: String = if spell.description.is_empty() { String::new() } else { decrypt_data_with_user_key(&spell.description, &user_key)? }; let decrypted_tags: Vec = if spell.tags.is_empty() { vec![] } else { let decrypted_tags_string: String = decrypt_data_with_user_key(&spell.tags, &user_key)?; serde_json::from_str(&decrypted_tags_string).unwrap_or_default() }; spells.push(SeriesSpellListProps { id: spell.spell_id, name: decrypted_name, description: decrypted_description, tags: decrypted_tags, }); } let mut tags: Vec = Vec::with_capacity(tags_result.len()); for tag in tags_result { let decrypted_name: String = if tag.name.is_empty() { String::new() } else { decrypt_data_with_user_key(&tag.name, &user_key)? }; tags.push(SeriesSpellTagProps { id: tag.tag_id, name: decrypted_name, color: tag.color, }); } Ok(SeriesSpellListResponse { spells, tags }) } /// Retrieves the details of a specific spell. /// * `conn` - Database connection /// * `user_id` - The unique identifier of the user /// * `spell_id` - The unique identifier of the spell /// * `lang` - The language for error messages /// Returns the spell details. pub fn get_spell_detail(conn: &Connection, user_id: &str, spell_id: &str, lang: Lang) -> AppResult { let user_key: String = get_user_encryption_key(user_id)?; let spell: Option = repo::fetch_spell_by_id(conn, user_id, spell_id, lang)?; let spell: repo::SeriesSpellResult = spell.ok_or_else(|| { AppError::Internal(if lang == Lang::Fr { "Sort non trouvé.".to_string() } else { "Spell not found.".to_string() }) })?; let decrypted_name: String = if spell.name.is_empty() { String::new() } else { decrypt_data_with_user_key(&spell.name, &user_key)? }; let decrypted_description: String = if spell.description.is_empty() { String::new() } else { decrypt_data_with_user_key(&spell.description, &user_key)? }; let decrypted_appearance: String = if spell.appearance.is_empty() { String::new() } else { decrypt_data_with_user_key(&spell.appearance, &user_key)? }; let decrypted_tags: Vec = if spell.tags.is_empty() { vec![] } else { let decrypted_tags_string: String = decrypt_data_with_user_key(&spell.tags, &user_key)?; serde_json::from_str(&decrypted_tags_string).unwrap_or_default() }; let decrypted_power_level: Option = if let Some(ref power_level) = spell.power_level { Some(decrypt_data_with_user_key(power_level, &user_key)?) } else { None }; let decrypted_components: Option = if let Some(ref components) = spell.components { Some(decrypt_data_with_user_key(components, &user_key)?) } else { None }; let decrypted_limitations: Option = if let Some(ref limitations) = spell.limitations { Some(decrypt_data_with_user_key(limitations, &user_key)?) } else { None }; let decrypted_notes: Option = if let Some(ref notes) = spell.notes { Some(decrypt_data_with_user_key(notes, &user_key)?) } else { None }; Ok(SeriesSpellDetailProps { id: spell.spell_id, name: decrypted_name, description: decrypted_description, appearance: decrypted_appearance, tags: decrypted_tags, power_level: decrypted_power_level, components: decrypted_components, limitations: decrypted_limitations, notes: decrypted_notes, }) } /// Adds a new spell to a series. /// * `conn` - Database connection /// * `user_id` - The unique identifier of the user /// * `series_id` - The unique identifier of the series /// * `name` - The spell name /// * `lang` - The language for error messages /// * `description` - The spell description /// * `appearance` - The spell appearance /// * `tags` - The spell tags /// * `power_level` - The spell power level /// * `components` - The spell components /// * `limitations` - The spell limitations /// * `notes` - The spell notes /// Returns the new spell ID. pub fn add_spell( conn: &Connection, user_id: &str, series_id: &str, name: &str, lang: Lang, description: Option<&str>, appearance: Option<&str>, tags: Option>, power_level: Option<&str>, components: Option<&str>, limitations: Option<&str>, notes: Option<&str>, ) -> AppResult { let user_key: String = get_user_encryption_key(user_id)?; let spell_id: String = create_unique_id(None); let encrypted_name: String = encrypt_data_with_user_key(name, &user_key)?; let name_hash: String = hash_element(name); let encrypted_description: String = if let Some(description) = description { encrypt_data_with_user_key(description, &user_key)? } else { String::new() }; let encrypted_appearance: String = if let Some(appearance) = appearance { encrypt_data_with_user_key(appearance, &user_key)? } else { String::new() }; let tags_json: String = serde_json::to_string(&tags.unwrap_or_default()).unwrap_or_else(|_| "[]".to_string()); let encrypted_tags: String = encrypt_data_with_user_key(&tags_json, &user_key)?; let encrypted_power_level: Option = if let Some(power_level) = power_level { Some(encrypt_data_with_user_key(power_level, &user_key)?) } else { None }; let encrypted_components: Option = if let Some(components) = components { Some(encrypt_data_with_user_key(components, &user_key)?) } else { None }; let encrypted_limitations: Option = if let Some(limitations) = limitations { Some(encrypt_data_with_user_key(limitations, &user_key)?) } else { None }; let encrypted_notes: Option = if let Some(notes) = notes { Some(encrypt_data_with_user_key(notes, &user_key)?) } else { None }; let last_update: i64 = timestamp_in_seconds(); repo::insert_spell( conn, &spell_id, series_id, user_id, &encrypted_name, &name_hash, &encrypted_description, &encrypted_appearance, &encrypted_tags, encrypted_power_level.as_deref(), encrypted_components.as_deref(), encrypted_limitations.as_deref(), encrypted_notes.as_deref(), last_update, lang, )?; Ok(spell_id) } /// Updates an existing spell. /// * `conn` - Database connection /// * `user_id` - The unique identifier of the user /// * `spell_id` - The unique identifier of the spell /// * `name` - The spell name /// * `lang` - The language for error messages /// * `description` - The spell description /// * `appearance` - The spell appearance /// * `tags` - The spell tags /// * `power_level` - The spell power level /// * `components` - The spell components /// * `limitations` - The spell limitations /// * `notes` - The spell notes /// Returns true if successful. pub fn update_spell( conn: &Connection, user_id: &str, spell_id: &str, name: &str, lang: Lang, description: Option<&str>, appearance: Option<&str>, tags: Option>, power_level: Option<&str>, components: Option<&str>, limitations: Option<&str>, notes: Option<&str>, ) -> AppResult { let user_key: String = get_user_encryption_key(user_id)?; let encrypted_name: String = encrypt_data_with_user_key(name, &user_key)?; let name_hash: String = hash_element(name); let encrypted_description: String = if let Some(description) = description { encrypt_data_with_user_key(description, &user_key)? } else { String::new() }; let encrypted_appearance: String = if let Some(appearance) = appearance { encrypt_data_with_user_key(appearance, &user_key)? } else { String::new() }; let tags_json: String = serde_json::to_string(&tags.unwrap_or_default()).unwrap_or_else(|_| "[]".to_string()); let encrypted_tags: String = encrypt_data_with_user_key(&tags_json, &user_key)?; let encrypted_power_level: Option = if let Some(power_level) = power_level { Some(encrypt_data_with_user_key(power_level, &user_key)?) } else { None }; let encrypted_components: Option = if let Some(components) = components { Some(encrypt_data_with_user_key(components, &user_key)?) } else { None }; let encrypted_limitations: Option = if let Some(limitations) = limitations { Some(encrypt_data_with_user_key(limitations, &user_key)?) } else { None }; let encrypted_notes: Option = if let Some(notes) = notes { Some(encrypt_data_with_user_key(notes, &user_key)?) } else { None }; let last_update: i64 = timestamp_in_seconds(); repo::update_spell( conn, user_id, spell_id, &encrypted_name, &name_hash, &encrypted_description, &encrypted_appearance, &encrypted_tags, encrypted_power_level.as_deref(), encrypted_components.as_deref(), encrypted_limitations.as_deref(), encrypted_notes.as_deref(), last_update, lang, ) } /// Deletes a spell. /// * `conn` - Database connection /// * `user_id` - The unique identifier of the user /// * `spell_id` - The unique identifier of the spell /// * `deleted_at` - The timestamp of deletion /// * `lang` - The language for error messages /// Returns true if successful. pub fn delete_spell(conn: &Connection, user_id: &str, spell_id: &str, deleted_at: i64, lang: Lang) -> AppResult { let deleted: bool = repo::delete_spell(conn, user_id, spell_id, lang)?; if deleted { tombstone_repo::insert(conn, spell_id, "series_spells", spell_id, None, user_id, deleted_at, lang)?; } Ok(deleted) } /// Adds a new tag to a series. /// * `conn` - Database connection /// * `user_id` - The unique identifier of the user /// * `series_id` - The unique identifier of the series /// * `name` - The name of the tag /// * `lang` - The language for error messages /// * `color` - The color of the tag (optional) /// Returns the new tag ID. pub fn add_tag( conn: &Connection, user_id: &str, series_id: &str, name: &str, lang: Lang, color: Option<&str>, ) -> AppResult { let user_key: String = get_user_encryption_key(user_id)?; let tag_id: String = create_unique_id(None); let encrypted_name: String = encrypt_data_with_user_key(name, &user_key)?; let hashed_name: String = hash_element(name); let last_update: i64 = timestamp_in_seconds(); repo::insert_tag(conn, &tag_id, series_id, user_id, &encrypted_name, &hashed_name, color, last_update, lang)?; Ok(tag_id) } /// Updates an existing tag. /// * `conn` - Database connection /// * `user_id` - The unique identifier of the user /// * `tag_id` - The unique identifier of the tag /// * `name` - The new name of the tag /// * `lang` - The language for error messages /// * `color` - The new color of the tag (optional) /// Returns true if successful. pub fn update_tag( conn: &Connection, user_id: &str, tag_id: &str, name: &str, lang: Lang, color: Option<&str>, ) -> AppResult { let user_key: String = get_user_encryption_key(user_id)?; let encrypted_name: String = encrypt_data_with_user_key(name, &user_key)?; let hashed_name: String = hash_element(name); let last_update: i64 = timestamp_in_seconds(); repo::update_tag(conn, user_id, tag_id, &encrypted_name, &hashed_name, color, last_update, lang) } /// Deletes a tag. /// * `conn` - Database connection /// * `user_id` - The unique identifier of the user /// * `tag_id` - The unique identifier of the tag /// * `deleted_at` - The timestamp of deletion /// * `lang` - The language for error messages /// Returns true if successful. pub fn delete_tag(conn: &Connection, user_id: &str, tag_id: &str, deleted_at: i64, lang: Lang) -> AppResult { let deleted: bool = repo::delete_tag(conn, user_id, tag_id, lang)?; if deleted { tombstone_repo::insert(conn, tag_id, "series_spell_tags", tag_id, None, user_id, deleted_at, lang)?; } Ok(deleted) }