fix: harden Tauri backend — replace unwrap panics, fix watcher lifecycle, strengthen CSP

Replace all .unwrap() calls on repo with repo_ref()/repo_mut() helpers
that return error strings instead of panicking. Stop the old file watcher
before starting a new one on workspace switch to prevent accumulation.
Add object-src and base-uri directives to CSP.
This commit is contained in:
Tristan Michael 2026-04-02 08:58:45 -07:00
parent e0c7292a7e
commit 3c11539f02
2 changed files with 28 additions and 36 deletions

View file

@ -88,6 +88,16 @@ fn ensure_repo(state: &mut AppState) -> Result<(), String> {
Ok(()) Ok(())
} }
/// Get an immutable reference to the repo, returning an error if not initialized.
fn repo_ref(state: &AppState) -> Result<&TaskRepository, String> {
state.repo.as_ref().ok_or_else(|| "Repository not initialized".to_string())
}
/// Get a mutable reference to the repo, returning an error if not initialized.
fn repo_mut(state: &mut AppState) -> Result<&mut TaskRepository, String> {
state.repo.as_mut().ok_or_else(|| "Repository not initialized".to_string())
}
// ── Config commands ────────────────────────────────────────────────── // ── Config commands ──────────────────────────────────────────────────
#[tauri::command] #[tauri::command]
@ -164,9 +174,7 @@ fn init_workspace(path: String) -> Result<(), String> {
fn get_lists(state: State<'_, Mutex<AppState>>) -> Result<Vec<TaskList>, String> { fn get_lists(state: State<'_, Mutex<AppState>>) -> Result<Vec<TaskList>, String> {
let mut s = lock_state(&state)?; let mut s = lock_state(&state)?;
ensure_repo(&mut s)?; ensure_repo(&mut s)?;
s.repo repo_ref(&s)?
.as_ref()
.unwrap()
.get_lists() .get_lists()
.map_err(|e| e.to_string()) .map_err(|e| e.to_string())
} }
@ -179,9 +187,7 @@ fn create_list(
let mut s = lock_state(&state)?; let mut s = lock_state(&state)?;
ensure_repo(&mut s)?; ensure_repo(&mut s)?;
mute_watcher(&mut s); mute_watcher(&mut s);
s.repo repo_mut(&mut s)?
.as_mut()
.unwrap()
.create_list(name) .create_list(name)
.map_err(|e| e.to_string()) .map_err(|e| e.to_string())
} }
@ -195,9 +201,7 @@ fn delete_list(
ensure_repo(&mut s)?; ensure_repo(&mut s)?;
mute_watcher(&mut s); mute_watcher(&mut s);
let id = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?; let id = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
s.repo repo_mut(&mut s)?
.as_mut()
.unwrap()
.delete_list(id) .delete_list(id)
.map_err(|e| e.to_string()) .map_err(|e| e.to_string())
} }
@ -212,9 +216,7 @@ fn list_tasks(
let mut s = lock_state(&state)?; let mut s = lock_state(&state)?;
ensure_repo(&mut s)?; ensure_repo(&mut s)?;
let id = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?; let id = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
s.repo repo_ref(&s)?
.as_ref()
.unwrap()
.list_tasks(id) .list_tasks(id)
.map_err(|e| e.to_string()) .map_err(|e| e.to_string())
} }
@ -239,9 +241,7 @@ fn create_task(
let parent_uuid = Uuid::parse_str(&pid).map_err(|e| e.to_string())?; let parent_uuid = Uuid::parse_str(&pid).map_err(|e| e.to_string())?;
task.parent_id = Some(parent_uuid); task.parent_id = Some(parent_uuid);
} }
s.repo repo_mut(&mut s)?
.as_mut()
.unwrap()
.create_task(id, task) .create_task(id, task)
.map_err(|e| e.to_string()) .map_err(|e| e.to_string())
} }
@ -256,9 +256,7 @@ fn update_task(
ensure_repo(&mut s)?; ensure_repo(&mut s)?;
mute_watcher(&mut s); mute_watcher(&mut s);
let id = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?; let id = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
s.repo repo_mut(&mut s)?
.as_mut()
.unwrap()
.update_task(id, task) .update_task(id, task)
.map_err(|e| e.to_string()) .map_err(|e| e.to_string())
} }
@ -274,7 +272,7 @@ fn delete_task(
mute_watcher(&mut s); mute_watcher(&mut s);
let lid = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?; let lid = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
let tid = Uuid::parse_str(&task_id).map_err(|e| e.to_string())?; let tid = Uuid::parse_str(&task_id).map_err(|e| e.to_string())?;
let repo = s.repo.as_mut().unwrap(); let repo = repo_mut(&mut s)?;
// Cascade-delete subtasks first // Cascade-delete subtasks first
let all_tasks = repo.list_tasks(lid).map_err(|e| e.to_string())?; let all_tasks = repo.list_tasks(lid).map_err(|e| e.to_string())?;
let child_ids: Vec<Uuid> = all_tasks let child_ids: Vec<Uuid> = all_tasks
@ -300,7 +298,7 @@ fn toggle_task(
mute_watcher(&mut s); mute_watcher(&mut s);
let lid = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?; let lid = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
let tid = Uuid::parse_str(&task_id).map_err(|e| e.to_string())?; let tid = Uuid::parse_str(&task_id).map_err(|e| e.to_string())?;
let repo = s.repo.as_mut().unwrap(); let repo = repo_mut(&mut s)?;
let mut task = repo.get_task(lid, tid).map_err(|e| e.to_string())?; let mut task = repo.get_task(lid, tid).map_err(|e| e.to_string())?;
match task.status { match task.status {
TaskStatus::Backlog => task.complete(), TaskStatus::Backlog => task.complete(),
@ -334,9 +332,7 @@ fn reorder_task(
mute_watcher(&mut s); mute_watcher(&mut s);
let lid = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?; let lid = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
let tid = Uuid::parse_str(&task_id).map_err(|e| e.to_string())?; let tid = Uuid::parse_str(&task_id).map_err(|e| e.to_string())?;
s.repo repo_mut(&mut s)?
.as_mut()
.unwrap()
.reorder_task(lid, tid, new_position) .reorder_task(lid, tid, new_position)
.map_err(|e| e.to_string()) .map_err(|e| e.to_string())
} }
@ -356,9 +352,7 @@ fn move_task(
let from = Uuid::parse_str(&from_list_id).map_err(|e| e.to_string())?; let from = Uuid::parse_str(&from_list_id).map_err(|e| e.to_string())?;
let to = Uuid::parse_str(&to_list_id).map_err(|e| e.to_string())?; let to = Uuid::parse_str(&to_list_id).map_err(|e| e.to_string())?;
let tid = Uuid::parse_str(&task_id).map_err(|e| e.to_string())?; let tid = Uuid::parse_str(&task_id).map_err(|e| e.to_string())?;
s.repo repo_mut(&mut s)?
.as_mut()
.unwrap()
.move_task(from, to, tid) .move_task(from, to, tid)
.map_err(|e| e.to_string()) .map_err(|e| e.to_string())
} }
@ -373,9 +367,7 @@ fn rename_list(
ensure_repo(&mut s)?; ensure_repo(&mut s)?;
mute_watcher(&mut s); mute_watcher(&mut s);
let id = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?; let id = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
s.repo repo_mut(&mut s)?
.as_mut()
.unwrap()
.rename_list(id, new_name) .rename_list(id, new_name)
.map_err(|e| e.to_string()) .map_err(|e| e.to_string())
} }
@ -390,9 +382,7 @@ fn set_group_by_due_date(
ensure_repo(&mut s)?; ensure_repo(&mut s)?;
mute_watcher(&mut s); mute_watcher(&mut s);
let id = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?; let id = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
s.repo repo_mut(&mut s)?
.as_mut()
.unwrap()
.set_group_by_due_date(id, enabled) .set_group_by_due_date(id, enabled)
.map_err(|e| e.to_string()) .map_err(|e| e.to_string())
} }
@ -405,9 +395,7 @@ fn get_group_by_due_date(
let mut s = lock_state(&state)?; let mut s = lock_state(&state)?;
ensure_repo(&mut s)?; ensure_repo(&mut s)?;
let id = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?; let id = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
s.repo repo_ref(&s)?
.as_ref()
.unwrap()
.get_group_by_due_date(id) .get_group_by_due_date(id)
.map_err(|e| e.to_string()) .map_err(|e| e.to_string())
} }
@ -498,6 +486,10 @@ async fn sync_workspace(
#[cfg(not(target_os = "android"))] #[cfg(not(target_os = "android"))]
fn start_watcher(handle: tauri::AppHandle, path: PathBuf) { fn start_watcher(handle: tauri::AppHandle, path: PathBuf) {
// Stop any existing watcher before starting a new one
if let Ok(mut w) = WATCHER.lock() {
*w = None;
}
let handle = handle.clone(); let handle = handle.clone();
let debouncer = new_debouncer( let debouncer = new_debouncer(
std::time::Duration::from_millis(500), std::time::Duration::from_millis(500),

View file

@ -23,7 +23,7 @@
} }
], ],
"security": { "security": {
"csp": "default-src 'self'; style-src 'self' 'unsafe-inline'; font-src 'self' https://fonts.gstatic.com; connect-src ipc: http://ipc.localhost" "csp": "default-src 'self'; style-src 'self' 'unsafe-inline'; font-src 'self' https://fonts.gstatic.com; connect-src ipc: http://ipc.localhost; object-src 'none'; base-uri 'self'"
} }
}, },
"bundle": { "bundle": {