diff --git a/src/trx-client/src/main.rs b/src/trx-client/src/main.rs index d34d613..df0ec28 100644 --- a/src/trx-client/src/main.rs +++ b/src/trx-client/src/main.rs @@ -189,6 +189,9 @@ async fn async_init() -> DynResult { let remote_token = cli.token.clone().or_else(|| cfg.remote.auth.token.clone()); let remote_rig_id = cli.rig_id.clone().or_else(|| cfg.remote.rig_id.clone()); + if let Ok(mut guard) = frontend_runtime.remote_active_rig_id.lock() { + *guard = remote_rig_id.clone(); + } let poll_interval_ms = cli.poll_interval_ms.unwrap_or(cfg.remote.poll_interval_ms); @@ -254,7 +257,8 @@ async fn async_init() -> DynResult { let remote_cfg = RemoteClientConfig { addr: remote_endpoint.connect_addr(), token: remote_token, - rig_id: remote_rig_id, + selected_rig_id: frontend_runtime.remote_active_rig_id.clone(), + known_rigs: frontend_runtime.remote_rigs.clone(), poll_interval: Duration::from_millis(poll_interval_ms), }; let remote_shutdown_rx = shutdown_rx.clone(); diff --git a/src/trx-client/src/remote_client.rs b/src/trx-client/src/remote_client.rs index 5a569a7..633f914 100644 --- a/src/trx-client/src/remote_client.rs +++ b/src/trx-client/src/remote_client.rs @@ -3,6 +3,7 @@ // SPDX-License-Identifier: BSD-2-Clause use std::time::Duration; +use std::{sync::Arc, sync::Mutex}; use tokio::io::{AsyncBufRead, AsyncBufReadExt, AsyncWriteExt, BufReader}; use tokio::net::TcpStream; @@ -13,7 +14,9 @@ use tracing::{info, warn}; use trx_core::rig::request::RigRequest; use trx_core::rig::state::RigState; use trx_core::{RigError, RigResult}; +use trx_frontend::RemoteRigEntry; use trx_protocol::rig_command_to_client; +use trx_protocol::types::RigEntry; use trx_protocol::{ClientCommand, ClientEnvelope, ClientResponse}; const DEFAULT_REMOTE_PORT: u16 = 4530; @@ -40,7 +43,8 @@ impl RemoteEndpoint { pub struct RemoteClientConfig { pub addr: String, pub token: Option, - pub rig_id: Option, + pub selected_rig_id: Arc>>, + pub known_rigs: Arc>>, pub poll_interval: Duration, } @@ -118,7 +122,9 @@ async fn handle_connection( continue; } last_poll = Instant::now(); - if let Err(e) = send_command(config, &mut writer, &mut reader, ClientCommand::GetState, state_tx).await { + if let Err(e) = + refresh_remote_snapshot(config, &mut writer, &mut reader, state_tx).await + { warn!("Remote poll failed: {}", e); } } @@ -187,11 +193,100 @@ async fn send_command( fn build_envelope(config: &RemoteClientConfig, cmd: ClientCommand) -> ClientEnvelope { ClientEnvelope { token: config.token.clone(), - rig_id: config.rig_id.clone(), + rig_id: selected_rig_id(config), cmd, } } +async fn refresh_remote_snapshot( + config: &RemoteClientConfig, + writer: &mut tokio::net::tcp::OwnedWriteHalf, + reader: &mut BufReader, + state_tx: &watch::Sender, +) -> RigResult<()> { + let rigs = send_get_rigs(config, writer, reader).await?; + cache_remote_rigs(config, &rigs); + if rigs.is_empty() { + return Err(RigError::communication("GetRigs returned no rigs")); + } + + let selected = selected_rig_id(config); + let fallback = &rigs[0]; + let target = selected + .as_deref() + .and_then(|id| rigs.iter().find(|entry| entry.rig_id == id)) + .unwrap_or(fallback); + + if selected.as_deref() != Some(target.rig_id.as_str()) { + set_selected_rig_id(config, Some(target.rig_id.clone())); + } + + let _ = state_tx.send(RigState::from_snapshot(target.state.clone())); + Ok(()) +} + +async fn send_get_rigs( + config: &RemoteClientConfig, + writer: &mut tokio::net::tcp::OwnedWriteHalf, + reader: &mut BufReader, +) -> RigResult> { + let envelope = build_envelope(config, ClientCommand::GetRigs); + let payload = serde_json::to_string(&envelope) + .map_err(|e| RigError::communication(format!("JSON serialize failed: {e}")))?; + + time::timeout( + IO_TIMEOUT, + writer.write_all(format!("{}\n", payload).as_bytes()), + ) + .await + .map_err(|_| RigError::communication(format!("write timed out after {:?}", IO_TIMEOUT)))? + .map_err(|e| RigError::communication(format!("write failed: {e}")))?; + time::timeout(IO_TIMEOUT, writer.flush()) + .await + .map_err(|_| RigError::communication(format!("flush timed out after {:?}", IO_TIMEOUT)))? + .map_err(|e| RigError::communication(format!("flush failed: {e}")))?; + + let line = time::timeout(IO_TIMEOUT, read_limited_line(reader, MAX_JSON_LINE_BYTES)) + .await + .map_err(|_| RigError::communication(format!("read timed out after {:?}", IO_TIMEOUT)))? + .map_err(|e| RigError::communication(format!("read failed: {e}")))?; + let line = line.ok_or_else(|| RigError::communication("connection closed by remote"))?; + + let resp: ClientResponse = serde_json::from_str(line.trim_end()) + .map_err(|e| RigError::communication(format!("invalid response: {e}")))?; + if resp.success { + return resp + .rigs + .ok_or_else(|| RigError::communication("missing rigs list in GetRigs response")); + } + + Err(RigError::communication( + resp.error.unwrap_or_else(|| "remote error".into()), + )) +} + +fn cache_remote_rigs(config: &RemoteClientConfig, rigs: &[RigEntry]) { + if let Ok(mut guard) = config.known_rigs.lock() { + *guard = rigs + .iter() + .map(|entry| RemoteRigEntry { + rig_id: entry.rig_id.clone(), + state: entry.state.clone(), + }) + .collect(); + } +} + +fn selected_rig_id(config: &RemoteClientConfig) -> Option { + config.selected_rig_id.lock().ok().and_then(|g| g.clone()) +} + +fn set_selected_rig_id(config: &RemoteClientConfig, value: Option) { + if let Ok(mut guard) = config.selected_rig_id.lock() { + *guard = value; + } +} + async fn read_limited_line( reader: &mut R, max_bytes: usize, @@ -316,6 +411,7 @@ fn parse_port(port_str: &str) -> Result { #[cfg(test)] mod tests { use super::{parse_remote_url, RemoteClientConfig, RemoteEndpoint}; + use std::sync::{Arc, Mutex}; use std::time::Duration; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; @@ -326,6 +422,7 @@ mod tests { use trx_core::rig::state::RigSnapshot; use trx_core::rig::{RigAccessMethod, RigCapabilities, RigInfo, RigStatus, RigTxStatus}; use trx_core::{RigMode, RigState}; + use trx_protocol::types::RigEntry; use trx_protocol::ClientResponse; #[test] @@ -441,11 +538,15 @@ mod tests { async fn reconnects_and_updates_state_after_drop() { let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind"); let addr = listener.local_addr().expect("local addr"); + let snapshot = sample_snapshot(); let response = serde_json::to_string(&ClientResponse { success: true, - rig_id: None, - state: Some(sample_snapshot()), - rigs: None, + rig_id: Some("server".to_string()), + state: None, + rigs: Some(vec![RigEntry { + rig_id: "default".to_string(), + state: snapshot.clone(), + }]), error: None, }) .expect("serialize response") @@ -481,7 +582,8 @@ mod tests { RemoteClientConfig { addr: addr.to_string(), token: None, - rig_id: None, + selected_rig_id: Arc::new(Mutex::new(None)), + known_rigs: Arc::new(Mutex::new(Vec::new())), poll_interval: Duration::from_millis(100), }, req_rx, @@ -515,7 +617,8 @@ mod tests { let config = RemoteClientConfig { addr: "127.0.0.1:4530".to_string(), token: Some("secret".to_string()), - rig_id: Some("sdr".to_string()), + selected_rig_id: Arc::new(Mutex::new(Some("sdr".to_string()))), + known_rigs: Arc::new(Mutex::new(Vec::new())), poll_interval: Duration::from_millis(500), }; let envelope = super::build_envelope(&config, trx_protocol::ClientCommand::GetState); diff --git a/src/trx-client/trx-frontend/src/lib.rs b/src/trx-client/trx-frontend/src/lib.rs index 320cbeb..3a69141 100644 --- a/src/trx-client/trx-frontend/src/lib.rs +++ b/src/trx-client/trx-frontend/src/lib.rs @@ -14,8 +14,15 @@ use tokio::task::JoinHandle; use trx_core::audio::AudioStreamInfo; use trx_core::decode::{AprsPacket, CwEvent, DecodedMessage, Ft8Message, WsprMessage}; +use trx_core::rig::state::RigSnapshot; use trx_core::{DynResult, RigRequest, RigState}; +#[derive(Clone, Debug)] +pub struct RemoteRigEntry { + pub rig_id: String, + pub state: RigSnapshot, +} + /// Trait implemented by concrete frontends to expose a runner entrypoint. pub trait FrontendSpawner { fn spawn_frontend( @@ -140,6 +147,10 @@ pub struct FrontendRuntimeContext { pub http_auth_cookie_secure: bool, /// HTTP frontend auth cookie same-site policy pub http_auth_cookie_same_site: String, + /// Currently selected remote rig id (used by remote client routing). + pub remote_active_rig_id: Arc>>, + /// Cached remote rig list from GetRigs polling. + pub remote_rigs: Arc>>, } impl FrontendRuntimeContext { @@ -165,6 +176,8 @@ impl FrontendRuntimeContext { http_auth_session_ttl_secs: 480 * 60, http_auth_cookie_secure: false, http_auth_cookie_same_site: "Lax".to_string(), + remote_active_rig_id: Arc::new(Mutex::new(None)), + remote_rigs: Arc::new(Mutex::new(Vec::new())), } } } diff --git a/src/trx-client/trx-frontend/trx-frontend-http-json/src/server.rs b/src/trx-client/trx-frontend/trx-frontend-http-json/src/server.rs index 45e3e23..21314e0 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http-json/src/server.rs +++ b/src/trx-client/trx-frontend/trx-frontend-http-json/src/server.rs @@ -19,6 +19,7 @@ use trx_frontend::{FrontendRuntimeContext, FrontendSpawner}; use trx_protocol::auth::{SimpleTokenValidator, TokenValidator}; use trx_protocol::codec::parse_envelope; use trx_protocol::mapping; +use trx_protocol::types::{ClientCommand, RigEntry}; use trx_protocol::ClientResponse; const IO_TIMEOUT: Duration = Duration::from_secs(10); @@ -125,6 +126,30 @@ async fn handle_client( continue; } + if let Some(rig_id) = envelope.rig_id.as_ref() { + if let Ok(mut active) = context.remote_active_rig_id.lock() { + *active = Some(rig_id.clone()); + } + } + + if matches!(&envelope.cmd, ClientCommand::GetRigs) { + let resp = ClientResponse { + success: true, + rig_id: Some("client".to_string()), + state: None, + rigs: Some(snapshot_remote_rigs(context.as_ref())), + error: None, + }; + send_response(&mut writer, &resp).await?; + continue; + } + + let active_rig_id = context + .remote_active_rig_id + .lock() + .ok() + .and_then(|v| v.clone()); + let rig_cmd = mapping::client_command_to_rig(envelope.cmd); let (resp_tx, resp_rx) = oneshot::channel(); @@ -139,7 +164,7 @@ async fn handle_client( error!("Failed to send request to rig_task: {:?}", e); let resp = ClientResponse { success: false, - rig_id: None, + rig_id: active_rig_id.clone(), state: None, rigs: None, error: Some("Internal error: rig task not available".into()), @@ -150,7 +175,7 @@ async fn handle_client( Err(_) => { let resp = ClientResponse { success: false, - rig_id: None, + rig_id: active_rig_id.clone(), state: None, rigs: None, error: Some("Internal error: request queue timeout".into()), @@ -164,7 +189,7 @@ async fn handle_client( Ok(Ok(Ok(snapshot))) => { let resp = ClientResponse { success: true, - rig_id: None, + rig_id: active_rig_id.clone(), state: Some(snapshot), rigs: None, error: None, @@ -174,7 +199,7 @@ async fn handle_client( Ok(Ok(Err(err))) => { let resp = ClientResponse { success: false, - rig_id: None, + rig_id: active_rig_id.clone(), state: None, rigs: None, error: Some(err.message), @@ -185,7 +210,7 @@ async fn handle_client( error!("Rig response oneshot recv error: {:?}", e); let resp = ClientResponse { success: false, - rig_id: None, + rig_id: active_rig_id.clone(), state: None, rigs: None, error: Some("Internal error waiting for rig response".into()), @@ -195,7 +220,7 @@ async fn handle_client( Err(_) => { let resp = ClientResponse { success: false, - rig_id: None, + rig_id: active_rig_id.clone(), state: None, rigs: None, error: Some("Request timed out waiting for rig response".into()), @@ -208,6 +233,23 @@ async fn handle_client( Ok(()) } +fn snapshot_remote_rigs(context: &FrontendRuntimeContext) -> Vec { + context + .remote_rigs + .lock() + .ok() + .map(|entries| { + entries + .iter() + .map(|entry| RigEntry { + rig_id: entry.rig_id.clone(), + state: entry.state.clone(), + }) + .collect() + }) + .unwrap_or_default() +} + async fn read_limited_line( reader: &mut R, max_bytes: usize, diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js index 9f03f8b..f9719c4 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js @@ -277,6 +277,8 @@ const loadingTitle = document.getElementById("loading-title"); const loadingSub = document.getElementById("loading-sub"); const headerSigCanvas = document.getElementById("header-sig-canvas"); const themeToggleBtn = document.getElementById("theme-toggle"); +const rigSwitchSelect = document.getElementById("rig-switch-select"); +const rigSwitchBtn = document.getElementById("rig-switch-btn"); let lastControl; let lastTxEn = null; @@ -303,6 +305,7 @@ function vfoColor(idx) { let jogAngle = 0; let lastClientCount = null; let lastLocked = false; +let lastRigIds = []; const originalTitle = document.title; const savedTheme = loadSetting("theme", null); @@ -941,6 +944,33 @@ function render(update) { if (typeof update.clients === "number") { document.getElementById("about-clients").textContent = update.clients; } + if (typeof update.active_rig_id === "string" && update.active_rig_id.length > 0) { + document.getElementById("about-active-rig").textContent = update.active_rig_id; + } + if (Array.isArray(update.rig_ids)) { + lastRigIds = update.rig_ids.filter((id) => typeof id === "string" && id.length > 0); + document.getElementById("about-rig-list").textContent = lastRigIds.length ? lastRigIds.join(", ") : "--"; + if (rigSwitchSelect) { + const selectedBefore = rigSwitchSelect.value; + rigSwitchSelect.innerHTML = ""; + lastRigIds.forEach((id) => { + const opt = document.createElement("option"); + opt.value = id; + opt.textContent = id; + rigSwitchSelect.appendChild(opt); + }); + const preferred = (typeof update.active_rig_id === "string" && lastRigIds.includes(update.active_rig_id)) + ? update.active_rig_id + : selectedBefore; + if (preferred && lastRigIds.includes(preferred)) { + rigSwitchSelect.value = preferred; + } + rigSwitchSelect.disabled = lastRigIds.length === 0; + if (rigSwitchBtn) { + rigSwitchBtn.disabled = lastRigIds.length === 0 || authRole === "rx"; + } + } + } if (typeof update.rigctl_clients === "number") { document.getElementById("about-rigctl-clients").textContent = update.rigctl_clients; } @@ -1080,6 +1110,36 @@ async function postPath(path) { return resp; } +async function switchRig() { + if (!rigSwitchSelect || !rigSwitchSelect.value) { + showHint("No rig selected", 1500); + return; + } + if (authRole === "rx") { + showHint("Control role required", 1500); + return; + } + if (!lastRigIds.includes(rigSwitchSelect.value)) { + showHint("Unknown rig", 1500); + return; + } + if (rigSwitchBtn) rigSwitchBtn.disabled = true; + showHint("Switching rig…"); + try { + await postPath(`/select_rig?rig_id=${encodeURIComponent(rigSwitchSelect.value)}`); + showHint("Rig switch requested", 1500); + } catch (err) { + showHint("Rig switch failed", 2000); + console.error(err); + } finally { + if (rigSwitchBtn) rigSwitchBtn.disabled = authRole === "rx"; + } +} + +if (rigSwitchBtn) { + rigSwitchBtn.addEventListener("click", switchRig); +} + powerBtn.addEventListener("click", async () => { powerBtn.disabled = true; showHint("Sending..."); diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html index 85ef939..a3dab1c 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html @@ -275,6 +275,9 @@ Server address-- Server callsign-- Rig-- + Active rig-- + Available rigs-- + Switch rig Rig connection-- Supported modes-- VFOs-- diff --git a/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs b/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs index 84edf15..56c342d 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs +++ b/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs @@ -16,7 +16,7 @@ use tokio_stream::wrappers::{IntervalStream, WatchStream}; use trx_core::radio::freq::Freq; use trx_core::rig::{RigAccessMethod, RigCapabilities, RigInfo}; use trx_core::{RigCommand, RigRequest, RigSnapshot, RigState}; -use trx_frontend::FrontendRuntimeContext; +use trx_frontend::{FrontendRuntimeContext, RemoteRigEntry}; use trx_protocol::{parse_mode, ClientResponse}; use crate::server::status; @@ -42,6 +42,8 @@ fn inject_frontend_meta( http_clients: usize, rigctl_clients: usize, rigctl_addr: Option, + active_rig_id: Option, + rig_ids: Vec, ) -> String { let mut value: serde_json::Value = match serde_json::from_str(json) { Ok(v) => v, @@ -59,6 +61,10 @@ fn inject_frontend_meta( if let Some(addr) = rigctl_addr { map.insert("rigctl_addr".to_string(), serde_json::json!(addr)); } + if let Some(rig_id) = active_rig_id { + map.insert("active_rig_id".to_string(), serde_json::json!(rig_id)); + } + map.insert("rig_ids".to_string(), serde_json::json!(rig_ids)); serde_json::to_string(&value).unwrap_or_else(|_| json.to_string()) } @@ -72,6 +78,23 @@ fn rigctl_addr_from_context(context: &FrontendRuntimeContext) -> Option .map(|addr| addr.to_string()) } +fn active_rig_id_from_context(context: &FrontendRuntimeContext) -> Option { + context + .remote_active_rig_id + .lock() + .ok() + .and_then(|v| v.clone()) +} + +fn rig_ids_from_context(context: &FrontendRuntimeContext) -> Vec { + context + .remote_rigs + .lock() + .ok() + .map(|entries| entries.iter().map(|r| r.rig_id.clone()).collect()) + .unwrap_or_default() +} + #[get("/events")] pub async fn events( state: web::Data>, @@ -91,6 +114,8 @@ pub async fn events( count, context.rigctl_clients.load(Ordering::Relaxed), rigctl_addr_from_context(context.get_ref().as_ref()), + active_rig_id_from_context(context.get_ref().as_ref()), + rig_ids_from_context(context.get_ref().as_ref()), ); let initial_stream = once(async move { Ok::(Bytes::from(format!("data: {initial_json}\n\n"))) }); @@ -108,6 +133,8 @@ pub async fn events( counter.load(Ordering::Relaxed), context.rigctl_clients.load(Ordering::Relaxed), rigctl_addr_from_context(context.as_ref()), + active_rig_id_from_context(context.as_ref()), + rig_ids_from_context(context.as_ref()), ); Ok::(Bytes::from(format!("data: {json}\n\n"))) }) @@ -471,9 +498,90 @@ pub async fn clear_cw_decode( send_command(&rig_tx, RigCommand::ResetCwDecoder).await } +#[derive(serde::Serialize)] +struct RigListItem { + rig_id: String, + manufacturer: String, + model: String, + initialized: bool, +} + +#[derive(serde::Serialize)] +struct RigListResponse { + active_rig_id: Option, + rigs: Vec, +} + +fn build_rig_list_payload(context: &FrontendRuntimeContext) -> RigListResponse { + let active_rig_id = active_rig_id_from_context(context); + let rigs = context + .remote_rigs + .lock() + .ok() + .map(|entries| entries.iter().map(map_rig_entry).collect()) + .unwrap_or_default(); + RigListResponse { + active_rig_id, + rigs, + } +} + +fn map_rig_entry(entry: &RemoteRigEntry) -> RigListItem { + RigListItem { + rig_id: entry.rig_id.clone(), + manufacturer: entry.state.info.manufacturer.clone(), + model: entry.state.info.model.clone(), + initialized: entry.state.initialized, + } +} + +#[get("/rigs")] +pub async fn list_rigs( + context: web::Data>, +) -> Result { + Ok(HttpResponse::Ok().json(build_rig_list_payload(context.get_ref().as_ref()))) +} + +#[derive(serde::Deserialize)] +pub struct SelectRigQuery { + pub rig_id: String, +} + +#[post("/select_rig")] +pub async fn select_rig( + query: web::Query, + context: web::Data>, +) -> Result { + let rig_id = query.rig_id.trim(); + if rig_id.is_empty() { + return Err(actix_web::error::ErrorBadRequest( + "rig_id must not be empty", + )); + } + + let known = context + .remote_rigs + .lock() + .ok() + .map(|entries| entries.iter().any(|entry| entry.rig_id == rig_id)) + .unwrap_or(false); + if !known { + return Err(actix_web::error::ErrorBadRequest(format!( + "unknown rig_id: {rig_id}" + ))); + } + + if let Ok(mut active) = context.remote_active_rig_id.lock() { + *active = Some(rig_id.to_string()); + } + + Ok(HttpResponse::Ok().json(build_rig_list_payload(context.get_ref().as_ref()))) +} + pub fn configure(cfg: &mut web::ServiceConfig) { cfg.service(index) .service(status_api) + .service(list_rigs) .service(events) .service(decode_events) .service(toggle_power) @@ -497,6 +605,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) { .service(clear_cw_decode) .service(clear_ft8_decode) .service(clear_wspr_decode) + .service(select_rig) .service(crate::server::audio::audio_ws) .service(favicon) .service(logo) diff --git a/src/trx-client/trx-frontend/trx-frontend-http/src/auth.rs b/src/trx-client/trx-frontend/trx-frontend-http/src/auth.rs index 8c6ae38..547a0c8 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/src/auth.rs +++ b/src/trx-client/trx-frontend/trx-frontend-http/src/auth.rs @@ -9,8 +9,9 @@ //! - `Control`: full access including TX/PTT control use actix_web::{ + cookie::Cookie, dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, - get, post, web, Error, HttpRequest, HttpResponse, Responder, cookie::Cookie, + get, post, web, Error, HttpRequest, HttpResponse, Responder, }; use futures_util::future::LocalBoxFuture; use serde::{Deserialize, Serialize}; @@ -18,7 +19,6 @@ use std::collections::HashMap; use std::sync::{Arc, RwLock}; use std::time::{Duration, SystemTime}; - /// Unique session identifier (hex-encoded 128-bit random) pub type SessionId = String; @@ -309,12 +309,10 @@ pub async fn login( let ttl_secs = auth_state.config.session_ttl.as_secs() as i64; cookie.set_max_age(actix_web::cookie::time::Duration::seconds(ttl_secs)); - Ok(HttpResponse::Ok() - .cookie(cookie) - .json(LoginResponse { - authenticated: true, - role: role.as_str().to_string(), - })) + Ok(HttpResponse::Ok().cookie(cookie).json(LoginResponse { + authenticated: true, + role: role.as_str().to_string(), + })) } /// POST /auth/logout @@ -338,11 +336,9 @@ pub async fn logout( cookie.set_http_only(true); cookie.set_max_age(actix_web::cookie::time::Duration::seconds(0)); - Ok(HttpResponse::Ok() - .cookie(cookie) - .json(serde_json::json!({ - "logged_out": true - }))) + Ok(HttpResponse::Ok().cookie(cookie).json(serde_json::json!({ + "logged_out": true + }))) } /// GET /auth/session @@ -424,10 +420,12 @@ impl RouteAccess { // Read-only routes if path == "/status" + || path == "/rigs" || path == "/events" || path == "/decode" || path == "/audio" || path.starts_with("/status?") + || path.starts_with("/rigs?") || path.starts_with("/events?") || path.starts_with("/decode?") || path.starts_with("/audio?") @@ -498,9 +496,7 @@ where } // For protected routes, check auth - let auth_state = req - .app_data::>() - .cloned(); + let auth_state = req.app_data::>().cloned(); if let Some(auth_state) = auth_state { if !auth_state.config.enabled { @@ -586,6 +582,7 @@ mod tests { #[test] fn test_route_access_read_paths() { assert_eq!(RouteAccess::from_path("/status"), RouteAccess::Read); + assert_eq!(RouteAccess::from_path("/rigs"), RouteAccess::Read); assert_eq!(RouteAccess::from_path("/events"), RouteAccess::Read); assert_eq!(RouteAccess::from_path("/decode"), RouteAccess::Read); assert_eq!(RouteAccess::from_path("/audio"), RouteAccess::Read); @@ -593,14 +590,8 @@ mod tests { #[test] fn test_route_access_control_paths() { - assert_eq!( - RouteAccess::from_path("/set_freq"), - RouteAccess::Control - ); - assert_eq!( - RouteAccess::from_path("/set_mode"), - RouteAccess::Control - ); + assert_eq!(RouteAccess::from_path("/set_freq"), RouteAccess::Control); + assert_eq!(RouteAccess::from_path("/set_mode"), RouteAccess::Control); } #[test]