Backend - regenerate_identity command stops the daemon, deletes priv_key.bin, leaves the user to restart for a fresh identity; falls back to the canonical XDG path when sidecar.key_path() isn't populated yet - tauri.conf.json ships the polkit policy via deb.files mapping; src-tauri/packaging/polkit/tech.threefold.mycellium-ui.policy declares the spawn action with auth_admin_keep so the dialog appears once per session Frontend - config store persists SidecarConfig (peers, tunName, noTun) through tauri-plugin-store; App.vue reads it and forwards to start_daemon, replacing the hard-coded defaults - Settings view: daemon-config form, identity panel with the destructive regenerate button, sidecar log viewer, About - README rewritten end-to-end: HTTP-loopback architecture, polkit install path, build commands, verification matrix, and a honest "known limitations" section
299 lines
9.5 KiB
Rust
299 lines
9.5 KiB
Rust
use crate::api::admin::NodeInfo;
|
|
use crate::api::messages::{
|
|
IncomingMessage, MessageDestination, MessageStatus, PushMessageBody, PushMessageReceipt,
|
|
};
|
|
use crate::api::peers::{AggregatedStats, PeerInfo};
|
|
use crate::api::pubkey::PubkeyLookup;
|
|
use crate::api::routes::RoutesSnapshot;
|
|
use crate::api::topics::DefaultAction;
|
|
use crate::api::MyceliumClient;
|
|
use crate::error::{AppError, AppResult};
|
|
use crate::sidecar::SidecarConfig;
|
|
use crate::state::AppState;
|
|
use serde::Serialize;
|
|
use tauri::{AppHandle, State};
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct DaemonStatus {
|
|
pub running: bool,
|
|
pub api_url: Option<String>,
|
|
pub key_path: Option<String>,
|
|
pub config_path: Option<String>,
|
|
}
|
|
|
|
fn snapshot(state: &AppState) -> DaemonStatus {
|
|
let sc = &state.sidecar;
|
|
DaemonStatus {
|
|
running: sc.is_running(),
|
|
api_url: sc.client().map(|c| c.base_url().to_string()),
|
|
key_path: sc.key_path().map(|p| p.display().to_string()),
|
|
config_path: sc.config_path().map(|p| p.display().to_string()),
|
|
}
|
|
}
|
|
|
|
fn require_client(state: &AppState) -> AppResult<MyceliumClient> {
|
|
state.sidecar.client().ok_or(AppError::DaemonNotRunning)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub fn daemon_status(state: State<'_, AppState>) -> DaemonStatus {
|
|
snapshot(&state)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn start_daemon(
|
|
app: AppHandle,
|
|
state: State<'_, AppState>,
|
|
config: Option<SidecarConfig>,
|
|
) -> AppResult<DaemonStatus> {
|
|
let cfg = config.unwrap_or_default();
|
|
state.sidecar.start(&app, &cfg).await?;
|
|
state
|
|
.poller
|
|
.start(app.clone(), std::sync::Arc::clone(&state.sidecar));
|
|
Ok(snapshot(&state))
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn stop_daemon(state: State<'_, AppState>) -> AppResult<DaemonStatus> {
|
|
state.poller.stop();
|
|
state.sidecar.stop().await;
|
|
Ok(snapshot(&state))
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn node_info(state: State<'_, AppState>) -> AppResult<NodeInfo> {
|
|
let client = require_client(&state)?;
|
|
client.node_info().await
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub fn sidecar_logs(state: State<'_, AppState>) -> Vec<String> {
|
|
state.sidecar.logs_snapshot()
|
|
}
|
|
|
|
// ─── Peers ───────────────────────────────────────────────────────────────────
|
|
|
|
#[tauri::command]
|
|
pub async fn peers_list(state: State<'_, AppState>) -> AppResult<Vec<PeerInfo>> {
|
|
require_client(&state)?.list_peers().await
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn peer_add(state: State<'_, AppState>, endpoint: String) -> AppResult<()> {
|
|
if endpoint.trim().is_empty() {
|
|
return Err(AppError::BadInput("endpoint must not be empty".into()));
|
|
}
|
|
require_client(&state)?.add_peer(endpoint.trim()).await
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn peer_remove(state: State<'_, AppState>, endpoint: String) -> AppResult<()> {
|
|
require_client(&state)?.remove_peer(&endpoint).await
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn peers_stats(state: State<'_, AppState>) -> AppResult<AggregatedStats> {
|
|
let peers = require_client(&state)?.list_peers().await?;
|
|
Ok(crate::api::peers::aggregate(&peers))
|
|
}
|
|
|
|
// ─── Routes ──────────────────────────────────────────────────────────────────
|
|
|
|
#[tauri::command]
|
|
pub async fn routes_snapshot(state: State<'_, AppState>) -> AppResult<RoutesSnapshot> {
|
|
require_client(&state)?.routes_snapshot().await
|
|
}
|
|
|
|
// ─── Messages ────────────────────────────────────────────────────────────────
|
|
|
|
#[tauri::command]
|
|
pub async fn send_message(
|
|
state: State<'_, AppState>,
|
|
destination: MessageDestination,
|
|
topic_b64: String,
|
|
payload_b64: String,
|
|
) -> AppResult<PushMessageReceipt> {
|
|
let body = PushMessageBody {
|
|
dst: destination,
|
|
topic: topic_b64,
|
|
payload: payload_b64,
|
|
};
|
|
require_client(&state)?.send_message(&body).await
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn reply_message(
|
|
state: State<'_, AppState>,
|
|
id: String,
|
|
destination: MessageDestination,
|
|
topic_b64: String,
|
|
payload_b64: String,
|
|
) -> AppResult<PushMessageReceipt> {
|
|
let body = PushMessageBody {
|
|
dst: destination,
|
|
topic: topic_b64,
|
|
payload: payload_b64,
|
|
};
|
|
require_client(&state)?.reply_message(&id, &body).await
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn message_status(state: State<'_, AppState>, id: String) -> AppResult<MessageStatus> {
|
|
require_client(&state)?.message_status(&id).await
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub fn inbox_messages(state: State<'_, AppState>) -> Vec<IncomingMessage> {
|
|
state.poller.inbox_snapshot()
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub fn inbox_clear(state: State<'_, AppState>) {
|
|
state.poller.clear_inbox();
|
|
}
|
|
|
|
// ─── Topics ──────────────────────────────────────────────────────────────────
|
|
|
|
#[tauri::command]
|
|
pub async fn topics_default_get(state: State<'_, AppState>) -> AppResult<DefaultAction> {
|
|
require_client(&state)?.topics_default_get().await
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn topics_default_set(state: State<'_, AppState>, accept: bool) -> AppResult<()> {
|
|
require_client(&state)?
|
|
.topics_default_set(&DefaultAction { accept })
|
|
.await
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn topics_list(state: State<'_, AppState>) -> AppResult<Vec<String>> {
|
|
require_client(&state)?.topics_list().await
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn topic_add(state: State<'_, AppState>, topic_b64: String) -> AppResult<()> {
|
|
if topic_b64.trim().is_empty() {
|
|
return Err(AppError::BadInput("topic must not be empty".into()));
|
|
}
|
|
require_client(&state)?.topic_add(topic_b64.trim()).await
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn topic_remove(state: State<'_, AppState>, topic_b64: String) -> AppResult<()> {
|
|
require_client(&state)?.topic_remove(&topic_b64).await
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn topic_sources_list(
|
|
state: State<'_, AppState>,
|
|
topic_b64: String,
|
|
) -> AppResult<Vec<String>> {
|
|
require_client(&state)?.topic_sources_list(&topic_b64).await
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn topic_source_add(
|
|
state: State<'_, AppState>,
|
|
topic_b64: String,
|
|
subnet: String,
|
|
) -> AppResult<()> {
|
|
require_client(&state)?
|
|
.topic_source_add(&topic_b64, &subnet)
|
|
.await
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn topic_source_remove(
|
|
state: State<'_, AppState>,
|
|
topic_b64: String,
|
|
subnet: String,
|
|
) -> AppResult<()> {
|
|
require_client(&state)?
|
|
.topic_source_remove(&topic_b64, &subnet)
|
|
.await
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn topic_forward_get(
|
|
state: State<'_, AppState>,
|
|
topic_b64: String,
|
|
) -> AppResult<Option<String>> {
|
|
require_client(&state)?.topic_forward_get(&topic_b64).await
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn topic_forward_set(
|
|
state: State<'_, AppState>,
|
|
topic_b64: String,
|
|
socket_path: String,
|
|
) -> AppResult<()> {
|
|
require_client(&state)?
|
|
.topic_forward_set(&topic_b64, &socket_path)
|
|
.await
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn topic_forward_remove(state: State<'_, AppState>, topic_b64: String) -> AppResult<()> {
|
|
require_client(&state)?
|
|
.topic_forward_remove(&topic_b64)
|
|
.await
|
|
}
|
|
|
|
// ─── Pubkey ──────────────────────────────────────────────────────────────────
|
|
|
|
#[tauri::command]
|
|
pub async fn lookup_pubkey(state: State<'_, AppState>, ip: String) -> AppResult<PubkeyLookup> {
|
|
require_client(&state)?.lookup_pubkey(&ip).await
|
|
}
|
|
|
|
// ─── Identity ────────────────────────────────────────────────────────────────
|
|
|
|
/// Stops the daemon (if running), removes the saved private key file, and
|
|
/// returns the daemon to the idle state. The caller restarts the daemon
|
|
/// to provoke regeneration.
|
|
#[tauri::command]
|
|
pub async fn regenerate_identity(state: State<'_, AppState>) -> AppResult<()> {
|
|
state.poller.stop();
|
|
let key_path = state.sidecar.key_path();
|
|
state.sidecar.stop().await;
|
|
|
|
if let Some(path) = key_path {
|
|
if path.exists() {
|
|
std::fs::remove_file(&path).map_err(AppError::from)?;
|
|
}
|
|
} else {
|
|
// Sidecar never started; resolve the canonical path via app_data_dir.
|
|
if let Some(p) = default_key_path() {
|
|
if p.exists() {
|
|
std::fs::remove_file(&p).map_err(AppError::from)?;
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn default_key_path() -> Option<std::path::PathBuf> {
|
|
// Best-effort: fall back to the same XDG location the sidecar uses.
|
|
dirs_like_app_data().ok().map(|d| d.join("priv_key.bin"))
|
|
}
|
|
|
|
fn dirs_like_app_data() -> std::io::Result<std::path::PathBuf> {
|
|
// We can't reach the AppHandle here, so we mirror Tauri's path:
|
|
// $XDG_DATA_HOME/<identifier>/ or $HOME/.local/share/<identifier>/.
|
|
let identifier = "tech.threefold.mycellium-ui";
|
|
if let Ok(d) = std::env::var("XDG_DATA_HOME") {
|
|
return Ok(std::path::PathBuf::from(d).join(identifier));
|
|
}
|
|
if let Ok(home) = std::env::var("HOME") {
|
|
return Ok(std::path::PathBuf::from(home)
|
|
.join(".local/share")
|
|
.join(identifier));
|
|
}
|
|
Err(std::io::Error::new(
|
|
std::io::ErrorKind::NotFound,
|
|
"no app data dir",
|
|
))
|
|
}
|