Remove unused assets, refactor migration handling, and add crash reporting

- Deleted obsolete icons, layout files, and SVG assets.
- Added `useAutoUpdate` hook for streamlined update logic.
- Introduced `auto_migrate_electron` and migration commands for Electron-to-Tauri data migration.
- Implemented `init_panic_hook` for improved crash reporting.
- Updated `.gitignore` for Tauri build output and IDE-specific files.
This commit is contained in:
natreex
2026-04-07 16:43:54 -04:00
parent 5c7e71ce9e
commit 19c8d0057c
26 changed files with 515 additions and 63 deletions

View File

@@ -0,0 +1,378 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use tauri::State;
use crate::crypto::key_manager;
use crate::db::connection::DbManager;
use crate::db::schema;
use crate::error::{AppError, AppResult};
use crate::shared::session::SessionState;
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ImportMigrationData {
pub migration_file_path: String,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MigrationResult {
pub success: bool,
pub user_id: Option<String>,
pub error: Option<String>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MigrationCheckResult {
pub found: bool,
pub user_id: Option<String>,
pub has_db: bool,
pub migration_path: Option<String>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AutoMigrationResult {
pub migrated: bool,
pub user_id: Option<String>,
pub token_migrated: bool,
pub key_migrated: bool,
pub pin_migrated: bool,
pub db_migrated: bool,
pub error: Option<String>,
}
#[derive(Deserialize)]
struct ElectronMigrationFile {
version: u32,
user_id: String,
encryption_key: String,
pin_hash: Option<String>,
db_source: String,
}
// ─── Electron safeStorage vault format ───────────────────────────────────────
#[derive(Deserialize, Default)]
struct ElectronVault {
#[serde(default)]
token: Option<String>,
#[serde(flatten)]
extra: HashMap<String, serde_json::Value>,
}
// ─── Auto-migration from Electron ────────────────────────────────────────────
fn electron_data_paths() -> Vec<PathBuf> {
let home = dirs_next::home_dir().unwrap_or_else(|| PathBuf::from("."));
let mut paths = Vec::new();
#[cfg(target_os = "macos")]
{
let app_support = home.join("Library").join("Application Support");
paths.push(app_support.join("ERitors Scribe"));
paths.push(app_support.join("Electron"));
}
#[cfg(target_os = "windows")]
{
if let Some(appdata) = dirs_next::data_dir() {
paths.push(appdata.join("ERitors Scribe"));
paths.push(appdata.join("Electron"));
}
}
paths
}
fn find_electron_vault(data_path: &PathBuf) -> Option<PathBuf> {
let candidate = data_path.join("secure-config.json");
if candidate.exists() { return Some(candidate); }
None
}
fn find_electron_dbs(data_path: &PathBuf) -> Vec<(String, PathBuf)> {
let mut result = Vec::new();
if let Ok(entries) = std::fs::read_dir(data_path) {
for entry in entries.flatten() {
let name = entry.file_name().to_string_lossy().to_string();
if name.starts_with("eritors-local-") && name.ends_with(".db") {
let user_id = name
.trim_start_matches("eritors-local-")
.trim_end_matches(".db")
.to_string();
result.push((user_id, entry.path()));
}
}
}
result
}
#[cfg(target_os = "macos")]
fn decrypt_electron_value(encrypted_b64: &str) -> Option<String> {
use aes::Aes128;
use cbc::cipher::{block_padding::Pkcs7, BlockDecryptMut as _, KeyIvInit};
use pbkdf2::pbkdf2_hmac;
use sha1::Sha1;
type Aes128CbcDec = cbc::Decryptor<Aes128>;
let b64 = if encrypted_b64.starts_with("encrypted:") {
&encrypted_b64[10..]
} else {
encrypted_b64
};
let data = base64::Engine::decode(
&base64::engine::general_purpose::STANDARD,
b64,
).ok()?;
if !data.starts_with(b"v10") {
return None;
}
let ciphertext = &data[3..];
let app_names = ["ERitors Scribe", "Electron"];
for app_name in &app_names {
let service = format!("{} Safe Storage", app_name);
if let Ok(entry) = keyring::Entry::new(&service, app_name) {
if let Ok(password) = entry.get_password() {
let mut key = [0u8; 16];
pbkdf2_hmac::<Sha1>(
password.as_bytes(),
b"saltysalt",
1003,
&mut key,
);
let iv = [0x20u8; 16];
let mut buf = ciphertext.to_vec();
if let Ok(decryptor) = Aes128CbcDec::new_from_slices(&key, &iv) {
if let Ok(decrypted) = decryptor.decrypt_padded_mut::<Pkcs7>(&mut buf) {
if let Ok(s) = String::from_utf8(decrypted.to_vec()) {
return Some(s);
}
}
}
}
}
}
None
}
#[cfg(not(target_os = "macos"))]
fn decrypt_electron_value(_encrypted_b64: &str) -> Option<String> {
None
}
fn read_electron_vault(vault_path: &PathBuf) -> HashMap<String, String> {
let mut result = HashMap::new();
let Ok(content) = std::fs::read_to_string(vault_path) else { return result; };
let Ok(parsed) = serde_json::from_str::<HashMap<String, serde_json::Value>>(&content) else { return result; };
for (key, value) in &parsed {
if let Some(s) = value.as_str() {
if let Some(decrypted) = decrypt_electron_value(s) {
result.insert(key.clone(), decrypted);
} else if !s.starts_with("encrypted:") {
result.insert(key.clone(), s.to_string());
}
}
}
result
}
/// Called automatically at startup in production — detects and migrates Electron data.
#[tauri::command]
pub fn auto_migrate_electron(
db: State<DbManager>,
session: State<SessionState>,
) -> AppResult<AutoMigrationResult> {
let already_migrated = key_manager::get_last_user_id().ok().flatten();
if already_migrated.is_some() {
return Ok(AutoMigrationResult {
migrated: false, user_id: None,
token_migrated: false, key_migrated: false,
pin_migrated: false, db_migrated: false,
error: None,
});
}
for data_path in electron_data_paths() {
if !data_path.exists() { continue; }
let dbs = find_electron_dbs(&data_path);
if dbs.is_empty() { continue; }
let vault_data = if let Some(vault_path) = find_electron_vault(&data_path) {
read_electron_vault(&vault_path)
} else {
HashMap::new()
};
let token = vault_data.get("token").cloned();
let last_user_id = vault_data.get("lastUserId").cloned()
.or_else(|| dbs.first().map(|(uid, _)| uid.clone()));
let Some(user_id) = last_user_id else { continue; };
let encryption_key = vault_data.get(&format!("encryptionKey-{}", user_id)).cloned();
let pin_hash = vault_data.get(&format!("pinHash-{}", user_id)).cloned();
// Copy DB files
let mut db_migrated = false;
for (uid, db_path) in &dbs {
if uid != &user_id { continue; }
{
let mut db_manager = db.lock()
.map_err(|e| AppError::Internal(format!("DB lock: {}", e)))?;
db_manager.initialize(uid)?;
let dest = db_manager.get_db_path(uid);
if let Some(parent) = dest.parent() {
let _ = std::fs::create_dir_all(parent);
}
if std::fs::copy(db_path, &dest).is_ok() {
for ext in &["db-wal", "db-shm"] {
let src_extra = db_path.with_extension(ext);
if src_extra.exists() {
let dst_extra = dest.with_extension(ext);
let _ = std::fs::copy(&src_extra, &dst_extra);
}
}
db_migrated = true;
}
let conn = db_manager.get_connection(uid)?;
schema::initialize_schema(conn)?;
if !cfg!(debug_assertions) { schema::run_migrations(conn)?; }
}
}
// Store secrets
let token_migrated = if let Some(ref t) = token {
key_manager::set_token(t).is_ok()
} else { false };
let key_migrated = if let Some(ref k) = encryption_key {
key_manager::set_user_encryption_key(&user_id, k).is_ok()
} else { false };
let pin_migrated = if let Some(ref p) = pin_hash {
key_manager::set_pin_hash(&user_id, p).is_ok()
} else { false };
key_manager::set_last_user_id(&user_id)?;
let mut session_guard = session.lock()
.map_err(|e| AppError::Internal(format!("Session lock: {}", e)))?;
session_guard.user_id = Some(user_id.clone());
return Ok(AutoMigrationResult {
migrated: true,
user_id: Some(user_id),
token_migrated,
key_migrated,
pin_migrated,
db_migrated,
error: None,
});
}
Ok(AutoMigrationResult {
migrated: false, user_id: None,
token_migrated: false, key_migrated: false,
pin_migrated: false, db_migrated: false,
error: None,
})
}
/// Check if an Electron migration file + DB exist at the given path (legacy).
#[tauri::command]
pub fn check_electron_migration(migration_file_path: String) -> AppResult<MigrationCheckResult> {
let path = PathBuf::from(&migration_file_path);
if !path.exists() {
return Ok(MigrationCheckResult { found: false, user_id: None, has_db: false, migration_path: None });
}
let content = std::fs::read_to_string(&path).map_err(AppError::Io)?;
let migration: ElectronMigrationFile = serde_json::from_str(&content)?;
let db_name = format!("eritors-local-{}.db", migration.user_id);
let db_path = path.parent().unwrap_or(&path).join(&db_name);
Ok(MigrationCheckResult {
found: true,
user_id: Some(migration.user_id),
has_db: db_path.exists(),
migration_path: Some(migration_file_path),
})
}
/// Import from Electron export file (legacy).
#[tauri::command]
pub fn import_from_electron(
data: ImportMigrationData,
db: State<DbManager>,
session: State<SessionState>,
) -> AppResult<MigrationResult> {
let migration_path = PathBuf::from(&data.migration_file_path);
if !migration_path.exists() {
return Ok(MigrationResult { success: false, user_id: None, error: Some("Migration file not found".into()) });
}
let content = std::fs::read_to_string(&migration_path)?;
let migration: ElectronMigrationFile = serde_json::from_str(&content)?;
if migration.version != 1 {
return Ok(MigrationResult { success: false, user_id: None, error: Some(format!("Unsupported version: {}", migration.version)) });
}
let source_db = migration_path.parent()
.unwrap_or_else(|| std::path::Path::new("."))
.join(format!("eritors-local-{}.db", migration.user_id));
if !source_db.exists() {
let electron_db = PathBuf::from(&migration.db_source);
if !electron_db.exists() {
return Ok(MigrationResult { success: false, user_id: None, error: Some("Database file not found.".into()) });
}
copy_db_files(&electron_db, &migration.user_id, &db)?;
} else {
copy_db_files(&source_db, &migration.user_id, &db)?;
}
key_manager::set_user_encryption_key(&migration.user_id, &migration.encryption_key)?;
if let Some(ref pin_hash) = migration.pin_hash {
key_manager::set_pin_hash(&migration.user_id, pin_hash)?;
}
key_manager::set_last_user_id(&migration.user_id)?;
{
let mut db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock: {}", e)))?;
db_manager.initialize(&migration.user_id)?;
let conn = db_manager.get_connection(&migration.user_id)?;
schema::initialize_schema(conn)?;
if !cfg!(debug_assertions) { schema::run_migrations(conn)?; }
}
let mut session_guard = session.lock().map_err(|e| AppError::Internal(format!("Session lock: {}", e)))?;
session_guard.user_id = Some(migration.user_id.clone());
cleanup_migration_files(&migration_path, &migration.user_id);
Ok(MigrationResult { success: true, user_id: Some(migration.user_id), error: None })
}
fn cleanup_migration_files(migration_path: &PathBuf, user_id: &str) {
let _ = std::fs::remove_file(migration_path);
if let Some(parent) = migration_path.parent() {
let db_copy = parent.join(format!("eritors-local-{}.db", user_id));
let _ = std::fs::remove_file(&db_copy);
let _ = std::fs::remove_file(db_copy.with_extension("db-wal"));
let _ = std::fs::remove_file(db_copy.with_extension("db-shm"));
}
}
fn copy_db_files(source_db: &PathBuf, user_id: &str, db: &State<DbManager>) -> AppResult<()> {
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock: {}", e)))?;
let dest_db = db_manager.get_db_path(user_id);
if let Some(parent) = dest_db.parent() { std::fs::create_dir_all(parent)?; }
std::fs::copy(source_db, &dest_db)?;
for ext in &["db-wal", "db-shm"] {
let src = source_db.with_extension(ext);
if src.exists() {
let dst = dest_db.with_file_name(format!("eritors-local-{}.{}", user_id, ext));
let _ = std::fs::copy(&src, &dst);
}
}
Ok(())
}