//! Server profiles, portable import/export and private-key selection. use super::{logged, main_core, text, CommandResult}; use crate::core::app::CoreSlot; use crate::core::error::{CoreError, CoreResult}; use crate::core::profile_transfer::{serialize_profiles, MAX_BYTES}; use crate::dialogs::{self, Filter}; use serde_json::{json, Value}; use std::io::Read; use tauri::{State, Webview}; #[tauri::command] pub async fn core_profiles_list(webview: Webview, slot: State<'_, CoreSlot>) -> CommandResult { let core = main_core(&webview, &slot)?; logged(&core, "profiles:list", core.profiles.list().map(Value::Array)) } #[tauri::command] pub async fn core_profiles_save(webview: Webview, slot: State<'_, CoreSlot>, payload: Option) -> CommandResult { let core = main_core(&webview, &slot)?; let payload = super::payload(payload); let result = core.profiles.save(&payload); core.services.availability.changed(); logged(&core, "profiles:save", result) } #[tauri::command] pub async fn core_profiles_delete(webview: Webview, slot: State<'_, CoreSlot>, payload: Option) -> CommandResult { let core = main_core(&webview, &slot)?; let payload = super::payload(payload); let result = async { core.services.biometrics.require_sensitive_action().await?; core.profiles.delete(text(&payload, "id")) } .await; core.services.availability.changed(); logged(&core, "profiles:delete", result) } #[tauri::command] pub async fn core_profiles_select_private_key(webview: Webview, slot: State<'_, CoreSlot>) -> CommandResult { let core = main_core(&webview, &slot)?; let window = dialogs::main_window(&core.app); let path = dialogs::pick_file( &core.app, window.as_ref(), Some(&core.t("native.privateKey.title")), &[Filter { name: &core.t("native.privateKey.keys"), extensions: &["pem", "key", "ppk"] }, Filter { name: &core.t("native.privateKey.all"), extensions: &["*"] }], ) .await; Ok(json!({ "canceled": path.is_none(), "path": path.map(|p| p.to_string_lossy().to_string()) })) } #[tauri::command] pub async fn core_profiles_export(webview: Webview, slot: State<'_, CoreSlot>, payload: Option) -> CommandResult { let core = main_core(&webview, &slot)?; let payload = super::payload(payload); let result = async { let format = payload.get("format").and_then(Value::as_str).filter(|f| !f.is_empty()).unwrap_or("json").to_string(); let profiles = core.profiles.list()?; let selected: Vec = match payload.get("ids") { None | Some(Value::Null) => profiles, Some(Value::Array(ids)) => profiles.into_iter().filter(|p| ids.contains(&p["id"])).collect(), Some(_) => Vec::new(), }; if selected.is_empty() { return Err(CoreError::plain("Select at least one server to export.")); } let content = serialize_profiles(&selected, &format)?; let window = dialogs::main_window(&core.app); let upper = format.to_uppercase(); let Some(path) = dialogs::save_file(&core.app, window.as_ref(), None, &format!("chj-server-profiles.{format}"), &[Filter { name: &upper, extensions: &[format.as_str()] }]).await else { return Ok(json!({ "canceled": true })); }; write_private(&path, content.as_bytes())?; Ok(json!({ "canceled": false, "count": selected.len() })) } .await; logged(&core, "profiles:export", result) } pub fn write_private(path: &std::path::Path, data: &[u8]) -> CoreResult<()> { let mut options = std::fs::OpenOptions::new(); options.write(true).create(true).truncate(true); #[cfg(unix)] { use std::os::unix::fs::OpenOptionsExt; options.mode(0o600); } use std::io::Write; options.open(path)?.write_all(data)?; Ok(()) } #[tauri::command] pub async fn core_profiles_preview_import(webview: Webview, slot: State<'_, CoreSlot>) -> CommandResult { let core = main_core(&webview, &slot)?; let result = async { core.profiles.list()?; // Require an unlocked vault before opening the picker. core.profile_transfer.cancel(); let window = dialogs::main_window(&core.app); let Some(path) = dialogs::pick_file(&core.app, window.as_ref(), None, &[Filter { name: &core.t("native.profiles.importFilter"), extensions: &["json", "csv"] }]).await else { return Ok(json!({ "canceled": true })); }; let mut buffer = Vec::new(); std::fs::File::open(&path)?.take(MAX_BYTES as u64 + 1).read_to_end(&mut buffer)?; if buffer.len() > MAX_BYTES { return Err(CoreError::plain("Profile file exceeds the 5 MiB limit.")); } let content = String::from_utf8(buffer).map_err(|_| CoreError::plain("The encoded data was not valid for encoding utf-8"))?; let mut preview = core.profile_transfer.preview(&core.profiles, &content)?; preview["canceled"] = json!(false); let mut ordered = serde_json::Map::new(); ordered.insert("canceled".into(), json!(false)); if let Value::Object(fields) = preview { for (k, v) in fields { if k != "canceled" { ordered.insert(k, v); } } } Ok(Value::Object(ordered)) } .await; logged(&core, "profiles:previewImport", result) } #[tauri::command] pub async fn core_profiles_commit_import(webview: Webview, slot: State<'_, CoreSlot>, payload: Option) -> CommandResult { let core = main_core(&webview, &slot)?; let payload = super::payload(payload); let result: CoreResult = async { core.services.biometrics.require_sensitive_action().await?; core.profile_transfer.commit(&core.profiles, text(&payload, "token"), text(&payload, "policy")) } .await; core.services.availability.changed(); logged(&core, "profiles:commitImport", result) } #[tauri::command] pub async fn core_profiles_cancel_import(webview: Webview, slot: State<'_, CoreSlot>) -> CommandResult { let core = main_core(&webview, &slot)?; core.profile_transfer.cancel(); Ok(json!({ "canceled": true })) }