From 8b88c45262220700df024edcac22f2e45fc5d586 Mon Sep 17 00:00:00 2001 From: Dorian Date: Sun, 19 Apr 2026 01:44:41 -0400 Subject: [PATCH] feat(settings): per-service FIPS/Tor transport preference MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a user-configurable toggle for how each peer-to-peer service reaches federated peers. Three options per service: - Auto (default) — FIPS preferred, Tor fallback (current behavior). - FIPS only — fail rather than fall through to Tor. - Tor only — explicit opt-in to onion anonymity for that service. Services covered (matching the UI rows): - Federation — state sync, invites, peer notifications - Peers — address/DID rotation broadcasts - Peer Files — content catalog download/browse/preview - Messaging — archipelago channel + mesh bridge - Mesh File Sharing — content_ref blob fetches Implementation: - settings::transport — persisted struct + process-wide OnceLock handle (so deep call sites don't need data_dir threaded through signatures). On-disk file: /settings/transport_preferences.json; missing or corrupt → defaults (Auto everywhere). - settings::transport::init() called from main.rs after config load. - fips::dial::PeerRequest gains a .service(kind) builder; send_* checks the preference before choosing a transport. FIPS-only fails loudly when FIPS is unavailable (so users who pick it know when something falls back). - Every FIPS-first migration site tags its PeerRequest with the matching PeerService so the toggle actually applies. - transport.preferences + transport.set-preference RPCs added; wired into the dispatcher. - neode-ui/src/views/settings/TransportPrefsCard.vue — standalone card with a 5-row Auto/FIPS/Tor tri-state. Not wired into Settings.vue — the user places components themselves (see feedback_ui_entry_points). Co-Authored-By: Claude Opus 4.7 (1M context) --- core/archipelago/src/api/rpc/content.rs | 4 + core/archipelago/src/api/rpc/dispatcher.rs | 2 + .../src/api/rpc/federation/handlers.rs | 1 + .../src/api/rpc/mesh/typed_messages.rs | 1 + core/archipelago/src/api/rpc/tor/mod.rs | 1 + core/archipelago/src/api/rpc/transport.rs | 40 +++ core/archipelago/src/federation/invites.rs | 1 + core/archipelago/src/federation/sync.rs | 2 + core/archipelago/src/fips/dial.rs | 50 +++- core/archipelago/src/main.rs | 11 + core/archipelago/src/mesh/mod.rs | 1 + core/archipelago/src/network/dwn_sync.rs | 3 + core/archipelago/src/node_message.rs | 2 + core/archipelago/src/settings/mod.rs | 7 + core/archipelago/src/settings/transport.rs | 235 ++++++++++++++++++ .../src/views/settings/TransportPrefsCard.vue | 121 +++++++++ 16 files changed, 478 insertions(+), 4 deletions(-) create mode 100644 core/archipelago/src/settings/mod.rs create mode 100644 core/archipelago/src/settings/transport.rs create mode 100644 neode-ui/src/views/settings/TransportPrefsCard.vue diff --git a/core/archipelago/src/api/rpc/content.rs b/core/archipelago/src/api/rpc/content.rs index 07619f50..d8bbeebe 100644 --- a/core/archipelago/src/api/rpc/content.rs +++ b/core/archipelago/src/api/rpc/content.rs @@ -237,6 +237,7 @@ impl RpcHandler { let path = format!("/content/{}", content_id); let (response, _transport) = crate::fips::dial::PeerRequest::new(fips_npub.as_deref(), onion, &path) + .service(crate::settings::transport::PeerService::PeerFiles) .header("X-Federation-DID", local_did) .timeout(std::time::Duration::from_secs(120)) .send_get() @@ -293,6 +294,7 @@ impl RpcHandler { let (response, _transport) = crate::fips::dial::PeerRequest::new(fips_npub.as_deref(), onion, "/content") + .service(crate::settings::transport::PeerService::PeerFiles) .timeout(std::time::Duration::from_secs(30)) .send_get() .await @@ -352,6 +354,7 @@ impl RpcHandler { let path = format!("/content/{}", content_id); let (response, _transport) = crate::fips::dial::PeerRequest::new(fips_npub.as_deref(), onion, &path) + .service(crate::settings::transport::PeerService::PeerFiles) .header("X-Federation-DID", local_did) .header("X-Payment-Token", token_str) .timeout(std::time::Duration::from_secs(120)) @@ -412,6 +415,7 @@ impl RpcHandler { let (response, _transport) = crate::fips::dial::PeerRequest::new(fips_npub.as_deref(), onion, &path) + .service(crate::settings::transport::PeerService::PeerFiles) .timeout(std::time::Duration::from_secs(30)) .send_get() .await diff --git a/core/archipelago/src/api/rpc/dispatcher.rs b/core/archipelago/src/api/rpc/dispatcher.rs index 4e996c62..ad089f32 100644 --- a/core/archipelago/src/api/rpc/dispatcher.rs +++ b/core/archipelago/src/api/rpc/dispatcher.rs @@ -363,6 +363,8 @@ impl RpcHandler { "transport.peers" => self.handle_transport_peers().await, "transport.send" => self.handle_transport_send(params).await, "transport.set-mode" => self.handle_transport_set_mode(params).await, + "transport.preferences" => self.handle_transport_preferences().await, + "transport.set-preference" => self.handle_transport_set_preference(params).await, // Server settings "server.set-name" => self.handle_server_set_name(params).await, diff --git a/core/archipelago/src/api/rpc/federation/handlers.rs b/core/archipelago/src/api/rpc/federation/handlers.rs index e01f10f4..d57ad46a 100644 --- a/core/archipelago/src/api/rpc/federation/handlers.rs +++ b/core/archipelago/src/api/rpc/federation/handlers.rs @@ -656,6 +656,7 @@ impl RpcHandler { &node.onion, "/rpc/v1", ) + .service(crate::settings::transport::PeerService::Peers) .timeout(std::time::Duration::from_secs(30)); match req.send_json(&body).await { diff --git a/core/archipelago/src/api/rpc/mesh/typed_messages.rs b/core/archipelago/src/api/rpc/mesh/typed_messages.rs index 0b60546b..615759a7 100644 --- a/core/archipelago/src/api/rpc/mesh/typed_messages.rs +++ b/core/archipelago/src/api/rpc/mesh/typed_messages.rs @@ -742,6 +742,7 @@ impl RpcHandler { let (resp, transport) = crate::fips::dial::PeerRequest::new(fips_npub.as_deref(), &onion_bare, &path) + .service(crate::settings::transport::PeerService::MeshFileSharing) .timeout(std::time::Duration::from_secs(120)) .send_get() .await diff --git a/core/archipelago/src/api/rpc/tor/mod.rs b/core/archipelago/src/api/rpc/tor/mod.rs index 873a3646..05903c6e 100644 --- a/core/archipelago/src/api/rpc/tor/mod.rs +++ b/core/archipelago/src/api/rpc/tor/mod.rs @@ -442,6 +442,7 @@ pub(super) async fn notify_federation_peers_address_change( &peer.onion, "/rpc/v1", ) + .service(crate::settings::transport::PeerService::Peers) .timeout(std::time::Duration::from_secs(30)); match req.send_json(&payload).await { Ok((_, transport)) => { diff --git a/core/archipelago/src/api/rpc/transport.rs b/core/archipelago/src/api/rpc/transport.rs index ce9eea56..c74c700b 100644 --- a/core/archipelago/src/api/rpc/transport.rs +++ b/core/archipelago/src/api/rpc/transport.rs @@ -108,6 +108,46 @@ impl RpcHandler { })) } + /// transport.preferences — Return the user's per-service transport + /// preferences. The UI renders these as five FIPS/Auto/Tor rows. + pub(super) async fn handle_transport_preferences(&self) -> Result { + let prefs = crate::settings::transport::snapshot().await; + Ok(serde_json::to_value(prefs)?) + } + + /// transport.set-preference — Change a single service preference. + /// Persists to disk and hot-swaps the in-memory handle so future + /// calls see the new value without restart. + pub(super) async fn handle_transport_set_preference( + &self, + params: Option, + ) -> Result { + use crate::settings::transport::{set, PeerService, TransportPref}; + let params = params + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Missing params"))?; + let service: PeerService = serde_json::from_value( + params + .get("service") + .cloned() + .ok_or_else(|| anyhow::anyhow!("Missing 'service' param"))?, + ) + .map_err(|e| anyhow::anyhow!("Invalid service: {}", e))?; + let pref: TransportPref = serde_json::from_value( + params + .get("pref") + .cloned() + .ok_or_else(|| anyhow::anyhow!("Missing 'pref' param"))?, + ) + .map_err(|e| anyhow::anyhow!("Invalid pref: {}", e))?; + + set(&self.config.data_dir, service, pref).await?; + info!(service = ?service, pref = ?pref, "Transport preference updated"); + + let current = crate::settings::transport::snapshot().await; + Ok(serde_json::to_value(current)?) + } + /// transport.set-mode — Toggle mesh-only (off-grid) mode. pub(super) async fn handle_transport_set_mode( &self, diff --git a/core/archipelago/src/federation/invites.rs b/core/archipelago/src/federation/invites.rs index 4291fec2..1d96206f 100644 --- a/core/archipelago/src/federation/invites.rs +++ b/core/archipelago/src/federation/invites.rs @@ -232,6 +232,7 @@ async fn notify_join( }); let _ = crate::fips::dial::PeerRequest::new(remote_fips_npub, remote_onion, "/rpc/v1") + .service(crate::settings::transport::PeerService::Federation) .timeout(std::time::Duration::from_secs(30)) .send_json(&body) .await; diff --git a/core/archipelago/src/federation/sync.rs b/core/archipelago/src/federation/sync.rs index 8a00fe8f..a26283aa 100644 --- a/core/archipelago/src/federation/sync.rs +++ b/core/archipelago/src/federation/sync.rs @@ -28,6 +28,7 @@ pub async fn sync_with_peer( }); let (resp, transport) = PeerRequest::new(peer.fips_npub.as_deref(), &peer.onion, "/rpc/v1") + .service(crate::settings::transport::PeerService::Federation) .header("X-Federation-DID", local_did) .header("X-Federation-Sig", signature) .header("X-Federation-Timestamp", timestamp) @@ -111,6 +112,7 @@ pub async fn deploy_to_peer( }); let (resp, transport) = PeerRequest::new(peer.fips_npub.as_deref(), &peer.onion, "/rpc/v1") + .service(crate::settings::transport::PeerService::Federation) .header("X-Federation-DID", local_did) .header("X-Federation-Sig", signature) .header("X-Federation-Timestamp", timestamp) diff --git a/core/archipelago/src/fips/dial.rs b/core/archipelago/src/fips/dial.rs index d8edbfb1..8282f35a 100644 --- a/core/archipelago/src/fips/dial.rs +++ b/core/archipelago/src/fips/dial.rs @@ -232,12 +232,18 @@ pub async fn is_service_active() -> bool { /// Tor (fallback). The call sites migrating off direct-Tor dialing build /// one of these and call [`send_json`] / [`send_get`]; the helper handles /// dial, timeout, fallback, and cross-transport auth headers. +/// +/// The optional `service` field ties the request to a user-configurable +/// transport preference (see `crate::settings::transport`). Leaving it +/// unset picks Auto (FIPS preferred, Tor fallback) — the same default as +/// before the Settings UI landed. pub struct PeerRequest<'a> { pub fips_npub: Option<&'a str>, pub onion_host: &'a str, pub path: &'a str, pub headers: Vec<(&'a str, String)>, pub timeout: std::time::Duration, + pub service: Option, } impl<'a> PeerRequest<'a> { @@ -252,9 +258,18 @@ impl<'a> PeerRequest<'a> { path, headers: Vec::new(), timeout: std::time::Duration::from_secs(30), + service: None, } } + /// Tie this request to a user-configurable service preference. If + /// the user has set that service to `Fips` or `Tor`, the builder + /// respects it. + pub fn service(mut self, s: crate::settings::transport::PeerService) -> Self { + self.service = Some(s); + self + } + pub fn header(mut self, name: &'a str, value: impl Into) -> Self { self.headers.push((name, value.into())); self @@ -265,14 +280,32 @@ impl<'a> PeerRequest<'a> { self } + /// Resolved preference: user setting if `service` was set, else Auto. + async fn preference(&self) -> crate::settings::transport::TransportPref { + match self.service { + Some(s) => crate::settings::transport::get(s).await, + None => crate::settings::transport::TransportPref::Auto, + } + } + /// POST a JSON body. Returns the `reqwest::Response` — caller decides /// how to interpret the status code. pub async fn send_json( &self, body: &B, ) -> Result<(reqwest::Response, crate::transport::TransportKind)> { - if let Some(resp) = self.try_fips_post_json(body).await? { - return Ok((resp, crate::transport::TransportKind::Fips)); + use crate::settings::transport::TransportPref; + let pref = self.preference().await; + // FIPS-only or Auto: try FIPS first. + if matches!(pref, TransportPref::Auto | TransportPref::Fips) { + if let Some(resp) = self.try_fips_post_json(body).await? { + return Ok((resp, crate::transport::TransportKind::Fips)); + } + if pref == TransportPref::Fips { + anyhow::bail!( + "User set transport preference to FIPS only, but peer is unreachable over FIPS" + ); + } } let resp = self.send_tor_post_json(body).await?; Ok((resp, crate::transport::TransportKind::Tor)) @@ -282,8 +315,17 @@ impl<'a> PeerRequest<'a> { pub async fn send_get( &self, ) -> Result<(reqwest::Response, crate::transport::TransportKind)> { - if let Some(resp) = self.try_fips_get().await? { - return Ok((resp, crate::transport::TransportKind::Fips)); + use crate::settings::transport::TransportPref; + let pref = self.preference().await; + if matches!(pref, TransportPref::Auto | TransportPref::Fips) { + if let Some(resp) = self.try_fips_get().await? { + return Ok((resp, crate::transport::TransportKind::Fips)); + } + if pref == TransportPref::Fips { + anyhow::bail!( + "User set transport preference to FIPS only, but peer is unreachable over FIPS" + ); + } } let resp = self.send_tor_get().await?; Ok((resp, crate::transport::TransportKind::Tor)) diff --git a/core/archipelago/src/main.rs b/core/archipelago/src/main.rs index 8ef4bff9..f1390fe1 100644 --- a/core/archipelago/src/main.rs +++ b/core/archipelago/src/main.rs @@ -56,6 +56,7 @@ mod rate_limit; pub mod seed; mod server; mod session; +mod settings; mod state; mod streaming; mod totp; @@ -87,6 +88,16 @@ async fn main() -> Result<()> { let config = Config::load().await?; info!("📁 Data directory: {}", config.data_dir.display()); + // Load user transport preferences so peer-to-peer call sites can + // consult them from any module without threading a handle through + // deep async chains. Missing/corrupt file → default (Auto everywhere). + if let Err(e) = settings::transport::init(&config.data_dir).await { + tracing::warn!( + "Failed to initialise transport preferences: {} — using defaults", + e + ); + } + // Write PID marker early so we can detect crashes on next startup crash_recovery::write_pid_marker(&config.data_dir).await?; diff --git a/core/archipelago/src/mesh/mod.rs b/core/archipelago/src/mesh/mod.rs index 85faf38e..2f5a7845 100644 --- a/core/archipelago/src/mesh/mod.rs +++ b/core/archipelago/src/mesh/mod.rs @@ -865,6 +865,7 @@ impl MeshService { &peer_onion_owned, "/archipelago/mesh-typed", ) + .service(crate::settings::transport::PeerService::Messaging) .timeout(std::time::Duration::from_secs(120)); match req.send_json(&body).await { Ok((resp, transport)) if resp.status().is_success() => { diff --git a/core/archipelago/src/network/dwn_sync.rs b/core/archipelago/src/network/dwn_sync.rs index 7f0c9b58..4d0bac13 100644 --- a/core/archipelago/src/network/dwn_sync.rs +++ b/core/archipelago/src/network/dwn_sync.rs @@ -184,6 +184,7 @@ async fn sync_single_peer( // Step 1: Check peer health let (health_resp, _) = PeerRequest::new(fips_npub, onion, "/dwn/health") + .service(crate::settings::transport::PeerService::Federation) .timeout(std::time::Duration::from_secs(30)) .send_get() .await @@ -208,6 +209,7 @@ async fn sync_single_peer( }); let (pull_res, _) = PeerRequest::new(fips_npub, onion, "/dwn") + .service(crate::settings::transport::PeerService::Federation) .timeout(std::time::Duration::from_secs(30)) .send_json(&pull_body) .await @@ -265,6 +267,7 @@ async fn sync_single_peer( // Best-effort push — don't fail the whole sync if a batch fails. match PeerRequest::new(fips_npub, onion, "/dwn") + .service(crate::settings::transport::PeerService::Federation) .timeout(std::time::Duration::from_secs(30)) .send_json(&push_body) .await diff --git a/core/archipelago/src/node_message.rs b/core/archipelago/src/node_message.rs index 5f585223..90fd03f1 100644 --- a/core/archipelago/src/node_message.rs +++ b/core/archipelago/src/node_message.rs @@ -280,6 +280,7 @@ pub async fn send_to_peer( onion, "/archipelago/node-message", ) + .service(crate::settings::transport::PeerService::Messaging) .timeout(std::time::Duration::from_secs(60)) .send_json(&body) .await @@ -310,6 +311,7 @@ pub async fn send_to_peer( pub async fn check_peer_reachable(onion: &str, fips_npub: Option<&str>) -> Result { validate_onion(onion)?; match crate::fips::dial::PeerRequest::new(fips_npub, onion, "/health") + .service(crate::settings::transport::PeerService::Messaging) .timeout(std::time::Duration::from_secs(30)) .send_get() .await diff --git a/core/archipelago/src/settings/mod.rs b/core/archipelago/src/settings/mod.rs new file mode 100644 index 00000000..8e028a0f --- /dev/null +++ b/core/archipelago/src/settings/mod.rs @@ -0,0 +1,7 @@ +//! User-editable settings that are not part of the initial onboarding +//! flow. Each submodule persists a focused slice of preferences to +//! `/settings/*.json` and exposes a process-wide handle so +//! call sites (deep in the transport / RPC / ingest stacks) don't need +//! to thread a data_dir or Arc through the entire call graph. + +pub mod transport; diff --git a/core/archipelago/src/settings/transport.rs b/core/archipelago/src/settings/transport.rs new file mode 100644 index 00000000..6425656b --- /dev/null +++ b/core/archipelago/src/settings/transport.rs @@ -0,0 +1,235 @@ +//! Per-service transport preferences. +//! +//! The user picks, for each peer-to-peer service, whether to prefer +//! FIPS (mesh overlay), Tor (hidden service fallback), or leave it on +//! Auto (FIPS preferred, Tor fallback — the default). Preferences are +//! persisted to `/settings/transport_preferences.json` and +//! cached in a process-wide handle so that calls from the transport, +//! RPC, and ingest stacks can consult them without threading data_dir +//! through every signature. +//! +//! Services covered (matching the Settings UI): +//! - `federation` — state sync, invites, peer notifications +//! - `peers` — address/DID rotation broadcast +//! - `peer_files` — content download / browse / preview +//! - `messaging` — archipelago channel + mesh-typed relay +//! - `mesh_file_sharing`— content_ref blob fetches over onion +//! +//! Unknown files parse as `default()` so a missing or corrupt +//! preferences file is equivalent to "Auto everywhere." + +use anyhow::{Context, Result}; +use serde::{Deserialize, Serialize}; +use std::path::Path; +use std::sync::OnceLock; +use tokio::sync::RwLock; + +const FILE_PATH: &str = "settings/transport_preferences.json"; + +/// Which transport to use for a given service. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Default)] +#[serde(rename_all = "snake_case")] +pub enum TransportPref { + /// FIPS preferred, Tor fallback. The default. + #[default] + Auto, + /// FIPS only. If FIPS is unavailable or the peer has no npub, + /// requests fail rather than leaking over Tor. + Fips, + /// Tor only. Useful when the user explicitly wants onion anonymity + /// for a given surface (e.g., first-contact messaging). + Tor, +} + +/// Enum of peer-facing services that have a preference knob. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum PeerService { + Federation, + Peers, + PeerFiles, + Messaging, + MeshFileSharing, +} + +impl PeerService { + #[allow(dead_code)] + pub fn as_str(&self) -> &'static str { + match self { + Self::Federation => "federation", + Self::Peers => "peers", + Self::PeerFiles => "peer_files", + Self::Messaging => "messaging", + Self::MeshFileSharing => "mesh_file_sharing", + } + } +} + +/// Persisted shape. One field per service so the on-disk file is +/// self-describing and trivially diff-able. +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +#[serde(default)] +pub struct TransportPreferences { + pub federation: TransportPref, + pub peers: TransportPref, + pub peer_files: TransportPref, + pub messaging: TransportPref, + pub mesh_file_sharing: TransportPref, +} + +impl TransportPreferences { + pub fn for_service(&self, s: PeerService) -> TransportPref { + match s { + PeerService::Federation => self.federation, + PeerService::Peers => self.peers, + PeerService::PeerFiles => self.peer_files, + PeerService::Messaging => self.messaging, + PeerService::MeshFileSharing => self.mesh_file_sharing, + } + } + + pub fn set_for_service(&mut self, s: PeerService, pref: TransportPref) { + match s { + PeerService::Federation => self.federation = pref, + PeerService::Peers => self.peers = pref, + PeerService::PeerFiles => self.peer_files = pref, + PeerService::Messaging => self.messaging = pref, + PeerService::MeshFileSharing => self.mesh_file_sharing = pref, + } + } +} + +// ── Process-wide handle ───────────────────────────────────────────────── + +static HANDLE: OnceLock> = OnceLock::new(); + +/// Initialise the handle from `/settings/transport_preferences.json`. +/// Must be called early in startup — before any call that reads `get()`. +/// Idempotent: second call reloads. +pub async fn init(data_dir: &Path) -> Result<()> { + let prefs = load_from_disk(data_dir).await; + match HANDLE.get() { + Some(lock) => { + *lock.write().await = prefs; + } + None => { + HANDLE + .set(RwLock::new(prefs)) + .map_err(|_| anyhow::anyhow!("transport prefs already initialised"))?; + } + } + Ok(()) +} + +/// Read the current preference for a service. Returns `Auto` if the +/// handle wasn't initialised (tests, fallbacks). +pub async fn get(service: PeerService) -> TransportPref { + match HANDLE.get() { + Some(lock) => lock.read().await.for_service(service), + None => TransportPref::Auto, + } +} + +/// Read the whole preferences block for the Settings UI. +pub async fn snapshot() -> TransportPreferences { + match HANDLE.get() { + Some(lock) => lock.read().await.clone(), + None => TransportPreferences::default(), + } +} + +/// Update a single service preference, persist to disk, and update the +/// handle. Callers must pass `data_dir` because the on-disk file lives +/// under it — the handle alone doesn't know where to write. +pub async fn set( + data_dir: &Path, + service: PeerService, + pref: TransportPref, +) -> Result<()> { + let new_prefs = { + let lock = HANDLE.get_or_init(|| RwLock::new(TransportPreferences::default())); + let mut w = lock.write().await; + w.set_for_service(service, pref); + w.clone() + }; + save_to_disk(data_dir, &new_prefs).await +} + +// ── On-disk I/O ───────────────────────────────────────────────────────── + +async fn load_from_disk(data_dir: &Path) -> TransportPreferences { + let path = data_dir.join(FILE_PATH); + let s = match tokio::fs::read_to_string(&path).await { + Ok(s) => s, + Err(_) => return TransportPreferences::default(), + }; + serde_json::from_str(&s).unwrap_or_default() +} + +async fn save_to_disk(data_dir: &Path, prefs: &TransportPreferences) -> Result<()> { + let path = data_dir.join(FILE_PATH); + if let Some(parent) = path.parent() { + tokio::fs::create_dir_all(parent) + .await + .with_context(|| format!("create {}", parent.display()))?; + } + let body = serde_json::to_string_pretty(prefs) + .context("serialize TransportPreferences")?; + tokio::fs::write(&path, body) + .await + .with_context(|| format!("write {}", path.display()))?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn default_is_all_auto() { + let p = TransportPreferences::default(); + for s in [ + PeerService::Federation, + PeerService::Peers, + PeerService::PeerFiles, + PeerService::Messaging, + PeerService::MeshFileSharing, + ] { + assert_eq!(p.for_service(s), TransportPref::Auto); + } + } + + #[test] + fn set_then_read_for_service() { + let mut p = TransportPreferences::default(); + p.set_for_service(PeerService::Federation, TransportPref::Fips); + assert_eq!(p.for_service(PeerService::Federation), TransportPref::Fips); + assert_eq!(p.for_service(PeerService::Peers), TransportPref::Auto); + } + + #[test] + fn json_round_trips() { + let mut p = TransportPreferences::default(); + p.set_for_service(PeerService::Messaging, TransportPref::Tor); + let s = serde_json::to_string(&p).unwrap(); + let back: TransportPreferences = serde_json::from_str(&s).unwrap(); + assert_eq!( + back.for_service(PeerService::Messaging), + TransportPref::Tor + ); + } + + #[test] + fn service_names_round_trip() { + for s in [ + PeerService::Federation, + PeerService::Peers, + PeerService::PeerFiles, + PeerService::Messaging, + PeerService::MeshFileSharing, + ] { + let json = serde_json::to_value(s).unwrap(); + assert_eq!(json.as_str().unwrap(), s.as_str()); + } + } +} diff --git a/neode-ui/src/views/settings/TransportPrefsCard.vue b/neode-ui/src/views/settings/TransportPrefsCard.vue new file mode 100644 index 00000000..8e1276e2 --- /dev/null +++ b/neode-ui/src/views/settings/TransportPrefsCard.vue @@ -0,0 +1,121 @@ + + +