feat(remote): persist SPKI pins and machine configs to SQLite
- remote_machines table in sessions.db (id, label, url, token, auto_connect, spki_pins as JSON array, created_at, updated_at) - session/machines.rs: save/load/delete/update_pins CRUD operations - RemoteManager: set_session_db() + load_from_db() for startup restoration - All mutations persist: add_machine, remove_machine, add_spki_pin, remove_spki_pin, TOFU auto-store — pins survive restart - 197 cargo tests passing, 0 warnings
This commit is contained in:
parent
d1463d4d1e
commit
538a31f85c
4 changed files with 170 additions and 2 deletions
77
src-tauri/src/session/machines.rs
Normal file
77
src-tauri/src/session/machines.rs
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
// Remote machine persistence — stores machine configs + SPKI pins in SQLite
|
||||
|
||||
use crate::error::AppError;
|
||||
use crate::session::SessionDb;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RemoteMachineRecord {
|
||||
pub id: String,
|
||||
pub label: String,
|
||||
pub url: String,
|
||||
pub token: String,
|
||||
pub auto_connect: bool,
|
||||
pub spki_pins: Vec<String>,
|
||||
}
|
||||
|
||||
impl SessionDb {
|
||||
pub fn save_remote_machine(&self, machine: &RemoteMachineRecord) -> Result<(), AppError> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let pins_json = serde_json::to_string(&machine.spki_pins)?;
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs() as i64;
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO remote_machines (id, label, url, token, auto_connect, spki_pins, created_at, updated_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, COALESCE((SELECT created_at FROM remote_machines WHERE id = ?1), ?7), ?7)",
|
||||
rusqlite::params![machine.id, machine.label, machine.url, machine.token, machine.auto_connect as i32, pins_json, now],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load_remote_machines(&self) -> Result<Vec<RemoteMachineRecord>, AppError> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT id, label, url, token, auto_connect, spki_pins FROM remote_machines"
|
||||
)?;
|
||||
let rows = stmt.query_map([], |row| {
|
||||
let pins_json: String = row.get("spki_pins")?;
|
||||
let pins: Vec<String> = serde_json::from_str(&pins_json).unwrap_or_default();
|
||||
Ok(RemoteMachineRecord {
|
||||
id: row.get("id")?,
|
||||
label: row.get("label")?,
|
||||
url: row.get("url")?,
|
||||
token: row.get("token")?,
|
||||
auto_connect: row.get::<_, i32>("auto_connect")? != 0,
|
||||
spki_pins: pins,
|
||||
})
|
||||
})?;
|
||||
let mut machines = Vec::new();
|
||||
for row in rows {
|
||||
machines.push(row?);
|
||||
}
|
||||
Ok(machines)
|
||||
}
|
||||
|
||||
pub fn delete_remote_machine(&self, id: &str) -> Result<(), AppError> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
conn.execute("DELETE FROM remote_machines WHERE id = ?1", [id])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update_remote_machine_pins(&self, id: &str, pins: &[String]) -> Result<(), AppError> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let pins_json = serde_json::to_string(pins)?;
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs() as i64;
|
||||
conn.execute(
|
||||
"UPDATE remote_machines SET spki_pins = ?1, updated_at = ?2 WHERE id = ?3",
|
||||
rusqlite::params![pins_json, now, id],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ mod ssh;
|
|||
mod agents;
|
||||
mod metrics;
|
||||
mod anchors;
|
||||
mod machines;
|
||||
|
||||
pub use sessions::Session;
|
||||
pub use layout::LayoutState;
|
||||
|
|
@ -15,6 +16,7 @@ pub use ssh::SshSession;
|
|||
pub use agents::{AgentMessageRecord, ProjectAgentState};
|
||||
pub use metrics::SessionMetric;
|
||||
pub use anchors::SessionAnchorRecord;
|
||||
pub use machines::RemoteMachineRecord;
|
||||
|
||||
use rusqlite::Connection;
|
||||
use std::path::PathBuf;
|
||||
|
|
@ -166,7 +168,18 @@ impl SessionDb {
|
|||
created_at INTEGER NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_session_anchors_project
|
||||
ON session_anchors(project_id);"
|
||||
ON session_anchors(project_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS remote_machines (
|
||||
id TEXT PRIMARY KEY,
|
||||
label TEXT NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
token TEXT NOT NULL,
|
||||
auto_connect INTEGER NOT NULL DEFAULT 0,
|
||||
spki_pins TEXT NOT NULL DEFAULT '[]',
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
);"
|
||||
).map_err(|e| format!("Migration (v3 tables) failed: {e}"))?;
|
||||
|
||||
Ok(())
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue