Identity IPC commands + frontend identity management UI

New Tauri commands: list_identities, create_identity, switch_identity,
delete_identity, import_identity_key, get_active_identity.

Settings UI: Identities section with list (active indicator, switch/delete
buttons), Create New Identity lightbox, Import Identity Key lightbox.
Export/Import buttons as placeholders for Phase 2/3.

Identity switch does hot-swap: tears down old Node, starts new one with
all background tasks, swaps AppNode RwLock. Frontend reloads after switch.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Scott Reimers 2026-03-31 19:08:21 -04:00
parent 75ce965b63
commit 18a40756d8
3 changed files with 250 additions and 1 deletions

View file

@ -2034,6 +2034,118 @@ fn hex_to_postid(hex_str: &str) -> Result<itsgoin_core::types::PostId, String> {
// --- App setup ---
// --- Identity management IPC commands ---
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct IdentityInfoDto {
node_id: String,
display_name: String,
created_at: u64,
last_used_at: u64,
is_active: bool,
}
#[tauri::command]
async fn list_identities(state: State<'_, AppIdentity>) -> Result<Vec<IdentityInfoDto>, String> {
let mgr = state.lock().await;
let active_id = mgr.active_id();
let identities = mgr.list_identities().map_err(|e| e.to_string())?;
Ok(identities.into_iter().map(|i| IdentityInfoDto {
is_active: active_id.map_or(false, |a| a == i.node_id),
node_id: i.node_id_hex,
display_name: i.display_name,
created_at: i.created_at,
last_used_at: i.last_used_at,
}).collect())
}
#[tauri::command]
async fn create_identity(state: State<'_, AppIdentity>, name: String) -> Result<String, String> {
let mgr = state.lock().await;
let node_id = mgr.create_identity(&name).map_err(|e| e.to_string())?;
Ok(hex::encode(node_id))
}
#[tauri::command]
async fn switch_identity(
node_state: State<'_, AppNode>,
id_state: State<'_, AppIdentity>,
node_id_hex: String,
) -> Result<String, String> {
let nid_bytes = hex::decode(&node_id_hex).map_err(|e| e.to_string())?;
let nid: NodeId = nid_bytes.try_into().map_err(|_| "Invalid node ID".to_string())?;
let mut mgr = id_state.lock().await;
let new_node = mgr.switch_identity(&nid).await.map_err(|e| e.to_string())?;
// Start background tasks on the new node
new_node.start_accept_loop();
new_node.start_pull_cycle(300);
new_node.start_diff_cycle(120);
new_node.start_rebalance_cycle(600);
new_node.start_growth_loop();
new_node.start_recovery_loop();
new_node.start_social_checkin_cycle(3600);
new_node.start_anchor_register_cycle(600);
new_node.start_upnp_renewal_cycle();
new_node.start_upnp_tcp_renewal_cycle();
new_node.start_http_server();
new_node.start_bootstrap_connectivity_check();
new_node.start_replication_cycle(600);
let cache_max_bytes: u64 = {
let storage = new_node.storage.get().await;
storage.get_setting("cache_size_bytes")
.ok()
.flatten()
.and_then(|s| s.parse().ok())
.unwrap_or(1_073_741_824u64)
};
Node::start_eviction_cycle(Arc::clone(&new_node), 300, cache_max_bytes);
// Hot-swap the active node
{
let mut current = node_state.write().await;
*current = new_node;
}
Ok(format!("Switched to {}", &node_id_hex[..12]))
}
#[tauri::command]
async fn delete_identity(state: State<'_, AppIdentity>, node_id_hex: String) -> Result<String, String> {
let nid_bytes = hex::decode(&node_id_hex).map_err(|e| e.to_string())?;
let nid: NodeId = nid_bytes.try_into().map_err(|_| "Invalid node ID".to_string())?;
let mgr = state.lock().await;
mgr.delete_identity(&nid).map_err(|e| e.to_string())?;
Ok("Identity deleted".to_string())
}
#[tauri::command]
async fn import_identity_key(state: State<'_, AppIdentity>, key_hex: String, name: String) -> Result<String, String> {
let mgr = state.lock().await;
let node_id = mgr.import_identity_from_key(&key_hex, &name).map_err(|e| e.to_string())?;
Ok(hex::encode(node_id))
}
#[tauri::command]
async fn get_active_identity(state: State<'_, AppIdentity>) -> Result<Option<IdentityInfoDto>, String> {
let mgr = state.lock().await;
let active_id = mgr.active_id();
if active_id.is_none() {
return Ok(None);
}
let identities = mgr.list_identities().map_err(|e| e.to_string())?;
Ok(identities.into_iter().find(|i| Some(i.node_id) == active_id).map(|i| IdentityInfoDto {
is_active: true,
node_id: i.node_id_hex,
display_name: i.display_name,
created_at: i.created_at,
last_used_at: i.last_used_at,
}))
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tracing_subscriber::fmt()
@ -2216,6 +2328,12 @@ pub fn run() {
get_badge_counts,
get_last_read_message,
generate_share_link,
list_identities,
create_identity,
switch_identity,
delete_identity,
import_identity_key,
get_active_identity,
])
.build(tauri::generate_context!())
.expect("error while building tauri application")